Files
MontaukOS/programs/src/dialogs/messagebox.cpp
T

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, &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 = {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