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
+2 -1
View File
@@ -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
+84 -11
View File
@@ -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;
}
+2 -1
View File
@@ -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 = "");
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) $(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