488 lines
15 KiB
C++
488 lines
15 KiB
C++
/*
|
|
* 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;
|
|
}
|
|
// 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
|