From df204cc15a6fb311c67dc87e101479c3ee2ccb13 Mon Sep 17 00:00:00 2001 From: Daniel Hammer Date: Fri, 5 Jun 2026 19:29:21 +0200 Subject: [PATCH] feat: libdialogs GUI tweaks --- kernel/src/Api/BuildNo.hpp | 2 +- programs/src/dialogs/filedialog.cpp | 239 +++++++++++++++------------- 2 files changed, 132 insertions(+), 109 deletions(-) diff --git a/kernel/src/Api/BuildNo.hpp b/kernel/src/Api/BuildNo.hpp index 87a4b36..e840156 100644 --- a/kernel/src/Api/BuildNo.hpp +++ b/kernel/src/Api/BuildNo.hpp @@ -12,4 +12,4 @@ #pragma once -#define MONTAUK_BUILD_NUMBER 49 +#define MONTAUK_BUILD_NUMBER 52 diff --git a/programs/src/dialogs/filedialog.cpp b/programs/src/dialogs/filedialog.cpp index b8d9b25..1211da4 100644 --- a/programs/src/dialogs/filedialog.cpp +++ b/programs/src/dialogs/filedialog.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include extern "C" { @@ -91,15 +92,12 @@ struct FileDialogState { FocusField focus; char path_buf[256]; - int path_len; - int path_cursor; + mtk::TextInputState path_input; char filename_buf[128]; - int filename_len; - int filename_cursor; + mtk::TextInputState filename_input; char pending_select[64]; - char error[160]; int mouse_x; int mouse_y; @@ -107,6 +105,15 @@ struct FileDialogState { FileDialogState g_file; +mtk::Theme file_theme() { + return mtk::make_theme(); +} + +// File names may not contain path separators. +bool filename_char_filter(char ch, void*) { + return ch != '/' && ch != '\\'; +} + void safe_copy(char* dst, int dst_len, const char* src) { gui::dialogs::safe_copy(dst, dst_len, src); } @@ -241,18 +248,36 @@ void fit_text_end(const char* src, char* out, int out_len, int max_px) { safe_copy(out, out_len, src); } -void draw_input(Canvas& c, const Rect& rect, const char* text, bool focused, int cursor_pos) { +// Path bar: the original plain white/square input look, but rendering the MTK +// text-input state so selection highlighting (and the right-click menu, drawn +// separately) keep working. +void draw_path_input(Canvas& c, const Rect& rect, const char* text, + const mtk::TextInputState& input, bool focused, const mtk::Theme& theme) { c.fill_rect(rect.x, rect.y, rect.w, rect.h, colors::WHITE); c.rect(rect.x, rect.y, rect.w, rect.h, focused ? colors::ACCENT : colors::BORDER); - int ty = rect.y + (rect.h - system_font_height()) / 2; - c.text(rect.x + 6, ty, text, colors::TEXT_COLOR); + + int fh = system_font_height(); + int tx = rect.x + 8; + int ty = rect.y + (rect.h - fh) / 2; + int len = mtk::text_input_len(text); + + if (focused && input.cursor != input.selection_anchor) { + int cur = mtk::text_input_clamp_cursor(input.cursor, len); + int anchor = mtk::text_input_clamp_cursor(input.selection_anchor, len); + int start = cur < anchor ? cur : anchor; + int end = cur > anchor ? cur : anchor; + int sx = tx + mtk::text_input_prefix_width(text, start, false); + int ex = tx + mtk::text_input_prefix_width(text, end, false); + int clip_x = gui_max(sx, rect.x + 2); + int clip_r = gui_min(ex, rect.x + rect.w - 2); + if (clip_r > clip_x) + c.fill_rect(clip_x, rect.y + 3, clip_r - clip_x, gui_max(rect.h - 6, 1), theme.text_selection); + } + + c.text(tx, ty, text, colors::TEXT_COLOR); + if (focused) { - char prefix[256]; - int plen = cursor_pos; - if (plen > 255) plen = 255; - for (int i = 0; i < plen; i++) prefix[i] = text[i]; - prefix[plen] = '\0'; - int cx = rect.x + 6 + text_width(prefix); + int cx = tx + mtk::text_input_prefix_width(text, gui_clamp(input.cursor, 0, len), false); c.vline(cx, rect.y + 4, rect.h - 8, colors::ACCENT); } } @@ -308,23 +333,16 @@ FileDialogLayout file_layout(const FileDialogState* st) { void file_sync_path_buffer(FileDialogState* st) { if (st->at_drives_root) { st->path_buf[0] = '\0'; - st->path_len = 0; - st->path_cursor = 0; + mtk::text_input_reset(st->path_input, 0); return; } safe_copy(st->path_buf, sizeof(st->path_buf), st->current_path); - st->path_len = montauk::slen(st->path_buf); - st->path_cursor = st->path_len; + mtk::text_input_reset(st->path_input, montauk::slen(st->path_buf)); } void file_set_filename(FileDialogState* st, const char* name) { safe_copy(st->filename_buf, sizeof(st->filename_buf), name); - st->filename_len = montauk::slen(st->filename_buf); - st->filename_cursor = st->filename_len; -} - -void file_set_error(FileDialogState* st, const char* msg) { - safe_copy(st->error, sizeof(st->error), msg); + mtk::text_input_reset(st->filename_input, montauk::slen(st->filename_buf)); } int special_folder_index(const char* name) { @@ -624,10 +642,7 @@ void file_navigate_into(FileDialogState* st, int idx) { } bool file_accept_open(FileDialogState* st) { - if (st->selected < 0 || st->selected >= st->entry_count) { - file_set_error(st, "Select a file to open"); - return false; - } + if (st->selected < 0 || st->selected >= st->entry_count) return false; if (st->is_dir[st->selected]) { file_navigate_into(st, st->selected); return false; @@ -640,14 +655,8 @@ bool file_accept_open(FileDialogState* st) { } bool file_accept_save(FileDialogState* st) { - if (st->at_drives_root) { - file_set_error(st, "Choose a folder before saving"); - return false; - } - if (!st->filename_buf[0]) { - file_set_error(st, "Enter a file name"); - return false; - } + if (st->at_drives_root) return false; + if (!st->filename_buf[0]) return false; char full[512]; build_fullpath(full, sizeof(full), st->current_path, st->filename_buf); @@ -768,8 +777,6 @@ void file_open_path(FileDialogState* st, const char* path) { } return; } - - file_set_error(st, "Path does not exist"); } void init_file_dialog(const gui::dialogs::Request& req) { @@ -930,6 +937,8 @@ void draw_file_dialog() { Canvas c = g_app.win.canvas(); c.fill(colors::WINDOW_BG); + mtk::Theme theme = file_theme(); + bool save_mode = g_app.request.mode == gui::dialogs::FILE_DIALOG_SAVE; FileDialogLayout lo = file_layout(st); c.fill_rect(0, 0, c.w, TOOLBAR_H, Color::from_rgb(0xF5, 0xF5, 0xF5)); @@ -949,10 +958,10 @@ void draw_file_dialog() { } c.hline(0, TOOLBAR_H - 1, c.w, colors::BORDER); - char path_label[256]; - if (st->at_drives_root && st->focus != FOCUS_PATH) safe_copy(path_label, sizeof(path_label), "Computer"); - else safe_copy(path_label, sizeof(path_label), st->path_buf); - draw_input(c, lo.path_rect, path_label, st->focus == FOCUS_PATH, st->path_cursor); + bool path_focused = st->focus == FOCUS_PATH; + const char* path_text = (st->at_drives_root && !path_focused && !st->path_buf[0]) + ? "Computer" : st->path_buf; + draw_path_input(c, lo.path_rect, path_text, st->path_input, path_focused, theme); if (st->grid_view) draw_grid_entries(c, st, lo); else draw_list_entries(c, st, lo); @@ -963,10 +972,12 @@ void draw_file_dialog() { c.hline(0, lo.footer_rect.y, c.w, colors::BORDER); bool confirm_enabled = false; - const char* confirm_label = (g_app.request.mode == gui::dialogs::FILE_DIALOG_SAVE) ? "Save" : "Open"; - if (g_app.request.mode == gui::dialogs::FILE_DIALOG_SAVE) { + const char* confirm_label = save_mode ? "Save" : "Open"; + if (save_mode) { c.text(12, lo.footer_rect.y + 18, "File name", Color::from_rgb(0x66, 0x66, 0x66)); - draw_input(c, lo.name_rect, st->filename_buf, st->focus == FOCUS_NAME, st->filename_cursor); + mtk::draw_text_field(c, lo.name_rect, st->filename_buf, st->filename_input.cursor, + st->focus == FOCUS_NAME, false, theme, + st->filename_input.selection_anchor); confirm_enabled = st->filename_buf[0] != '\0' && !st->at_drives_root; } else { char selection[256] = {}; @@ -990,38 +1001,19 @@ void draw_file_dialog() { confirm_enabled = st->selected >= 0; } - if (st->error[0]) { - char err[160]; - fit_text_end(st->error, err, sizeof(err), lo.cancel_btn.x - 16); - c.text(12, lo.footer_rect.y + lo.footer_rect.h - system_font_height() - 12, err, Color::from_rgb(0xC0, 0x33, 0x33)); - } - draw_action_button(c, lo.cancel_btn, "Cancel", true, lo.cancel_btn.contains(st->mouse_x, st->mouse_y), false); draw_action_button(c, lo.confirm_btn, confirm_label, confirm_enabled, lo.confirm_btn.contains(st->mouse_x, st->mouse_y), true); -} -void text_insert(char* buf, int* len, int* cursor, int max_len, char ch) { - if (!buf || !len || !cursor || *len >= max_len - 1) return; - for (int i = *len; i > *cursor; i--) buf[i] = buf[i - 1]; - buf[*cursor] = ch; - (*len)++; - (*cursor)++; - buf[*len] = '\0'; -} - -void text_backspace(char* buf, int* len, int* cursor) { - if (!buf || !len || !cursor || *cursor <= 0 || *len <= 0) return; - for (int i = *cursor - 1; i < *len - 1; i++) buf[i] = buf[i + 1]; - (*len)--; - (*cursor)--; - buf[*len] = '\0'; -} - -void text_delete(char* buf, int* len, int* cursor) { - if (!buf || !len || !cursor || *cursor >= *len || *len <= 0) return; - for (int i = *cursor; i < *len - 1; i++) buf[i] = buf[i + 1]; - (*len)--; - buf[*len] = '\0'; + // Text-field context menus draw last so they overlay every other element. + mtk::draw_text_input_context_menu( + c, st->path_input, theme, + mtk::text_input_has_selection(st->path_input, st->path_buf, (int)sizeof(st->path_buf))); + if (save_mode) { + mtk::draw_text_input_context_menu( + c, st->filename_input, theme, + mtk::text_input_has_selection(st->filename_input, st->filename_buf, + (int)sizeof(st->filename_buf))); + } } int file_entry_at(FileDialogState* st, int mx, int my) { @@ -1047,6 +1039,14 @@ int file_entry_at(FileDialogState* st, int mx, int my) { return -1; } +int file_field_mouse(mtk::TextInputState& input, const Rect& rect, char* buf, int cap, + const MouseEvent& me, bool focused, mtk::TextInputCharFilter filter) { + return mtk::text_input_handle_mouse(input, rect, buf, cap, + me.x, me.y, me.buttons, me.prev_buttons, + g_app.win.width, g_app.win.height, + focused, false, filter); +} + void handle_file_mouse(const Montauk::WinEvent& ev) { FileDialogState* st = &g_file; st->mouse_x = ev.mouse.x; @@ -1054,11 +1054,43 @@ void handle_file_mouse(const Montauk::WinEvent& ev) { FileDialogLayout lo = file_layout(st); MouseEvent me = {ev.mouse.x, ev.mouse.y, ev.mouse.buttons, ev.mouse.prev_buttons, ev.mouse.scroll}; + bool save_mode = g_app.request.mode == gui::dialogs::FILE_DIALOG_SAVE; + + // While a text field owns the interaction (an open context menu or an + // in-progress drag-selection), route all mouse input to it first. + if (st->path_input.context.open || st->path_input.dragging) { + if (file_field_mouse(st->path_input, lo.path_rect, st->path_buf, (int)sizeof(st->path_buf), + me, st->focus == FOCUS_PATH, nullptr) != mtk::TEXT_INPUT_NONE) + return; + } + if (save_mode && (st->filename_input.context.open || st->filename_input.dragging)) { + if (file_field_mouse(st->filename_input, lo.name_rect, st->filename_buf, + (int)sizeof(st->filename_buf), me, st->focus == FOCUS_NAME, + filename_char_filter) != mtk::TEXT_INPUT_NONE) + return; + } + st->scrollbar.handle_mouse(me); - if (me.left_pressed()) { - st->error[0] = '\0'; + // Right-click inside a field opens its Cut/Copy/Paste/Delete menu. + if (me.right_pressed()) { + if (lo.path_rect.contains(me.x, me.y)) { + st->focus = FOCUS_PATH; + if (save_mode) mtk::context_menu_close(st->filename_input.context); + file_field_mouse(st->path_input, lo.path_rect, st->path_buf, (int)sizeof(st->path_buf), + me, true, nullptr); + return; + } + if (save_mode && lo.name_rect.contains(me.x, me.y)) { + st->focus = FOCUS_NAME; + mtk::context_menu_close(st->path_input.context); + file_field_mouse(st->filename_input, lo.name_rect, st->filename_buf, + (int)sizeof(st->filename_buf), me, true, filename_char_filter); + return; + } + } + if (me.left_pressed()) { if (lo.back_btn.contains(me.x, me.y)) { file_go_back(st); return; } if (lo.forward_btn.contains(me.x, me.y)) { file_go_forward(st); return; } if (lo.up_btn.contains(me.x, me.y)) { file_go_up(st); return; } @@ -1070,19 +1102,29 @@ void handle_file_mouse(const Montauk::WinEvent& ev) { } if (lo.cancel_btn.contains(me.x, me.y)) { dialog_cancel(); return; } if (lo.confirm_btn.contains(me.x, me.y)) { - if (g_app.request.mode == gui::dialogs::FILE_DIALOG_SAVE) file_accept_save(st); + if (save_mode) file_accept_save(st); else file_accept_open(st); return; } if (lo.path_rect.contains(me.x, me.y)) { st->focus = FOCUS_PATH; + if (save_mode) mtk::context_menu_close(st->filename_input.context); + file_field_mouse(st->path_input, lo.path_rect, st->path_buf, (int)sizeof(st->path_buf), + me, true, nullptr); return; } - if (g_app.request.mode == gui::dialogs::FILE_DIALOG_SAVE && lo.name_rect.contains(me.x, me.y)) { + if (save_mode && lo.name_rect.contains(me.x, me.y)) { st->focus = FOCUS_NAME; + mtk::context_menu_close(st->path_input.context); + file_field_mouse(st->filename_input, lo.name_rect, st->filename_buf, + (int)sizeof(st->filename_buf), me, true, filename_char_filter); return; } + // Clicking elsewhere dismisses any open field menu and leaves the fields. + mtk::context_menu_close(st->path_input.context); + if (save_mode) mtk::context_menu_close(st->filename_input.context); + int idx = file_entry_at(st, me.x, me.y); if (idx >= 0) { uint64_t now = montauk::get_milliseconds(); @@ -1096,7 +1138,7 @@ void handle_file_mouse(const Montauk::WinEvent& ev) { st->selected = idx; st->last_click_item = idx; st->last_click_time = now; - if (g_app.request.mode == gui::dialogs::FILE_DIALOG_SAVE && !st->is_dir[idx]) { + if (save_mode && !st->is_dir[idx]) { file_set_filename(st, st->entry_names[idx]); } } @@ -1121,9 +1163,13 @@ void handle_file_key(const Montauk::KeyEvent& key) { if (!key.pressed) return; FileDialogState* st = &g_file; - st->error[0] = '\0'; + + bool save_mode = g_app.request.mode == gui::dialogs::FILE_DIALOG_SAVE; if (key.scancode == 0x01) { + // Escape closes an open field menu first, otherwise cancels the dialog. + if (st->path_input.context.open) { mtk::context_menu_close(st->path_input.context); return; } + if (st->filename_input.context.open) { mtk::context_menu_close(st->filename_input.context); return; } dialog_cancel(); return; } @@ -1134,42 +1180,19 @@ void handle_file_key(const Montauk::KeyEvent& key) { st->focus = FOCUS_LIST; return; } - if (key.ascii == '\b' || key.scancode == 0x0E) { - text_backspace(st->path_buf, &st->path_len, &st->path_cursor); - return; - } - if (key.scancode == 0x53) { text_delete(st->path_buf, &st->path_len, &st->path_cursor); return; } - if (key.scancode == 0x4B) { if (st->path_cursor > 0) st->path_cursor--; return; } - if (key.scancode == 0x4D) { if (st->path_cursor < st->path_len) st->path_cursor++; return; } - if (key.scancode == 0x47) { st->path_cursor = 0; return; } - if (key.scancode == 0x4F) { st->path_cursor = st->path_len; return; } - if (key.ascii >= 32 && key.ascii < 127) { - text_insert(st->path_buf, &st->path_len, &st->path_cursor, (int)sizeof(st->path_buf), key.ascii); - return; - } + mtk::text_input_key(st->path_input, st->path_buf, (int)sizeof(st->path_buf), key); return; } - if (st->focus == FOCUS_NAME && g_app.request.mode == gui::dialogs::FILE_DIALOG_SAVE) { + if (st->focus == FOCUS_NAME && save_mode) { if (key.ascii == '\n' || key.ascii == '\r') { file_accept_save(st); return; } - if (key.ascii == '\b' || key.scancode == 0x0E) { - text_backspace(st->filename_buf, &st->filename_len, &st->filename_cursor); - return; - } - if (key.scancode == 0x53) { text_delete(st->filename_buf, &st->filename_len, &st->filename_cursor); return; } - if (key.scancode == 0x4B) { if (st->filename_cursor > 0) st->filename_cursor--; return; } - if (key.scancode == 0x4D) { if (st->filename_cursor < st->filename_len) st->filename_cursor++; return; } - if (key.scancode == 0x47) { st->filename_cursor = 0; return; } - if (key.scancode == 0x4F) { st->filename_cursor = st->filename_len; return; } - if (key.ascii >= 32 && key.ascii < 127 && key.ascii != '/' && key.ascii != '\\') { - text_insert(st->filename_buf, &st->filename_len, &st->filename_cursor, (int)sizeof(st->filename_buf), key.ascii); - return; - } + mtk::text_input_key(st->filename_input, st->filename_buf, (int)sizeof(st->filename_buf), + key, filename_char_filter); return; } if (key.ascii == '\n' || key.ascii == '\r') { - if (g_app.request.mode == gui::dialogs::FILE_DIALOG_SAVE) file_accept_save(st); + if (save_mode) file_accept_save(st); else file_accept_open(st); return; }