diff --git a/programs/GNUmakefile b/programs/GNUmakefile index 85a8a54..82a2b59 100644 --- a/programs/GNUmakefile +++ b/programs/GNUmakefile @@ -59,7 +59,7 @@ BINDIR := bin PROGRAMS := $(notdir $(wildcard src/*)) # Programs with custom Makefiles (built separately). -CUSTOM_BUILDS := doom fetch wiki wikipedia weather imageviewer fontpreview spreadsheet wordprocessor pdfviewer disks devexplorer installer volume music video bluetooth terminal klog procmgr calculator desktop login shell rpgdemo paint tcc lua screenshot texteditor mandelbrot printers printd printctl dialogs test_dl libloader libhello crashpad +CUSTOM_BUILDS := doom fetch wiki wikipedia weather imageviewer fontpreview spreadsheet wordprocessor pdfviewer disks devexplorer installer volume music video bluetooth terminal klog procmgr calculator desktop login shell rpgdemo paint tcc lua screenshot texteditor mandelbrot printers printd printctl dialogs test_dialogs test_dl libloader libhello crashpad SYSTEM_PROGRAMS := $(filter-out $(CUSTOM_BUILDS),$(PROGRAMS)) # Build targets: system programs go to bin/os/, apps go to bin/apps//. @@ -81,9 +81,9 @@ CA_CERTS := $(BINDIR)/etc/ca-certificates.crt # Common shared assets (wallpapers, etc.) COMMONKEEP := $(BINDIR)/common/.keep -.PHONY: all clean doom fetch wiki wikipedia weather imageviewer fontpreview spreadsheet wordprocessor pdfviewer disks devexplorer installer volume music video bluetooth terminal klog procmgr calculator login desktop shell rpgdemo paint tcc lua screenshot texteditor mandelbrot printers printd printctl dialogs icons fonts bearssl libc tls libjpeg libjpegwrite install-apps libloader libs libhello test_dl crashpad +.PHONY: all clean doom fetch wiki wikipedia weather imageviewer fontpreview spreadsheet wordprocessor pdfviewer disks devexplorer installer volume music video bluetooth terminal klog procmgr calculator login desktop shell rpgdemo paint tcc lua screenshot texteditor mandelbrot printers printd printctl dialogs icons fonts bearssl libc tls libjpeg libjpegwrite install-apps libloader libs libhello test_dialogs test_dl crashpad -all: bearssl libc libjpeg libjpegwrite tls libloader libs libhello $(TARGETS) fetch wiki wikipedia weather imageviewer fontpreview spreadsheet wordprocessor pdfviewer disks devexplorer installer volume music video bluetooth terminal klog procmgr calculator doom rpgdemo paint tcc lua screenshot texteditor mandelbrot printers printd printctl dialogs login desktop shell icons fonts install-apps test_dl crashpad $(MANDST) $(WWWDST) $(CA_CERTS) $(COMMONKEEP) +all: bearssl libc libjpeg libjpegwrite tls libloader libs libhello $(TARGETS) fetch wiki wikipedia weather imageviewer fontpreview spreadsheet wordprocessor pdfviewer disks devexplorer installer volume music video bluetooth terminal klog procmgr calculator doom rpgdemo paint tcc lua screenshot texteditor mandelbrot printers printd printctl dialogs login desktop shell icons fonts install-apps test_dialogs test_dl crashpad $(MANDST) $(WWWDST) $(CA_CERTS) $(COMMONKEEP) # Build BearSSL static library (cross-compiled for freestanding x86_64). BEARSSL_INCLUDES := -isystem $(shell cd .. && pwd)/kernel/freestnd-c-hdrs/x86_64/include -isystem $(abspath include/libc) @@ -285,6 +285,10 @@ crashpad: libc test_dl: libloader libs libhello $(MAKE) -C src/test_dl +# Build test_dialogs (minimal libdialogs message box test). +test_dialogs: libc libloader dialogs + $(MAKE) -C src/test_dialogs + # Install app bundles (manifests, icons, data files) into bin/apps//. install-apps: doom rpgdemo paint spreadsheet wordprocessor weather wikipedia imageviewer fontpreview pdfviewer disks devexplorer installer volume music video bluetooth terminal klog procmgr calculator screenshot texteditor mandelbrot printers crashpad ../scripts/install_apps.sh @@ -358,4 +362,5 @@ clean: $(MAKE) -C src/libloader clean $(MAKE) -C libs/libmath clean $(MAKE) -C libs/libhello clean + $(MAKE) -C src/test_dialogs clean $(MAKE) -C src/test_dl clean diff --git a/programs/include/gui/dialogs.hpp b/programs/include/gui/dialogs.hpp index a8f4163..d518259 100644 --- a/programs/include/gui/dialogs.hpp +++ b/programs/include/gui/dialogs.hpp @@ -17,6 +17,7 @@ namespace gui::dialogs { inline constexpr const char* LIBRARY_PATH = "0:/os/libdialogs.lib"; inline constexpr const char* RUN_REQUEST_SYMBOL = "dialogs_run_request"; +inline constexpr const char* MESSAGE_BOX_SYMBOL = "dialogs_message_box"; enum RequestKind : uint8_t { REQUEST_KIND_FILE = 1, @@ -33,6 +34,21 @@ enum PrintDialogMode : uint8_t { PRINT_DIALOG_SUBMIT = 2, }; +enum MessageBoxButtons : uint8_t { + MESSAGE_BOX_OK = 1, + MESSAGE_BOX_OK_CANCEL = 2, + MESSAGE_BOX_YES_NO = 3, + MESSAGE_BOX_YES_NO_CANCEL = 4, +}; + +enum MessageBoxResult : uint8_t { + MESSAGE_BOX_RESULT_NONE = 0, + MESSAGE_BOX_RESULT_OK = 1, + MESSAGE_BOX_RESULT_CANCEL = 2, + MESSAGE_BOX_RESULT_YES = 3, + MESSAGE_BOX_RESULT_NO = 4, +}; + struct Request { uint8_t kind; uint8_t mode; @@ -75,10 +91,12 @@ inline void reset_result(Result* res) { #ifndef GUI_DIALOGS_LIBRARY_BUILD using RunRequestFn = bool (*)(const Request*, Result*); +using MessageBoxFn = uint8_t (*)(const char*, const char*, uint8_t); struct Runtime { LibHandle* handle; RunRequestFn run_request; + MessageBoxFn message_box; }; inline Runtime g_runtime = {}; @@ -101,6 +119,24 @@ inline RunRequestFn resolve_run_request(char* out_message, int out_message_len) return g_runtime.run_request; } +inline MessageBoxFn resolve_message_box(char* out_message, int out_message_len) { + if (g_runtime.message_box) return g_runtime.message_box; + + if (!g_runtime.handle) { + g_runtime.handle = libloader::dlopen(LIBRARY_PATH); + if (!g_runtime.handle) { + if (out_message && out_message_len > 0) + safe_copy(out_message, out_message_len, "failed to load dialog library"); + return nullptr; + } + } + + g_runtime.message_box = (MessageBoxFn)libloader::dlsym(g_runtime.handle, MESSAGE_BOX_SYMBOL); + if (!g_runtime.message_box && out_message && out_message_len > 0) + safe_copy(out_message, out_message_len, "failed to resolve message box entry point"); + return g_runtime.message_box; +} + inline bool run_request(const Request* req, Result* out_res, char* out_message, int out_message_len) { if (out_message && out_message_len > 0) out_message[0] = '\0'; if (!req) { @@ -202,6 +238,22 @@ inline bool configure_print(const char* title, return ok; } +inline MessageBoxResult message_box(const char* title, + const char* message, + MessageBoxButtons buttons = MESSAGE_BOX_OK, + char* out_message = nullptr, int out_message_len = 0) { + if (out_message && out_message_len > 0) out_message[0] = '\0'; + MessageBoxFn fn = resolve_message_box(out_message, out_message_len); + if (!fn) return MESSAGE_BOX_RESULT_NONE; + + uint8_t result = fn(title ? title : "Message", + message ? message : "", + (uint8_t)buttons); + if (result == MESSAGE_BOX_RESULT_NONE && out_message && out_message_len > 0 && !out_message[0]) + safe_copy(out_message, out_message_len, "message box invocation failed"); + return (MessageBoxResult)result; +} + #endif } // namespace gui::dialogs diff --git a/programs/src/dialogs/Makefile b/programs/src/dialogs/Makefile index e43f123..93ce364 100644 --- a/programs/src/dialogs/Makefile +++ b/programs/src/dialogs/Makefile @@ -60,6 +60,7 @@ LDFLAGS := \ --gc-sections \ --hash-style=sysv \ -u dialogs_run_request \ + -u dialogs_message_box \ -m elf_x86_64 \ -z max-page-size=0x1000 @@ -84,7 +85,7 @@ LIBC_CFLAGS := \ -isystem $(PROG_INC)/libc \ -isystem $(GCC_INCLUDE) -SRCS := main.cpp filedialog.cpp printdialog.cpp stb_truetype_impl.cpp font_data.cpp +SRCS := main.cpp filedialog.cpp printdialog.cpp messagebox.cpp stb_truetype_impl.cpp font_data.cpp OBJS := $(addprefix $(OBJDIR)/,$(SRCS:.cpp=.o)) LIBC_PIC_OBJ := $(OBJDIR)/libc_pic.o diff --git a/programs/src/dialogs/main.cpp b/programs/src/dialogs/main.cpp index 8c4db88..5ceb7f5 100644 --- a/programs/src/dialogs/main.cpp +++ b/programs/src/dialogs/main.cpp @@ -15,6 +15,7 @@ #include "main.hpp" #include "filedialog.hpp" #include "printdialog.hpp" +#include "messagebox.hpp" using namespace gui; namespace dlg = gui::dialogs; @@ -41,6 +42,8 @@ void dialog_cancel(const char* message) { void cleanup() { if (g_app.is_print) { printdialog::cleanup(); + } else if (g_app.is_message) { + messagebox::cleanup(); } else { filedialog::cleanup(); } @@ -54,20 +57,26 @@ static void set_error_result(dlg::Result* result, const char* message) { dlg::safe_copy(result->message, sizeof(result->message), message); } +static bool ensure_fonts(dlg::Result* result) { + if (g_fonts_ready) return true; + if (!fonts::init()) { + if (result) + set_error_result(result, "failed to initialize dialog fonts"); + return false; + } + g_fonts_ready = true; + return true; +} + extern "C" bool dialogs_run_request(const dlg::Request* request, dlg::Result* out_result) { if (!request || !out_result) return false; set_error_result(out_result, "dialog failed"); - if (!g_fonts_ready) { - if (!fonts::init()) { - set_error_result(out_result, "failed to initialize dialog fonts"); - return true; - } - g_fonts_ready = true; - } + if (!ensure_fonts(out_result)) return true; - if (request->kind != dlg::REQUEST_KIND_FILE && request->kind != dlg::REQUEST_KIND_PRINT) { + if (request->kind != dlg::REQUEST_KIND_FILE && + request->kind != dlg::REQUEST_KIND_PRINT) { set_error_result(out_result, "unsupported dialog request"); return true; } @@ -78,9 +87,16 @@ extern "C" bool dialogs_run_request(const dlg::Request* request, dlg::Result* ou dlg::reset_result(&g_app.result); g_app.is_print = g_app.request.kind == dlg::REQUEST_KIND_PRINT; - const int init_w = g_app.is_print ? printdialog::PRINT_INIT_W : filedialog::FILE_INIT_W; - const int init_h = g_app.is_print ? printdialog::PRINT_INIT_H : filedialog::FILE_INIT_H; - const char* title = g_app.request.title[0] ? g_app.request.title : (g_app.is_print ? "Print" : "Open"); + + int init_w = filedialog::FILE_INIT_W; + int init_h = filedialog::FILE_INIT_H; + if (g_app.is_print) { + init_w = printdialog::PRINT_INIT_W; + init_h = printdialog::PRINT_INIT_H; + } + + const char* default_title = g_app.is_print ? "Print" : "Open"; + const char* title = g_app.request.title[0] ? g_app.request.title : default_title; if (g_app.is_print) printdialog::init(g_app.request); else filedialog::init(g_app.request); @@ -136,3 +152,60 @@ extern "C" bool dialogs_run_request(const dlg::Request* request, dlg::Result* ou *out_result = g_app.result; return true; } + +extern "C" uint8_t dialogs_message_box(const char* title, + const char* message, + uint8_t buttons) { + if (!ensure_fonts(nullptr)) + return dlg::MESSAGE_BOX_RESULT_NONE; + + g_app = AppState {}; + g_app.running = true; + g_app.is_message = true; + + const char* window_title = title && title[0] ? title : "Message"; + messagebox::init(message ? message : "", buttons); + + if (!g_app.win.create(window_title, messagebox::MESSAGE_INIT_W, messagebox::MESSAGE_INIT_H)) { + cleanup(); + return dlg::MESSAGE_BOX_RESULT_NONE; + } + + messagebox::draw(); + g_app.win.present(); + + while (g_app.running) { + messagebox::draw(); + g_app.win.present(); + + Montauk::WinEvent ev; + int rc = g_app.win.poll(&ev); + if (rc < 0) { + messagebox::dismiss(); + break; + } + if (rc == 0) { + montauk::sleep_ms(16); + continue; + } + + if (ev.type == 3) { + messagebox::dismiss(); + break; + } + if (ev.type == 2 || ev.type == 4) { + continue; + } + if (ev.type == 1) { + messagebox::handle_mouse(ev); + continue; + } + if (ev.type == 0) { + messagebox::handle_key(ev.key); + } + } + + uint8_t choice = messagebox::selected_choice(); + cleanup(); + return choice ? choice : (uint8_t)dlg::MESSAGE_BOX_RESULT_NONE; +} diff --git a/programs/src/dialogs/main.hpp b/programs/src/dialogs/main.hpp index eddbab5..6fe08d2 100644 --- a/programs/src/dialogs/main.hpp +++ b/programs/src/dialogs/main.hpp @@ -16,10 +16,11 @@ struct AppState { gui::dialogs::Result result; bool running; bool is_print; + bool is_message; }; extern AppState g_app; void dialog_finish(const char* status, const char* path, const char* job_id, const char* message, const char* printer_uri = "", const char* printer_name = "", uint32_t copies = 0); -void dialog_cancel(const char* message = ""); \ No newline at end of file +void dialog_cancel(const char* message = ""); diff --git a/programs/src/dialogs/messagebox.cpp b/programs/src/dialogs/messagebox.cpp new file mode 100644 index 0000000..107345a --- /dev/null +++ b/programs/src/dialogs/messagebox.cpp @@ -0,0 +1,416 @@ +/* + * 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; + } + 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, ¤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 = {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 diff --git a/programs/src/dialogs/messagebox.hpp b/programs/src/dialogs/messagebox.hpp new file mode 100644 index 0000000..a5293d4 --- /dev/null +++ b/programs/src/dialogs/messagebox.hpp @@ -0,0 +1,40 @@ +/* + * messagebox.hpp + * Message box dialog declarations + * Copyright (c) 2026 Daniel Hammer +*/ + +#pragma once + +#include +#include +#include + +struct MessageBoxLayout { + gui::Rect text_rect; + gui::Rect footer_rect; + gui::Rect buttons[3]; + int button_count; +}; + +namespace messagebox { + +constexpr int MESSAGE_INIT_W = 460; +constexpr int MESSAGE_INIT_H = 220; +constexpr int FOOTER_H = 56; +constexpr int BUTTON_W = 88; +constexpr int BUTTON_H = 30; +constexpr int BUTTON_GAP = 8; +constexpr int MAX_BUTTONS = 3; +constexpr int MAX_LINES = 7; +constexpr int MAX_LINE_LEN = 128; + +void init(const char* message, uint8_t buttons, uint8_t default_choice = 0); +void draw(); +void handle_mouse(const Montauk::WinEvent& ev); +void handle_key(const Montauk::KeyEvent& key); +void dismiss(); +uint8_t selected_choice(); +void cleanup(); + +} // namespace messagebox diff --git a/programs/src/test_dialogs/Makefile b/programs/src/test_dialogs/Makefile new file mode 100644 index 0000000..e16f693 --- /dev/null +++ b/programs/src/test_dialogs/Makefile @@ -0,0 +1,76 @@ +# Makefile for test_dialogs +# Copyright (c) 2026 Daniel Hammer + +MAKEFLAGS += -rR +.SUFFIXES: + +TOOLCHAIN_PREFIX := $(shell cd ../../.. && pwd)/toolchain/local/bin/x86_64-elf- +ifneq ($(wildcard $(TOOLCHAIN_PREFIX)gcc),) + CXX := $(TOOLCHAIN_PREFIX)g++ +else + CXX := g++ +endif + +PROG_INC := ../../include +LINK_LD := ../../link.ld +BINDIR := ../../bin +OBJDIR := obj +LIBDIR := ../../lib + +CXXFLAGS := \ + -std=gnu++20 \ + -g -O2 -pipe \ + -Wall \ + -Wextra \ + -Wno-unused-parameter \ + -Wno-unused-function \ + -nostdinc \ + -ffreestanding \ + -fno-stack-protector \ + -fno-stack-check \ + -fno-PIC \ + -fno-rtti \ + -fno-exceptions \ + -ffunction-sections \ + -fdata-sections \ + -m64 \ + -march=x86-64 \ + -msse \ + -msse2 \ + -mno-red-zone \ + -mcmodel=small \ + -I $(PROG_INC) \ + -isystem $(PROG_INC)/libc \ + -isystem ../../../kernel/freestnd-c-hdrs/x86_64/include \ + -isystem ../../../kernel/freestnd-cxx-hdrs/x86_64/include + +LDFLAGS := \ + -nostdlib \ + -static \ + -Wl,--build-id=none \ + -Wl,--gc-sections \ + -Wl,-m,elf_x86_64 \ + -z max-page-size=0x1000 \ + -T $(LINK_LD) + +SRCS := main.cpp +OBJS := $(addprefix $(OBJDIR)/,$(SRCS:.cpp=.o)) + +TARGET := $(BINDIR)/os/test_dialogs.elf +LOADER_LIB := $(BINDIR)/liblibloader.a +LIBC_LIB := $(LIBDIR)/libc/liblibc.a + +.PHONY: all clean + +all: $(TARGET) + +$(TARGET): $(OBJS) $(LINK_LD) Makefile $(LOADER_LIB) $(LIBC_LIB) + mkdir -p $(BINDIR)/os + $(CXX) $(CXXFLAGS) $(LDFLAGS) $(OBJS) $(LOADER_LIB) $(LIBC_LIB) -o $@ + +$(OBJDIR)/%.o: %.cpp Makefile + mkdir -p $(OBJDIR) + $(CXX) $(CXXFLAGS) -c $< -o $@ + +clean: + rm -rf $(OBJDIR) $(TARGET) diff --git a/programs/src/test_dialogs/main.cpp b/programs/src/test_dialogs/main.cpp new file mode 100644 index 0000000..498f8dc --- /dev/null +++ b/programs/src/test_dialogs/main.cpp @@ -0,0 +1,15 @@ +/* + * main.cpp + * Minimal libdialogs message box test + * Copyright (c) 2026 Daniel Hammer +*/ + +#include +#include + +extern "C" void _start() { + gui::dialogs::message_box("Test Dialogs", + "Hello from libdialogs::message_box.", + gui::dialogs::MESSAGE_BOX_OK); + montauk::exit(0); +} diff --git a/programs/src/test_dl/Makefile b/programs/src/test_dl/Makefile index d508aa8..6bbf85f 100644 --- a/programs/src/test_dl/Makefile +++ b/programs/src/test_dl/Makefile @@ -54,7 +54,6 @@ $(BINDIR)/test_dl.elf: main.cpp Makefile $(LIBDIR)/liblibloader.a ../../lib/libc $(CXX) $(CXXFLAGS) -c main.cpp -o main.o $(CXX) $(CXXFLAGS) $(LDFLAGS) main.o $(LIBDIR)/liblibloader.a ../../lib/libc/liblibc.a -o $@ rm -f main.o - rm -f $(LIBDIR)/liblibloader.a clean: rm -f $(BINDIR)/test_dl.elf