feat: add message box dialogs

This commit is contained in:
2026-04-22 18:44:25 +02:00
parent 6c2796f8bd
commit 15a092d0b4
10 changed files with 695 additions and 17 deletions
+8 -3
View File
@@ -59,7 +59,7 @@ BINDIR := bin
PROGRAMS := $(notdir $(wildcard src/*)) PROGRAMS := $(notdir $(wildcard src/*))
# Programs with custom Makefiles (built separately). # 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)) SYSTEM_PROGRAMS := $(filter-out $(CUSTOM_BUILDS),$(PROGRAMS))
# Build targets: system programs go to bin/os/, apps go to bin/apps/<name>/. # Build targets: system programs go to bin/os/, apps go to bin/apps/<name>/.
@@ -81,9 +81,9 @@ CA_CERTS := $(BINDIR)/etc/ca-certificates.crt
# Common shared assets (wallpapers, etc.) # Common shared assets (wallpapers, etc.)
COMMONKEEP := $(BINDIR)/common/.keep 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). # 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) 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 test_dl: libloader libs libhello
$(MAKE) -C src/test_dl $(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/<name>/. # Install app bundles (manifests, icons, data files) into bin/apps/<name>/.
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 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 ../scripts/install_apps.sh
@@ -358,4 +362,5 @@ clean:
$(MAKE) -C src/libloader clean $(MAKE) -C src/libloader clean
$(MAKE) -C libs/libmath clean $(MAKE) -C libs/libmath clean
$(MAKE) -C libs/libhello clean $(MAKE) -C libs/libhello clean
$(MAKE) -C src/test_dialogs clean
$(MAKE) -C src/test_dl clean $(MAKE) -C src/test_dl clean
+52
View File
@@ -17,6 +17,7 @@ namespace gui::dialogs {
inline constexpr const char* LIBRARY_PATH = "0:/os/libdialogs.lib"; inline constexpr const char* LIBRARY_PATH = "0:/os/libdialogs.lib";
inline constexpr const char* RUN_REQUEST_SYMBOL = "dialogs_run_request"; inline constexpr const char* RUN_REQUEST_SYMBOL = "dialogs_run_request";
inline constexpr const char* MESSAGE_BOX_SYMBOL = "dialogs_message_box";
enum RequestKind : uint8_t { enum RequestKind : uint8_t {
REQUEST_KIND_FILE = 1, REQUEST_KIND_FILE = 1,
@@ -33,6 +34,21 @@ enum PrintDialogMode : uint8_t {
PRINT_DIALOG_SUBMIT = 2, 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 { struct Request {
uint8_t kind; uint8_t kind;
uint8_t mode; uint8_t mode;
@@ -75,10 +91,12 @@ inline void reset_result(Result* res) {
#ifndef GUI_DIALOGS_LIBRARY_BUILD #ifndef GUI_DIALOGS_LIBRARY_BUILD
using RunRequestFn = bool (*)(const Request*, Result*); using RunRequestFn = bool (*)(const Request*, Result*);
using MessageBoxFn = uint8_t (*)(const char*, const char*, uint8_t);
struct Runtime { struct Runtime {
LibHandle* handle; LibHandle* handle;
RunRequestFn run_request; RunRequestFn run_request;
MessageBoxFn message_box;
}; };
inline Runtime g_runtime = {}; 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; 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) { 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 (out_message && out_message_len > 0) out_message[0] = '\0';
if (!req) { if (!req) {
@@ -202,6 +238,22 @@ inline bool configure_print(const char* title,
return ok; 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 #endif
} // namespace gui::dialogs } // namespace gui::dialogs
+2 -1
View File
@@ -60,6 +60,7 @@ LDFLAGS := \
--gc-sections \ --gc-sections \
--hash-style=sysv \ --hash-style=sysv \
-u dialogs_run_request \ -u dialogs_run_request \
-u dialogs_message_box \
-m elf_x86_64 \ -m elf_x86_64 \
-z max-page-size=0x1000 -z max-page-size=0x1000
@@ -84,7 +85,7 @@ LIBC_CFLAGS := \
-isystem $(PROG_INC)/libc \ -isystem $(PROG_INC)/libc \
-isystem $(GCC_INCLUDE) -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)) OBJS := $(addprefix $(OBJDIR)/,$(SRCS:.cpp=.o))
LIBC_PIC_OBJ := $(OBJDIR)/libc_pic.o LIBC_PIC_OBJ := $(OBJDIR)/libc_pic.o
+84 -11
View File
@@ -15,6 +15,7 @@
#include "main.hpp" #include "main.hpp"
#include "filedialog.hpp" #include "filedialog.hpp"
#include "printdialog.hpp" #include "printdialog.hpp"
#include "messagebox.hpp"
using namespace gui; using namespace gui;
namespace dlg = gui::dialogs; namespace dlg = gui::dialogs;
@@ -41,6 +42,8 @@ void dialog_cancel(const char* message) {
void cleanup() { void cleanup() {
if (g_app.is_print) { if (g_app.is_print) {
printdialog::cleanup(); printdialog::cleanup();
} else if (g_app.is_message) {
messagebox::cleanup();
} else { } else {
filedialog::cleanup(); 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); 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) { extern "C" bool dialogs_run_request(const dlg::Request* request, dlg::Result* out_result) {
if (!request || !out_result) return false; if (!request || !out_result) return false;
set_error_result(out_result, "dialog failed"); set_error_result(out_result, "dialog failed");
if (!g_fonts_ready) { if (!ensure_fonts(out_result)) return true;
if (!fonts::init()) {
set_error_result(out_result, "failed to initialize dialog fonts");
return true;
}
g_fonts_ready = 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"); set_error_result(out_result, "unsupported dialog request");
return true; 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); dlg::reset_result(&g_app.result);
g_app.is_print = g_app.request.kind == dlg::REQUEST_KIND_PRINT; 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; int init_w = filedialog::FILE_INIT_W;
const char* title = g_app.request.title[0] ? g_app.request.title : (g_app.is_print ? "Print" : "Open"); 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); if (g_app.is_print) printdialog::init(g_app.request);
else filedialog::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; *out_result = g_app.result;
return true; 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;
}
+2 -1
View File
@@ -16,10 +16,11 @@ struct AppState {
gui::dialogs::Result result; gui::dialogs::Result result;
bool running; bool running;
bool is_print; bool is_print;
bool is_message;
}; };
extern AppState g_app; extern AppState g_app;
void dialog_finish(const char* status, const char* path, const char* job_id, const char* message, 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); const char* printer_uri = "", const char* printer_name = "", uint32_t copies = 0);
void dialog_cancel(const char* message = ""); void dialog_cancel(const char* message = "");
+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
+40
View File
@@ -0,0 +1,40 @@
/*
* messagebox.hpp
* Message box dialog declarations
* Copyright (c) 2026 Daniel Hammer
*/
#pragma once
#include <montauk/syscall.h>
#include <gui/gui.hpp>
#include <gui/dialogs.hpp>
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
+76
View File
@@ -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)
+15
View File
@@ -0,0 +1,15 @@
/*
* main.cpp
* Minimal libdialogs message box test
* Copyright (c) 2026 Daniel Hammer
*/
#include <montauk/syscall.h>
#include <gui/dialogs.hpp>
extern "C" void _start() {
gui::dialogs::message_box("Test Dialogs",
"Hello from libdialogs::message_box.",
gui::dialogs::MESSAGE_BOX_OK);
montauk::exit(0);
}
-1
View File
@@ -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) -c main.cpp -o main.o
$(CXX) $(CXXFLAGS) $(LDFLAGS) main.o $(LIBDIR)/liblibloader.a ../../lib/libc/liblibc.a -o $@ $(CXX) $(CXXFLAGS) $(LDFLAGS) main.o $(LIBDIR)/liblibloader.a ../../lib/libc/liblibc.a -o $@
rm -f main.o rm -f main.o
rm -f $(LIBDIR)/liblibloader.a
clean: clean:
rm -f $(BINDIR)/test_dl.elf rm -f $(BINDIR)/test_dl.elf