Files
MontaukOS/programs/src/dialogs/filedialog.cpp
T

1210 lines
43 KiB
C++

/*
* filedialog.cpp
* File open/save dialog implementation
* Copyright (c) 2026 Daniel Hammer
*/
#include "filedialog.hpp"
#include "main.hpp"
#include <gui/svg.hpp>
#include <gui/canvas.hpp>
#include <montauk/user.h>
extern "C" {
#include <stdio.h>
}
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