/* * messagebox.cpp * Message box dialog implementation * Copyright (c) 2026 Daniel Hammer */ #include "messagebox.hpp" #include "main.hpp" #include #include #include #include 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; } // Drop one whole character from the front of the tail keep = slen - montauk::utf8::next_boundary(src, slen, slen - 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'; } int clamp_int(int value, int min_value, int max_value) { if (value < min_value) return min_value; if (value > max_value) return max_value; return value; } int button_row_width(const MessageBoxState* st) { if (!st || st->button_count <= 0) return 0; int total_w = st->button_count * messagebox::BUTTON_W; if (st->button_count > 1) total_w += (st->button_count - 1) * messagebox::BUTTON_GAP; return total_w; } int widest_explicit_message_line(const MessageBoxState* st, int max_px) { if (!st || !st->text[0]) return 0; char line[messagebox::MAX_LINE_LEN] = {}; int line_len = 0; int widest = 0; for (const char* p = st->text; ; p++) { char ch = *p; bool at_end = ch == '\0'; if (at_end || ch == '\n') { line[line_len] = '\0'; int width = text_width(line); if (width > widest) widest = width; line_len = 0; line[0] = '\0'; if (at_end) break; continue; } if (line_len >= messagebox::MAX_LINE_LEN - 1) return max_px; line[line_len++] = ch; } return widest > max_px ? max_px : widest; } 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, ¤t_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, ¤t_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 = {messagebox::TEXT_X, messagebox::TEXT_Y, g_app.win.width - messagebox::TEXT_X * 2, lo.footer_rect.y - messagebox::TEXT_Y - messagebox::TEXT_BOTTOM_GAP}; int total_w = button_row_width(st); int x = g_app.win.width - messagebox::BUTTON_RIGHT_PAD - 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::abi::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::abi::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, messagebox::MESSAGE_MIN_W - messagebox::TEXT_X * 2); } void preferred_size(const char* title, int* out_w, int* out_h) { MessageBoxState* st = &g_message; int max_text_w = messagebox::MESSAGE_MAX_W - messagebox::TEXT_X * 2; int desired_text_w = widest_explicit_message_line(st, max_text_w); if (title && title[0]) { int title_w = text_width(title) + 120; if (title_w > desired_text_w) desired_text_w = title_w; } int desired_w = desired_text_w + messagebox::TEXT_X * 2; int buttons_w = button_row_width(st) + messagebox::BUTTON_RIGHT_PAD * 2; if (buttons_w > desired_w) desired_w = buttons_w; desired_w = clamp_int(desired_w, messagebox::MESSAGE_MIN_W, messagebox::MESSAGE_MAX_W); wrap_message(st, desired_w - messagebox::TEXT_X * 2); int line_h = system_font_height() + 5; int text_h = st->line_count > 0 ? st->line_count * line_h : line_h; int desired_h = messagebox::TEXT_Y + text_h + messagebox::TEXT_BOTTOM_GAP + messagebox::FOOTER_H; desired_h = clamp_int(desired_h, messagebox::MESSAGE_MIN_H, messagebox::MESSAGE_MAX_H); if (out_w) *out_w = desired_w; if (out_h) *out_h = desired_h; } void draw() { draw_message_box(); } void handle_mouse(const montauk::abi::WinEvent& ev) { handle_message_mouse(ev); } void handle_key(const montauk::abi::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