/* * filedialog.cpp * File open/save dialog implementation * Copyright (c) 2026 Daniel Hammer */ #include "filedialog.hpp" #include "main.hpp" #include #include #include extern "C" { #include } namespace { using namespace gui; using namespace filedialog; constexpr const char* SPECIAL_FOLDER_NAMES[] = { "Documents", "Desktop", "Music", "Videos", "Pictures", "Downloads" }; constexpr const char* SPECIAL_FOLDER_ICONS[] = { "folder-blue-documents.svg", "folder-blue-desktop.svg", "folder-blue-music.svg", "folder-blue-videos.svg", "folder-blue-pictures.svg", "folder-blue-downloads.svg" }; constexpr int SPECIAL_FOLDER_COUNT = 6; enum EntryType : uint8_t { ENTRY_FILE = 0, ENTRY_DIR = 1, ENTRY_DRIVE = 3, ENTRY_HOME = 4, ENTRY_SPECIAL = 7, }; enum FocusField : uint8_t { FOCUS_LIST = 0, FOCUS_PATH = 1, FOCUS_NAME = 2, }; struct FileDialogIcons { SvgIcon icon_go_back; SvgIcon icon_go_forward; SvgIcon icon_go_up; SvgIcon icon_home; SvgIcon icon_folder; SvgIcon icon_folder_lg; SvgIcon icon_file; SvgIcon icon_file_lg; SvgIcon icon_drive; SvgIcon icon_drive_lg; SvgIcon icon_home_folder; SvgIcon icon_home_folder_lg; SvgIcon icon_special[ SPECIAL_FOLDER_COUNT ]; SvgIcon icon_special_lg[ SPECIAL_FOLDER_COUNT ]; }; struct FileDialogState { FileDialogIcons icons; char home_dir[256]; char current_path[256]; char history[MAX_HISTORY][256]; int history_pos; int history_count; char entry_names[MAX_ENTRIES][64]; uint8_t entry_types[MAX_ENTRIES]; int entry_sizes[MAX_ENTRIES]; bool is_dir[MAX_ENTRIES]; int drive_indices[MAX_ENTRIES]; int entry_count; int selected; int last_click_item; uint64_t last_click_time; Scrollbar scrollbar; bool grid_view; bool at_drives_root; FocusField focus; char path_buf[256]; int path_len; int path_cursor; char filename_buf[128]; int filename_len; int filename_cursor; char pending_select[64]; char error[160]; int mouse_x; int mouse_y; }; FileDialogState g_file; void safe_copy(char* dst, int dst_len, const char* src) { gui::dialogs::safe_copy(dst, dst_len, src); } int str_compare_ci(const char* a, const char* b) { while (*a && *b) { char ca = (*a >= 'A' && *a <= 'Z') ? (*a + 32) : *a; char cb = (*b >= 'A' && *b <= 'Z') ? (*b + 32) : *b; if (ca != cb) return ca - cb; a++; b++; } return (unsigned char)*a - (unsigned char)*b; } bool str_ends_with(const char* s, const char* suffix) { int slen = montauk::slen(s); int suflen = montauk::slen(suffix); if (suflen > slen) return false; for (int i = 0; i < suflen; i++) { char sc = s[slen - suflen + i]; char ec = suffix[i]; if (sc >= 'A' && sc <= 'Z') sc += 32; if (ec >= 'A' && ec <= 'Z') ec += 32; if (sc != ec) return false; } return true; } void str_append(char* dst, const char* src, int max_len) { int len = montauk::slen(dst); int i = 0; while (src[i] && len < max_len - 1) { dst[len++] = src[i++]; } dst[len] = '\0'; } const char* path_basename(const char* path) { const char* last = path; for (const char* p = path; *p; p++) { if (*p == '/' && *(p + 1)) last = p + 1; } return last; } void build_fullpath(char* out, int out_len, const char* dir, const char* name) { safe_copy(out, out_len, dir); int len = montauk::slen(out); if (len > 0 && out[len - 1] != '/') str_append(out, "/", out_len); str_append(out, name, out_len); } void split_parent_path(const char* path, char* out_dir, int out_dir_len, char* out_name, int out_name_len) { safe_copy(out_dir, out_dir_len, ""); safe_copy(out_name, out_name_len, ""); if (!path || !path[0]) return; int last_slash = -1; for (int i = 0; path[i]; i++) if (path[i] == '/') last_slash = i; if (last_slash < 0) { safe_copy(out_name, out_name_len, path); return; } int dir_len = last_slash + 1; if (dir_len >= out_dir_len) dir_len = out_dir_len - 1; montauk::memcpy(out_dir, path, (uint64_t)dir_len); out_dir[dir_len] = '\0'; safe_copy(out_name, out_name_len, path + last_slash + 1); } bool is_dir_path(const char* path) { if (!path || !path[0]) return false; const char* names[1]; return montauk::readdir(path, names, 1) >= 0; } bool path_exists_via_open(const char* path) { if (!path || !path[0]) return false; int fd = montauk::open(path); if (fd < 0) return false; montauk::close(fd); return true; } void format_size(char* buf, int buf_len, int size) { if (size < 1024) snprintf(buf, buf_len, "%d B", size); else if (size < 1024 * 1024) { int kb = size / 1024; int frac = ((size % 1024) * 10) / 1024; if (kb < 10) snprintf(buf, buf_len, "%d.%d KB", kb, frac); else snprintf(buf, buf_len, "%d KB", kb); } else { int mb = size / (1024 * 1024); int frac = ((size % (1024 * 1024)) * 10) / (1024 * 1024); if (mb < 10) snprintf(buf, buf_len, "%d.%d MB", mb, frac); else snprintf(buf, buf_len, "%d MB", mb); } } void fit_text_end(const char* src, char* out, int out_len, int max_px) { if (!src || !src[0]) { safe_copy(out, out_len, ""); return; } if (text_width(src) <= max_px) { safe_copy(out, out_len, src); return; } int slen = montauk::slen(src); int keep = slen; while (keep > 1) { char candidate[256]; candidate[0] = '.'; candidate[1] = '.'; int pos = 2; const char* tail = src + (slen - keep); while (*tail && pos < (int)sizeof(candidate) - 1) candidate[pos++] = *tail++; candidate[pos] = '\0'; if (text_width(candidate) <= max_px) { safe_copy(out, out_len, candidate); return; } keep--; } safe_copy(out, out_len, src); } void draw_input(Canvas& c, const Rect& rect, const char* text, bool focused, int cursor_pos) { 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); 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); c.vline(cx, rect.y + 4, rect.h - 8, colors::ACCENT); } } void draw_action_button(Canvas& c, const Rect& rect, const char* label, bool enabled, bool hovered, bool primary) { Color bg = primary ? colors::ACCENT : Color::from_rgb(0xE8, 0xE8, 0xE8); Color fg = primary ? colors::WHITE : colors::TEXT_COLOR; if (!enabled) { bg = Color::from_rgb(0xF0, 0xF0, 0xF0); fg = Color::from_rgb(0x9A, 0x9A, 0x9A); } else if (hovered) { bg = primary ? Color::from_rgb(0x2B, 0x6B, 0xE0) : Color::from_rgb(0xDC, 0xDC, 0xDC); } c.fill_rounded_rect(rect.x, rect.y, rect.w, rect.h, 4, bg); int tx = rect.x + (rect.w - text_width(label)) / 2; int ty = rect.y + (rect.h - system_font_height()) / 2; c.text(tx, ty, label, fg); } void draw_scrollbar(Canvas& c, const Scrollbar& sb) { if (sb.content_height <= sb.view_height) return; c.fill_rect(sb.bounds.x, sb.bounds.y, sb.bounds.w, sb.bounds.h, sb.bg); int th = sb.thumb_height(); int ty = sb.thumb_y(); Color thumb = (sb.hovered || sb.dragging) ? sb.hover_fg : sb.fg; c.fill_rounded_rect(sb.bounds.x + 1, ty, sb.bounds.w - 2, th, 3, thumb); } FileDialogLayout file_layout(const FileDialogState* st) { FileDialogLayout lo = {}; int footer_h = (g_app.request.mode == gui::dialogs::FILE_DIALOG_SAVE) ? FOOTER_H_SAVE : FOOTER_H_OPEN; lo.back_btn = {4, 4, 24, 24}; lo.forward_btn = {32, 4, 24, 24}; lo.up_btn = {60, 4, 24, 24}; lo.home_btn = {88, 4, 24, 24}; lo.view_btn = {120, 4, 24, 24}; lo.path_rect = {8, TOOLBAR_H + 4, g_app.win.width - 16, PATHBAR_H - 8}; lo.content_rect = {0, TOOLBAR_H + PATHBAR_H, g_app.win.width, g_app.win.height - TOOLBAR_H - PATHBAR_H - footer_h}; lo.footer_rect = {0, g_app.win.height - footer_h, g_app.win.width, footer_h}; lo.list_rect = lo.content_rect; int btn_y = (g_app.request.mode == gui::dialogs::FILE_DIALOG_SAVE) ? lo.footer_rect.y + 12 : lo.footer_rect.y + (footer_h - BUTTON_H) / 2; lo.confirm_btn = {g_app.win.width - BUTTON_W - 8, btn_y, BUTTON_W, BUTTON_H}; lo.cancel_btn = {lo.confirm_btn.x - BUTTON_W - 8, btn_y, BUTTON_W, BUTTON_H}; lo.name_rect = {88, lo.footer_rect.y + 12, lo.cancel_btn.x - 100, 30}; if (lo.name_rect.w < 120) lo.name_rect.w = 120; (void)st; return lo; } 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; 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; } 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); } int special_folder_index(const char* name) { for (int i = 0; i < SPECIAL_FOLDER_COUNT; i++) { if (montauk::streq(name, SPECIAL_FOLDER_NAMES[i])) return i; } return -1; } void load_file_icons(FileDialogState* st) { Color def = colors::ICON_COLOR; st->icons.icon_go_back = svg_load("0:/icons/go-previous-symbolic.svg", 16, 16, def); st->icons.icon_go_forward = svg_load("0:/icons/go-next-symbolic.svg", 16, 16, def); st->icons.icon_go_up = svg_load("0:/icons/go-up-symbolic.svg", 16, 16, def); st->icons.icon_home = svg_load("0:/icons/user-home.svg", 16, 16, def); st->icons.icon_folder = svg_load("0:/icons/folder.svg", 16, 16, def); st->icons.icon_folder_lg = svg_load("0:/icons/folder.svg", 48, 48, def); st->icons.icon_file = svg_load("0:/icons/text-x-generic.svg", 16, 16, def); st->icons.icon_file_lg = svg_load("0:/icons/text-x-generic.svg", 48, 48, def); st->icons.icon_drive = svg_load("0:/icons/drive-harddisk.svg", 16, 16, def); st->icons.icon_drive_lg = svg_load("0:/icons/drive-harddisk.svg", 48, 48, def); st->icons.icon_home_folder = svg_load("0:/icons/folder-blue-home.svg", 16, 16, def); st->icons.icon_home_folder_lg = svg_load("0:/icons/folder-blue-home.svg", 48, 48, def); for (int i = 0; i < SPECIAL_FOLDER_COUNT; i++) { char path[128]; snprintf(path, sizeof(path), "0:/icons/%s", SPECIAL_FOLDER_ICONS[i]); st->icons.icon_special[i] = svg_load(path, 16, 16, def); st->icons.icon_special_lg[i] = svg_load(path, 48, 48, def); } } void free_icon(SvgIcon* icon) { if (icon && icon->pixels) { svg_free(*icon); icon->pixels = nullptr; } } void free_file_icons(FileDialogState* st) { free_icon(&st->icons.icon_go_back); free_icon(&st->icons.icon_go_forward); free_icon(&st->icons.icon_go_up); free_icon(&st->icons.icon_home); free_icon(&st->icons.icon_folder); free_icon(&st->icons.icon_folder_lg); free_icon(&st->icons.icon_file); free_icon(&st->icons.icon_file_lg); free_icon(&st->icons.icon_drive); free_icon(&st->icons.icon_drive_lg); free_icon(&st->icons.icon_home_folder); free_icon(&st->icons.icon_home_folder_lg); for (int i = 0; i < SPECIAL_FOLDER_COUNT; i++) { free_icon(&st->icons.icon_special[i]); free_icon(&st->icons.icon_special_lg[i]); } } void file_push_history(FileDialogState* st) { if (st->history_count > 0 && st->history_pos >= 0) { if (montauk::streq(st->history[st->history_pos], st->current_path)) return; } st->history_pos++; if (st->history_pos >= MAX_HISTORY) st->history_pos = MAX_HISTORY - 1; safe_copy(st->history[st->history_pos], sizeof(st->history[st->history_pos]), st->current_path); st->history_count = st->history_pos + 1; } void file_apply_pending_select(FileDialogState* st) { st->selected = -1; if (!st->pending_select[0]) return; for (int i = 0; i < st->entry_count; i++) { if (montauk::streq(st->entry_names[i], st->pending_select)) { st->selected = i; break; } } st->pending_select[0] = '\0'; } void file_read_drives(FileDialogState* st) { st->entry_count = 0; st->at_drives_root = true; st->current_path[0] = '\0'; if (st->home_dir[0] != '\0') { int i = st->entry_count++; safe_copy(st->entry_names[i], sizeof(st->entry_names[i]), "Home"); st->entry_types[i] = ENTRY_HOME; st->entry_sizes[i] = 0; st->is_dir[i] = true; st->drive_indices[i] = -1; } if (st->home_dir[0] != '\0') { for (int sf = 0; sf < SPECIAL_FOLDER_COUNT && st->entry_count < MAX_ENTRIES; sf++) { char probe[256]; safe_copy(probe, sizeof(probe), st->home_dir); int plen = montauk::slen(probe); if (plen > 0 && probe[plen - 1] != '/') str_append(probe, "/", sizeof(probe)); str_append(probe, SPECIAL_FOLDER_NAMES[sf], sizeof(probe)); if (path_exists_via_open(probe)) { int i = st->entry_count++; safe_copy(st->entry_names[i], sizeof(st->entry_names[i]), SPECIAL_FOLDER_NAMES[sf]); st->entry_types[i] = ENTRY_SPECIAL; st->entry_sizes[i] = 0; st->is_dir[i] = true; st->drive_indices[i] = sf; } } } int drives[MAX_DRIVES]; int drive_count = montauk::drivelist(drives, MAX_DRIVES); for (int di = 0; di < drive_count && st->entry_count < MAX_ENTRIES; di++) { int d = drives[di]; int i = st->entry_count++; char label[64]; if (d < 10) snprintf(label, sizeof(label), "Drive %d:/", d); else snprintf(label, sizeof(label), "Drive %d:/", d); safe_copy(st->entry_names[i], sizeof(st->entry_names[i]), label); st->entry_types[i] = ENTRY_DRIVE; st->entry_sizes[i] = 0; st->is_dir[i] = true; st->drive_indices[i] = d; } st->scrollbar.scroll_offset = 0; st->last_click_item = -1; st->last_click_time = 0; file_sync_path_buffer(st); file_apply_pending_select(st); } void file_read_dir(FileDialogState* st) { if (!st->current_path[0] || !is_dir_path(st->current_path)) { file_read_drives(st); return; } st->at_drives_root = false; const char* names[MAX_ENTRIES]; st->entry_count = montauk::readdir(st->current_path, names, MAX_ENTRIES); if (st->entry_count < 0) st->entry_count = 0; const char* after_drive = st->current_path; for (int k = 0; after_drive[k]; k++) { if (after_drive[k] == ':' && after_drive[k + 1] == '/') { after_drive += k + 2; break; } } char prefix[256] = {}; int prefix_len = 0; if (after_drive[0] != '\0') { safe_copy(prefix, sizeof(prefix), after_drive); prefix_len = montauk::slen(prefix); if (prefix_len > 0 && prefix[prefix_len - 1] != '/') { prefix[prefix_len++] = '/'; prefix[prefix_len] = '\0'; } } for (int i = 0; i < st->entry_count; i++) { const char* raw = names[i]; if (prefix_len > 0) { bool match = true; for (int k = 0; k < prefix_len; k++) { if (raw[k] != prefix[k]) { match = false; break; } } if (match) raw += prefix_len; } safe_copy(st->entry_names[i], sizeof(st->entry_names[i]), raw); int len = montauk::slen(st->entry_names[i]); if (len > 0 && st->entry_names[i][len - 1] == '/') { st->is_dir[i] = true; st->entry_names[i][len - 1] = '\0'; } else { st->is_dir[i] = false; } st->entry_types[i] = st->is_dir[i] ? ENTRY_DIR : ENTRY_FILE; st->entry_sizes[i] = 0; st->drive_indices[i] = -1; if (!st->is_dir[i]) { char full[512]; build_fullpath(full, sizeof(full), st->current_path, st->entry_names[i]); int fd = montauk::open(full); if (fd >= 0) { st->entry_sizes[i] = (int)montauk::getsize(fd); montauk::close(fd); } } } for (int i = 1; i < st->entry_count; i++) { char tmp_name[64]; uint8_t tmp_type = st->entry_types[i]; int tmp_size = st->entry_sizes[i]; bool tmp_isdir = st->is_dir[i]; safe_copy(tmp_name, sizeof(tmp_name), st->entry_names[i]); int j = i - 1; while (j >= 0) { bool swap = false; if (tmp_isdir && !st->is_dir[j]) swap = true; else if (tmp_isdir == st->is_dir[j] && str_compare_ci(tmp_name, st->entry_names[j]) < 0) swap = true; if (!swap) break; safe_copy(st->entry_names[j + 1], sizeof(st->entry_names[j + 1]), st->entry_names[j]); st->entry_types[j + 1] = st->entry_types[j]; st->entry_sizes[j + 1] = st->entry_sizes[j]; st->is_dir[j + 1] = st->is_dir[j]; st->drive_indices[j + 1] = st->drive_indices[j]; j--; safe_copy(st->entry_names[j + 1], sizeof(st->entry_names[j + 1]), tmp_name); st->entry_types[j + 1] = tmp_type; st->entry_sizes[j + 1] = tmp_size; st->is_dir[j + 1] = tmp_isdir; st->drive_indices[j + 1] = -1; } } st->scrollbar.scroll_offset = 0; st->last_click_item = -1; st->last_click_time = 0; file_sync_path_buffer(st); file_apply_pending_select(st); } void file_ensure_selected_visible(FileDialogState* st) { if (st->selected < 0 || st->selected >= st->entry_count) return; FileDialogLayout lo = file_layout(st); if (st->grid_view) { int cols = (lo.content_rect.w - SCROLLBAR_W) / GRID_CELL_W; if (cols < 1) cols = 1; int row = st->selected / cols; int top = row * GRID_CELL_H; int bottom = top + GRID_CELL_H; if (top < st->scrollbar.scroll_offset) st->scrollbar.scroll_offset = top; else if (bottom > st->scrollbar.scroll_offset + lo.content_rect.h) st->scrollbar.scroll_offset = bottom - lo.content_rect.h; } else { int view_h = lo.content_rect.h - HEADER_H; int top = st->selected * ITEM_H; int bottom = top + ITEM_H; if (top < st->scrollbar.scroll_offset) st->scrollbar.scroll_offset = top; else if (bottom > st->scrollbar.scroll_offset + view_h) st->scrollbar.scroll_offset = bottom - view_h; } int ms = st->scrollbar.max_scroll(); if (st->scrollbar.scroll_offset < 0) st->scrollbar.scroll_offset = 0; if (st->scrollbar.scroll_offset > ms) st->scrollbar.scroll_offset = ms; } void file_navigate_into(FileDialogState* st, int idx) { if (idx < 0 || idx >= st->entry_count) return; if (st->at_drives_root && st->entry_types[idx] == ENTRY_HOME) { safe_copy(st->current_path, sizeof(st->current_path), st->home_dir); file_push_history(st); file_read_dir(st); return; } if (st->at_drives_root && st->entry_types[idx] == ENTRY_SPECIAL) { safe_copy(st->current_path, sizeof(st->current_path), st->home_dir); int len = montauk::slen(st->current_path); if (len > 0 && st->current_path[len - 1] != '/') str_append(st->current_path, "/", sizeof(st->current_path)); str_append(st->current_path, st->entry_names[idx], sizeof(st->current_path)); file_push_history(st); file_read_dir(st); return; } if (st->at_drives_root && st->entry_types[idx] == ENTRY_DRIVE) { snprintf(st->current_path, sizeof(st->current_path), "%d:/", st->drive_indices[idx]); file_push_history(st); file_read_dir(st); return; } if (st->is_dir[idx]) { char next[256]; build_fullpath(next, sizeof(next), st->current_path, st->entry_names[idx]); safe_copy(st->current_path, sizeof(st->current_path), next); file_push_history(st); file_read_dir(st); } } 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->is_dir[st->selected]) { file_navigate_into(st, st->selected); return false; } char full[512]; build_fullpath(full, sizeof(full), st->current_path, st->entry_names[st->selected]); dialog_finish("ok", full, "", full); return true; } 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; } char full[512]; build_fullpath(full, sizeof(full), st->current_path, st->filename_buf); dialog_finish("ok", full, "", full); return true; } void file_activate_selected(FileDialogState* st, bool from_double_click) { if (st->selected < 0 || st->selected >= st->entry_count) return; if (st->is_dir[st->selected]) { file_navigate_into(st, st->selected); return; } if (g_app.request.mode == gui::dialogs::FILE_DIALOG_SAVE) { file_set_filename(st, st->entry_names[st->selected]); if (from_double_click) file_accept_save(st); } else { file_accept_open(st); } } void file_go_home(FileDialogState* st) { file_read_drives(st); file_push_history(st); } void file_go_back(FileDialogState* st) { if (st->history_pos <= 0) return; st->history_pos--; safe_copy(st->current_path, sizeof(st->current_path), st->history[st->history_pos]); if (st->current_path[0]) file_read_dir(st); else file_read_drives(st); } void file_go_forward(FileDialogState* st) { if (st->history_pos >= st->history_count - 1) return; st->history_pos++; safe_copy(st->current_path, sizeof(st->current_path), st->history[st->history_pos]); if (st->current_path[0]) file_read_dir(st); else file_read_drives(st); } void file_go_up(FileDialogState* st) { if (st->at_drives_root) return; int len = montauk::slen(st->current_path); if (len >= 3 && st->current_path[len - 1] == '/') { int colon = -1; for (int i = 0; i < len; i++) { if (st->current_path[i] == ':') { colon = i; break; } } if (colon >= 0 && colon + 2 == len) { file_read_drives(st); file_push_history(st); return; } } if (len > 0 && st->current_path[len - 1] == '/') { st->current_path[len - 1] = '\0'; len--; } int last_slash = -1; for (int i = len - 1; i >= 0; i--) { if (st->current_path[i] == '/') { last_slash = i; break; } } if (last_slash >= 0) { if (last_slash > 0 && st->current_path[last_slash - 1] == ':') st->current_path[last_slash + 1] = '\0'; else st->current_path[last_slash] = '\0'; } file_push_history(st); file_read_dir(st); } void file_open_path(FileDialogState* st, const char* path) { if (!path || !path[0]) { if (st->home_dir[0]) { safe_copy(st->current_path, sizeof(st->current_path), st->home_dir); file_push_history(st); file_read_dir(st); } else { file_read_drives(st); file_push_history(st); } return; } if (is_dir_path(path)) { safe_copy(st->current_path, sizeof(st->current_path), path); file_push_history(st); file_read_dir(st); return; } char dir[256]; char name[128]; split_parent_path(path, dir, sizeof(dir), name, sizeof(name)); if (dir[0] && is_dir_path(dir)) { safe_copy(st->current_path, sizeof(st->current_path), dir); safe_copy(st->pending_select, sizeof(st->pending_select), name); if (g_app.request.mode == gui::dialogs::FILE_DIALOG_SAVE && name[0]) file_set_filename(st, name); file_push_history(st); file_read_dir(st); if (g_app.request.mode == gui::dialogs::FILE_DIALOG_OPEN && name[0]) { for (int i = 0; i < st->entry_count; i++) { if (montauk::streq(st->entry_names[i], name)) { st->selected = i; break; } } } return; } file_set_error(st, "Path does not exist"); } void init_file_dialog(const gui::dialogs::Request& req) { FileDialogState* st = &g_file; montauk::memset(st, 0, sizeof(*st)); load_file_icons(st); st->selected = -1; st->last_click_item = -1; st->history_pos = -1; st->grid_view = true; st->focus = FOCUS_LIST; st->scrollbar.init(0, 0, SCROLLBAR_W, 100); montauk::user::get_home_dir(st->home_dir, sizeof(st->home_dir)); char initial_dir[256] = {}; char initial_name[128] = {}; if (req.initial_path[0]) split_parent_path(req.initial_path, initial_dir, sizeof(initial_dir), initial_name, sizeof(initial_name)); if (req.initial_path[0] && is_dir_path(req.initial_path)) { safe_copy(st->current_path, sizeof(st->current_path), req.initial_path); file_read_dir(st); file_push_history(st); } else if (initial_dir[0] && is_dir_path(initial_dir)) { safe_copy(st->current_path, sizeof(st->current_path), initial_dir); safe_copy(st->pending_select, sizeof(st->pending_select), initial_name); file_read_dir(st); file_push_history(st); } else if (st->home_dir[0] && is_dir_path(st->home_dir)) { safe_copy(st->current_path, sizeof(st->current_path), st->home_dir); file_read_dir(st); file_push_history(st); } else { file_read_drives(st); file_push_history(st); } if (req.mode == gui::dialogs::FILE_DIALOG_SAVE) { if (req.suggested_name[0]) file_set_filename(st, req.suggested_name); else if (initial_name[0]) file_set_filename(st, initial_name); } } void draw_toolbar_icon(Canvas& c, const Rect& rect, const SvgIcon& icon) { c.fill_rect(rect.x, rect.y, rect.w, rect.h, Color::from_rgb(0xE8, 0xE8, 0xE8)); if (icon.pixels) { int ix = rect.x + (rect.w - icon.width) / 2; int iy = rect.y + (rect.h - icon.height) / 2; c.icon(ix, iy, icon); } } void draw_grid_entries(Canvas& c, FileDialogState* st, const FileDialogLayout& lo) { int cols = (lo.content_rect.w - SCROLLBAR_W) / GRID_CELL_W; if (cols < 1) cols = 1; int rows = (st->entry_count + cols - 1) / cols; int content_h = rows * GRID_CELL_H; st->scrollbar.bounds = {c.w - SCROLLBAR_W, lo.content_rect.y, SCROLLBAR_W, lo.content_rect.h}; st->scrollbar.content_height = content_h; st->scrollbar.view_height = lo.content_rect.h; for (int i = 0; i < st->entry_count; i++) { int col = i % cols; int row = i / cols; int cell_x = col * GRID_CELL_W; int cell_y = lo.content_rect.y + row * GRID_CELL_H - st->scrollbar.scroll_offset; if (cell_y + GRID_CELL_H <= lo.content_rect.y || cell_y >= lo.content_rect.y + lo.content_rect.h) continue; if (i == st->selected) { int sy = gui_max(cell_y, lo.content_rect.y); int sh = gui_min(cell_y + GRID_CELL_H, lo.content_rect.y + lo.content_rect.h) - sy; int sw = gui_min(GRID_CELL_W, c.w - SCROLLBAR_W - cell_x); if (sh > 0 && sw > 0) c.fill_rect(cell_x, sy, sw, sh, colors::MENU_HOVER); } int icon_x = cell_x + (GRID_CELL_W - GRID_ICON) / 2; int icon_y = cell_y + GRID_PAD; SvgIcon* icon = &st->icons.icon_file_lg; int sfi = -1; if (st->entry_types[i] == ENTRY_SPECIAL) sfi = st->drive_indices[i]; if (sfi >= 0 && st->icons.icon_special_lg[sfi].pixels) icon = &st->icons.icon_special_lg[sfi]; else if (st->entry_types[i] == ENTRY_HOME && st->icons.icon_home_folder_lg.pixels) icon = &st->icons.icon_home_folder_lg; else if (st->entry_types[i] == ENTRY_DRIVE && st->icons.icon_drive_lg.pixels) icon = &st->icons.icon_drive_lg; else if (st->is_dir[i] && st->icons.icon_folder_lg.pixels) icon = &st->icons.icon_folder_lg; if (icon->pixels) c.icon(icon_x, icon_y, *icon); char label[16]; int nlen = montauk::slen(st->entry_names[i]); if (nlen > 11) { for (int k = 0; k < 9; k++) label[k] = st->entry_names[i][k]; label[9] = '.'; label[10] = '.'; label[11] = '\0'; } else { safe_copy(label, sizeof(label), st->entry_names[i]); } int tx = cell_x + (GRID_CELL_W - text_width(label)) / 2; if (tx < cell_x) tx = cell_x; int ty = icon_y + GRID_ICON + 2; if (ty >= lo.content_rect.y && ty + system_font_height() <= lo.content_rect.y + lo.content_rect.h) c.text(tx, ty, label, colors::TEXT_COLOR); } } void draw_list_entries(Canvas& c, FileDialogState* st, const FileDialogLayout& lo) { int header_y = lo.content_rect.y; int rows_y = header_y + HEADER_H; int rows_h = lo.content_rect.h - HEADER_H; if (rows_h < 0) rows_h = 0; c.fill_rect(0, header_y, c.w, HEADER_H, Color::from_rgb(0xF8, 0xF8, 0xF8)); c.text(8, header_y + 2, "Name", Color::from_rgb(0x88, 0x88, 0x88)); c.text(c.w - SCROLLBAR_W - 120, header_y + 2, "Size", Color::from_rgb(0x88, 0x88, 0x88)); c.text(c.w - SCROLLBAR_W - 60, header_y + 2, "Type", Color::from_rgb(0x88, 0x88, 0x88)); c.hline(0, header_y + HEADER_H - 1, c.w, colors::BORDER); c.vline(c.w - SCROLLBAR_W - 124, header_y, lo.content_rect.h, colors::BORDER); c.vline(c.w - SCROLLBAR_W - 64, header_y, lo.content_rect.h, colors::BORDER); st->scrollbar.bounds = {c.w - SCROLLBAR_W, rows_y, SCROLLBAR_W, rows_h}; st->scrollbar.content_height = st->entry_count * ITEM_H; st->scrollbar.view_height = rows_h; for (int i = 0; i < st->entry_count; i++) { int y = rows_y + i * ITEM_H - st->scrollbar.scroll_offset; if (y + ITEM_H <= rows_y || y >= rows_y + rows_h) continue; if (i == st->selected) c.fill_rect(0, y, c.w - SCROLLBAR_W, ITEM_H, colors::MENU_HOVER); int sfi = -1; if (st->entry_types[i] == ENTRY_SPECIAL) sfi = st->drive_indices[i]; if (sfi >= 0 && st->icons.icon_special[sfi].pixels) c.icon(8, y + 4, st->icons.icon_special[sfi]); else if (st->entry_types[i] == ENTRY_HOME && st->icons.icon_home_folder.pixels) c.icon(8, y + 4, st->icons.icon_home_folder); else if (st->entry_types[i] == ENTRY_DRIVE && st->icons.icon_drive.pixels) c.icon(8, y + 4, st->icons.icon_drive); else if (st->is_dir[i] && st->icons.icon_folder.pixels) c.icon(8, y + 4, st->icons.icon_folder); else if (st->icons.icon_file.pixels) c.icon(8, y + 4, st->icons.icon_file); c.text(30, y + 3, st->entry_names[i], colors::TEXT_COLOR); if (!st->is_dir[i]) { char size_buf[16]; format_size(size_buf, sizeof(size_buf), st->entry_sizes[i]); c.text(c.w - SCROLLBAR_W - 120, y + 3, size_buf, Color::from_rgb(0x66, 0x66, 0x66)); } const char* type_label = st->is_dir[i] ? "Folder" : "File"; if (st->entry_types[i] == ENTRY_DRIVE) type_label = "Drive"; else if (st->entry_types[i] == ENTRY_HOME) type_label = "Home"; c.text(c.w - SCROLLBAR_W - 60, y + 3, type_label, Color::from_rgb(0x66, 0x66, 0x66)); c.hline(0, y + ITEM_H - 1, c.w, Color::from_rgb(0xF0, 0xF0, 0xF0)); } } void draw_file_dialog() { FileDialogState* st = &g_file; Canvas c = g_app.win.canvas(); c.fill(colors::WINDOW_BG); FileDialogLayout lo = file_layout(st); c.fill_rect(0, 0, c.w, TOOLBAR_H, Color::from_rgb(0xF5, 0xF5, 0xF5)); draw_toolbar_icon(c, lo.back_btn, st->icons.icon_go_back); draw_toolbar_icon(c, lo.forward_btn, st->icons.icon_go_forward); draw_toolbar_icon(c, lo.up_btn, st->icons.icon_go_up); draw_toolbar_icon(c, lo.home_btn, st->icons.icon_home); c.fill_rect(lo.view_btn.x, lo.view_btn.y, lo.view_btn.w, lo.view_btn.h, Color::from_rgb(0xE8, 0xE8, 0xE8)); if (st->grid_view) { for (int r = 0; r < 2; r++) for (int cc = 0; cc < 2; cc++) c.fill_rect(lo.view_btn.x + 5 + cc * 8, lo.view_btn.y + 5 + r * 8, 6, 6, colors::TEXT_COLOR); } else { for (int r = 0; r < 3; r++) c.fill_rect(lo.view_btn.x + 5, lo.view_btn.y + 5 + r * 5, 14, 2, colors::TEXT_COLOR); } 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); if (st->grid_view) draw_grid_entries(c, st, lo); else draw_list_entries(c, st, lo); draw_scrollbar(c, st->scrollbar); c.fill_rect(0, lo.footer_rect.y, c.w, lo.footer_rect.h, Color::from_rgb(0xF7, 0xF7, 0xF7)); 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) { 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); confirm_enabled = st->filename_buf[0] != '\0' && !st->at_drives_root; } else { char selection[256] = {}; if (st->selected >= 0 && st->selected < st->entry_count) { char full[512]; if (st->at_drives_root && st->entry_types[st->selected] == ENTRY_HOME) safe_copy(full, sizeof(full), st->home_dir); else if (st->at_drives_root && st->entry_types[st->selected] == ENTRY_DRIVE) snprintf(full, sizeof(full), "%d:/", st->drive_indices[st->selected]); else if (st->at_drives_root && st->entry_types[st->selected] == ENTRY_SPECIAL) { safe_copy(full, sizeof(full), st->home_dir); int len = montauk::slen(full); if (len > 0 && full[len - 1] != '/') str_append(full, "/", sizeof(full)); str_append(full, st->entry_names[st->selected], sizeof(full)); } else { build_fullpath(full, sizeof(full), st->current_path, st->entry_names[st->selected]); } fit_text_end(full, selection, sizeof(selection), lo.cancel_btn.x - 16); } else { safe_copy(selection, sizeof(selection), "Choose a file"); } c.text(12, lo.footer_rect.y + 14, selection, Color::from_rgb(0x66, 0x66, 0x66)); 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'; } int file_entry_at(FileDialogState* st, int mx, int my) { FileDialogLayout lo = file_layout(st); if (!lo.content_rect.contains(mx, my)) return -1; if (st->grid_view) { if (mx >= lo.content_rect.w - SCROLLBAR_W) return -1; int cols = (lo.content_rect.w - SCROLLBAR_W) / GRID_CELL_W; if (cols < 1) cols = 1; int col = mx / GRID_CELL_W; int row = (my - lo.content_rect.y + st->scrollbar.scroll_offset) / GRID_CELL_H; int idx = row * cols + col; if (idx >= 0 && idx < st->entry_count && col < cols) return idx; return -1; } int rows_y = lo.content_rect.y + HEADER_H; if (my < rows_y || mx >= lo.content_rect.w - SCROLLBAR_W) return -1; int rel_y = my - rows_y + st->scrollbar.scroll_offset; int idx = rel_y / ITEM_H; if (idx >= 0 && idx < st->entry_count) return idx; return -1; } void handle_file_mouse(const Montauk::WinEvent& ev) { FileDialogState* st = &g_file; st->mouse_x = ev.mouse.x; st->mouse_y = ev.mouse.y; FileDialogLayout lo = file_layout(st); MouseEvent me = {ev.mouse.x, ev.mouse.y, ev.mouse.buttons, ev.mouse.prev_buttons, ev.mouse.scroll}; st->scrollbar.handle_mouse(me); if (me.left_pressed()) { st->error[0] = '\0'; 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; } if (lo.home_btn.contains(me.x, me.y)) { file_go_home(st); return; } if (lo.view_btn.contains(me.x, me.y)) { st->grid_view = !st->grid_view; st->scrollbar.scroll_offset = 0; return; } 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); else file_accept_open(st); return; } if (lo.path_rect.contains(me.x, me.y)) { st->focus = FOCUS_PATH; return; } if (g_app.request.mode == gui::dialogs::FILE_DIALOG_SAVE && lo.name_rect.contains(me.x, me.y)) { st->focus = FOCUS_NAME; return; } int idx = file_entry_at(st, me.x, me.y); if (idx >= 0) { uint64_t now = montauk::get_milliseconds(); st->focus = FOCUS_LIST; if (st->last_click_item == idx && (now - st->last_click_time) < 400) { st->selected = idx; file_activate_selected(st, true); st->last_click_item = -1; st->last_click_time = 0; } else { 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]) { file_set_filename(st, st->entry_names[idx]); } } file_ensure_selected_visible(st); return; } st->selected = -1; st->focus = FOCUS_LIST; } if (me.scroll != 0 && lo.content_rect.contains(me.x, me.y)) { int step = st->grid_view ? GRID_CELL_H : ITEM_H; st->scrollbar.scroll_offset -= me.scroll * step; int ms = st->scrollbar.max_scroll(); if (st->scrollbar.scroll_offset < 0) st->scrollbar.scroll_offset = 0; if (st->scrollbar.scroll_offset > ms) st->scrollbar.scroll_offset = ms; } } void handle_file_key(const Montauk::KeyEvent& key) { if (!key.pressed) return; FileDialogState* st = &g_file; st->error[0] = '\0'; if (key.scancode == 0x01) { dialog_cancel(); return; } if (st->focus == FOCUS_PATH) { if (key.ascii == '\n' || key.ascii == '\r') { file_open_path(st, st->path_buf); 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; } return; } if (st->focus == FOCUS_NAME && g_app.request.mode == gui::dialogs::FILE_DIALOG_SAVE) { 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; } return; } if (key.ascii == '\n' || key.ascii == '\r') { if (g_app.request.mode == gui::dialogs::FILE_DIALOG_SAVE) file_accept_save(st); else file_accept_open(st); return; } if (key.scancode == 0x4B || key.scancode == 0x48) { if (st->selected > 0) st->selected--; else if (st->entry_count > 0) st->selected = 0; file_ensure_selected_visible(st); return; } if (key.scancode == 0x4D || key.scancode == 0x50) { if (st->selected < st->entry_count - 1) st->selected++; else if (st->entry_count > 0) st->selected = st->entry_count - 1; file_ensure_selected_visible(st); return; } if (key.ctrl && (key.ascii == 'l' || key.ascii == 'L')) { st->focus = FOCUS_PATH; return; } if (key.ctrl && g_app.request.mode == gui::dialogs::FILE_DIALOG_SAVE && (key.ascii == 'n' || key.ascii == 'N')) { st->focus = FOCUS_NAME; return; } } } // namespace namespace filedialog { void init(const gui::dialogs::Request& req) { init_file_dialog(req); } void draw() { draw_file_dialog(); } void handle_mouse(const Montauk::WinEvent& ev) { handle_file_mouse(ev); } void handle_key(const Montauk::KeyEvent& key) { handle_file_key(key); } void cleanup() { free_file_icons(&g_file); } } // namespace filedialog