feat: libdialogs GUI tweaks

This commit is contained in:
2026-06-05 19:29:21 +02:00
parent 6b44b3ccc9
commit df204cc15a
2 changed files with 132 additions and 109 deletions
+131 -108
View File
@@ -10,6 +10,7 @@
#include <gui/canvas.hpp>
#include <gui/mtk/theme.hpp>
#include <gui/mtk/settings.hpp>
#include <gui/mtk/widgets.hpp>
#include <montauk/user.h>
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;
}