/* * events.cpp * Mouse and keyboard handling for the embedded desktop file manager * Copyright (c) 2026 Daniel Hammer */ #include "filemanager_internal.hpp" namespace filemanager { static void filemanager_show_selection_properties(FileManagerState* fm, int target) { if (fm->multi_count > 1 && fm->entry_count > 0) { int* sel = (int*)montauk::malloc((uint64_t)fm->entry_count * sizeof(int)); if (sel) { int n = filemanager_collect_selection(fm, sel, fm->entry_count); if (n > 0) filemanager_show_multi_properties(fm, sel, n); montauk::mfree(sel); } } else { filemanager_show_properties(fm, target); } } static bool filemanager_is_listing(const FileManagerState* fm) { return !filemanager_is_computer_view(fm) && !filemanager_is_virtual_view(fm); } // Left click on an entry. Ctrl toggles it, Shift selects the range from the // anchor (Ctrl+Shift adds that range); a plain click selects it alone, and a // second plain click within 400 ms opens it. static void filemanager_click_entry(FileManagerState* fm, int idx) { DesktopState* ds = fm->desktop; bool ctrl = ds && ds->kbd_ctrl_down; bool shift = ds && ds->kbd_shift_down; if (ctrl || shift) { if (shift) filemanager_select_range(fm, idx, ctrl); else filemanager_toggle_selection(fm, idx); fm->last_click_item = -1; fm->last_click_time = 0; return; } uint64_t now = montauk::get_milliseconds(); if (fm->last_click_item == idx && (now - fm->last_click_time) < 400) { filemanager_open_entry(fm, idx); fm->last_click_item = -1; fm->last_click_time = 0; } else { filemanager_select_single(fm, idx); fm->last_click_item = idx; fm->last_click_time = now; } } void filemanager_on_mouse(Window* win, MouseEvent& ev) { FileManagerState* fm = (FileManagerState*)win->app_data; if (!fm) return; Rect cr = win->content_rect(); int local_x = ev.x - cr.x; int local_y = ev.y - cr.y; int cw = cr.w; // Scrollbar interaction MouseEvent local_ev = ev; local_ev.x = local_x; local_ev.y = local_y; fm->scrollbar.handle_mouse(local_ev); // ---- Context menu interaction ---- if (fm->ctx_open) { mtk::ContextMenuState state = filemanager_context_state(fm); mtk::ContextMenuItem items[FM_CTX_MAX_ITEMS]; filemanager_build_context_items(fm, items, FM_CTX_MAX_ITEMS); int action = mtk::context_menu_handle_mouse(state, items, fm->ctx_item_count, local_x, local_y, ev.buttons, ev.prev_buttons, cw, win->content_h, filemanager_context_menu_width( items, fm->ctx_item_count)); filemanager_store_context_state(fm, state); if (action >= 0) { int target = fm->ctx_target_idx; // Select target if needed if (target >= 0 && target < fm->entry_count) fm->selected = target; switch (action) { case CTX_OPEN: filemanager_open_entry(fm, target); break; case CTX_COPY: filemanager_do_copy(fm); break; case CTX_CUT: filemanager_do_cut(fm); break; case CTX_PASTE: filemanager_do_paste(fm); break; case CTX_RENAME: filemanager_start_rename(fm); break; case CTX_DELETE: filemanager_delete_selected(fm, true); break; case CTX_TRASH: filemanager_delete_selected(fm); break; case CTX_RESTORE: filemanager_restore_selected(fm); break; case CTX_EMPTY_TRASH: filemanager_show_empty_trash_confirmation(fm); break; case CTX_NEW_FOLDER: filemanager_new_folder(fm); break; case CTX_PROPERTIES: filemanager_show_selection_properties(fm, target); break; case CTX_REFRESH: filemanager_refresh(fm); break; case CTX_OPEN_WITH: filemanager_show_open_with(fm, target); break; case CTX_SELECT_ALL: filemanager_select_all(fm); break; case CTX_SORT_NAME: filemanager_set_sort(fm, FM_SORT_NAME); break; case CTX_SORT_SIZE: filemanager_set_sort(fm, FM_SORT_SIZE); break; case CTX_SORT_TYPE: filemanager_set_sort(fm, FM_SORT_TYPE); break; case CTX_SORT_MODIFIED: filemanager_set_sort(fm, FM_SORT_MODIFIED); break; } return; } if (action == mtk::CONTEXT_MENU_DISMISSED) return; return; } // ---- Cancel rename on click outside rename area ---- if (fm->rename_active && ev.left_pressed()) { filemanager_finish_rename(fm); } if (ev.left_pressed()) { // Click on path bar area if (local_y >= FM_TOOLBAR_H && local_y < FM_TOOLBAR_H + FM_PATHBAR_H) { if (!fm->pathbar_editing) filemanager_start_pathbar(fm); return; } // Clicking anywhere else while pathbar is editing commits the path if (fm->pathbar_editing) { filemanager_commit_pathbar(fm); } // Toolbar button clicks if (local_y < FM_TOOLBAR_H) { // Navigation buttons if (local_x >= 4 && local_x < 28) filemanager_go_back(fm); else if (local_x >= 32 && local_x < 56) filemanager_go_forward(fm); else if (local_x >= 60 && local_x < 84) filemanager_go_up(fm); else if (local_x >= 88 && local_x < 112) filemanager_go_home(fm); else if (local_x >= 116 && local_x < 140) filemanager_refresh(fm); else if (local_x >= 148 && local_x < 172) { fm->grid_view = !fm->grid_view; fm->scrollbar.scroll_offset = 0; fm->marquee_active = false; filemanager_clear_multi_selection(fm); } // Action buttons else if (local_x >= 188 && local_x < 212) filemanager_do_copy(fm); else if (local_x >= 216 && local_x < 240) filemanager_do_cut(fm); else if (local_x >= 244 && local_x < 268) filemanager_do_paste(fm); else if (local_x >= 272 && local_x < 296) filemanager_start_rename(fm); else if (local_x >= 300 && local_x < 324) filemanager_new_folder(fm); else if (local_x >= 328 && local_x < 352) filemanager_delete_selected(fm, fm->desktop && fm->desktop->kbd_shift_down); return; } // File clicks (grid vs list) if (fm->grid_view) { int list_y = FM_TOOLBAR_H + FM_PATHBAR_H; if (local_y >= list_y && local_x < cw - FM_SCROLLBAR_W) { int cols = (cw - FM_SCROLLBAR_W) / FM_GRID_CELL_W; if (cols < 1) cols = 1; int rel_y = local_y - list_y + fm->scrollbar.scroll_offset; int clicked_idx = filemanager_grid_index_at(fm, cols, local_x, rel_y); if (clicked_idx >= 0 && clicked_idx < fm->entry_count) { filemanager_click_entry(fm, clicked_idx); } else { // Background click in grid area: start marquee drag filemanager_select_single(fm, -1); fm->marquee_active = true; fm->marquee_start_x = local_x; fm->marquee_start_y = rel_y; fm->marquee_cur_x = local_x; fm->marquee_cur_y = rel_y; } } } else { // List view clicks int header_y = FM_TOOLBAR_H + FM_PATHBAR_H; int list_y = header_y + FM_HEADER_H; if (local_y >= header_y && local_y < list_y && filemanager_is_listing(fm)) { // Column header click: sort by that column (again = reverse). FileManagerListColumns cols = filemanager_list_columns(cw); if (cols.type_x >= 0 && local_x >= cols.type_x) filemanager_set_sort(fm, FM_SORT_TYPE); else if (cols.size_x >= 0 && local_x >= cols.size_x) filemanager_set_sort(fm, FM_SORT_SIZE); else if (cols.modified_x >= 0 && local_x >= cols.modified_x - 4) filemanager_set_sort(fm, FM_SORT_MODIFIED); else filemanager_set_sort(fm, FM_SORT_NAME); return; } if (local_y >= list_y && local_x < cw - FM_SCROLLBAR_W) { int rel_y = local_y - list_y + fm->scrollbar.scroll_offset; int clicked_idx = rel_y / FM_ITEM_H; if (clicked_idx >= 0 && clicked_idx < fm->entry_count) { filemanager_click_entry(fm, clicked_idx); } else { // Background click in list area: start marquee drag filemanager_select_single(fm, -1); fm->marquee_active = true; fm->marquee_start_x = local_x; fm->marquee_start_y = rel_y; fm->marquee_cur_x = local_x; fm->marquee_cur_y = rel_y; } } } } // ---- Marquee drag (grid or list view) ---- if (fm->marquee_active) { int list_y = fm->grid_view ? FM_TOOLBAR_H + FM_PATHBAR_H : FM_TOOLBAR_H + FM_PATHBAR_H + FM_HEADER_H; int rel_y = local_y - list_y + fm->scrollbar.scroll_offset; int clamp_x = local_x; if (clamp_x < 0) clamp_x = 0; int max_x = cw - FM_SCROLLBAR_W - 1; if (clamp_x > max_x) clamp_x = max_x; if (rel_y < 0) rel_y = 0; if (ev.left_held()) { fm->marquee_cur_x = clamp_x; fm->marquee_cur_y = rel_y; } int rx0 = fm->marquee_start_x < fm->marquee_cur_x ? fm->marquee_start_x : fm->marquee_cur_x; int ry0 = fm->marquee_start_y < fm->marquee_cur_y ? fm->marquee_start_y : fm->marquee_cur_y; int rx1 = fm->marquee_start_x > fm->marquee_cur_x ? fm->marquee_start_x : fm->marquee_cur_x; int ry1 = fm->marquee_start_y > fm->marquee_cur_y ? fm->marquee_start_y : fm->marquee_cur_y; // Recompute hit-set every frame so entries highlight live during the drag. filemanager_clear_multi_selection(fm); int limit = fm->entry_count; if (fm->grid_view) { int cols = (cw - FM_SCROLLBAR_W) / FM_GRID_CELL_W; if (cols < 1) cols = 1; for (int i = 0; i < limit; i++) { int col = i % cols; int row = i / cols; int cell_x = col * FM_GRID_CELL_W; int cell_y = filemanager_grid_row_y(fm, row, cols); int cell_h = filemanager_grid_row_height(fm, row, cols); int cx1 = cell_x + FM_GRID_CELL_W; int cy1 = cell_y + cell_h; if (rx1 < cell_x || rx0 >= cx1) continue; if (ry1 < cell_y || ry0 >= cy1) continue; fm->multi_selected[i] = true; fm->multi_count++; } } else { // List rows span the full width, so select any row whose vertical // extent overlaps the marquee box. for (int i = 0; i < limit; i++) { int cell_y = i * FM_ITEM_H; int cy1 = cell_y + FM_ITEM_H; if (ry1 < cell_y || ry0 >= cy1) continue; fm->multi_selected[i] = true; fm->multi_count++; } } fm->selected = -1; if (!ev.left_held()) { // Release: collapse a single-item marquee to a normal selection. if (fm->multi_count == 1) { for (int i = 0; i < limit; i++) { if (fm->multi_selected[i]) { fm->selected = i; break; } } filemanager_clear_multi_selection(fm); } fm->marquee_active = false; } } // ---- Right-click: open context menu ---- if (ev.right_pressed()) { filemanager_cancel_rename(fm); // Determine what was right-clicked int target_idx = -1; if (local_y >= FM_TOOLBAR_H + FM_PATHBAR_H) { if (fm->grid_view) { int list_y = FM_TOOLBAR_H + FM_PATHBAR_H; if (local_x < cw - FM_SCROLLBAR_W) { int cols = (cw - FM_SCROLLBAR_W) / FM_GRID_CELL_W; if (cols < 1) cols = 1; int rel_y = local_y - list_y + fm->scrollbar.scroll_offset; int idx = filemanager_grid_index_at(fm, cols, local_x, rel_y); if (idx >= 0 && idx < fm->entry_count) { target_idx = idx; // Preserve multi-selection if right-clicking inside it, // otherwise replace it with a single selection. if (!filemanager_is_entry_selected(fm, idx)) filemanager_select_single(fm, idx); } else { filemanager_select_single(fm, -1); } } } else { int list_y = FM_TOOLBAR_H + FM_PATHBAR_H + FM_HEADER_H; if (local_y >= list_y && local_x < cw - FM_SCROLLBAR_W) { int rel_y = local_y - list_y + fm->scrollbar.scroll_offset; int idx = rel_y / FM_ITEM_H; if (idx >= 0 && idx < fm->entry_count) { target_idx = idx; // Preserve multi-selection if right-clicking inside it, // otherwise replace it with a single selection. if (!filemanager_is_entry_selected(fm, idx)) filemanager_select_single(fm, idx); } else { filemanager_select_single(fm, -1); } } } } filemanager_open_ctx_menu(fm, local_x, local_y, target_idx); } // Scroll handling if (ev.scroll != 0) { int list_y_start = fm->grid_view ? FM_TOOLBAR_H + FM_PATHBAR_H : FM_TOOLBAR_H + FM_PATHBAR_H + FM_HEADER_H; int scroll_step = fm->grid_view ? FM_GRID_CELL_H : FM_ITEM_H; if (local_y >= list_y_start) { fm->scrollbar.scroll_offset -= ev.scroll * scroll_step; int ms = fm->scrollbar.max_scroll(); if (fm->scrollbar.scroll_offset < 0) fm->scrollbar.scroll_offset = 0; if (fm->scrollbar.scroll_offset > ms) fm->scrollbar.scroll_offset = ms; } } } static int filemanager_grid_cols(Window* win) { int cols = (win->content_rect().w - FM_SCROLLBAR_W) / FM_GRID_CELL_W; return cols < 1 ? 1 : cols; } // Keep the focused entry inside the visible area so keyboard navigation // scrolls the view along with the selection. static void filemanager_scroll_to_selected(Window* win, FileManagerState* fm) { if (fm->selected < 0 || fm->selected >= fm->entry_count) return; int list_y = fm->grid_view ? FM_TOOLBAR_H + FM_PATHBAR_H : FM_TOOLBAR_H + FM_PATHBAR_H + FM_HEADER_H; int list_h = win->content_h - list_y; if (list_h <= 0) return; int top, item_h, content_h; if (fm->grid_view) { int cols = filemanager_grid_cols(win); int row = fm->selected / cols; top = filemanager_grid_row_y(fm, row, cols); item_h = filemanager_grid_row_height(fm, row, cols); content_h = filemanager_grid_content_height(fm, cols); } else { top = fm->selected * FM_ITEM_H; item_h = FM_ITEM_H; content_h = fm->entry_count * FM_ITEM_H; } int off = fm->scrollbar.scroll_offset; if (top < off) off = top; else if (top + item_h > off + list_h) off = top + item_h - list_h; int max_scroll = content_h - list_h; if (off > max_scroll) off = max_scroll; if (off < 0) off = 0; fm->scrollbar.scroll_offset = off; } // Move keyboard focus to target (clamped). With extend (Shift held) the // selection grows from the anchor instead of moving with the focus. static void filemanager_move_focus(Window* win, FileManagerState* fm, int target, bool extend) { if (fm->entry_count <= 0) return; if (target < 0) target = 0; if (target >= fm->entry_count) target = fm->entry_count - 1; if (extend) filemanager_select_range(fm, target, false); else filemanager_select_single(fm, target); filemanager_scroll_to_selected(win, fm); } static bool filemanager_prefix_match_ci(const char* name, const char* prefix, int len) { for (int i = 0; i < len; i++) { char a = name[i]; char b = prefix[i]; if (!a) return false; if (a >= 'A' && a <= 'Z') a += 32; if (b >= 'A' && b <= 'Z') b += 32; if (a != b) return false; } return true; } // Type-to-jump. Keys typed within a second of each other build up a prefix; // repeating a single character instead cycles through the names starting // with it. static void filemanager_typeahead(Window* win, FileManagerState* fm, const montauk::abi::KeyEvent& key) { char ch[4]; int n = montauk::utf8::key_utf8(key, ch); if (n <= 0 || fm->entry_count <= 0) return; uint64_t now = montauk::get_milliseconds(); if (now - fm->typeahead_time > 1000) fm->typeahead_len = 0; fm->typeahead_time = now; if (fm->typeahead_len == 0 && ch[0] == ' ') return; if (fm->typeahead_len + n >= (int)sizeof(fm->typeahead)) return; montauk::memcpy(fm->typeahead + fm->typeahead_len, ch, n); fm->typeahead_len += n; fm->typeahead[fm->typeahead_len] = '\0'; bool cycle = fm->typeahead_len % n == 0; for (int off = n; cycle && off < fm->typeahead_len; off += n) { for (int k = 0; k < n; k++) { if (fm->typeahead[off + k] != ch[k]) { cycle = false; break; } } } int prefix_len = cycle ? n : fm->typeahead_len; int start = fm->selected < 0 ? 0 : fm->selected + (cycle ? 1 : 0); for (int k = 0; k < fm->entry_count; k++) { int i = (start + k) % fm->entry_count; if (filemanager_prefix_match_ci(fm->entry_names[i], fm->typeahead, prefix_len)) { filemanager_move_focus(win, fm, i, false); return; } } } void filemanager_on_key(Window* win, const montauk::abi::KeyEvent& key) { FileManagerState* fm = (FileManagerState*)win->app_data; if (!fm || !key.pressed) return; // ---- Path bar editing key handling ---- if (fm->pathbar_editing) { if (key.ascii == '\n' || key.ascii == '\r') { filemanager_commit_pathbar(fm); } else if (key.scancode == 0x01) { filemanager_cancel_pathbar(fm); } else if (key.ascii == '\b' || key.scancode == 0x0E) { montauk::utf8::erase_before(fm->pathbar_buf, &fm->pathbar_len, &fm->pathbar_cursor); } else if (montauk::nav_scancode(key) == 0x53) { montauk::utf8::erase_at(fm->pathbar_buf, &fm->pathbar_len, fm->pathbar_cursor); } else if (montauk::nav_scancode(key) == 0x4B) { fm->pathbar_cursor = montauk::utf8::prev_boundary(fm->pathbar_buf, fm->pathbar_cursor); } else if (montauk::nav_scancode(key) == 0x4D) { fm->pathbar_cursor = montauk::utf8::next_boundary(fm->pathbar_buf, fm->pathbar_len, fm->pathbar_cursor); } else if (montauk::nav_scancode(key) == 0x47) { fm->pathbar_cursor = 0; } else if (montauk::nav_scancode(key) == 0x4F) { fm->pathbar_cursor = fm->pathbar_len; } else { montauk::utf8::insert_key(fm->pathbar_buf, &fm->pathbar_len, 255, &fm->pathbar_cursor, key); } return; } // ---- Rename mode key handling ---- if (fm->rename_active) { if (key.ascii == '\n' || key.ascii == '\r') { filemanager_finish_rename(fm); } else if (key.scancode == 0x01) { // Escape filemanager_cancel_rename(fm); } else if (key.ascii == '\b' || key.scancode == 0x0E) { // Backspace montauk::utf8::erase_before(fm->rename_buf, &fm->rename_len, &fm->rename_cursor); } else if (montauk::nav_scancode(key) == 0x53) { // Delete key montauk::utf8::erase_at(fm->rename_buf, &fm->rename_len, fm->rename_cursor); } else if (montauk::nav_scancode(key) == 0x4B) { // Left arrow fm->rename_cursor = montauk::utf8::prev_boundary(fm->rename_buf, fm->rename_cursor); } else if (montauk::nav_scancode(key) == 0x4D) { // Right arrow fm->rename_cursor = montauk::utf8::next_boundary(fm->rename_buf, fm->rename_len, fm->rename_cursor); } else if (montauk::nav_scancode(key) == 0x47) { // Home fm->rename_cursor = 0; } else if (montauk::nav_scancode(key) == 0x4F) { // End fm->rename_cursor = fm->rename_len; } else if (key.ascii != '/' && key.ascii != '\\') { // Printable character (reject / and other FS-unsafe chars) montauk::utf8::insert_key(fm->rename_buf, &fm->rename_len, 63, &fm->rename_cursor, key); } return; } // ---- Context menu: Escape to close ---- if (fm->ctx_open && key.scancode == 0x01) { filemanager_close_ctx_menu(fm); return; } // ---- Keyboard shortcuts ---- if (key.ctrl && !key.alt) { char ch = key.ascii; if (ch >= 'A' && ch <= 'Z') ch += 32; switch (ch) { case 'c': filemanager_do_copy(fm); return; case 'x': filemanager_do_cut(fm); return; case 'v': filemanager_do_paste(fm); return; case 'a': if (filemanager_is_listing(fm)) filemanager_select_all(fm); return; case 'l': filemanager_start_pathbar(fm); return; case 'n': if (key.shift) filemanager_new_folder(fm); return; default: break; } } // F2 = rename, F5 = refresh if (key.scancode == 0x3C) { filemanager_start_rename(fm); return; } if (key.scancode == 0x3F) { filemanager_refresh(fm); return; } uint8_t nav = montauk::nav_scancode(key); int cur = fm->selected; bool have_focus = cur >= 0 && cur < fm->entry_count; int cols = fm->grid_view ? filemanager_grid_cols(win) : 1; if (key.ascii == '\b' || key.scancode == 0x0E) { filemanager_go_up(fm); } else if (key.scancode == 0x01) { // Escape: drop the selection filemanager_select_single(fm, -1); } else if (nav == 0x48 && !key.alt) { // Up arrow (one row up in the grid) int target = !have_focus ? 0 : (cur >= cols ? cur - cols : cur); filemanager_move_focus(win, fm, target, key.shift); } else if (nav == 0x50 && !key.alt) { // Down arrow (one row down in the grid) int target = !have_focus ? 0 : (cur + cols < fm->entry_count ? cur + cols : cur); filemanager_move_focus(win, fm, target, key.shift); } else if (nav == 0x4B && !key.alt && fm->grid_view) { // Left arrow (grid view only) filemanager_move_focus(win, fm, have_focus ? cur - 1 : 0, key.shift); } else if (nav == 0x4D && !key.alt && fm->grid_view) { // Right arrow (grid view only) filemanager_move_focus(win, fm, have_focus ? cur + 1 : 0, key.shift); } else if (nav == 0x47) { // Home filemanager_move_focus(win, fm, 0, key.shift); } else if (nav == 0x4F) { // End filemanager_move_focus(win, fm, fm->entry_count - 1, key.shift); } else if ((key.ascii == '\n' || key.ascii == '\r') && key.alt) { // Alt+Enter: properties if (have_focus) filemanager_show_selection_properties(fm, cur); } else if (key.ascii == '\n' || key.ascii == '\r') { if (have_focus && filemanager_is_entry_selected(fm, cur)) filemanager_open_entry(fm, cur); } else if (nav == 0x53) { // Delete key: Move to Trash; Shift+Delete deletes permanently filemanager_delete_selected(fm, key.shift); } else if (key.alt && montauk::nav_scancode(key) == 0x4B) { // Alt+Left: go back filemanager_go_back(fm); } else if (key.alt && montauk::nav_scancode(key) == 0x4D) { // Alt+Right: go forward filemanager_go_forward(fm); } else if (!key.ctrl && !key.alt) { filemanager_typeahead(win, fm, key); } } void filemanager_on_close(Window* win) { if (win->app_data) { FileManagerState* fm = (FileManagerState*)win->app_data; filemanager_free_arrays(fm); montauk::mfree(win->app_data); win->app_data = nullptr; } } } // namespace filemanager