feat: add message box dialogs

This commit is contained in:
daniel committed 2026-04-22 18:44:25 +02:00
1 parent 7fa2b6c54b
commit 8c9ba7d0be
10 files changed
+695 -17

No files matched your search

+416
View File
@@ -0,0 +1,416 @@
/*
* messagebox.cpp
* Message box dialog implementation
* Copyright (c) 2026 Daniel Hammer
*/
#include "messagebox.hpp"
#include "main.hpp"
#include <montauk/string.h>
#include <gui/canvas.hpp>
#include <gui/font.hpp>
#include <gui/mtk/widgets.hpp>
namespace {
using namespace gui;
struct MessageButton {
const char* label;
uint8_t choice;
bool primary;
};
struct MessageBoxState {
char text[512];
uint8_t buttons;
uint8_t default_choice;
int mouse_x;
int mouse_y;
int wrapped_width;
int line_count;
bool overflow;
char lines[messagebox::MAX_LINES][messagebox::MAX_LINE_LEN];
MessageButton button_specs[messagebox::MAX_BUTTONS];
int button_count;
uint8_t choice;
};
MessageBoxState g_message;
uint8_t sanitize_buttons(uint8_t buttons) {
switch (buttons) {
case gui::dialogs::MESSAGE_BOX_OK:
case gui::dialogs::MESSAGE_BOX_OK_CANCEL:
case gui::dialogs::MESSAGE_BOX_YES_NO:
case gui::dialogs::MESSAGE_BOX_YES_NO_CANCEL:
return buttons;
default:
return gui::dialogs::MESSAGE_BOX_OK;
}
}
void add_button(MessageBoxState* st, const char* label, uint8_t choice, bool primary) {
if (!st || st->button_count >= messagebox::MAX_BUTTONS) return;
st->button_specs[st->button_count++] = {label, choice, primary};
}
bool has_choice(const MessageBoxState* st, uint8_t choice) {
if (!st) return false;
for (int i = 0; i < st->button_count; i++) {
if (st->button_specs[i].choice == choice) return true;
}
return false;
}
uint8_t default_choice_for(uint8_t buttons) {
if (buttons == gui::dialogs::MESSAGE_BOX_YES_NO ||
buttons == gui::dialogs::MESSAGE_BOX_YES_NO_CANCEL) {
return gui::dialogs::MESSAGE_BOX_RESULT_YES;
}
return gui::dialogs::MESSAGE_BOX_RESULT_OK;
}
uint8_t dismiss_choice_for(uint8_t buttons) {
switch (buttons) {
case gui::dialogs::MESSAGE_BOX_OK:
return gui::dialogs::MESSAGE_BOX_RESULT_OK;
case gui::dialogs::MESSAGE_BOX_OK_CANCEL:
case gui::dialogs::MESSAGE_BOX_YES_NO_CANCEL:
return gui::dialogs::MESSAGE_BOX_RESULT_CANCEL;
case gui::dialogs::MESSAGE_BOX_YES_NO:
return gui::dialogs::MESSAGE_BOX_RESULT_NO;
default:
return gui::dialogs::MESSAGE_BOX_RESULT_NONE;
}
}
void configure_buttons(MessageBoxState* st) {
if (!st) return;
st->button_count = 0;
switch (st->buttons) {
case gui::dialogs::MESSAGE_BOX_OK_CANCEL:
add_button(st, "OK", gui::dialogs::MESSAGE_BOX_RESULT_OK, true);
add_button(st, "Cancel", gui::dialogs::MESSAGE_BOX_RESULT_CANCEL, false);
break;
case gui::dialogs::MESSAGE_BOX_YES_NO:
add_button(st, "Yes", gui::dialogs::MESSAGE_BOX_RESULT_YES, true);
add_button(st, "No", gui::dialogs::MESSAGE_BOX_RESULT_NO, false);
break;
case gui::dialogs::MESSAGE_BOX_YES_NO_CANCEL:
add_button(st, "Yes", gui::dialogs::MESSAGE_BOX_RESULT_YES, true);
add_button(st, "No", gui::dialogs::MESSAGE_BOX_RESULT_NO, false);
add_button(st, "Cancel", gui::dialogs::MESSAGE_BOX_RESULT_CANCEL, false);
break;
case gui::dialogs::MESSAGE_BOX_OK:
default:
add_button(st, "OK", gui::dialogs::MESSAGE_BOX_RESULT_OK, true);
break;
}
}
void safe_append(char* dst, int dst_len, const char* src) {
if (!dst || dst_len <= 0 || !src) return;
int len = montauk::slen(dst);
int i = 0;
while (src[i] && len < dst_len - 1)
dst[len++] = src[i++];
dst[len] = '\0';
}
void fit_text_end(const char* src, char* out, int out_len, int max_px) {
if (!out || out_len <= 0) return;
if (!src || !src[0]) {
out[0] = '\0';
return;
}
if (text_width(src) <= max_px) {
gui::dialogs::safe_copy(out, out_len, src);
return;
}
int slen = montauk::slen(src);
int keep = slen;
while (keep > 1) {
char candidate[messagebox::MAX_LINE_LEN];
candidate[0] = '.';
candidate[1] = '.';
candidate[2] = '.';
candidate[3] = '\0';
const char* tail = src + (slen - keep);
safe_append(candidate, sizeof(candidate), tail);
if (text_width(candidate) <= max_px) {
gui::dialogs::safe_copy(out, out_len, candidate);
return;
}
keep--;
}
gui::dialogs::safe_copy(out, out_len, "...");
}
void mark_overflow(MessageBoxState* st) {
if (!st) return;
st->overflow = true;
if (messagebox::MAX_LINES <= 0) return;
if (st->line_count <= 0) st->line_count = 1;
if (st->line_count > messagebox::MAX_LINES) st->line_count = messagebox::MAX_LINES;
gui::dialogs::safe_copy(st->lines[st->line_count - 1], messagebox::MAX_LINE_LEN, "...");
}
void push_line(MessageBoxState* st, const char* line) {
if (!st || st->overflow) return;
if (st->line_count >= messagebox::MAX_LINES) {
mark_overflow(st);
return;
}
gui::dialogs::safe_copy(st->lines[st->line_count], messagebox::MAX_LINE_LEN, line ? line : "");
st->line_count++;
}
void commit_word(MessageBoxState* st, char* current, int* current_len, const char* word, int max_px) {
if (!st || st->overflow || !current || !current_len || !word || !word[0]) return;
char candidate[messagebox::MAX_LINE_LEN];
gui::dialogs::safe_copy(candidate, sizeof(candidate), current);
if (*current_len > 0) safe_append(candidate, sizeof(candidate), " ");
safe_append(candidate, sizeof(candidate), word);
if (*current_len == 0 && text_width(word) > max_px) {
char fitted[messagebox::MAX_LINE_LEN];
fit_text_end(word, fitted, sizeof(fitted), max_px);
push_line(st, fitted);
current[0] = '\0';
*current_len = 0;
return;
}
if (*current_len == 0 || text_width(candidate) <= max_px) {
gui::dialogs::safe_copy(current, messagebox::MAX_LINE_LEN, candidate);
*current_len = montauk::slen(current);
return;
}
push_line(st, current);
if (st->overflow) return;
current[0] = '\0';
*current_len = 0;
if (text_width(word) > max_px) {
char fitted[messagebox::MAX_LINE_LEN];
fit_text_end(word, fitted, sizeof(fitted), max_px);
push_line(st, fitted);
} else {
gui::dialogs::safe_copy(current, messagebox::MAX_LINE_LEN, word);
*current_len = montauk::slen(current);
}
}
bool is_space(char ch) {
return ch == ' ' || ch == '\t' || ch == '\r';
}
void wrap_message(MessageBoxState* st, int max_px) {
if (!st) return;
if (max_px < 40) max_px = 40;
st->line_count = 0;
st->overflow = false;
char current[messagebox::MAX_LINE_LEN] = {};
int current_len = 0;
char word[messagebox::MAX_LINE_LEN] = {};
int word_len = 0;
for (const char* p = st->text; ; p++) {
char ch = *p;
bool at_end = ch == '\0';
bool separator = at_end || ch == '\n' || is_space(ch);
if (separator) {
if (word_len > 0) {
word[word_len] = '\0';
commit_word(st, current, &current_len, word, max_px);
word_len = 0;
word[0] = '\0';
}
if (ch == '\n') {
push_line(st, current);
current[0] = '\0';
current_len = 0;
}
if (at_end) break;
if (st->overflow) break;
continue;
}
if (word_len >= messagebox::MAX_LINE_LEN - 1) {
word[word_len] = '\0';
commit_word(st, current, &current_len, word, max_px);
word_len = 0;
word[0] = '\0';
}
if (word_len < messagebox::MAX_LINE_LEN - 1)
word[word_len++] = ch;
}
if (!st->overflow && (current_len > 0 || st->line_count == 0))
push_line(st, current);
st->wrapped_width = max_px;
}
MessageBoxLayout message_layout(const MessageBoxState* st) {
MessageBoxLayout lo = {};
lo.button_count = st ? st->button_count : 0;
lo.footer_rect = {0, g_app.win.height - messagebox::FOOTER_H, g_app.win.width, messagebox::FOOTER_H};
lo.text_rect = {20, 24, g_app.win.width - 40, lo.footer_rect.y - 32};
int total_w = lo.button_count * messagebox::BUTTON_W;
if (lo.button_count > 1) total_w += (lo.button_count - 1) * messagebox::BUTTON_GAP;
int x = g_app.win.width - 16 - total_w;
int y = lo.footer_rect.y + (messagebox::FOOTER_H - messagebox::BUTTON_H) / 2;
for (int i = 0; i < lo.button_count && i < messagebox::MAX_BUTTONS; i++) {
lo.buttons[i] = {x, y, messagebox::BUTTON_W, messagebox::BUTTON_H};
x += messagebox::BUTTON_W + messagebox::BUTTON_GAP;
}
return lo;
}
mtk::Theme message_theme() {
mtk::Theme theme = mtk::make_theme();
theme.surface = Color::from_rgb(0xF7, 0xF7, 0xF7);
theme.surface_hover = mtk::mix(theme.surface, theme.accent, 18);
return theme;
}
void finish(uint8_t choice) {
g_message.choice = choice;
g_app.running = false;
}
void draw_message_box() {
MessageBoxState* st = &g_message;
Canvas c = g_app.win.canvas();
c.fill(colors::WINDOW_BG);
MessageBoxLayout lo = message_layout(st);
if (st->wrapped_width != lo.text_rect.w)
wrap_message(st, lo.text_rect.w);
int line_h = system_font_height() + 5;
int y = lo.text_rect.y;
for (int i = 0; i < st->line_count; i++) {
if (y + line_h > lo.footer_rect.y - 8) break;
c.text(lo.text_rect.x, y, st->lines[i], colors::TEXT_COLOR);
y += line_h;
}
c.fill_rect(lo.footer_rect.x, lo.footer_rect.y, lo.footer_rect.w, lo.footer_rect.h,
Color::from_rgb(0xF7, 0xF7, 0xF7));
c.hline(0, lo.footer_rect.y, c.w, colors::BORDER);
mtk::Theme theme = message_theme();
for (int i = 0; i < st->button_count && i < messagebox::MAX_BUTTONS; i++) {
bool hovered = lo.buttons[i].contains(st->mouse_x, st->mouse_y);
mtk::WidgetState state = mtk::widget_state(false, hovered, true);
mtk::draw_button(c, lo.buttons[i], st->button_specs[i].label,
st->button_specs[i].primary ? mtk::BUTTON_PRIMARY : mtk::BUTTON_SECONDARY,
state, theme);
}
}
void handle_message_mouse(const Montauk::WinEvent& ev) {
MessageBoxState* st = &g_message;
st->mouse_x = ev.mouse.x;
st->mouse_y = ev.mouse.y;
bool left_pressed = (ev.mouse.buttons & 0x01) && !(ev.mouse.prev_buttons & 0x01);
if (!left_pressed) return;
MessageBoxLayout lo = message_layout(st);
for (int i = 0; i < st->button_count && i < messagebox::MAX_BUTTONS; i++) {
if (lo.buttons[i].contains(ev.mouse.x, ev.mouse.y)) {
finish(st->button_specs[i].choice);
return;
}
}
}
void handle_message_key(const Montauk::KeyEvent& key) {
if (!key.pressed) return;
MessageBoxState* st = &g_message;
if (key.scancode == 0x01) {
finish(dismiss_choice_for(st->buttons));
return;
}
if (key.ascii == '\n' || key.ascii == '\r') {
finish(st->default_choice);
return;
}
if ((key.ascii == 'o' || key.ascii == 'O') &&
has_choice(st, gui::dialogs::MESSAGE_BOX_RESULT_OK)) {
finish(gui::dialogs::MESSAGE_BOX_RESULT_OK);
return;
}
if ((key.ascii == 'c' || key.ascii == 'C') &&
has_choice(st, gui::dialogs::MESSAGE_BOX_RESULT_CANCEL)) {
finish(gui::dialogs::MESSAGE_BOX_RESULT_CANCEL);
return;
}
if ((key.ascii == 'y' || key.ascii == 'Y') &&
has_choice(st, gui::dialogs::MESSAGE_BOX_RESULT_YES)) {
finish(gui::dialogs::MESSAGE_BOX_RESULT_YES);
return;
}
if ((key.ascii == 'n' || key.ascii == 'N') &&
has_choice(st, gui::dialogs::MESSAGE_BOX_RESULT_NO)) {
finish(gui::dialogs::MESSAGE_BOX_RESULT_NO);
}
}
} // namespace
namespace messagebox {
void init(const char* message, uint8_t buttons, uint8_t default_choice) {
MessageBoxState* st = &g_message;
montauk::memset(st, 0, sizeof(*st));
gui::dialogs::safe_copy(st->text, sizeof(st->text), message ? message : "");
st->buttons = sanitize_buttons(buttons);
st->default_choice = default_choice ? default_choice : default_choice_for(st->buttons);
configure_buttons(st);
if (!has_choice(st, st->default_choice))
st->default_choice = default_choice_for(st->buttons);
wrap_message(st, MESSAGE_INIT_W - 40);
}
void draw() {
draw_message_box();
}
void handle_mouse(const Montauk::WinEvent& ev) {
handle_message_mouse(ev);
}
void handle_key(const Montauk::KeyEvent& key) {
handle_message_key(key);
}
void dismiss() {
finish(dismiss_choice_for(g_message.buttons));
}
uint8_t selected_choice() {
return g_message.choice;
}
void cleanup() {
}
} // namespace messagebox