refactor: split Files and Settings apps into multiple files
This commit is contained in:
@@ -68,9 +68,9 @@ LDFLAGS := \
|
||||
# ---- C++ source files ----
|
||||
|
||||
CORE_SRCS := main.cpp window.cpp panel.cpp compose.cpp input.cpp dialogs.cpp launcher.cpp font_data.cpp stb_truetype_impl.cpp
|
||||
APP_SRCS := $(wildcard apps/*.cpp)
|
||||
APP_SRCS := $(sort $(shell find apps -name '*.cpp' -print))
|
||||
SRCS := $(CORE_SRCS) $(APP_SRCS)
|
||||
OBJS := $(addprefix $(OBJDIR)/,$(notdir $(SRCS:.cpp=.o)))
|
||||
OBJS := $(addprefix $(OBJDIR)/,$(SRCS:.cpp=.o))
|
||||
|
||||
# ---- Target ----
|
||||
|
||||
@@ -88,14 +88,9 @@ $(TARGET): $(OBJS) $(LIBS) $(LINK_LD) Makefile
|
||||
mkdir -p $(BINDIR)/os
|
||||
$(CXX) $(CXXFLAGS) $(LDFLAGS) $(OBJS) $(LIBS) -o $@
|
||||
|
||||
# Core sources
|
||||
# Sources
|
||||
$(OBJDIR)/%.o: %.cpp Makefile
|
||||
mkdir -p $(OBJDIR)
|
||||
$(CXX) $(CXXFLAGS) -c $< -o $@
|
||||
|
||||
# App sources
|
||||
$(OBJDIR)/%.o: apps/%.cpp Makefile
|
||||
mkdir -p $(OBJDIR)
|
||||
mkdir -p $(dir $@)
|
||||
$(CXX) $(CXXFLAGS) -c $< -o $@
|
||||
|
||||
-include $(OBJS:.o=.d)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,178 @@
|
||||
/*
|
||||
* actions.cpp
|
||||
* Clipboard, rename, delete, and context-menu actions for the embedded desktop file manager
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#include "filemanager_internal.hpp"
|
||||
|
||||
namespace filemanager {
|
||||
|
||||
void filemanager_do_copy(FileManagerState* fm) {
|
||||
if (fm->selected < 0 || fm->selected >= fm->entry_count) return;
|
||||
if (fm->at_drives_root || fm->entry_types[fm->selected] == 3) return;
|
||||
|
||||
filemanager_build_fullpath(fm->clipboard_path, 256,
|
||||
fm->current_path, fm->entry_names[fm->selected]);
|
||||
fm->clipboard_has_data = true;
|
||||
fm->clipboard_is_cut = false;
|
||||
}
|
||||
|
||||
void filemanager_do_cut(FileManagerState* fm) {
|
||||
if (fm->selected < 0 || fm->selected >= fm->entry_count) return;
|
||||
if (fm->at_drives_root || fm->entry_types[fm->selected] == 3) return;
|
||||
|
||||
filemanager_build_fullpath(fm->clipboard_path, 256,
|
||||
fm->current_path, fm->entry_names[fm->selected]);
|
||||
fm->clipboard_has_data = true;
|
||||
fm->clipboard_is_cut = true;
|
||||
}
|
||||
|
||||
void filemanager_do_paste(FileManagerState* fm) {
|
||||
if (!fm->clipboard_has_data || fm->at_drives_root) return;
|
||||
|
||||
const char* basename = path_basename(fm->clipboard_path);
|
||||
char dst[512];
|
||||
filemanager_build_fullpath(dst, 512, fm->current_path, basename);
|
||||
|
||||
// Check if source is a directory by trying to readdir it
|
||||
const char* probe[1];
|
||||
int probe_count = montauk::readdir(fm->clipboard_path, probe, 1);
|
||||
bool src_is_dir = (probe_count >= 0);
|
||||
|
||||
bool ok;
|
||||
if (src_is_dir)
|
||||
ok = filemanager_copy_dir_recursive(fm->clipboard_path, dst);
|
||||
else
|
||||
ok = filemanager_copy_file(fm->clipboard_path, dst);
|
||||
|
||||
if (ok && fm->clipboard_is_cut) {
|
||||
if (src_is_dir)
|
||||
filemanager_delete_recursive(fm->clipboard_path);
|
||||
else
|
||||
montauk::fdelete(fm->clipboard_path);
|
||||
fm->clipboard_has_data = false;
|
||||
}
|
||||
|
||||
filemanager_read_dir(fm);
|
||||
}
|
||||
|
||||
void filemanager_start_rename(FileManagerState* fm) {
|
||||
if (fm->selected < 0 || fm->selected >= fm->entry_count) return;
|
||||
if (fm->at_drives_root || fm->entry_types[fm->selected] == 3) return;
|
||||
|
||||
fm->rename_active = true;
|
||||
fm->rename_idx = fm->selected;
|
||||
montauk::strcpy(fm->rename_buf, fm->entry_names[fm->selected]);
|
||||
fm->rename_len = montauk::slen(fm->rename_buf);
|
||||
fm->rename_cursor = fm->rename_len;
|
||||
}
|
||||
|
||||
void filemanager_finish_rename(FileManagerState* fm) {
|
||||
if (!fm->rename_active) return;
|
||||
fm->rename_active = false;
|
||||
|
||||
// If name unchanged, skip
|
||||
if (montauk::streq(fm->rename_buf, fm->entry_names[fm->rename_idx])) return;
|
||||
if (fm->rename_len == 0) return;
|
||||
|
||||
char old_path[512], new_path[512];
|
||||
filemanager_build_fullpath(old_path, 512,
|
||||
fm->current_path, fm->entry_names[fm->rename_idx]);
|
||||
filemanager_build_fullpath(new_path, 512,
|
||||
fm->current_path, fm->rename_buf);
|
||||
|
||||
if (fm->is_dir[fm->rename_idx]) {
|
||||
// Directory rename: copy recursively then delete old
|
||||
if (filemanager_copy_dir_recursive(old_path, new_path))
|
||||
filemanager_delete_recursive(old_path);
|
||||
} else {
|
||||
// File rename: copy then delete old
|
||||
if (filemanager_copy_file(old_path, new_path))
|
||||
montauk::fdelete(old_path);
|
||||
}
|
||||
|
||||
filemanager_read_dir(fm);
|
||||
}
|
||||
|
||||
void filemanager_cancel_rename(FileManagerState* fm) {
|
||||
fm->rename_active = false;
|
||||
}
|
||||
|
||||
void filemanager_new_folder(FileManagerState* fm) {
|
||||
if (fm->at_drives_root) return;
|
||||
|
||||
// Find a unique name
|
||||
char name[64] = "New Folder";
|
||||
char path[512];
|
||||
filemanager_build_fullpath(path, 512, fm->current_path, name);
|
||||
int attempt = 1;
|
||||
// Try creating; if it fails, append a number
|
||||
if (montauk::fmkdir(path) != 0) {
|
||||
for (attempt = 2; attempt <= 99; attempt++) {
|
||||
snprintf(name, 64, "New Folder %d", attempt);
|
||||
filemanager_build_fullpath(path, 512, fm->current_path, name);
|
||||
if (montauk::fmkdir(path) == 0) break;
|
||||
}
|
||||
}
|
||||
|
||||
filemanager_read_dir(fm);
|
||||
|
||||
// Select the new folder and start rename
|
||||
for (int i = 0; i < fm->entry_count; i++) {
|
||||
if (montauk::streq(fm->entry_names[i], name)) {
|
||||
fm->selected = i;
|
||||
filemanager_start_rename(fm);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void filemanager_delete_selected(FileManagerState* fm) {
|
||||
if (fm->selected < 0 || fm->selected >= fm->entry_count) return;
|
||||
if (fm->at_drives_root || fm->entry_types[fm->selected] == 3) return;
|
||||
|
||||
char fullpath[512];
|
||||
filemanager_build_fullpath(fullpath, 512,
|
||||
fm->current_path, fm->entry_names[fm->selected]);
|
||||
if (fm->is_dir[fm->selected])
|
||||
filemanager_delete_recursive(fullpath);
|
||||
else
|
||||
montauk::fdelete(fullpath);
|
||||
filemanager_read_dir(fm);
|
||||
}
|
||||
|
||||
void filemanager_open_ctx_menu(FileManagerState* fm, int local_x, int local_y, int target_idx) {
|
||||
fm->ctx_open = true;
|
||||
fm->ctx_x = local_x;
|
||||
fm->ctx_y = local_y;
|
||||
fm->ctx_target_idx = target_idx;
|
||||
fm->ctx_hover = -1;
|
||||
fm->ctx_item_count = 0;
|
||||
|
||||
if (target_idx >= 0 && target_idx < fm->entry_count) {
|
||||
int tt = fm->entry_types[target_idx];
|
||||
if ((fm->at_drives_root && (tt == 3 || tt == 4 || tt == 5 || tt == 7)) ||
|
||||
(fm->at_apps_view && tt == 6)) {
|
||||
// Drive, Home, Apps shortcut, or app entry: only Open
|
||||
fm->ctx_items[fm->ctx_item_count++] = CTX_OPEN;
|
||||
} else {
|
||||
fm->ctx_items[fm->ctx_item_count++] = CTX_OPEN;
|
||||
fm->ctx_items[fm->ctx_item_count++] = CTX_COPY;
|
||||
fm->ctx_items[fm->ctx_item_count++] = CTX_CUT;
|
||||
fm->ctx_items[fm->ctx_item_count++] = CTX_RENAME;
|
||||
fm->ctx_items[fm->ctx_item_count++] = CTX_DELETE;
|
||||
}
|
||||
} else {
|
||||
// Background click
|
||||
if (fm->clipboard_has_data)
|
||||
fm->ctx_items[fm->ctx_item_count++] = CTX_PASTE;
|
||||
fm->ctx_items[fm->ctx_item_count++] = CTX_NEW_FOLDER;
|
||||
}
|
||||
}
|
||||
|
||||
void filemanager_close_ctx_menu(FileManagerState* fm) {
|
||||
fm->ctx_open = false;
|
||||
}
|
||||
|
||||
} // namespace filemanager
|
||||
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
* app.cpp
|
||||
* File manager launcher and desktop integration entry points
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#include "filemanager_internal.hpp"
|
||||
|
||||
namespace filemanager {
|
||||
|
||||
static void open_filemanager_internal(DesktopState* ds, const char* initial_path) {
|
||||
int idx = desktop_create_window(ds, "Files", 150, 120, 560, 420);
|
||||
if (idx < 0) return;
|
||||
|
||||
Window* win = &ds->windows[idx];
|
||||
FileManagerState* fm = (FileManagerState*)montauk::malloc(sizeof(FileManagerState));
|
||||
montauk::memset(fm, 0, sizeof(FileManagerState));
|
||||
fm->selected = -1;
|
||||
fm->last_click_item = -1;
|
||||
fm->history_pos = -1;
|
||||
fm->history_count = 0;
|
||||
fm->desktop = ds;
|
||||
fm->grid_view = true;
|
||||
|
||||
fm->scrollbar.init(0, 0, FM_SCROLLBAR_W, 100);
|
||||
|
||||
ensure_filemanager_icons_loaded(ds);
|
||||
|
||||
if (initial_path && initial_path[0] != '\0') {
|
||||
int probe_fd = montauk::open(initial_path);
|
||||
if (probe_fd >= 0) {
|
||||
montauk::close(probe_fd);
|
||||
montauk::strncpy(fm->current_path, initial_path, sizeof(fm->current_path));
|
||||
filemanager_read_dir(fm);
|
||||
} else {
|
||||
filemanager_read_drives(fm);
|
||||
}
|
||||
} else {
|
||||
filemanager_read_drives(fm);
|
||||
}
|
||||
filemanager_push_history(fm);
|
||||
|
||||
win->app_data = fm;
|
||||
win->on_draw = filemanager_on_draw;
|
||||
win->on_mouse = filemanager_on_mouse;
|
||||
win->on_key = filemanager_on_key;
|
||||
win->on_close = filemanager_on_close;
|
||||
}
|
||||
|
||||
} // namespace filemanager
|
||||
|
||||
void ensure_filemanager_icons_loaded(DesktopState* ds) {
|
||||
if (!ds || ds->icon_drive.pixels) return;
|
||||
|
||||
Color defColor = colors::ICON_COLOR;
|
||||
ds->icon_drive = svg_load("0:/icons/drive-harddisk.svg", 16, 16, defColor);
|
||||
ds->icon_drive_lg = svg_load("0:/icons/drive-harddisk.svg", 48, 48, defColor);
|
||||
ds->icon_home_folder = svg_load("0:/icons/folder-blue-home.svg", 16, 16, defColor);
|
||||
ds->icon_home_folder_lg = svg_load("0:/icons/folder-blue-home.svg", 48, 48, defColor);
|
||||
ds->icon_apps = svg_load("0:/icons/folder-blue-development.svg", 16, 16, defColor);
|
||||
ds->icon_apps_lg = svg_load("0:/icons/folder-blue-development.svg", 48, 48, defColor);
|
||||
|
||||
for (int sf = 0; sf < filemanager::SF_COUNT; sf++) {
|
||||
char icon_path[128];
|
||||
snprintf(icon_path, 128, "0:/icons/%s", filemanager::sf_icons[sf]);
|
||||
ds->icon_special_folder[sf] = svg_load(icon_path, 16, 16, defColor);
|
||||
ds->icon_special_folder_lg[sf] = svg_load(icon_path, 48, 48, defColor);
|
||||
}
|
||||
ds->icon_delete = svg_load("0:/icons/trash-empty.svg", 16, 16, defColor);
|
||||
ds->icon_copy = svg_load("0:/icons/edit-copy.svg", 16, 16, defColor);
|
||||
ds->icon_cut = svg_load("0:/icons/edit-cut.svg", 16, 16, defColor);
|
||||
ds->icon_paste = svg_load("0:/icons/edit-paste.svg", 16, 16, defColor);
|
||||
ds->icon_rename = svg_load("0:/icons/edit-rename.svg", 16, 16, defColor);
|
||||
ds->icon_folder_new = svg_load("0:/icons/folder-new.svg", 16, 16, defColor);
|
||||
}
|
||||
|
||||
void open_filemanager(DesktopState* ds) {
|
||||
filemanager::open_filemanager_internal(ds, nullptr);
|
||||
}
|
||||
|
||||
void open_filemanager_path(DesktopState* ds, const char* path) {
|
||||
filemanager::open_filemanager_internal(ds, path);
|
||||
}
|
||||
@@ -0,0 +1,395 @@
|
||||
/*
|
||||
* events.cpp
|
||||
* Mouse and keyboard handling for the embedded desktop file manager
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#include "filemanager_internal.hpp"
|
||||
|
||||
namespace filemanager {
|
||||
|
||||
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) {
|
||||
int cmx = fm->ctx_x;
|
||||
int cmy = fm->ctx_y;
|
||||
int cmh = fm->ctx_item_count * CTX_ITEM_H + 8;
|
||||
if (cmx + CTX_MENU_W > cw) cmx = cw - CTX_MENU_W;
|
||||
if (cmy + cmh > win->content_h) cmy = win->content_h - cmh;
|
||||
if (cmx < 0) cmx = 0;
|
||||
if (cmy < 0) cmy = 0;
|
||||
|
||||
// Update hover
|
||||
if (local_x >= cmx && local_x < cmx + CTX_MENU_W &&
|
||||
local_y >= cmy + 4 && local_y < cmy + 4 + fm->ctx_item_count * CTX_ITEM_H) {
|
||||
fm->ctx_hover = (local_y - cmy - 4) / CTX_ITEM_H;
|
||||
} else {
|
||||
fm->ctx_hover = -1;
|
||||
}
|
||||
|
||||
if (ev.left_pressed()) {
|
||||
if (fm->ctx_hover >= 0 && fm->ctx_hover < fm->ctx_item_count) {
|
||||
int action = fm->ctx_items[fm->ctx_hover];
|
||||
int target = fm->ctx_target_idx;
|
||||
filemanager_close_ctx_menu(fm);
|
||||
|
||||
// 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); break;
|
||||
case CTX_NEW_FOLDER: filemanager_new_folder(fm); break;
|
||||
}
|
||||
} else {
|
||||
filemanager_close_ctx_menu(fm);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (ev.right_pressed()) {
|
||||
filemanager_close_ctx_menu(fm);
|
||||
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 >= 120 && local_x < 144) {
|
||||
fm->grid_view = !fm->grid_view;
|
||||
fm->scrollbar.scroll_offset = 0;
|
||||
}
|
||||
// Action buttons
|
||||
else if (local_x >= 160 && local_x < 184) filemanager_do_copy(fm);
|
||||
else if (local_x >= 188 && local_x < 212) filemanager_do_cut(fm);
|
||||
else if (local_x >= 216 && local_x < 240) filemanager_do_paste(fm);
|
||||
else if (local_x >= 244 && local_x < 268) filemanager_start_rename(fm);
|
||||
else if (local_x >= 272 && local_x < 296) filemanager_new_folder(fm);
|
||||
else if (local_x >= 300 && local_x < 324) filemanager_delete_selected(fm);
|
||||
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 col = local_x / FM_GRID_CELL_W;
|
||||
int row = (local_y - list_y + fm->scrollbar.scroll_offset) / FM_GRID_CELL_H;
|
||||
int clicked_idx = row * cols + col;
|
||||
|
||||
if (clicked_idx >= 0 && clicked_idx < fm->entry_count && col < cols) {
|
||||
uint64_t now = montauk::get_milliseconds();
|
||||
|
||||
if (fm->last_click_item == clicked_idx &&
|
||||
(now - fm->last_click_time) < 400) {
|
||||
filemanager_open_entry(fm, clicked_idx);
|
||||
fm->last_click_item = -1;
|
||||
fm->last_click_time = 0;
|
||||
} else {
|
||||
fm->selected = clicked_idx;
|
||||
fm->last_click_item = clicked_idx;
|
||||
fm->last_click_time = now;
|
||||
}
|
||||
} else {
|
||||
fm->selected = -1;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// List view clicks
|
||||
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 clicked_idx = rel_y / FM_ITEM_H;
|
||||
|
||||
if (clicked_idx >= 0 && clicked_idx < fm->entry_count) {
|
||||
uint64_t now = montauk::get_milliseconds();
|
||||
|
||||
// Double-click detection
|
||||
if (fm->last_click_item == clicked_idx &&
|
||||
(now - fm->last_click_time) < 400) {
|
||||
filemanager_open_entry(fm, clicked_idx);
|
||||
fm->last_click_item = -1;
|
||||
fm->last_click_time = 0;
|
||||
} else {
|
||||
fm->selected = clicked_idx;
|
||||
fm->last_click_item = clicked_idx;
|
||||
fm->last_click_time = now;
|
||||
}
|
||||
} else {
|
||||
fm->selected = -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 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 col = local_x / FM_GRID_CELL_W;
|
||||
int row = (local_y - list_y + fm->scrollbar.scroll_offset) / FM_GRID_CELL_H;
|
||||
int idx = row * cols + col;
|
||||
if (idx >= 0 && idx < fm->entry_count && col < cols) {
|
||||
target_idx = idx;
|
||||
fm->selected = idx;
|
||||
}
|
||||
}
|
||||
} 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;
|
||||
fm->selected = idx;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void filemanager_on_key(Window* win, const Montauk::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) {
|
||||
if (fm->pathbar_cursor > 0) {
|
||||
for (int i = fm->pathbar_cursor - 1; i < fm->pathbar_len - 1; i++)
|
||||
fm->pathbar_buf[i] = fm->pathbar_buf[i + 1];
|
||||
fm->pathbar_len--;
|
||||
fm->pathbar_cursor--;
|
||||
fm->pathbar_buf[fm->pathbar_len] = '\0';
|
||||
}
|
||||
} else if (key.scancode == 0x53) {
|
||||
if (fm->pathbar_cursor < fm->pathbar_len) {
|
||||
for (int i = fm->pathbar_cursor; i < fm->pathbar_len - 1; i++)
|
||||
fm->pathbar_buf[i] = fm->pathbar_buf[i + 1];
|
||||
fm->pathbar_len--;
|
||||
fm->pathbar_buf[fm->pathbar_len] = '\0';
|
||||
}
|
||||
} else if (key.scancode == 0x4B) {
|
||||
if (fm->pathbar_cursor > 0) fm->pathbar_cursor--;
|
||||
} else if (key.scancode == 0x4D) {
|
||||
if (fm->pathbar_cursor < fm->pathbar_len) fm->pathbar_cursor++;
|
||||
} else if (key.scancode == 0x47) {
|
||||
fm->pathbar_cursor = 0;
|
||||
} else if (key.scancode == 0x4F) {
|
||||
fm->pathbar_cursor = fm->pathbar_len;
|
||||
} else if (key.ascii >= 32 && key.ascii < 127 && fm->pathbar_len < 254) {
|
||||
for (int i = fm->pathbar_len; i > fm->pathbar_cursor; i--)
|
||||
fm->pathbar_buf[i] = fm->pathbar_buf[i - 1];
|
||||
fm->pathbar_buf[fm->pathbar_cursor] = key.ascii;
|
||||
fm->pathbar_cursor++;
|
||||
fm->pathbar_len++;
|
||||
fm->pathbar_buf[fm->pathbar_len] = '\0';
|
||||
}
|
||||
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
|
||||
if (fm->rename_cursor > 0) {
|
||||
for (int i = fm->rename_cursor - 1; i < fm->rename_len - 1; i++)
|
||||
fm->rename_buf[i] = fm->rename_buf[i + 1];
|
||||
fm->rename_len--;
|
||||
fm->rename_cursor--;
|
||||
fm->rename_buf[fm->rename_len] = '\0';
|
||||
}
|
||||
} else if (key.scancode == 0x53) {
|
||||
// Delete key
|
||||
if (fm->rename_cursor < fm->rename_len) {
|
||||
for (int i = fm->rename_cursor; i < fm->rename_len - 1; i++)
|
||||
fm->rename_buf[i] = fm->rename_buf[i + 1];
|
||||
fm->rename_len--;
|
||||
fm->rename_buf[fm->rename_len] = '\0';
|
||||
}
|
||||
} else if (key.scancode == 0x4B) {
|
||||
// Left arrow
|
||||
if (fm->rename_cursor > 0) fm->rename_cursor--;
|
||||
} else if (key.scancode == 0x4D) {
|
||||
// Right arrow
|
||||
if (fm->rename_cursor < fm->rename_len) fm->rename_cursor++;
|
||||
} else if (key.scancode == 0x47) {
|
||||
// Home
|
||||
fm->rename_cursor = 0;
|
||||
} else if (key.scancode == 0x4F) {
|
||||
// End
|
||||
fm->rename_cursor = fm->rename_len;
|
||||
} else if (key.ascii >= 32 && key.ascii < 127) {
|
||||
// Printable character (reject / and other FS-unsafe chars)
|
||||
if (key.ascii != '/' && key.ascii != '\\' && fm->rename_len < 62) {
|
||||
for (int i = fm->rename_len; i > fm->rename_cursor; i--)
|
||||
fm->rename_buf[i] = fm->rename_buf[i - 1];
|
||||
fm->rename_buf[fm->rename_cursor] = key.ascii;
|
||||
fm->rename_cursor++;
|
||||
fm->rename_len++;
|
||||
fm->rename_buf[fm->rename_len] = '\0';
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// ---- Context menu: Escape to close ----
|
||||
if (fm->ctx_open && key.scancode == 0x01) {
|
||||
filemanager_close_ctx_menu(fm);
|
||||
return;
|
||||
}
|
||||
|
||||
// ---- Keyboard shortcuts ----
|
||||
if (key.ctrl) {
|
||||
if (key.ascii == 'c' || key.ascii == 'C') {
|
||||
filemanager_do_copy(fm);
|
||||
return;
|
||||
}
|
||||
if (key.ascii == 'x' || key.ascii == 'X') {
|
||||
filemanager_do_cut(fm);
|
||||
return;
|
||||
}
|
||||
if (key.ascii == 'v' || key.ascii == 'V') {
|
||||
filemanager_do_paste(fm);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// F2 = rename
|
||||
if (key.scancode == 0x3C) {
|
||||
filemanager_start_rename(fm);
|
||||
return;
|
||||
}
|
||||
|
||||
if (key.ascii == '\b' || key.scancode == 0x0E) {
|
||||
filemanager_go_up(fm);
|
||||
} else if (key.scancode == 0x48) {
|
||||
// Up arrow
|
||||
if (fm->grid_view) {
|
||||
Rect cr_r = win->content_rect();
|
||||
int cols = (cr_r.w - FM_SCROLLBAR_W) / FM_GRID_CELL_W;
|
||||
if (cols < 1) cols = 1;
|
||||
if (fm->selected >= cols) fm->selected -= cols;
|
||||
} else {
|
||||
if (fm->selected > 0) fm->selected--;
|
||||
}
|
||||
} else if (key.scancode == 0x50) {
|
||||
// Down arrow
|
||||
if (fm->grid_view) {
|
||||
Rect cr_r = win->content_rect();
|
||||
int cols = (cr_r.w - FM_SCROLLBAR_W) / FM_GRID_CELL_W;
|
||||
if (cols < 1) cols = 1;
|
||||
if (fm->selected + cols < fm->entry_count) fm->selected += cols;
|
||||
} else {
|
||||
if (fm->selected < fm->entry_count - 1) fm->selected++;
|
||||
}
|
||||
} else if (key.scancode == 0x4B && !key.alt && fm->grid_view) {
|
||||
// Left arrow (grid view only)
|
||||
if (fm->selected > 0) fm->selected--;
|
||||
} else if (key.scancode == 0x4D && !key.alt && fm->grid_view) {
|
||||
// Right arrow (grid view only)
|
||||
if (fm->selected < fm->entry_count - 1) fm->selected++;
|
||||
} else if (key.ascii == '\n' || key.ascii == '\r') {
|
||||
if (fm->selected >= 0 && fm->selected < fm->entry_count)
|
||||
filemanager_open_entry(fm, fm->selected);
|
||||
} else if (key.scancode == 0x53) {
|
||||
// Delete key
|
||||
filemanager_delete_selected(fm);
|
||||
} else if (key.alt && key.scancode == 0x4B) {
|
||||
// Alt+Left: go back
|
||||
filemanager_go_back(fm);
|
||||
} else if (key.alt && key.scancode == 0x4D) {
|
||||
// Alt+Right: go forward
|
||||
filemanager_go_forward(fm);
|
||||
}
|
||||
}
|
||||
|
||||
void filemanager_on_close(Window* win) {
|
||||
if (win->app_data) {
|
||||
FileManagerState* fm = (FileManagerState*)win->app_data;
|
||||
filemanager_free_app_icons(fm);
|
||||
montauk::mfree(win->app_data);
|
||||
win->app_data = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace filemanager
|
||||
@@ -0,0 +1,160 @@
|
||||
/*
|
||||
* filemanager_internal.hpp
|
||||
* Shared declarations for the embedded desktop file manager
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "../apps_common.hpp"
|
||||
|
||||
namespace filemanager {
|
||||
|
||||
inline constexpr int FM_MAX_DRIVES = 16;
|
||||
|
||||
struct FileManagerState {
|
||||
char current_path[256];
|
||||
char history[16][256];
|
||||
int history_pos;
|
||||
int history_count;
|
||||
char entry_names[64][64];
|
||||
int entry_types[64]; // 0=file, 1=dir, 2=exec, 3=drive, 4=home, 5=apps, 6=app, 7=special_dir
|
||||
int entry_sizes[64];
|
||||
int entry_count;
|
||||
int selected;
|
||||
int scroll_offset;
|
||||
bool is_dir[64];
|
||||
int last_click_item;
|
||||
uint64_t last_click_time;
|
||||
Scrollbar scrollbar;
|
||||
DesktopState* desktop;
|
||||
bool grid_view;
|
||||
bool at_drives_root;
|
||||
int drive_indices[FM_MAX_DRIVES]; // which drive number each entry maps to
|
||||
|
||||
// Clipboard
|
||||
char clipboard_path[256];
|
||||
bool clipboard_has_data;
|
||||
bool clipboard_is_cut;
|
||||
|
||||
// Context menu
|
||||
bool ctx_open;
|
||||
int ctx_x, ctx_y;
|
||||
int ctx_target_idx; // -1 = background
|
||||
int ctx_items[8];
|
||||
int ctx_item_count;
|
||||
int ctx_hover;
|
||||
|
||||
// Rename
|
||||
bool rename_active;
|
||||
int rename_idx;
|
||||
char rename_buf[64];
|
||||
int rename_cursor;
|
||||
int rename_len;
|
||||
|
||||
// Path bar editing
|
||||
bool pathbar_editing;
|
||||
char pathbar_buf[256];
|
||||
int pathbar_cursor;
|
||||
int pathbar_len;
|
||||
|
||||
// Apps view
|
||||
bool at_apps_view;
|
||||
int app_map[16]; // entry index -> external_apps[] index
|
||||
SvgIcon app_icons_lg[16]; // 48x48 icons for grid view
|
||||
SvgIcon app_icons_sm[16]; // 16x16 icons for list view
|
||||
int app_icon_count;
|
||||
};
|
||||
|
||||
inline constexpr int FM_TOOLBAR_H = 32;
|
||||
inline constexpr int FM_PATHBAR_H = 24;
|
||||
inline constexpr int FM_HEADER_H = 20;
|
||||
inline constexpr int FM_ITEM_H = 24;
|
||||
inline constexpr int FM_SCROLLBAR_W = 12;
|
||||
inline constexpr int FM_GRID_CELL_W = 80;
|
||||
inline constexpr int FM_GRID_CELL_H = 80;
|
||||
inline constexpr int FM_GRID_ICON = 48;
|
||||
inline constexpr int FM_GRID_PAD = 4;
|
||||
|
||||
// Special user folders: name, icon filename, index into DesktopState arrays
|
||||
inline constexpr int SF_COUNT = DesktopState::SPECIAL_FOLDER_COUNT;
|
||||
inline constexpr const char* sf_names[SF_COUNT] = {
|
||||
"Documents", "Desktop", "Music", "Videos", "Pictures", "Downloads"
|
||||
};
|
||||
inline constexpr const char* sf_icons[SF_COUNT] = {
|
||||
"folder-blue-documents.svg", "folder-blue-desktop.svg",
|
||||
"folder-blue-music.svg", "folder-blue-videos.svg",
|
||||
"folder-blue-pictures.svg", "folder-blue-downloads.svg"
|
||||
};
|
||||
|
||||
// Context menu action codes
|
||||
inline constexpr int CTX_OPEN = 0;
|
||||
inline constexpr int CTX_COPY = 1;
|
||||
inline constexpr int CTX_CUT = 2;
|
||||
inline constexpr int CTX_PASTE = 3;
|
||||
inline constexpr int CTX_RENAME = 4;
|
||||
inline constexpr int CTX_DELETE = 5;
|
||||
inline constexpr int CTX_NEW_FOLDER = 6;
|
||||
|
||||
inline constexpr int CTX_MENU_W = 140;
|
||||
inline constexpr int CTX_ITEM_H = 24;
|
||||
|
||||
int special_folder_index(const char* name);
|
||||
const char* ctx_label(int action);
|
||||
|
||||
bool str_ends_with(const char* s, const char* suffix);
|
||||
bool is_image_file(const char* name);
|
||||
bool is_font_file(const char* name);
|
||||
bool is_pdf_file(const char* name);
|
||||
bool is_spreadsheet_file(const char* name);
|
||||
bool is_wordprocessor_file(const char* name);
|
||||
bool is_audio_file(const char* name);
|
||||
bool is_video_file(const char* name);
|
||||
int detect_file_type(const char* name, bool is_dir);
|
||||
|
||||
void filemanager_build_fullpath(char* out, int out_max, const char* dir, const char* name);
|
||||
const char* path_basename(const char* path);
|
||||
|
||||
void filemanager_read_drives(FileManagerState* fm);
|
||||
void filemanager_read_dir(FileManagerState* fm);
|
||||
void filemanager_free_app_icons(FileManagerState* fm);
|
||||
void filemanager_read_apps(FileManagerState* fm);
|
||||
|
||||
bool filemanager_delete_recursive(const char* path);
|
||||
bool filemanager_copy_file(const char* src, const char* dst);
|
||||
bool filemanager_copy_dir_recursive(const char* src, const char* dst);
|
||||
|
||||
void filemanager_do_copy(FileManagerState* fm);
|
||||
void filemanager_do_cut(FileManagerState* fm);
|
||||
void filemanager_do_paste(FileManagerState* fm);
|
||||
|
||||
void filemanager_start_rename(FileManagerState* fm);
|
||||
void filemanager_finish_rename(FileManagerState* fm);
|
||||
void filemanager_cancel_rename(FileManagerState* fm);
|
||||
void filemanager_new_folder(FileManagerState* fm);
|
||||
void filemanager_delete_selected(FileManagerState* fm);
|
||||
|
||||
void filemanager_open_ctx_menu(FileManagerState* fm, int local_x, int local_y, int target_idx);
|
||||
void filemanager_close_ctx_menu(FileManagerState* fm);
|
||||
|
||||
void filemanager_push_history(FileManagerState* fm);
|
||||
void filemanager_navigate(FileManagerState* fm, const char* name);
|
||||
void filemanager_go_up(FileManagerState* fm);
|
||||
void filemanager_navigate_to_history(FileManagerState* fm);
|
||||
void filemanager_go_back(FileManagerState* fm);
|
||||
void filemanager_go_forward(FileManagerState* fm);
|
||||
void filemanager_go_home(FileManagerState* fm);
|
||||
|
||||
void filemanager_start_pathbar(FileManagerState* fm);
|
||||
void filemanager_cancel_pathbar(FileManagerState* fm);
|
||||
void filemanager_commit_pathbar(FileManagerState* fm);
|
||||
|
||||
void filemanager_open_entry(FileManagerState* fm, int idx);
|
||||
|
||||
void filemanager_draw_header(Canvas& c, FileManagerState* fm, Color toolbar_color, Color btn_bg);
|
||||
void filemanager_on_draw(Window* win, Framebuffer& fb);
|
||||
void filemanager_on_mouse(Window* win, MouseEvent& ev);
|
||||
void filemanager_on_key(Window* win, const Montauk::KeyEvent& key);
|
||||
void filemanager_on_close(Window* win);
|
||||
|
||||
} // namespace filemanager
|
||||
@@ -0,0 +1,469 @@
|
||||
/*
|
||||
* filesystem.cpp
|
||||
* Filesystem enumeration and copy/delete helpers for the embedded desktop file manager
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#include "filemanager_internal.hpp"
|
||||
#include <montauk/toml.h>
|
||||
|
||||
namespace filemanager {
|
||||
|
||||
void filemanager_read_drives(FileManagerState* fm) {
|
||||
fm->entry_count = 0;
|
||||
fm->at_drives_root = true;
|
||||
fm->current_path[0] = '\0';
|
||||
|
||||
// Home folder entry (if we have a valid home directory)
|
||||
DesktopState* ds = fm->desktop;
|
||||
if (ds && ds->home_dir[0] != '\0') {
|
||||
int i = fm->entry_count;
|
||||
montauk::strncpy(fm->entry_names[i], "Home", 63);
|
||||
fm->entry_types[i] = 4; // home
|
||||
fm->entry_sizes[i] = 0;
|
||||
fm->is_dir[i] = true;
|
||||
fm->drive_indices[i] = -1;
|
||||
fm->entry_count++;
|
||||
}
|
||||
|
||||
// Apps entry
|
||||
{
|
||||
int i = fm->entry_count;
|
||||
montauk::strncpy(fm->entry_names[i], "Apps", 63);
|
||||
fm->entry_types[i] = 5; // apps
|
||||
fm->entry_sizes[i] = 0;
|
||||
fm->is_dir[i] = true;
|
||||
fm->drive_indices[i] = -1;
|
||||
fm->entry_count++;
|
||||
}
|
||||
|
||||
// Special user folders (Documents, Desktop, Music, etc.) - only if they exist
|
||||
if (ds && ds->home_dir[0] != '\0') {
|
||||
for (int sf = 0; sf < SF_COUNT && fm->entry_count < 64; sf++) {
|
||||
char probe_path[256];
|
||||
montauk::strcpy(probe_path, ds->home_dir);
|
||||
int plen = montauk::slen(probe_path);
|
||||
if (plen > 0 && probe_path[plen - 1] != '/')
|
||||
str_append(probe_path, "/", 256);
|
||||
str_append(probe_path, sf_names[sf], 256);
|
||||
|
||||
// Probe: try open to check if the directory exists
|
||||
int probe_fd = montauk::open(probe_path);
|
||||
if (probe_fd >= 0) {
|
||||
montauk::close(probe_fd);
|
||||
int i = fm->entry_count;
|
||||
montauk::strncpy(fm->entry_names[i], sf_names[sf], 63);
|
||||
fm->entry_types[i] = 7; // special_dir
|
||||
fm->entry_sizes[i] = 0;
|
||||
fm->is_dir[i] = true;
|
||||
fm->drive_indices[i] = sf; // special folder index
|
||||
fm->entry_count++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Drive entries
|
||||
int drives[FM_MAX_DRIVES];
|
||||
int driveCount = montauk::drivelist(drives, FM_MAX_DRIVES);
|
||||
|
||||
for (int di = 0; di < driveCount; di++) {
|
||||
int d = drives[di];
|
||||
int i = fm->entry_count;
|
||||
char probe[8];
|
||||
if (d < 10) {
|
||||
probe[0] = '0' + d;
|
||||
probe[1] = ':';
|
||||
probe[2] = '/';
|
||||
probe[3] = '\0';
|
||||
} else {
|
||||
probe[0] = '1';
|
||||
probe[1] = '0' + (d - 10);
|
||||
probe[2] = ':';
|
||||
probe[3] = '/';
|
||||
probe[4] = '\0';
|
||||
}
|
||||
char label[64];
|
||||
montauk::strcpy(label, "Drive ");
|
||||
str_append(label, probe, 64);
|
||||
montauk::strncpy(fm->entry_names[i], label, 63);
|
||||
fm->entry_types[i] = 3; // drive
|
||||
fm->entry_sizes[i] = 0;
|
||||
fm->is_dir[i] = true;
|
||||
fm->drive_indices[i] = d;
|
||||
fm->entry_count++;
|
||||
if (fm->entry_count >= 64) break;
|
||||
}
|
||||
|
||||
fm->selected = -1;
|
||||
fm->scroll_offset = 0;
|
||||
fm->scrollbar.scroll_offset = 0;
|
||||
fm->last_click_item = -1;
|
||||
fm->last_click_time = 0;
|
||||
}
|
||||
|
||||
void filemanager_read_dir(FileManagerState* fm) {
|
||||
fm->at_drives_root = false;
|
||||
if (fm->at_apps_view) {
|
||||
fm->at_apps_view = false;
|
||||
filemanager_free_app_icons(fm);
|
||||
}
|
||||
const char* names[64];
|
||||
fm->entry_count = montauk::readdir(fm->current_path, names, 64);
|
||||
if (fm->entry_count < 0) fm->entry_count = 0;
|
||||
|
||||
// readdir returns full paths from the VFS (e.g. "man/fetch.1" instead
|
||||
// of just "fetch.1"). Compute the prefix to strip so we get basenames.
|
||||
const char* after_drive = fm->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] = {0};
|
||||
int prefix_len = 0;
|
||||
if (after_drive[0] != '\0') {
|
||||
montauk::strcpy(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 < fm->entry_count; i++) {
|
||||
const char* raw = names[i];
|
||||
// Strip directory prefix if it matches
|
||||
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;
|
||||
}
|
||||
montauk::strncpy(fm->entry_names[i], raw, 63);
|
||||
int len = montauk::slen(fm->entry_names[i]);
|
||||
|
||||
// Detect directory
|
||||
if (len > 0 && fm->entry_names[i][len - 1] == '/') {
|
||||
fm->is_dir[i] = true;
|
||||
fm->entry_names[i][len - 1] = '\0';
|
||||
} else {
|
||||
fm->is_dir[i] = false;
|
||||
}
|
||||
|
||||
fm->entry_types[i] = detect_file_type(fm->entry_names[i], fm->is_dir[i]);
|
||||
|
||||
// Get file size
|
||||
fm->entry_sizes[i] = 0;
|
||||
if (!fm->is_dir[i]) {
|
||||
char fullpath[512];
|
||||
montauk::strcpy(fullpath, fm->current_path);
|
||||
int plen = montauk::slen(fullpath);
|
||||
if (plen > 0 && fullpath[plen - 1] != '/') {
|
||||
str_append(fullpath, "/", 512);
|
||||
}
|
||||
str_append(fullpath, fm->entry_names[i], 512);
|
||||
int fd = montauk::open(fullpath);
|
||||
if (fd >= 0) {
|
||||
fm->entry_sizes[i] = (int)montauk::getsize(fd);
|
||||
montauk::close(fd);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sort: directories first, then alphabetical (case-insensitive)
|
||||
for (int i = 1; i < fm->entry_count; i++) {
|
||||
char tmp_name[64];
|
||||
int tmp_type = fm->entry_types[i];
|
||||
int tmp_size = fm->entry_sizes[i];
|
||||
bool tmp_isdir = fm->is_dir[i];
|
||||
montauk::strcpy(tmp_name, fm->entry_names[i]);
|
||||
|
||||
int j = i - 1;
|
||||
while (j >= 0) {
|
||||
bool swap = false;
|
||||
if (tmp_isdir && !fm->is_dir[j]) {
|
||||
swap = true;
|
||||
} else if (tmp_isdir == fm->is_dir[j]) {
|
||||
if (str_compare_ci(tmp_name, fm->entry_names[j]) < 0) {
|
||||
swap = true;
|
||||
}
|
||||
}
|
||||
if (!swap) break;
|
||||
|
||||
montauk::strcpy(fm->entry_names[j + 1], fm->entry_names[j]);
|
||||
fm->entry_types[j + 1] = fm->entry_types[j];
|
||||
fm->entry_sizes[j + 1] = fm->entry_sizes[j];
|
||||
fm->is_dir[j + 1] = fm->is_dir[j];
|
||||
j--;
|
||||
}
|
||||
montauk::strcpy(fm->entry_names[j + 1], tmp_name);
|
||||
fm->entry_types[j + 1] = tmp_type;
|
||||
fm->entry_sizes[j + 1] = tmp_size;
|
||||
fm->is_dir[j + 1] = tmp_isdir;
|
||||
}
|
||||
|
||||
fm->selected = -1;
|
||||
fm->scroll_offset = 0;
|
||||
fm->scrollbar.scroll_offset = 0;
|
||||
fm->last_click_item = -1;
|
||||
fm->last_click_time = 0;
|
||||
}
|
||||
|
||||
void filemanager_free_app_icons(FileManagerState* fm) {
|
||||
for (int i = 0; i < fm->app_icon_count; i++) {
|
||||
if (fm->app_icons_lg[i].pixels) {
|
||||
montauk::mfree(fm->app_icons_lg[i].pixels);
|
||||
fm->app_icons_lg[i].pixels = nullptr;
|
||||
}
|
||||
if (fm->app_icons_sm[i].pixels) {
|
||||
montauk::mfree(fm->app_icons_sm[i].pixels);
|
||||
fm->app_icons_sm[i].pixels = nullptr;
|
||||
}
|
||||
}
|
||||
fm->app_icon_count = 0;
|
||||
}
|
||||
|
||||
void filemanager_read_apps(FileManagerState* fm) {
|
||||
filemanager_free_app_icons(fm);
|
||||
|
||||
fm->at_drives_root = false;
|
||||
fm->at_apps_view = true;
|
||||
fm->entry_count = 0;
|
||||
|
||||
DesktopState* ds = fm->desktop;
|
||||
if (!ds) return;
|
||||
|
||||
// Scan manifests directly (like desktop_scan_apps but load icons at FM sizes)
|
||||
montauk::fmkdir("0:/apps");
|
||||
const char* entries[32];
|
||||
int count = montauk::readdir("0:/apps", entries, 32);
|
||||
|
||||
// Extract basenames from readdir results (strip "apps/" prefix)
|
||||
for (int i = 0; i < count && fm->entry_count < 16; i++) {
|
||||
const char* raw = entries[i];
|
||||
// Strip "apps/" prefix
|
||||
if (raw[0] == 'a' && raw[1] == 'p' && raw[2] == 'p' && raw[3] == 's' && raw[4] == '/')
|
||||
raw += 5;
|
||||
char dirname[64];
|
||||
montauk::strncpy(dirname, raw, 63);
|
||||
int dlen = montauk::slen(dirname);
|
||||
if (dlen > 0 && dirname[dlen - 1] == '/') dirname[dlen - 1] = '\0';
|
||||
if (dirname[0] == '\0') continue;
|
||||
|
||||
// Open manifest
|
||||
char manifest_path[128];
|
||||
snprintf(manifest_path, 128, "0:/apps/%s/manifest.toml", dirname);
|
||||
int fh = montauk::open(manifest_path);
|
||||
if (fh < 0) continue;
|
||||
|
||||
uint64_t sz = montauk::getsize(fh);
|
||||
if (sz == 0 || sz > 4096) { montauk::close(fh); continue; }
|
||||
|
||||
char* text = (char*)montauk::malloc(sz + 1);
|
||||
montauk::read(fh, (uint8_t*)text, 0, sz);
|
||||
montauk::close(fh);
|
||||
text[sz] = '\0';
|
||||
|
||||
auto doc = montauk::toml::parse(text);
|
||||
montauk::mfree(text);
|
||||
|
||||
const char* name = doc.get_string("app.name", "Unknown");
|
||||
const char* binary = doc.get_string("app.binary", "");
|
||||
const char* icon_file = doc.get_string("app.icon", "");
|
||||
|
||||
int idx = fm->entry_count;
|
||||
montauk::strncpy(fm->entry_names[idx], name, 63);
|
||||
fm->entry_types[idx] = 6; // app entry
|
||||
fm->entry_sizes[idx] = 0;
|
||||
fm->is_dir[idx] = false;
|
||||
|
||||
// Store binary path in drive_indices as an app_map index
|
||||
fm->app_map[idx] = -1;
|
||||
// Find matching external_app by binary path
|
||||
char bin_path[128];
|
||||
snprintf(bin_path, 128, "0:/apps/%s/%s", dirname, binary);
|
||||
for (int x = 0; x < ds->external_app_count; x++) {
|
||||
if (montauk::streq(ds->external_apps[x].binary_path, bin_path)) {
|
||||
fm->app_map[idx] = x;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Load icons at file manager sizes
|
||||
fm->app_icons_lg[idx] = {};
|
||||
fm->app_icons_sm[idx] = {};
|
||||
if (icon_file[0]) {
|
||||
char icon_path[128];
|
||||
snprintf(icon_path, 128, "0:/apps/%s/%s", dirname, icon_file);
|
||||
Color defColor = colors::ICON_COLOR;
|
||||
fm->app_icons_lg[idx] = svg_load(icon_path, 48, 48, defColor);
|
||||
fm->app_icons_sm[idx] = svg_load(icon_path, 16, 16, defColor);
|
||||
}
|
||||
|
||||
doc.destroy();
|
||||
fm->entry_count++;
|
||||
fm->app_icon_count = fm->entry_count;
|
||||
}
|
||||
|
||||
fm->selected = -1;
|
||||
fm->scroll_offset = 0;
|
||||
fm->scrollbar.scroll_offset = 0;
|
||||
fm->last_click_item = -1;
|
||||
fm->last_click_time = 0;
|
||||
}
|
||||
|
||||
bool filemanager_delete_recursive(const char* path) {
|
||||
// Try deleting as a file (or empty directory) first
|
||||
if (montauk::fdelete(path) == 0) return true;
|
||||
|
||||
// If that failed, it may be a non-empty directory -- enumerate and delete children
|
||||
const char* names[64];
|
||||
int count = montauk::readdir(path, names, 64);
|
||||
if (count < 0) return false;
|
||||
|
||||
// Compute the prefix to strip (readdir returns paths relative to drive root)
|
||||
const char* after_drive = 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] = {0};
|
||||
int prefix_len = 0;
|
||||
if (after_drive[0] != '\0') {
|
||||
montauk::strcpy(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 < 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;
|
||||
}
|
||||
|
||||
char child[512];
|
||||
montauk::strcpy(child, path);
|
||||
int plen = montauk::slen(child);
|
||||
if (plen > 0 && child[plen - 1] != '/')
|
||||
str_append(child, "/", 512);
|
||||
str_append(child, raw, 512);
|
||||
|
||||
// Strip trailing slash if present (directory marker)
|
||||
int clen = montauk::slen(child);
|
||||
if (clen > 0 && child[clen - 1] == '/') child[clen - 1] = '\0';
|
||||
|
||||
filemanager_delete_recursive(child);
|
||||
}
|
||||
|
||||
// Now the directory should be empty -- delete it
|
||||
return montauk::fdelete(path) == 0;
|
||||
}
|
||||
|
||||
bool filemanager_copy_file(const char* src, const char* dst) {
|
||||
int sfd = montauk::open(src);
|
||||
if (sfd < 0) return false;
|
||||
int size = (int)montauk::getsize(sfd);
|
||||
|
||||
// Create destination
|
||||
int dfd = montauk::fcreate(dst);
|
||||
if (dfd < 0) {
|
||||
montauk::close(sfd);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (size > 0) {
|
||||
// Cap at 4 MB to avoid exhausting memory
|
||||
if (size > 4 * 1024 * 1024) {
|
||||
montauk::close(sfd);
|
||||
montauk::close(dfd);
|
||||
montauk::fdelete(dst);
|
||||
return false;
|
||||
}
|
||||
uint8_t* buf = (uint8_t*)montauk::malloc(size);
|
||||
if (!buf) {
|
||||
montauk::close(sfd);
|
||||
montauk::close(dfd);
|
||||
montauk::fdelete(dst);
|
||||
return false;
|
||||
}
|
||||
montauk::read(sfd, buf, 0, size);
|
||||
montauk::fwrite(dfd, buf, 0, size);
|
||||
montauk::mfree(buf);
|
||||
}
|
||||
|
||||
montauk::close(sfd);
|
||||
montauk::close(dfd);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Recursive directory copy
|
||||
bool filemanager_copy_dir_recursive(const char* src, const char* dst) {
|
||||
montauk::fmkdir(dst);
|
||||
|
||||
const char* names[64];
|
||||
int count = montauk::readdir(src, names, 64);
|
||||
if (count < 0) return false;
|
||||
|
||||
// Compute prefix to strip
|
||||
const char* after_drive = src;
|
||||
for (int k = 0; after_drive[k]; k++) {
|
||||
if (after_drive[k] == ':' && after_drive[k + 1] == '/') {
|
||||
after_drive += k + 2;
|
||||
break;
|
||||
}
|
||||
}
|
||||
char prefix[256] = {0};
|
||||
int prefix_len = 0;
|
||||
if (after_drive[0] != '\0') {
|
||||
montauk::strcpy(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 < 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;
|
||||
}
|
||||
|
||||
bool child_is_dir = false;
|
||||
char basename[64];
|
||||
montauk::strncpy(basename, raw, 63);
|
||||
int blen = montauk::slen(basename);
|
||||
if (blen > 0 && basename[blen - 1] == '/') {
|
||||
child_is_dir = true;
|
||||
basename[blen - 1] = '\0';
|
||||
}
|
||||
|
||||
char src_child[512], dst_child[512];
|
||||
filemanager_build_fullpath(src_child, 512, src, basename);
|
||||
filemanager_build_fullpath(dst_child, 512, dst, basename);
|
||||
|
||||
if (child_is_dir)
|
||||
filemanager_copy_dir_recursive(src_child, dst_child);
|
||||
else
|
||||
filemanager_copy_file(src_child, dst_child);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace filemanager
|
||||
@@ -0,0 +1,95 @@
|
||||
/*
|
||||
* helpers.cpp
|
||||
* Shared helpers for the embedded desktop file manager
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#include "filemanager_internal.hpp"
|
||||
|
||||
namespace filemanager {
|
||||
|
||||
int special_folder_index(const char* name) {
|
||||
for (int i = 0; i < SF_COUNT; i++) {
|
||||
if (montauk::streq(name, sf_names[i])) return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
const char* ctx_label(int action) {
|
||||
switch (action) {
|
||||
case CTX_OPEN: return "Open";
|
||||
case CTX_COPY: return "Copy";
|
||||
case CTX_CUT: return "Cut";
|
||||
case CTX_PASTE: return "Paste";
|
||||
case CTX_RENAME: return "Rename";
|
||||
case CTX_DELETE: return "Delete";
|
||||
case CTX_NEW_FOLDER: return "New Folder";
|
||||
default: return "?";
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
bool is_image_file(const char* name) {
|
||||
return str_ends_with(name, ".jpg") || str_ends_with(name, ".jpeg");
|
||||
}
|
||||
|
||||
bool is_font_file(const char* name) {
|
||||
return str_ends_with(name, ".ttf");
|
||||
}
|
||||
|
||||
bool is_pdf_file(const char* name) {
|
||||
return str_ends_with(name, ".pdf");
|
||||
}
|
||||
|
||||
bool is_spreadsheet_file(const char* name) {
|
||||
return str_ends_with(name, ".mss");
|
||||
}
|
||||
|
||||
bool is_wordprocessor_file(const char* name) {
|
||||
return str_ends_with(name, ".mwp");
|
||||
}
|
||||
|
||||
bool is_audio_file(const char* name) {
|
||||
return str_ends_with(name, ".mp3") || str_ends_with(name, ".wav");
|
||||
}
|
||||
|
||||
bool is_video_file(const char* name) {
|
||||
return str_ends_with(name, ".avi");
|
||||
}
|
||||
|
||||
int detect_file_type(const char* name, bool is_dir) {
|
||||
if (is_dir) return 1;
|
||||
if (str_ends_with(name, ".elf")) return 2;
|
||||
return 0;
|
||||
}
|
||||
|
||||
void filemanager_build_fullpath(char* out, int out_max, const char* dir, const char* name) {
|
||||
montauk::strcpy(out, dir);
|
||||
int plen = montauk::slen(out);
|
||||
if (plen > 0 && out[plen - 1] != '/') str_append(out, "/", out_max);
|
||||
str_append(out, name, out_max);
|
||||
}
|
||||
|
||||
// Extract basename from a path (pointer into the path string)
|
||||
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;
|
||||
}
|
||||
|
||||
} // namespace filemanager
|
||||
@@ -0,0 +1,233 @@
|
||||
/*
|
||||
* navigation.cpp
|
||||
* Navigation and file opening logic for the embedded desktop file manager
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#include "filemanager_internal.hpp"
|
||||
|
||||
namespace filemanager {
|
||||
|
||||
void filemanager_push_history(FileManagerState* fm) {
|
||||
// Don't push if same as current position
|
||||
if (fm->history_count > 0 && fm->history_pos >= 0) {
|
||||
if (montauk::streq(fm->history[fm->history_pos], fm->current_path)) return;
|
||||
}
|
||||
fm->history_pos++;
|
||||
if (fm->history_pos >= 16) fm->history_pos = 15;
|
||||
montauk::strcpy(fm->history[fm->history_pos], fm->current_path);
|
||||
fm->history_count = fm->history_pos + 1;
|
||||
}
|
||||
|
||||
void filemanager_navigate(FileManagerState* fm, const char* name) {
|
||||
int path_len = montauk::slen(fm->current_path);
|
||||
if (path_len > 0 && fm->current_path[path_len - 1] != '/') {
|
||||
str_append(fm->current_path, "/", 256);
|
||||
}
|
||||
str_append(fm->current_path, name, 256);
|
||||
filemanager_push_history(fm);
|
||||
filemanager_read_dir(fm);
|
||||
}
|
||||
|
||||
void filemanager_go_up(FileManagerState* fm) {
|
||||
if (fm->at_drives_root) return;
|
||||
|
||||
// From apps view, go back to Computer
|
||||
if (fm->at_apps_view) {
|
||||
fm->at_apps_view = false;
|
||||
filemanager_free_app_icons(fm);
|
||||
filemanager_read_drives(fm);
|
||||
filemanager_push_history(fm);
|
||||
return;
|
||||
}
|
||||
|
||||
int len = montauk::slen(fm->current_path);
|
||||
|
||||
// At drive root (e.g. "0:/" or "10:/") -- go to drives view
|
||||
bool is_drive_root = false;
|
||||
if (len >= 3 && fm->current_path[len - 1] == '/') {
|
||||
// Check if path is just "N:/" or "NN:/"
|
||||
int colon = -1;
|
||||
for (int i = 0; i < len; i++) {
|
||||
if (fm->current_path[i] == ':') { colon = i; break; }
|
||||
}
|
||||
if (colon >= 0 && colon + 2 == len) is_drive_root = true;
|
||||
}
|
||||
if (is_drive_root) {
|
||||
filemanager_read_drives(fm);
|
||||
filemanager_push_history(fm);
|
||||
return;
|
||||
}
|
||||
|
||||
if (len > 0 && fm->current_path[len - 1] == '/') {
|
||||
fm->current_path[len - 1] = '\0';
|
||||
len--;
|
||||
}
|
||||
|
||||
int last_slash = -1;
|
||||
for (int i = len - 1; i >= 0; i--) {
|
||||
if (fm->current_path[i] == '/') { last_slash = i; break; }
|
||||
}
|
||||
if (last_slash >= 0) {
|
||||
// Keep the slash only if it's the root slash (right after "N:")
|
||||
if (last_slash > 0 && fm->current_path[last_slash - 1] == ':') {
|
||||
fm->current_path[last_slash + 1] = '\0';
|
||||
} else {
|
||||
fm->current_path[last_slash] = '\0';
|
||||
}
|
||||
}
|
||||
filemanager_push_history(fm);
|
||||
filemanager_read_dir(fm);
|
||||
}
|
||||
|
||||
void filemanager_navigate_to_history(FileManagerState* fm) {
|
||||
montauk::strcpy(fm->current_path, fm->history[fm->history_pos]);
|
||||
if (fm->current_path[0] == '\0') {
|
||||
filemanager_read_drives(fm);
|
||||
} else {
|
||||
filemanager_read_dir(fm);
|
||||
}
|
||||
}
|
||||
|
||||
void filemanager_go_back(FileManagerState* fm) {
|
||||
if (fm->history_pos <= 0) return;
|
||||
fm->history_pos--;
|
||||
filemanager_navigate_to_history(fm);
|
||||
}
|
||||
|
||||
void filemanager_go_forward(FileManagerState* fm) {
|
||||
if (fm->history_pos >= fm->history_count - 1) return;
|
||||
fm->history_pos++;
|
||||
filemanager_navigate_to_history(fm);
|
||||
}
|
||||
|
||||
void filemanager_go_home(FileManagerState* fm) {
|
||||
if (fm->at_apps_view) {
|
||||
fm->at_apps_view = false;
|
||||
filemanager_free_app_icons(fm);
|
||||
}
|
||||
filemanager_read_drives(fm);
|
||||
filemanager_push_history(fm);
|
||||
}
|
||||
|
||||
void filemanager_start_pathbar(FileManagerState* fm) {
|
||||
fm->pathbar_editing = true;
|
||||
if (fm->at_drives_root) {
|
||||
fm->pathbar_buf[0] = '\0';
|
||||
fm->pathbar_len = 0;
|
||||
} else {
|
||||
montauk::strcpy(fm->pathbar_buf, fm->current_path);
|
||||
fm->pathbar_len = montauk::slen(fm->pathbar_buf);
|
||||
}
|
||||
fm->pathbar_cursor = fm->pathbar_len;
|
||||
}
|
||||
|
||||
void filemanager_cancel_pathbar(FileManagerState* fm) {
|
||||
fm->pathbar_editing = false;
|
||||
}
|
||||
|
||||
void filemanager_commit_pathbar(FileManagerState* fm) {
|
||||
fm->pathbar_editing = false;
|
||||
if (fm->pathbar_len == 0) {
|
||||
filemanager_read_drives(fm);
|
||||
filemanager_push_history(fm);
|
||||
return;
|
||||
}
|
||||
montauk::strcpy(fm->current_path, fm->pathbar_buf);
|
||||
filemanager_push_history(fm);
|
||||
filemanager_read_dir(fm);
|
||||
}
|
||||
|
||||
void filemanager_open_entry(FileManagerState* fm, int idx) {
|
||||
if (idx < 0 || idx >= fm->entry_count) return;
|
||||
|
||||
// Special user folder in Computer view
|
||||
if (fm->at_drives_root && fm->entry_types[idx] == 7) {
|
||||
DesktopState* ds = fm->desktop;
|
||||
if (ds && ds->home_dir[0] != '\0') {
|
||||
montauk::strcpy(fm->current_path, ds->home_dir);
|
||||
int plen = montauk::slen(fm->current_path);
|
||||
if (plen > 0 && fm->current_path[plen - 1] != '/')
|
||||
str_append(fm->current_path, "/", 256);
|
||||
str_append(fm->current_path, fm->entry_names[idx], 256);
|
||||
filemanager_push_history(fm);
|
||||
filemanager_read_dir(fm);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Apps shortcut in Computer view
|
||||
if (fm->at_drives_root && fm->entry_types[idx] == 5) {
|
||||
filemanager_read_apps(fm);
|
||||
return;
|
||||
}
|
||||
|
||||
// App entry in apps view - launch it
|
||||
if (fm->at_apps_view && fm->entry_types[idx] == 6) {
|
||||
DesktopState* ds = fm->desktop;
|
||||
if (ds && fm->app_map[idx] >= 0 && fm->app_map[idx] < ds->external_app_count) {
|
||||
const ExternalApp& app = ds->external_apps[fm->app_map[idx]];
|
||||
if (app.launch_with_home) {
|
||||
montauk::spawn(app.binary_path, ds->home_dir);
|
||||
} else {
|
||||
montauk::spawn(app.binary_path);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (fm->at_drives_root && fm->entry_types[idx] == 4) {
|
||||
// Home folder
|
||||
DesktopState* ds = fm->desktop;
|
||||
if (ds && ds->home_dir[0] != '\0') {
|
||||
montauk::strcpy(fm->current_path, ds->home_dir);
|
||||
filemanager_push_history(fm);
|
||||
filemanager_read_dir(fm);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (fm->at_drives_root && fm->entry_types[idx] == 3) {
|
||||
int d = fm->drive_indices[idx];
|
||||
char dpath[8];
|
||||
if (d < 10) {
|
||||
dpath[0] = '0' + d; dpath[1] = ':'; dpath[2] = '/'; dpath[3] = '\0';
|
||||
} else {
|
||||
dpath[0] = '1'; dpath[1] = '0' + (d - 10); dpath[2] = ':'; dpath[3] = '/'; dpath[4] = '\0';
|
||||
}
|
||||
montauk::strcpy(fm->current_path, dpath);
|
||||
filemanager_push_history(fm);
|
||||
filemanager_read_dir(fm);
|
||||
return;
|
||||
}
|
||||
|
||||
if (fm->is_dir[idx]) {
|
||||
filemanager_navigate(fm, fm->entry_names[idx]);
|
||||
return;
|
||||
}
|
||||
|
||||
// Open file with appropriate app
|
||||
char fullpath[512];
|
||||
filemanager_build_fullpath(fullpath, 512, fm->current_path, fm->entry_names[idx]);
|
||||
if (is_image_file(fm->entry_names[idx])) {
|
||||
montauk::spawn("0:/apps/imageviewer/imageviewer.elf", fullpath);
|
||||
} else if (is_font_file(fm->entry_names[idx])) {
|
||||
montauk::spawn("0:/apps/fontpreview/fontpreview.elf", fullpath);
|
||||
} else if (is_pdf_file(fm->entry_names[idx])) {
|
||||
montauk::spawn("0:/apps/pdfviewer/pdfviewer.elf", fullpath);
|
||||
} else if (is_spreadsheet_file(fm->entry_names[idx])) {
|
||||
montauk::spawn("0:/apps/spreadsheet/spreadsheet.elf", fullpath);
|
||||
} else if (is_wordprocessor_file(fm->entry_names[idx])) {
|
||||
montauk::spawn("0:/apps/wordprocessor/wordprocessor.elf", fullpath);
|
||||
} else if (is_audio_file(fm->entry_names[idx])) {
|
||||
montauk::spawn("0:/apps/music/music.elf", fullpath);
|
||||
} else if (is_video_file(fm->entry_names[idx])) {
|
||||
montauk::spawn("0:/apps/video/video.elf", fullpath);
|
||||
} else if (str_ends_with(fm->entry_names[idx], ".elf")) {
|
||||
montauk::spawn(fullpath);
|
||||
} else {
|
||||
montauk::spawn("0:/apps/texteditor/texteditor.elf", fullpath);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace filemanager
|
||||
@@ -0,0 +1,421 @@
|
||||
/*
|
||||
* render.cpp
|
||||
* Drawing code for the embedded desktop file manager
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#include "filemanager_internal.hpp"
|
||||
|
||||
namespace filemanager {
|
||||
|
||||
void filemanager_draw_header(Canvas& c, FileManagerState* fm, Color toolbar_color, Color btn_bg) {
|
||||
DesktopState* ds = fm->desktop;
|
||||
|
||||
// ---- Toolbar (32px) ----
|
||||
c.fill_rect(0, 0, c.w, FM_TOOLBAR_H, toolbar_color);
|
||||
|
||||
// Navigation buttons: Back, Forward, Up, Home
|
||||
struct ToolBtn { int x; SvgIcon* icon; };
|
||||
ToolBtn nav_btns[4] = {
|
||||
{ 4, ds ? &ds->icon_go_back : nullptr },
|
||||
{ 32, ds ? &ds->icon_go_forward : nullptr },
|
||||
{ 60, ds ? &ds->icon_go_up : nullptr },
|
||||
{ 88, ds ? &ds->icon_home : nullptr },
|
||||
};
|
||||
|
||||
for (int i = 0; i < 4; i++) {
|
||||
int bx = nav_btns[i].x;
|
||||
int by = 4;
|
||||
c.fill_rect(bx, by, 24, 24, btn_bg);
|
||||
if (nav_btns[i].icon) {
|
||||
int ix = bx + (24 - nav_btns[i].icon->width) / 2;
|
||||
int iy = by + (24 - nav_btns[i].icon->height) / 2;
|
||||
c.icon(ix, iy, *nav_btns[i].icon);
|
||||
}
|
||||
}
|
||||
|
||||
// View toggle button
|
||||
{
|
||||
int bx = 120, by = 4;
|
||||
c.fill_rect(bx, by, 24, 24, btn_bg);
|
||||
if (fm->grid_view) {
|
||||
for (int r = 0; r < 2; r++)
|
||||
for (int cc = 0; cc < 2; cc++)
|
||||
c.fill_rect(bx + 5 + cc * 8, by + 5 + r * 8, 6, 6, colors::TEXT_COLOR);
|
||||
} else {
|
||||
for (int r = 0; r < 3; r++)
|
||||
c.fill_rect(bx + 5, by + 5 + r * 5, 14, 2, colors::TEXT_COLOR);
|
||||
}
|
||||
}
|
||||
|
||||
// Separator line between nav group and action group
|
||||
c.vline(152, 6, 20, colors::BORDER);
|
||||
|
||||
// Action buttons: Copy, Cut, Paste, Rename, New Folder, Delete
|
||||
bool has_sel = fm->selected >= 0 && fm->selected < fm->entry_count
|
||||
&& !fm->at_drives_root && !fm->at_apps_view
|
||||
&& fm->entry_types[fm->selected] != 3;
|
||||
bool has_clip = fm->clipboard_has_data && !fm->at_drives_root && !fm->at_apps_view;
|
||||
Color dim_bg = Color::from_rgb(0xF0, 0xF0, 0xF0);
|
||||
|
||||
// Icon toolbar buttons: Copy, Cut, Paste, Rename, New Folder, Delete
|
||||
struct ActionBtn { int x; SvgIcon* icon; bool enabled; };
|
||||
ActionBtn action_btns[] = {
|
||||
{ 160, ds ? &ds->icon_copy : nullptr, has_sel },
|
||||
{ 188, ds ? &ds->icon_cut : nullptr, has_sel },
|
||||
{ 216, ds ? &ds->icon_paste : nullptr, has_clip },
|
||||
{ 244, ds ? &ds->icon_rename : nullptr, has_sel },
|
||||
{ 272, ds ? &ds->icon_folder_new : nullptr, !fm->at_drives_root && !fm->at_apps_view },
|
||||
{ 300, ds ? &ds->icon_delete : nullptr, has_sel },
|
||||
};
|
||||
|
||||
for (int i = 0; i < 6; i++) {
|
||||
int bx = action_btns[i].x;
|
||||
int by = 4;
|
||||
c.fill_rect(bx, by, 24, 24, action_btns[i].enabled ? btn_bg : dim_bg);
|
||||
if (action_btns[i].icon && action_btns[i].icon->pixels) {
|
||||
int ix = bx + (24 - action_btns[i].icon->width) / 2;
|
||||
int iy = by + (24 - action_btns[i].icon->height) / 2;
|
||||
c.icon(ix, iy, *action_btns[i].icon);
|
||||
}
|
||||
}
|
||||
|
||||
// Toolbar separator
|
||||
c.hline(0, FM_TOOLBAR_H - 1, c.w, colors::BORDER);
|
||||
|
||||
// ---- Path bar ----
|
||||
int pathbar_y = FM_TOOLBAR_H;
|
||||
if (fm->pathbar_editing) {
|
||||
// Editable text input
|
||||
c.fill_rect(0, pathbar_y, c.w, FM_PATHBAR_H, colors::WHITE);
|
||||
c.rect(0, pathbar_y, c.w, FM_PATHBAR_H, colors::ACCENT);
|
||||
int fh = system_font_height();
|
||||
int ty = pathbar_y + (FM_PATHBAR_H - fh) / 2;
|
||||
c.text(8, ty, fm->pathbar_buf, colors::TEXT_COLOR);
|
||||
// Cursor
|
||||
char prefix[256];
|
||||
int cpos = fm->pathbar_cursor;
|
||||
if (cpos > 255) cpos = 255;
|
||||
for (int k = 0; k < cpos; k++) prefix[k] = fm->pathbar_buf[k];
|
||||
prefix[cpos] = '\0';
|
||||
int cx = 8 + text_width(prefix);
|
||||
c.vline(cx, ty, fh, colors::ACCENT);
|
||||
} else {
|
||||
c.fill_rect(0, pathbar_y, c.w, FM_PATHBAR_H, Color::from_rgb(0xF0, 0xF0, 0xF0));
|
||||
const char* pathbar_text = fm->current_path;
|
||||
if (fm->at_drives_root) pathbar_text = "Computer";
|
||||
else if (fm->at_apps_view) pathbar_text = "Apps";
|
||||
c.text(8, pathbar_y + 4, pathbar_text, colors::TEXT_COLOR);
|
||||
}
|
||||
|
||||
// Path bar separator
|
||||
c.hline(0, pathbar_y + FM_PATHBAR_H - 1, c.w, colors::BORDER);
|
||||
}
|
||||
|
||||
void filemanager_on_draw(Window* win, Framebuffer& fb) {
|
||||
FileManagerState* fm = (FileManagerState*)win->app_data;
|
||||
if (!fm) return;
|
||||
|
||||
Canvas c(win);
|
||||
c.fill(colors::WINDOW_BG);
|
||||
|
||||
Color toolbar_color = Color::from_rgb(0xF5, 0xF5, 0xF5);
|
||||
Color btn_bg = Color::from_rgb(0xE8, 0xE8, 0xE8);
|
||||
Color dim = Color::from_rgb(0x88, 0x88, 0x88);
|
||||
DesktopState* ds = fm->desktop;
|
||||
|
||||
// Draw header (toolbar + path bar)
|
||||
filemanager_draw_header(c, fm, toolbar_color, btn_bg);
|
||||
|
||||
if (fm->grid_view) {
|
||||
// ---- Grid View ----
|
||||
int list_y = FM_TOOLBAR_H + FM_PATHBAR_H;
|
||||
int list_h = c.h - list_y;
|
||||
int cols = (c.w - FM_SCROLLBAR_W) / FM_GRID_CELL_W;
|
||||
if (cols < 1) cols = 1;
|
||||
int rows = (fm->entry_count + cols - 1) / cols;
|
||||
int content_h = rows * FM_GRID_CELL_H;
|
||||
|
||||
// Update scrollbar
|
||||
fm->scrollbar.bounds = {c.w - FM_SCROLLBAR_W, list_y, FM_SCROLLBAR_W, list_h};
|
||||
fm->scrollbar.content_height = content_h;
|
||||
fm->scrollbar.view_height = list_h;
|
||||
|
||||
for (int i = 0; i < fm->entry_count; i++) {
|
||||
int col = i % cols;
|
||||
int row = i / cols;
|
||||
int cell_x = col * FM_GRID_CELL_W;
|
||||
int cell_y = list_y + row * FM_GRID_CELL_H - fm->scrollbar.scroll_offset;
|
||||
|
||||
// Skip if entirely off-screen
|
||||
if (cell_y + FM_GRID_CELL_H <= list_y || cell_y >= c.h) continue;
|
||||
|
||||
// Selection highlight
|
||||
if (i == fm->selected) {
|
||||
int sy = gui_max(cell_y, list_y);
|
||||
int sh = gui_min(cell_y + FM_GRID_CELL_H, c.h) - sy;
|
||||
int sw = gui_min(FM_GRID_CELL_W, c.w - FM_SCROLLBAR_W - cell_x);
|
||||
if (sh > 0 && sw > 0)
|
||||
c.fill_rect(cell_x, sy, sw, sh, colors::MENU_HOVER);
|
||||
}
|
||||
|
||||
// Large icon centered horizontally
|
||||
int icon_x = cell_x + (FM_GRID_CELL_W - FM_GRID_ICON) / 2;
|
||||
int icon_y = cell_y + FM_GRID_PAD;
|
||||
// Check for special folder icon (type 7 in Computer, or type 1 dir with matching name)
|
||||
int sfi = -1;
|
||||
if (fm->entry_types[i] == 7) sfi = fm->drive_indices[i];
|
||||
else if (ds && fm->entry_types[i] == 1) sfi = special_folder_index(fm->entry_names[i]);
|
||||
|
||||
if (sfi >= 0 && ds && ds->icon_special_folder_lg[sfi].pixels) {
|
||||
c.icon(icon_x, icon_y, ds->icon_special_folder_lg[sfi]);
|
||||
} else if (fm->entry_types[i] == 6 && i < fm->app_icon_count && fm->app_icons_lg[i].pixels) {
|
||||
c.icon(icon_x, icon_y, fm->app_icons_lg[i]);
|
||||
} else if (ds && fm->entry_types[i] == 5 && ds->icon_apps_lg.pixels) {
|
||||
c.icon(icon_x, icon_y, ds->icon_apps_lg);
|
||||
} else if (ds && fm->entry_types[i] == 4 && ds->icon_home_folder_lg.pixels) {
|
||||
c.icon(icon_x, icon_y, ds->icon_home_folder_lg);
|
||||
} else if (ds && fm->entry_types[i] == 3 && ds->icon_drive_lg.pixels) {
|
||||
c.icon(icon_x, icon_y, ds->icon_drive_lg);
|
||||
} else if (ds && fm->entry_types[i] == 1 && ds->icon_folder_lg.pixels) {
|
||||
c.icon(icon_x, icon_y, ds->icon_folder_lg);
|
||||
} else if (ds && fm->entry_types[i] == 2 && ds->icon_exec_lg.pixels) {
|
||||
c.icon(icon_x, icon_y, ds->icon_exec_lg);
|
||||
} else if (ds && ds->icon_file_lg.pixels) {
|
||||
c.icon(icon_x, icon_y, ds->icon_file_lg);
|
||||
} else {
|
||||
Color icon_c = fm->is_dir[i]
|
||||
? Color::from_rgb(0xFF, 0xBD, 0x2E)
|
||||
: Color::from_rgb(0x90, 0x90, 0x90);
|
||||
int iy_clip = gui_max(icon_y, list_y);
|
||||
int ih_clip = FM_GRID_ICON - (iy_clip - icon_y);
|
||||
if (ih_clip > 0)
|
||||
c.fill_rect(icon_x, iy_clip, FM_GRID_ICON, ih_clip, icon_c);
|
||||
}
|
||||
|
||||
// Filename centered below icon, truncated if needed
|
||||
// If renaming this entry, draw the rename textbox instead
|
||||
if (fm->rename_active && fm->rename_idx == i) {
|
||||
int ty = icon_y + FM_GRID_ICON + 2;
|
||||
int tw = FM_GRID_CELL_W - 4;
|
||||
if (ty >= list_y && ty + system_font_height() + 4 <= c.h) {
|
||||
c.fill_rect(cell_x + 2, ty - 1, tw, system_font_height() + 2, colors::WHITE);
|
||||
c.rect(cell_x + 2, ty - 1, tw, system_font_height() + 2, colors::ACCENT);
|
||||
// Draw rename text (truncated to cell width)
|
||||
char display[16];
|
||||
int rlen = fm->rename_len;
|
||||
if (rlen > 10) rlen = 10;
|
||||
for (int k = 0; k < rlen; k++) display[k] = fm->rename_buf[k];
|
||||
display[rlen] = '\0';
|
||||
c.text(cell_x + 4, ty, display, colors::TEXT_COLOR);
|
||||
// Cursor
|
||||
char prefix[64];
|
||||
int cpos = fm->rename_cursor < rlen ? fm->rename_cursor : rlen;
|
||||
for (int k = 0; k < cpos; k++) prefix[k] = fm->rename_buf[k];
|
||||
prefix[cpos] = '\0';
|
||||
int cx = cell_x + 4 + text_width(prefix);
|
||||
c.vline(cx, ty, system_font_height(), colors::ACCENT);
|
||||
}
|
||||
} else {
|
||||
char label[16];
|
||||
int nlen = montauk::slen(fm->entry_names[i]);
|
||||
if (nlen > 9) {
|
||||
for (int k = 0; k < 9; k++) label[k] = fm->entry_names[i][k];
|
||||
label[9] = '.';
|
||||
label[10] = '.';
|
||||
label[11] = '\0';
|
||||
} else {
|
||||
montauk::strncpy(label, fm->entry_names[i], 15);
|
||||
}
|
||||
int tw = text_width(label);
|
||||
int tx = cell_x + (FM_GRID_CELL_W - tw) / 2;
|
||||
if (tx < cell_x) tx = cell_x;
|
||||
int ty = icon_y + FM_GRID_ICON + 2;
|
||||
if (ty >= list_y && ty + system_font_height() <= c.h)
|
||||
c.text(tx, ty, label, colors::TEXT_COLOR);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// ---- List View ----
|
||||
|
||||
// ---- Column headers ----
|
||||
int header_y = FM_TOOLBAR_H + FM_PATHBAR_H;
|
||||
c.fill_rect(0, header_y, c.w, FM_HEADER_H, Color::from_rgb(0xF8, 0xF8, 0xF8));
|
||||
|
||||
int name_col_x = 8;
|
||||
int size_col_x = c.w - FM_SCROLLBAR_W - 120;
|
||||
int type_col_x = c.w - FM_SCROLLBAR_W - 60;
|
||||
|
||||
c.text(name_col_x, header_y + 2, "Name", dim);
|
||||
if (size_col_x > 100)
|
||||
c.text(size_col_x, header_y + 2, "Size", dim);
|
||||
if (type_col_x > 160)
|
||||
c.text(type_col_x, header_y + 2, "Type", dim);
|
||||
|
||||
// Header separator
|
||||
c.hline(0, header_y + FM_HEADER_H - 1, c.w, colors::BORDER);
|
||||
|
||||
// Column separator lines
|
||||
if (size_col_x > 100) {
|
||||
c.vline(size_col_x - 4, header_y, c.h - header_y, colors::BORDER);
|
||||
}
|
||||
|
||||
// ---- File entries ----
|
||||
int list_y = header_y + FM_HEADER_H;
|
||||
int list_h = c.h - list_y;
|
||||
int visible_items = list_h / FM_ITEM_H;
|
||||
int content_h = fm->entry_count * FM_ITEM_H;
|
||||
|
||||
// Update scrollbar
|
||||
fm->scrollbar.bounds = {c.w - FM_SCROLLBAR_W, list_y, FM_SCROLLBAR_W, list_h};
|
||||
fm->scrollbar.content_height = content_h;
|
||||
fm->scrollbar.view_height = list_h;
|
||||
|
||||
int scroll_items = fm->scrollbar.scroll_offset / FM_ITEM_H;
|
||||
|
||||
for (int i = scroll_items; i < fm->entry_count && (i - scroll_items) < visible_items + 1; i++) {
|
||||
int iy = list_y + (i - scroll_items) * FM_ITEM_H - (fm->scrollbar.scroll_offset % FM_ITEM_H);
|
||||
if (iy + FM_ITEM_H <= list_y || iy >= c.h) continue;
|
||||
|
||||
// Highlight selected
|
||||
if (i == fm->selected) {
|
||||
int sy = gui_max(iy, list_y);
|
||||
int sh = gui_min(iy + FM_ITEM_H, c.h) - sy;
|
||||
if (sh > 0)
|
||||
c.fill_rect(0, sy, c.w - FM_SCROLLBAR_W, sh, colors::MENU_HOVER);
|
||||
}
|
||||
|
||||
// Icon (skip if it would bleed above the list area)
|
||||
int ico_x = 8;
|
||||
int ico_y = iy + (FM_ITEM_H - 16) / 2;
|
||||
// Check for special folder icon
|
||||
int sfi_sm = -1;
|
||||
if (fm->entry_types[i] == 7) sfi_sm = fm->drive_indices[i];
|
||||
else if (ds && fm->entry_types[i] == 1) sfi_sm = special_folder_index(fm->entry_names[i]);
|
||||
|
||||
if (ico_y < list_y) { /* clipped by header repaint */ }
|
||||
else if (sfi_sm >= 0 && ds && ds->icon_special_folder[sfi_sm].pixels) {
|
||||
c.icon(ico_x, ico_y, ds->icon_special_folder[sfi_sm]);
|
||||
} else if (fm->entry_types[i] == 6 && i < fm->app_icon_count && fm->app_icons_sm[i].pixels) {
|
||||
c.icon(ico_x, ico_y, fm->app_icons_sm[i]);
|
||||
} else if (ds && fm->entry_types[i] == 5 && ds->icon_apps.pixels) {
|
||||
c.icon(ico_x, ico_y, ds->icon_apps);
|
||||
} else if (ds && fm->entry_types[i] == 4 && ds->icon_home_folder.pixels) {
|
||||
c.icon(ico_x, ico_y, ds->icon_home_folder);
|
||||
} else if (ds && fm->entry_types[i] == 3 && ds->icon_drive.pixels) {
|
||||
c.icon(ico_x, ico_y, ds->icon_drive);
|
||||
} else if (ds && fm->entry_types[i] == 1 && ds->icon_folder.pixels) {
|
||||
c.icon(ico_x, ico_y, ds->icon_folder);
|
||||
} else if (ds && fm->entry_types[i] == 2 && ds->icon_exec.pixels) {
|
||||
c.icon(ico_x, ico_y, ds->icon_exec);
|
||||
} else if (ds && ds->icon_file.pixels) {
|
||||
c.icon(ico_x, ico_y, ds->icon_file);
|
||||
} else {
|
||||
Color icon_c = fm->is_dir[i]
|
||||
? Color::from_rgb(0xFF, 0xBD, 0x2E)
|
||||
: Color::from_rgb(0x90, 0x90, 0x90);
|
||||
int iy_clip = gui_max(ico_y, list_y);
|
||||
int ih_clip = 16 - (iy_clip - ico_y);
|
||||
if (ih_clip > 0)
|
||||
c.fill_rect(ico_x, iy_clip, 16, ih_clip, icon_c);
|
||||
}
|
||||
|
||||
// Name (or rename textbox)
|
||||
int tx = 30;
|
||||
int fm_sfh = system_font_height();
|
||||
int ty = iy + (FM_ITEM_H - fm_sfh) / 2;
|
||||
|
||||
if (fm->rename_active && fm->rename_idx == i && ty >= list_y && ty + fm_sfh <= c.h) {
|
||||
// Rename textbox inline
|
||||
int rw = (size_col_x > 100 ? size_col_x - 8 : c.w - FM_SCROLLBAR_W - 8) - tx;
|
||||
c.fill_rect(tx - 2, ty - 1, rw, fm_sfh + 2, colors::WHITE);
|
||||
c.rect(tx - 2, ty - 1, rw, fm_sfh + 2, colors::ACCENT);
|
||||
c.text(tx, ty, fm->rename_buf, colors::TEXT_COLOR);
|
||||
// Cursor
|
||||
char prefix[64];
|
||||
int cpos = fm->rename_cursor;
|
||||
if (cpos > 63) cpos = 63;
|
||||
for (int k = 0; k < cpos; k++) prefix[k] = fm->rename_buf[k];
|
||||
prefix[cpos] = '\0';
|
||||
int cx = tx + text_width(prefix);
|
||||
c.vline(cx, ty, fm_sfh, colors::ACCENT);
|
||||
} else {
|
||||
if (ty >= list_y && ty + fm_sfh <= c.h)
|
||||
c.text(tx, ty, fm->entry_names[i], colors::TEXT_COLOR);
|
||||
}
|
||||
|
||||
// Size
|
||||
if (size_col_x > 100 && !fm->is_dir[i] && ty >= list_y && ty + fm_sfh <= c.h) {
|
||||
char size_str[16];
|
||||
format_size(size_str, fm->entry_sizes[i]);
|
||||
c.text(size_col_x, ty, size_str, dim);
|
||||
}
|
||||
|
||||
// Type
|
||||
if (type_col_x > 160 && ty >= list_y && ty + fm_sfh <= c.h) {
|
||||
const char* type_str = "File";
|
||||
if (fm->entry_types[i] == 6) type_str = "App";
|
||||
else if (fm->entry_types[i] == 5) type_str = "Apps";
|
||||
else if (fm->entry_types[i] == 4) type_str = "Home";
|
||||
else if (fm->entry_types[i] == 3) type_str = "Drive";
|
||||
else if (fm->entry_types[i] == 1) type_str = "Dir";
|
||||
else if (fm->entry_types[i] == 2) type_str = "Exec";
|
||||
c.text(type_col_x, ty, type_str, dim);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Scrollbar ----
|
||||
if (fm->scrollbar.content_height > fm->scrollbar.view_height) {
|
||||
Color sb_fg_color = (fm->scrollbar.hovered || fm->scrollbar.dragging)
|
||||
? fm->scrollbar.hover_fg : fm->scrollbar.fg;
|
||||
|
||||
int sbx = fm->scrollbar.bounds.x;
|
||||
int sby = fm->scrollbar.bounds.y;
|
||||
int sbw = fm->scrollbar.bounds.w;
|
||||
int sbh = fm->scrollbar.bounds.h;
|
||||
|
||||
c.fill_rect(sbx, sby, sbw, sbh, colors::SCROLLBAR_BG);
|
||||
|
||||
int th = fm->scrollbar.thumb_height();
|
||||
int tty = fm->scrollbar.thumb_y();
|
||||
c.fill_rect(sbx + 1, tty, sbw - 2, th, sb_fg_color);
|
||||
}
|
||||
|
||||
// Repaint header on top of content to clip scrolled icon overflow
|
||||
if (fm->scrollbar.scroll_offset > 0)
|
||||
filemanager_draw_header(c, fm, toolbar_color, btn_bg);
|
||||
|
||||
// ---- Context menu overlay ----
|
||||
if (fm->ctx_open && fm->ctx_item_count > 0) {
|
||||
int cmx = fm->ctx_x;
|
||||
int cmy = fm->ctx_y;
|
||||
int cmh = fm->ctx_item_count * CTX_ITEM_H + 8;
|
||||
|
||||
// Clamp to window bounds
|
||||
if (cmx + CTX_MENU_W > c.w) cmx = c.w - CTX_MENU_W;
|
||||
if (cmy + cmh > c.h) cmy = c.h - cmh;
|
||||
if (cmx < 0) cmx = 0;
|
||||
if (cmy < 0) cmy = 0;
|
||||
|
||||
// Shadow
|
||||
c.fill_rect(cmx + 2, cmy + 2, CTX_MENU_W, cmh, Color::from_rgb(0x80, 0x80, 0x80));
|
||||
// Background
|
||||
c.fill_rounded_rect(cmx, cmy, CTX_MENU_W, cmh, 4, colors::WHITE);
|
||||
c.rect(cmx, cmy, CTX_MENU_W, cmh, colors::BORDER);
|
||||
|
||||
for (int i = 0; i < fm->ctx_item_count; i++) {
|
||||
int iy = cmy + 4 + i * CTX_ITEM_H;
|
||||
|
||||
// Hover highlight
|
||||
if (i == fm->ctx_hover)
|
||||
c.fill_rect(cmx + 2, iy, CTX_MENU_W - 4, CTX_ITEM_H, colors::MENU_HOVER);
|
||||
|
||||
int fh = system_font_height();
|
||||
int ty = iy + (CTX_ITEM_H - fh) / 2;
|
||||
c.text(cmx + 12, ty, ctx_label(fm->ctx_items[i]), colors::TEXT_COLOR);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace filemanager
|
||||
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* about.cpp
|
||||
* About tab for the embedded desktop settings app
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#include "settings_internal.hpp"
|
||||
|
||||
namespace settings_app {
|
||||
|
||||
void settings_draw_about(Canvas& c, SettingsState* st) {
|
||||
st->uptime_ms = montauk::get_milliseconds();
|
||||
|
||||
Color dim = Color::from_rgb(0x88, 0x88, 0x88);
|
||||
int x = 16;
|
||||
int y = 20;
|
||||
char line[128];
|
||||
int sfh = system_font_height();
|
||||
int line_h = sfh + 6;
|
||||
|
||||
// OS name in 2x size
|
||||
c.text_2x(x, y, st->sys_info.osName, st->desktop->settings.accent_color);
|
||||
int large_h = (fonts::system_font && fonts::system_font->valid)
|
||||
? fonts::system_font->get_line_height(fonts::LARGE_SIZE) : (FONT_HEIGHT * 2);
|
||||
y += large_h + 8;
|
||||
|
||||
snprintf(line, sizeof(line), "Version %s", st->sys_info.osVersion);
|
||||
c.text(x, y, line, colors::TEXT_COLOR);
|
||||
y += line_h + 8;
|
||||
|
||||
c.hline(x, y, c.w - 2 * x, colors::BORDER);
|
||||
y += 12;
|
||||
|
||||
snprintf(line, sizeof(line), "API version: %d", (int)st->sys_info.apiVersion);
|
||||
c.kv_line(x, &y, line, colors::TEXT_COLOR, line_h);
|
||||
|
||||
int up_sec = (int)(st->uptime_ms / 1000);
|
||||
int up_min = up_sec / 60;
|
||||
int up_hr = up_min / 60;
|
||||
snprintf(line, sizeof(line), "Uptime: %d:%02d:%02d", up_hr, up_min % 60, up_sec % 60);
|
||||
c.kv_line(x, &y, line, colors::TEXT_COLOR, line_h);
|
||||
y += 8;
|
||||
|
||||
c.hline(x, y, c.w - 2 * x, colors::BORDER);
|
||||
y += 12;
|
||||
|
||||
c.text(x, y, "Copyright (c) 2026 Daniel Hammer", dim);
|
||||
}
|
||||
|
||||
} // namespace settings_app
|
||||
@@ -0,0 +1,128 @@
|
||||
/*
|
||||
* app.cpp
|
||||
* Settings launcher and desktop integration entry points
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#include "settings_internal.hpp"
|
||||
|
||||
namespace settings_app {
|
||||
|
||||
static void settings_on_draw(Window* win, Framebuffer& fb) {
|
||||
SettingsState* st = (SettingsState*)win->app_data;
|
||||
if (!st) return;
|
||||
|
||||
Canvas c(win);
|
||||
c.fill(colors::WINDOW_BG);
|
||||
|
||||
Color accent = st->desktop->settings.accent_color;
|
||||
int sfh = system_font_height();
|
||||
|
||||
// Draw tab bar background
|
||||
c.fill_rect(0, 0, c.w, TAB_BAR_H, Color::from_rgb(0xF5, 0xF5, 0xF5));
|
||||
c.hline(0, TAB_BAR_H - 1, c.w, colors::BORDER);
|
||||
|
||||
// Draw tabs
|
||||
int tab_w = c.w / TAB_COUNT;
|
||||
for (int i = 0; i < TAB_COUNT; i++) {
|
||||
int tx = i * tab_w;
|
||||
bool active = (i == st->active_tab);
|
||||
|
||||
if (active) {
|
||||
c.fill_rect(tx, 0, tab_w, TAB_BAR_H, colors::WINDOW_BG);
|
||||
// Active tab underline
|
||||
c.fill_rect(tx + 4, TAB_BAR_H - 3, tab_w - 8, 3, accent);
|
||||
}
|
||||
|
||||
int tw = text_width(tab_labels[i]);
|
||||
Color tc = active ? accent : Color::from_rgb(0x66, 0x66, 0x66);
|
||||
c.text(tx + (tab_w - tw) / 2, (TAB_BAR_H - sfh) / 2, tab_labels[i], tc);
|
||||
}
|
||||
|
||||
// Draw tab content below the tab bar
|
||||
Canvas content(win->content + TAB_BAR_H * win->content_w,
|
||||
win->content_w, win->content_h - TAB_BAR_H);
|
||||
|
||||
switch (st->active_tab) {
|
||||
case 0: settings_draw_appearance(content, st); break;
|
||||
case 1: settings_draw_display(content, st); break;
|
||||
case 2: settings_draw_users(content, st); break;
|
||||
case 3: settings_draw_about(content, st); break;
|
||||
}
|
||||
}
|
||||
|
||||
static void settings_on_mouse(Window* win, MouseEvent& ev) {
|
||||
SettingsState* st = (SettingsState*)win->app_data;
|
||||
if (!st || !ev.left_pressed()) return;
|
||||
|
||||
Rect cr = win->content_rect();
|
||||
int mx = ev.x - cr.x;
|
||||
int my = ev.y - cr.y;
|
||||
|
||||
// Tab bar click
|
||||
if (my >= 0 && my < TAB_BAR_H) {
|
||||
int tab_w = win->content_w / TAB_COUNT;
|
||||
int tab = mx / tab_w;
|
||||
if (tab >= 0 && tab < TAB_COUNT) {
|
||||
st->active_tab = tab;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Content area (offset by TAB_BAR_H)
|
||||
int cy = my - TAB_BAR_H;
|
||||
|
||||
switch (st->active_tab) {
|
||||
case 0:
|
||||
settings_handle_appearance_click(win, st, mx, cy);
|
||||
break;
|
||||
case 1:
|
||||
settings_handle_display_click(st, mx, cy);
|
||||
break;
|
||||
case 2:
|
||||
settings_handle_users_click(win, st, mx, cy);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static void settings_on_key(Window* win, const Montauk::KeyEvent& key) {
|
||||
// Settings main window no longer needs keyboard handling.
|
||||
(void)win;
|
||||
(void)key;
|
||||
}
|
||||
|
||||
static void settings_on_close(Window* win) {
|
||||
if (win->app_data) {
|
||||
montauk::mfree(win->app_data);
|
||||
win->app_data = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace settings_app
|
||||
|
||||
void open_settings(DesktopState* ds) {
|
||||
int idx = desktop_create_window(ds, "Settings", 200, 100, 480, 420);
|
||||
if (idx < 0) return;
|
||||
|
||||
Window* win = &ds->windows[idx];
|
||||
settings_app::SettingsState* st =
|
||||
(settings_app::SettingsState*)montauk::malloc(sizeof(settings_app::SettingsState));
|
||||
montauk::memset(st, 0, sizeof(settings_app::SettingsState));
|
||||
st->desktop = ds;
|
||||
st->active_tab = 0;
|
||||
montauk::get_info(&st->sys_info);
|
||||
st->uptime_ms = montauk::get_milliseconds();
|
||||
st->wp_scanned = false;
|
||||
st->wp_files.count = 0;
|
||||
st->selected_user = -1;
|
||||
st->users_loaded = false;
|
||||
st->status_msg[0] = '\0';
|
||||
|
||||
win->app_data = st;
|
||||
win->on_draw = settings_app::settings_on_draw;
|
||||
win->on_mouse = settings_app::settings_on_mouse;
|
||||
win->on_key = settings_app::settings_on_key;
|
||||
win->on_close = settings_app::settings_on_close;
|
||||
}
|
||||
@@ -0,0 +1,234 @@
|
||||
/*
|
||||
* appearance.cpp
|
||||
* Appearance tab for the embedded desktop settings app
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#include "settings_internal.hpp"
|
||||
|
||||
namespace settings_app {
|
||||
|
||||
void settings_draw_appearance(Canvas& c, SettingsState* st) {
|
||||
DesktopSettings& s = st->desktop->settings;
|
||||
Color accent = s.accent_color;
|
||||
Color dim = Color::from_rgb(0x88, 0x88, 0x88);
|
||||
int x = 16;
|
||||
int y = 12;
|
||||
int sfh = system_font_height();
|
||||
int line_h = sfh + 10;
|
||||
|
||||
// Section: Background
|
||||
c.text(x, y, "Background", colors::TEXT_COLOR);
|
||||
y += line_h;
|
||||
|
||||
// Radio buttons: Gradient / Solid Color / Image
|
||||
bool mode_grad = s.bg_gradient && !s.bg_image;
|
||||
bool mode_solid = !s.bg_gradient && !s.bg_image;
|
||||
bool mode_image = s.bg_image;
|
||||
|
||||
draw_radio(c, x, y, mode_grad, accent);
|
||||
c.text(x + 20, y + 2, "Gradient", colors::TEXT_COLOR);
|
||||
|
||||
draw_radio(c, x + 120, y, mode_solid, accent);
|
||||
c.text(x + 140, y + 2, "Solid", colors::TEXT_COLOR);
|
||||
|
||||
draw_radio(c, x + 220, y, mode_image, accent);
|
||||
c.text(x + 240, y + 2, "Image", colors::TEXT_COLOR);
|
||||
y += line_h + 4;
|
||||
|
||||
if (mode_image) {
|
||||
// Scan for images lazily
|
||||
if (!st->wp_scanned) {
|
||||
wallpaper_scan_home(st->desktop->home_dir, &st->wp_files);
|
||||
st->wp_scanned = true;
|
||||
}
|
||||
|
||||
c.text(x, y, "Wallpapers", dim);
|
||||
y += sfh + 6;
|
||||
|
||||
if (st->wp_files.count == 0) {
|
||||
c.text(x + 8, y, "No .jpg files found", dim);
|
||||
y += WP_ITEM_H;
|
||||
} else {
|
||||
for (int i = 0; i < st->wp_files.count && i < 8; i++) {
|
||||
// Build full path for comparison
|
||||
char fullpath[256];
|
||||
montauk::strcpy(fullpath, st->desktop->home_dir);
|
||||
str_append(fullpath, "/", 256);
|
||||
str_append(fullpath, st->wp_files.names[i], 256);
|
||||
|
||||
bool selected = s.bg_image &&
|
||||
montauk::streq(s.bg_image_path, fullpath);
|
||||
|
||||
if (selected) {
|
||||
c.fill_rounded_rect(x, y, c.w - 2 * x, WP_ITEM_H, 3, accent);
|
||||
}
|
||||
|
||||
// Truncate long filenames
|
||||
char label[40];
|
||||
int nlen = montauk::slen(st->wp_files.names[i]);
|
||||
if (nlen > 35) {
|
||||
montauk::strncpy(label, st->wp_files.names[i], 32);
|
||||
label[32] = '.';
|
||||
label[33] = '.';
|
||||
label[34] = '.';
|
||||
label[35] = '\0';
|
||||
} else {
|
||||
montauk::strncpy(label, st->wp_files.names[i], 39);
|
||||
}
|
||||
|
||||
Color tc = selected ? colors::WHITE : colors::TEXT_COLOR;
|
||||
c.text(x + 8, y + (WP_ITEM_H - sfh) / 2, label, tc);
|
||||
y += WP_ITEM_H;
|
||||
}
|
||||
}
|
||||
y += 6;
|
||||
} else if (mode_grad) {
|
||||
// Top color swatches
|
||||
c.text(x, y + 4, "Top", dim);
|
||||
draw_swatch_row(c, x + 70, y, bg_palette, s.bg_grad_top, accent);
|
||||
y += SWATCH_SIZE + 14;
|
||||
|
||||
// Bottom color swatches
|
||||
c.text(x, y + 4, "Bottom", dim);
|
||||
draw_swatch_row(c, x + 70, y, bg_palette, s.bg_grad_bottom, accent);
|
||||
y += SWATCH_SIZE + 14;
|
||||
} else {
|
||||
// Solid color swatches
|
||||
c.text(x, y + 4, "Color", dim);
|
||||
draw_swatch_row(c, x + 70, y, bg_palette, s.bg_solid, accent);
|
||||
y += SWATCH_SIZE + 14;
|
||||
}
|
||||
|
||||
// Separator
|
||||
c.hline(x, y, c.w - 2 * x, colors::BORDER);
|
||||
y += 12;
|
||||
|
||||
// Panel color
|
||||
c.text(x, y + 4, "Panel Color", colors::TEXT_COLOR);
|
||||
draw_swatch_row(c, x + 110, y, panel_palette, s.panel_color, accent);
|
||||
y += SWATCH_SIZE + 14;
|
||||
|
||||
// Separator
|
||||
c.hline(x, y, c.w - 2 * x, colors::BORDER);
|
||||
y += 12;
|
||||
|
||||
// Accent color
|
||||
c.text(x, y + 4, "Accent Color", colors::TEXT_COLOR);
|
||||
draw_swatch_row(c, x + 110, y, accent_palette, s.accent_color, accent);
|
||||
}
|
||||
|
||||
bool settings_handle_appearance_click(Window* win, SettingsState* st, int mx, int cy) {
|
||||
DesktopSettings& s = st->desktop->settings;
|
||||
int x = 16;
|
||||
int sfh = system_font_height();
|
||||
int line_h = sfh + 10;
|
||||
int y = 12;
|
||||
|
||||
bool mode_grad = s.bg_gradient && !s.bg_image;
|
||||
bool mode_image = s.bg_image;
|
||||
|
||||
// "Background" label
|
||||
y += line_h;
|
||||
|
||||
// Radio: Gradient
|
||||
if (mx >= x && mx < x + 100 && cy >= y && cy < y + 16) {
|
||||
s.bg_gradient = true;
|
||||
s.bg_image = false;
|
||||
settings_persist(st);
|
||||
return true;
|
||||
}
|
||||
// Radio: Solid
|
||||
if (mx >= x + 120 && mx < x + 210 && cy >= y && cy < y + 16) {
|
||||
s.bg_gradient = false;
|
||||
s.bg_image = false;
|
||||
settings_persist(st);
|
||||
return true;
|
||||
}
|
||||
// Radio: Image
|
||||
if (mx >= x + 220 && mx < x + 320 && cy >= y && cy < y + 16) {
|
||||
s.bg_image = true;
|
||||
s.bg_gradient = false;
|
||||
if (!st->wp_scanned) {
|
||||
wallpaper_scan_home(st->desktop->home_dir, &st->wp_files);
|
||||
st->wp_scanned = true;
|
||||
}
|
||||
settings_persist(st);
|
||||
return true;
|
||||
}
|
||||
y += line_h + 4;
|
||||
|
||||
int idx;
|
||||
if (mode_image) {
|
||||
// Wallpaper file list
|
||||
y += sfh + 6;
|
||||
|
||||
// File list clicks
|
||||
for (int i = 0; i < st->wp_files.count && i < 8; i++) {
|
||||
if (cy >= y && cy < y + WP_ITEM_H &&
|
||||
mx >= x && mx < win->content_w - x) {
|
||||
// Build full path and load wallpaper
|
||||
char fullpath[256];
|
||||
montauk::strcpy(fullpath, st->desktop->home_dir);
|
||||
str_append(fullpath, "/", 256);
|
||||
str_append(fullpath, st->wp_files.names[i], 256);
|
||||
wallpaper_load(&s, fullpath,
|
||||
st->desktop->screen_w, st->desktop->screen_h);
|
||||
settings_persist(st);
|
||||
return true;
|
||||
}
|
||||
y += WP_ITEM_H;
|
||||
}
|
||||
if (st->wp_files.count == 0) y += WP_ITEM_H;
|
||||
y += 6;
|
||||
} else if (mode_grad) {
|
||||
// Top swatches
|
||||
if (swatch_hit(mx, cy, x + 70, y, &idx)) {
|
||||
s.bg_grad_top = bg_palette[idx];
|
||||
settings_persist(st);
|
||||
return true;
|
||||
}
|
||||
y += SWATCH_SIZE + 14;
|
||||
|
||||
// Bottom swatches
|
||||
if (swatch_hit(mx, cy, x + 70, y, &idx)) {
|
||||
s.bg_grad_bottom = bg_palette[idx];
|
||||
settings_persist(st);
|
||||
return true;
|
||||
}
|
||||
y += SWATCH_SIZE + 14;
|
||||
} else {
|
||||
// Solid color swatches
|
||||
if (swatch_hit(mx, cy, x + 70, y, &idx)) {
|
||||
s.bg_solid = bg_palette[idx];
|
||||
settings_persist(st);
|
||||
return true;
|
||||
}
|
||||
y += SWATCH_SIZE + 14;
|
||||
}
|
||||
|
||||
// Separator
|
||||
y += 12;
|
||||
|
||||
// Panel color swatches
|
||||
if (swatch_hit(mx, cy, x + 110, y, &idx)) {
|
||||
s.panel_color = panel_palette[idx];
|
||||
settings_persist(st);
|
||||
return true;
|
||||
}
|
||||
y += SWATCH_SIZE + 14;
|
||||
|
||||
// Separator
|
||||
y += 12;
|
||||
|
||||
// Accent color swatches
|
||||
if (swatch_hit(mx, cy, x + 110, y, &idx)) {
|
||||
s.accent_color = accent_palette[idx];
|
||||
settings_persist(st);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace settings_app
|
||||
@@ -0,0 +1,519 @@
|
||||
/*
|
||||
* dialogs.cpp
|
||||
* User-management dialogs for the embedded desktop settings app
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#include "settings_internal.hpp"
|
||||
|
||||
namespace settings_app {
|
||||
|
||||
static void dialog_append(char* buf, int* len, int max, char ch) {
|
||||
if (*len < max - 1) { buf[*len] = ch; (*len)++; buf[*len] = '\0'; }
|
||||
}
|
||||
|
||||
static void dialog_backspace(char* buf, int* len) {
|
||||
if (*len > 0) { (*len)--; buf[*len] = '\0'; }
|
||||
}
|
||||
|
||||
static void dialog_close_self(DesktopState* ds, void* app_data) {
|
||||
for (int i = 0; i < ds->window_count; i++) {
|
||||
if (ds->windows[i].app_data == app_data) {
|
||||
desktop_close_window(ds, i);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void draw_input_field(Canvas& c, int x, int y, int w, int h,
|
||||
const char* label, const char* value,
|
||||
bool focused, bool masked, Color accent) {
|
||||
int sfh = system_font_height();
|
||||
Color dim = Color::from_rgb(0x88, 0x88, 0x88);
|
||||
|
||||
c.text(x, y, label, dim);
|
||||
y += sfh + 2;
|
||||
Color border = focused ? accent : colors::BORDER;
|
||||
c.fill_rounded_rect(x, y, w, h, 3, Color::from_rgb(0xF5, 0xF5, 0xF5));
|
||||
c.rect(x, y, w, h, border);
|
||||
|
||||
if (masked) {
|
||||
int vlen = montauk::slen(value);
|
||||
char dots[65];
|
||||
for (int i = 0; i < vlen && i < 64; i++) dots[i] = '*';
|
||||
dots[vlen < 64 ? vlen : 64] = '\0';
|
||||
c.text(x + 8, y + (h - sfh) / 2, dots, colors::TEXT_COLOR);
|
||||
} else {
|
||||
c.text(x + 8, y + (h - sfh) / 2, value, colors::TEXT_COLOR);
|
||||
}
|
||||
|
||||
if (focused) {
|
||||
int tw = masked ? text_width("*") * montauk::slen(value) : text_width(value);
|
||||
c.fill_rect(x + 8 + tw, y + 6, 2, h - 12, accent);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Add User Dialog
|
||||
// ============================================================================
|
||||
|
||||
struct AddUserDialogState {
|
||||
DesktopState* ds;
|
||||
SettingsState* parent;
|
||||
Color accent;
|
||||
int field; // 0=username, 1=display_name, 2=password
|
||||
char username[32]; int username_len;
|
||||
char display[64]; int display_len;
|
||||
char password[64]; int password_len;
|
||||
bool role_admin;
|
||||
char error[64];
|
||||
};
|
||||
|
||||
static void adduser_on_draw(Window* win, Framebuffer& fb) {
|
||||
AddUserDialogState* st = (AddUserDialogState*)win->app_data;
|
||||
if (!st) return;
|
||||
|
||||
Canvas c(win);
|
||||
c.fill(colors::WINDOW_BG);
|
||||
Color accent = st->accent;
|
||||
int sfh = system_font_height();
|
||||
int pad = 16;
|
||||
int fw = c.w - 2 * pad;
|
||||
int y = pad;
|
||||
|
||||
// Fields
|
||||
draw_input_field(c, pad, y, fw, USER_FIELD_H, "Username", st->username,
|
||||
st->field == 0, false, accent);
|
||||
y += sfh + 2 + USER_FIELD_H + 10;
|
||||
|
||||
draw_input_field(c, pad, y, fw, USER_FIELD_H, "Display Name", st->display,
|
||||
st->field == 1, false, accent);
|
||||
y += sfh + 2 + USER_FIELD_H + 10;
|
||||
|
||||
draw_input_field(c, pad, y, fw, USER_FIELD_H, "Password", st->password,
|
||||
st->field == 2, true, accent);
|
||||
y += sfh + 2 + USER_FIELD_H + 12;
|
||||
|
||||
// Role toggle
|
||||
Color dim = Color::from_rgb(0x88, 0x88, 0x88);
|
||||
c.text(pad, y + 4, "Role:", dim);
|
||||
int rbx = pad + 60;
|
||||
draw_toggle_btn(c, rbx, y, 60, USER_BTN_H, "User", !st->role_admin, accent);
|
||||
draw_toggle_btn(c, rbx + 68, y, 60, USER_BTN_H, "Admin", st->role_admin, accent);
|
||||
y += USER_BTN_H + 14;
|
||||
|
||||
// Error message
|
||||
if (st->error[0]) {
|
||||
c.text(pad, y, st->error, Color::from_rgb(0xD0, 0x3E, 0x3E));
|
||||
y += sfh + 6;
|
||||
}
|
||||
|
||||
// Buttons at bottom
|
||||
int btn_y = c.h - USER_BTN_H - pad;
|
||||
c.fill_rounded_rect(pad, btn_y, 80, USER_BTN_H, 4, accent);
|
||||
int tw = text_width("Create");
|
||||
c.text(pad + (80 - tw) / 2, btn_y + (USER_BTN_H - sfh) / 2, "Create", colors::WHITE);
|
||||
|
||||
c.fill_rounded_rect(pad + 88, btn_y, 80, USER_BTN_H, 4, colors::WINDOW_BG);
|
||||
c.rect(pad + 88, btn_y, 80, USER_BTN_H, colors::BORDER);
|
||||
tw = text_width("Cancel");
|
||||
c.text(pad + 88 + (80 - tw) / 2, btn_y + (USER_BTN_H - sfh) / 2, "Cancel", colors::TEXT_COLOR);
|
||||
}
|
||||
|
||||
static void adduser_submit(AddUserDialogState* st) {
|
||||
if (st->username_len == 0) {
|
||||
montauk::strcpy(st->error, "Username is required");
|
||||
return;
|
||||
}
|
||||
if (st->password_len == 0) {
|
||||
montauk::strcpy(st->error, "Password is required");
|
||||
return;
|
||||
}
|
||||
const char* dname = st->display_len > 0 ? st->display : st->username;
|
||||
const char* role = st->role_admin ? "admin" : "user";
|
||||
if (montauk::user::create_user(st->username, dname, st->password, role)) {
|
||||
st->parent->users_loaded = false;
|
||||
settings_set_status(st->parent, "User created");
|
||||
dialog_close_self(st->ds, st);
|
||||
} else {
|
||||
montauk::strcpy(st->error, "Failed (username taken?)");
|
||||
}
|
||||
}
|
||||
|
||||
static void adduser_on_mouse(Window* win, MouseEvent& ev) {
|
||||
AddUserDialogState* st = (AddUserDialogState*)win->app_data;
|
||||
if (!st || !ev.left_pressed()) return;
|
||||
|
||||
Rect cr = win->content_rect();
|
||||
int mx = ev.x - cr.x;
|
||||
int my = ev.y - cr.y;
|
||||
int pad = 16;
|
||||
int sfh = system_font_height();
|
||||
int y = pad;
|
||||
|
||||
// Username field hit
|
||||
y += sfh + 2;
|
||||
if (my >= y && my < y + USER_FIELD_H) { st->field = 0; return; }
|
||||
y += USER_FIELD_H + 10;
|
||||
|
||||
// Display field hit
|
||||
y += sfh + 2;
|
||||
if (my >= y && my < y + USER_FIELD_H) { st->field = 1; return; }
|
||||
y += USER_FIELD_H + 10;
|
||||
|
||||
// Password field hit
|
||||
y += sfh + 2;
|
||||
if (my >= y && my < y + USER_FIELD_H) { st->field = 2; return; }
|
||||
y += USER_FIELD_H + 12;
|
||||
|
||||
// Role toggle
|
||||
int rbx = pad + 60;
|
||||
if (mx >= rbx && mx < rbx + 60 && my >= y && my < y + USER_BTN_H) {
|
||||
st->role_admin = false; return;
|
||||
}
|
||||
if (mx >= rbx + 68 && mx < rbx + 128 && my >= y && my < y + USER_BTN_H) {
|
||||
st->role_admin = true; return;
|
||||
}
|
||||
|
||||
// Bottom buttons
|
||||
int btn_y = win->content_h - USER_BTN_H - pad;
|
||||
if (my >= btn_y && my < btn_y + USER_BTN_H) {
|
||||
if (mx >= pad && mx < pad + 80) { adduser_submit(st); return; }
|
||||
if (mx >= pad + 88 && mx < pad + 168) { dialog_close_self(st->ds, st); return; }
|
||||
}
|
||||
}
|
||||
|
||||
static void adduser_on_key(Window* win, const Montauk::KeyEvent& key) {
|
||||
AddUserDialogState* st = (AddUserDialogState*)win->app_data;
|
||||
if (!st || !key.pressed) return;
|
||||
|
||||
if (key.ascii == '\n' || key.ascii == '\r' || key.scancode == 0x1C) {
|
||||
adduser_submit(st); return;
|
||||
}
|
||||
if (key.scancode == 0x01) { dialog_close_self(st->ds, st); return; }
|
||||
if (key.scancode == 0x0F) { st->field = (st->field + 1) % 3; return; }
|
||||
if (key.ascii == '\b' || key.scancode == 0x0E) {
|
||||
switch (st->field) {
|
||||
case 0: dialog_backspace(st->username, &st->username_len); break;
|
||||
case 1: dialog_backspace(st->display, &st->display_len); break;
|
||||
case 2: dialog_backspace(st->password, &st->password_len); break;
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (key.ascii >= 0x20 && key.ascii < 0x7F) {
|
||||
switch (st->field) {
|
||||
case 0: dialog_append(st->username, &st->username_len, 31, key.ascii); break;
|
||||
case 1: dialog_append(st->display, &st->display_len, 63, key.ascii); break;
|
||||
case 2: dialog_append(st->password, &st->password_len, 63, key.ascii); break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void adduser_on_close(Window* win) {
|
||||
if (win->app_data) { montauk::mfree(win->app_data); win->app_data = nullptr; }
|
||||
}
|
||||
|
||||
void open_add_user_dialog(SettingsState* parent) {
|
||||
DesktopState* ds = parent->desktop;
|
||||
int w = 340, h = 340;
|
||||
int wx = (ds->screen_w - w) / 2;
|
||||
int wy = (ds->screen_h - h) / 2;
|
||||
int idx = desktop_create_window(ds, "Add User", wx, wy, w, h);
|
||||
if (idx < 0) return;
|
||||
|
||||
Window* win = &ds->windows[idx];
|
||||
AddUserDialogState* st = (AddUserDialogState*)montauk::malloc(sizeof(AddUserDialogState));
|
||||
montauk::memset(st, 0, sizeof(AddUserDialogState));
|
||||
st->ds = ds;
|
||||
st->parent = parent;
|
||||
st->accent = ds->settings.accent_color;
|
||||
|
||||
win->app_data = st;
|
||||
win->on_draw = adduser_on_draw;
|
||||
win->on_mouse = adduser_on_mouse;
|
||||
win->on_key = adduser_on_key;
|
||||
win->on_close = adduser_on_close;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Change Password Dialog
|
||||
// ============================================================================
|
||||
|
||||
struct ChPwdDialogState {
|
||||
DesktopState* ds;
|
||||
SettingsState* parent;
|
||||
Color accent;
|
||||
char username[32];
|
||||
char display_name[64];
|
||||
int field; // 0=new, 1=confirm
|
||||
char new_pwd[64]; int new_len;
|
||||
char confirm[64]; int confirm_len;
|
||||
char error[64];
|
||||
};
|
||||
|
||||
static void chpwd_on_draw(Window* win, Framebuffer& fb) {
|
||||
ChPwdDialogState* st = (ChPwdDialogState*)win->app_data;
|
||||
if (!st) return;
|
||||
|
||||
Canvas c(win);
|
||||
c.fill(colors::WINDOW_BG);
|
||||
Color accent = st->accent;
|
||||
int sfh = system_font_height();
|
||||
int pad = 16;
|
||||
int fw = c.w - 2 * pad;
|
||||
int y = pad;
|
||||
|
||||
// Title
|
||||
char title[80];
|
||||
snprintf(title, sizeof(title), "Change password for %s", st->display_name);
|
||||
c.text(pad, y, title, colors::TEXT_COLOR);
|
||||
y += sfh + 12;
|
||||
|
||||
draw_input_field(c, pad, y, fw, USER_FIELD_H, "New Password", st->new_pwd,
|
||||
st->field == 0, true, accent);
|
||||
y += sfh + 2 + USER_FIELD_H + 10;
|
||||
|
||||
draw_input_field(c, pad, y, fw, USER_FIELD_H, "Confirm Password", st->confirm,
|
||||
st->field == 1, true, accent);
|
||||
y += sfh + 2 + USER_FIELD_H + 12;
|
||||
|
||||
// Error
|
||||
if (st->error[0]) {
|
||||
c.text(pad, y, st->error, Color::from_rgb(0xD0, 0x3E, 0x3E));
|
||||
}
|
||||
|
||||
// Buttons at bottom
|
||||
int btn_y = c.h - USER_BTN_H - pad;
|
||||
c.fill_rounded_rect(pad, btn_y, 80, USER_BTN_H, 4, accent);
|
||||
int tw = text_width("Save");
|
||||
c.text(pad + (80 - tw) / 2, btn_y + (USER_BTN_H - sfh) / 2, "Save", colors::WHITE);
|
||||
|
||||
c.fill_rounded_rect(pad + 88, btn_y, 80, USER_BTN_H, 4, colors::WINDOW_BG);
|
||||
c.rect(pad + 88, btn_y, 80, USER_BTN_H, colors::BORDER);
|
||||
tw = text_width("Cancel");
|
||||
c.text(pad + 88 + (80 - tw) / 2, btn_y + (USER_BTN_H - sfh) / 2, "Cancel", colors::TEXT_COLOR);
|
||||
}
|
||||
|
||||
static void chpwd_submit(ChPwdDialogState* st) {
|
||||
if (st->new_len == 0) {
|
||||
montauk::strcpy(st->error, "Password cannot be empty");
|
||||
return;
|
||||
}
|
||||
if (!montauk::streq(st->new_pwd, st->confirm)) {
|
||||
montauk::strcpy(st->error, "Passwords don't match");
|
||||
return;
|
||||
}
|
||||
montauk::user::change_password(st->username, st->new_pwd);
|
||||
settings_set_status(st->parent, "Password changed");
|
||||
dialog_close_self(st->ds, st);
|
||||
}
|
||||
|
||||
static void chpwd_on_mouse(Window* win, MouseEvent& ev) {
|
||||
ChPwdDialogState* st = (ChPwdDialogState*)win->app_data;
|
||||
if (!st || !ev.left_pressed()) return;
|
||||
|
||||
Rect cr = win->content_rect();
|
||||
int mx = ev.x - cr.x;
|
||||
int my = ev.y - cr.y;
|
||||
int pad = 16;
|
||||
int sfh = system_font_height();
|
||||
int y = pad + sfh + 12;
|
||||
|
||||
// New password field
|
||||
y += sfh + 2;
|
||||
if (my >= y && my < y + USER_FIELD_H) { st->field = 0; return; }
|
||||
y += USER_FIELD_H + 10;
|
||||
|
||||
// Confirm field
|
||||
y += sfh + 2;
|
||||
if (my >= y && my < y + USER_FIELD_H) { st->field = 1; return; }
|
||||
|
||||
// Bottom buttons
|
||||
int btn_y = win->content_h - USER_BTN_H - pad;
|
||||
if (my >= btn_y && my < btn_y + USER_BTN_H) {
|
||||
if (mx >= pad && mx < pad + 80) { chpwd_submit(st); return; }
|
||||
if (mx >= pad + 88 && mx < pad + 168) { dialog_close_self(st->ds, st); return; }
|
||||
}
|
||||
}
|
||||
|
||||
static void chpwd_on_key(Window* win, const Montauk::KeyEvent& key) {
|
||||
ChPwdDialogState* st = (ChPwdDialogState*)win->app_data;
|
||||
if (!st || !key.pressed) return;
|
||||
|
||||
if (key.ascii == '\n' || key.ascii == '\r' || key.scancode == 0x1C) {
|
||||
chpwd_submit(st); return;
|
||||
}
|
||||
if (key.scancode == 0x01) { dialog_close_self(st->ds, st); return; }
|
||||
if (key.scancode == 0x0F) { st->field = (st->field + 1) % 2; return; }
|
||||
if (key.ascii == '\b' || key.scancode == 0x0E) {
|
||||
if (st->field == 0) dialog_backspace(st->new_pwd, &st->new_len);
|
||||
else dialog_backspace(st->confirm, &st->confirm_len);
|
||||
return;
|
||||
}
|
||||
if (key.ascii >= 0x20 && key.ascii < 0x7F) {
|
||||
if (st->field == 0) dialog_append(st->new_pwd, &st->new_len, 63, key.ascii);
|
||||
else dialog_append(st->confirm, &st->confirm_len, 63, key.ascii);
|
||||
}
|
||||
}
|
||||
|
||||
static void chpwd_on_close(Window* win) {
|
||||
if (win->app_data) { montauk::mfree(win->app_data); win->app_data = nullptr; }
|
||||
}
|
||||
|
||||
void open_change_pwd_dialog(SettingsState* parent) {
|
||||
if (parent->selected_user < 0) return;
|
||||
DesktopState* ds = parent->desktop;
|
||||
int w = 340, h = 260;
|
||||
int wx = (ds->screen_w - w) / 2;
|
||||
int wy = (ds->screen_h - h) / 2;
|
||||
int idx = desktop_create_window(ds, "Change Password", wx, wy, w, h);
|
||||
if (idx < 0) return;
|
||||
|
||||
Window* win = &ds->windows[idx];
|
||||
ChPwdDialogState* st = (ChPwdDialogState*)montauk::malloc(sizeof(ChPwdDialogState));
|
||||
montauk::memset(st, 0, sizeof(ChPwdDialogState));
|
||||
st->ds = ds;
|
||||
st->parent = parent;
|
||||
st->accent = ds->settings.accent_color;
|
||||
montauk::strncpy(st->username, parent->users[parent->selected_user].username, 31);
|
||||
montauk::strncpy(st->display_name, parent->users[parent->selected_user].display_name, 63);
|
||||
|
||||
win->app_data = st;
|
||||
win->on_draw = chpwd_on_draw;
|
||||
win->on_mouse = chpwd_on_mouse;
|
||||
win->on_key = chpwd_on_key;
|
||||
win->on_close = chpwd_on_close;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Delete User Dialog
|
||||
// ============================================================================
|
||||
|
||||
struct DeleteUserDialogState {
|
||||
DesktopState* ds;
|
||||
SettingsState* parent;
|
||||
char username[32];
|
||||
bool hover_delete, hover_cancel;
|
||||
};
|
||||
|
||||
static void deluser_on_draw(Window* win, Framebuffer& fb) {
|
||||
DeleteUserDialogState* st = (DeleteUserDialogState*)win->app_data;
|
||||
if (!st) return;
|
||||
|
||||
Canvas c(win);
|
||||
c.fill(colors::WINDOW_BG);
|
||||
int sfh = system_font_height();
|
||||
|
||||
char msg[96];
|
||||
snprintf(msg, sizeof(msg), "Delete user \"%s\"?", st->username);
|
||||
int tw = text_width(msg);
|
||||
c.text((c.w - tw) / 2, 24, msg, colors::TEXT_COLOR);
|
||||
|
||||
const char* warn = "This action cannot be undone.";
|
||||
tw = text_width(warn);
|
||||
c.text((c.w - tw) / 2, 24 + sfh + 8, warn, Color::from_rgb(0x88, 0x88, 0x88));
|
||||
|
||||
// Buttons
|
||||
int btn_w = 100, btn_h = 32;
|
||||
int btn_y = c.h - btn_h - 20;
|
||||
int gap = 20;
|
||||
int total = btn_w * 2 + gap;
|
||||
int bx = (c.w - total) / 2;
|
||||
|
||||
Color del_bg = st->hover_delete
|
||||
? Color::from_rgb(0xDD, 0x44, 0x44)
|
||||
: Color::from_rgb(0xD0, 0x3E, 0x3E);
|
||||
c.button(bx, btn_y, btn_w, btn_h, "Delete", del_bg, colors::WHITE, 4);
|
||||
|
||||
Color cancel_bg = st->hover_cancel
|
||||
? Color::from_rgb(0x99, 0x99, 0x99)
|
||||
: Color::from_rgb(0x88, 0x88, 0x88);
|
||||
c.button(bx + btn_w + gap, btn_y, btn_w, btn_h, "Cancel", cancel_bg, colors::WHITE, 4);
|
||||
}
|
||||
|
||||
static void deluser_on_mouse(Window* win, MouseEvent& ev) {
|
||||
DeleteUserDialogState* st = (DeleteUserDialogState*)win->app_data;
|
||||
if (!st) return;
|
||||
|
||||
Rect cr = win->content_rect();
|
||||
int lx = ev.x - cr.x;
|
||||
int ly = ev.y - cr.y;
|
||||
|
||||
int btn_w = 100, btn_h = 32;
|
||||
int btn_y = win->content_h - btn_h - 20;
|
||||
int gap = 20;
|
||||
int total = btn_w * 2 + gap;
|
||||
int bx = (win->content_w - total) / 2;
|
||||
|
||||
Rect db = {bx, btn_y, btn_w, btn_h};
|
||||
Rect cb = {bx + btn_w + gap, btn_y, btn_w, btn_h};
|
||||
st->hover_delete = db.contains(lx, ly);
|
||||
st->hover_cancel = cb.contains(lx, ly);
|
||||
|
||||
if (ev.left_pressed()) {
|
||||
if (st->hover_delete) {
|
||||
montauk::user::delete_user(st->username);
|
||||
st->parent->users_loaded = false;
|
||||
st->parent->selected_user = -1;
|
||||
settings_set_status(st->parent, "User deleted");
|
||||
dialog_close_self(st->ds, st);
|
||||
return;
|
||||
}
|
||||
if (st->hover_cancel) {
|
||||
dialog_close_self(st->ds, st);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void deluser_on_key(Window* win, const Montauk::KeyEvent& key) {
|
||||
DeleteUserDialogState* st = (DeleteUserDialogState*)win->app_data;
|
||||
if (!st || !key.pressed) return;
|
||||
|
||||
if (key.ascii == '\n' || key.ascii == '\r') {
|
||||
montauk::user::delete_user(st->username);
|
||||
st->parent->users_loaded = false;
|
||||
st->parent->selected_user = -1;
|
||||
settings_set_status(st->parent, "User deleted");
|
||||
dialog_close_self(st->ds, st);
|
||||
}
|
||||
if (key.scancode == 0x01) {
|
||||
dialog_close_self(st->ds, st);
|
||||
}
|
||||
}
|
||||
|
||||
static void deluser_on_close(Window* win) {
|
||||
if (win->app_data) { montauk::mfree(win->app_data); win->app_data = nullptr; }
|
||||
}
|
||||
|
||||
void open_delete_user_dialog(SettingsState* parent) {
|
||||
if (parent->selected_user < 0) return;
|
||||
DesktopState* ds = parent->desktop;
|
||||
|
||||
// Don't allow deleting yourself
|
||||
if (montauk::streq(parent->users[parent->selected_user].username, ds->current_user)) {
|
||||
settings_set_status(parent, "Cannot delete current user");
|
||||
return;
|
||||
}
|
||||
|
||||
int w = 300, h = 150;
|
||||
int wx = (ds->screen_w - w) / 2;
|
||||
int wy = (ds->screen_h - h) / 2;
|
||||
int idx = desktop_create_window(ds, "Delete User", wx, wy, w, h);
|
||||
if (idx < 0) return;
|
||||
|
||||
Window* win = &ds->windows[idx];
|
||||
DeleteUserDialogState* st = (DeleteUserDialogState*)montauk::malloc(sizeof(DeleteUserDialogState));
|
||||
montauk::memset(st, 0, sizeof(DeleteUserDialogState));
|
||||
st->ds = ds;
|
||||
st->parent = parent;
|
||||
montauk::strncpy(st->username, parent->users[parent->selected_user].username, 31);
|
||||
|
||||
win->app_data = st;
|
||||
win->on_draw = deluser_on_draw;
|
||||
win->on_mouse = deluser_on_mouse;
|
||||
win->on_key = deluser_on_key;
|
||||
win->on_close = deluser_on_close;
|
||||
}
|
||||
|
||||
} // namespace settings_app
|
||||
@@ -0,0 +1,174 @@
|
||||
/*
|
||||
* display.cpp
|
||||
* Display tab for the embedded desktop settings app
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#include "settings_internal.hpp"
|
||||
|
||||
namespace settings_app {
|
||||
|
||||
void apply_ui_scale(int scale) {
|
||||
switch (scale) {
|
||||
case 0: fonts::UI_SIZE=14; fonts::TITLE_SIZE=14; fonts::TERM_SIZE=14; fonts::LARGE_SIZE=22; break;
|
||||
case 2: fonts::UI_SIZE=22; fonts::TITLE_SIZE=22; fonts::TERM_SIZE=22; fonts::LARGE_SIZE=34; break;
|
||||
default: fonts::UI_SIZE=18; fonts::TITLE_SIZE=18; fonts::TERM_SIZE=18; fonts::LARGE_SIZE=28; break;
|
||||
}
|
||||
}
|
||||
|
||||
void settings_draw_display(Canvas& c, SettingsState* st) {
|
||||
DesktopSettings& s = st->desktop->settings;
|
||||
Color accent = s.accent_color;
|
||||
int x = 16;
|
||||
int y = 20;
|
||||
int btn_w = 60;
|
||||
int btn_h = 28;
|
||||
|
||||
// Window Shadows
|
||||
c.text(x, y + 6, "Window Shadows", colors::TEXT_COLOR);
|
||||
int bx = x + 180;
|
||||
draw_toggle_btn(c, bx, y, btn_w, btn_h, "On", s.show_shadows, accent);
|
||||
draw_toggle_btn(c, bx + btn_w + 8, y, btn_w, btn_h, "Off", !s.show_shadows, accent);
|
||||
y += btn_h + 20;
|
||||
|
||||
// Separator
|
||||
c.hline(x, y, c.w - 2 * x, colors::BORDER);
|
||||
y += 16;
|
||||
|
||||
// Clock Format
|
||||
c.text(x, y + 6, "Clock Format", colors::TEXT_COLOR);
|
||||
bx = x + 180;
|
||||
draw_toggle_btn(c, bx, y, btn_w, btn_h, "24h", s.clock_24h, accent);
|
||||
draw_toggle_btn(c, bx + btn_w + 8, y, btn_w, btn_h, "12h", !s.clock_24h, accent);
|
||||
y += btn_h + 20;
|
||||
|
||||
// Separator
|
||||
c.hline(x, y, c.w - 2 * x, colors::BORDER);
|
||||
y += 16;
|
||||
|
||||
// UI Scale
|
||||
c.text(x, y + 6, "UI Scale", colors::TEXT_COLOR);
|
||||
bx = x + 180;
|
||||
int sbw = 68;
|
||||
draw_toggle_btn(c, bx, y, sbw, btn_h, "Small", s.ui_scale == 0, accent);
|
||||
draw_toggle_btn(c, bx + sbw + 8, y, sbw, btn_h, "Default", s.ui_scale == 1, accent);
|
||||
draw_toggle_btn(c, bx + (sbw + 8) * 2, y, sbw, btn_h, "Large", s.ui_scale == 2, accent);
|
||||
y += btn_h + 20;
|
||||
|
||||
// Separator
|
||||
c.hline(x, y, c.w - 2 * x, colors::BORDER);
|
||||
y += 16;
|
||||
|
||||
// UTC Offset
|
||||
c.text(x, y + 6, "UTC Offset", colors::TEXT_COLOR);
|
||||
bx = x + 180;
|
||||
|
||||
// [-] button
|
||||
draw_toggle_btn(c, bx, y, 36, btn_h, "-", false, accent);
|
||||
|
||||
// Offset label
|
||||
int off = s.tz_offset_minutes;
|
||||
int offH = off / 60;
|
||||
int offM = off % 60;
|
||||
if (offM < 0) offM = -offM;
|
||||
char tz_label[16];
|
||||
if (offM)
|
||||
snprintf(tz_label, sizeof(tz_label), "UTC%s%d:%02d", offH >= 0 ? "+" : "", offH, offM);
|
||||
else
|
||||
snprintf(tz_label, sizeof(tz_label), "UTC%s%d", offH >= 0 ? "+" : "", offH);
|
||||
int tw = text_width(tz_label);
|
||||
int label_w = 90;
|
||||
c.text(bx + 36 + (label_w - tw) / 2, y + 6, tz_label, colors::TEXT_COLOR);
|
||||
|
||||
// [+] button
|
||||
draw_toggle_btn(c, bx + 36 + label_w, y, 36, btn_h, "+", false, accent);
|
||||
}
|
||||
|
||||
bool settings_handle_display_click(SettingsState* st, int mx, int cy) {
|
||||
DesktopSettings& s = st->desktop->settings;
|
||||
int x = 16;
|
||||
int y = 20;
|
||||
int btn_w = 60;
|
||||
int btn_h = 28;
|
||||
int bx = x + 180;
|
||||
|
||||
// Window Shadows: On
|
||||
if (mx >= bx && mx < bx + btn_w && cy >= y && cy < y + btn_h) {
|
||||
s.show_shadows = true;
|
||||
settings_persist(st);
|
||||
return true;
|
||||
}
|
||||
// Window Shadows: Off
|
||||
if (mx >= bx + btn_w + 8 && mx < bx + btn_w * 2 + 8 && cy >= y && cy < y + btn_h) {
|
||||
s.show_shadows = false;
|
||||
settings_persist(st);
|
||||
return true;
|
||||
}
|
||||
y += btn_h + 20 + 16;
|
||||
|
||||
// Clock: 24h
|
||||
if (mx >= bx && mx < bx + btn_w && cy >= y && cy < y + btn_h) {
|
||||
s.clock_24h = true;
|
||||
settings_persist(st);
|
||||
return true;
|
||||
}
|
||||
// Clock: 12h
|
||||
if (mx >= bx + btn_w + 8 && mx < bx + btn_w * 2 + 8 && cy >= y && cy < y + btn_h) {
|
||||
s.clock_24h = false;
|
||||
settings_persist(st);
|
||||
return true;
|
||||
}
|
||||
y += btn_h + 20 + 16;
|
||||
|
||||
// UI Scale buttons
|
||||
int sbw = 68;
|
||||
// Small
|
||||
if (mx >= bx && mx < bx + sbw && cy >= y && cy < y + btn_h) {
|
||||
s.ui_scale = 0;
|
||||
apply_ui_scale(0);
|
||||
montauk::win_setscale(0);
|
||||
settings_persist(st);
|
||||
return true;
|
||||
}
|
||||
// Default
|
||||
if (mx >= bx + sbw + 8 && mx < bx + sbw * 2 + 8 && cy >= y && cy < y + btn_h) {
|
||||
s.ui_scale = 1;
|
||||
apply_ui_scale(1);
|
||||
montauk::win_setscale(1);
|
||||
settings_persist(st);
|
||||
return true;
|
||||
}
|
||||
// Large
|
||||
if (mx >= bx + (sbw + 8) * 2 && mx < bx + sbw * 3 + 16 && cy >= y && cy < y + btn_h) {
|
||||
s.ui_scale = 2;
|
||||
apply_ui_scale(2);
|
||||
montauk::win_setscale(2);
|
||||
settings_persist(st);
|
||||
return true;
|
||||
}
|
||||
y += btn_h + 20 + 16;
|
||||
|
||||
// UTC Offset: [-] button
|
||||
if (mx >= bx && mx < bx + 36 && cy >= y && cy < y + btn_h) {
|
||||
if (s.tz_offset_minutes > -720) {
|
||||
s.tz_offset_minutes -= 60;
|
||||
montauk::settz(s.tz_offset_minutes);
|
||||
settings_persist_tz(st);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
// UTC Offset: [+] button
|
||||
int label_w = 90;
|
||||
if (mx >= bx + 36 + label_w && mx < bx + 36 + label_w + 36 && cy >= y && cy < y + btn_h) {
|
||||
if (s.tz_offset_minutes < 840) {
|
||||
s.tz_offset_minutes += 60;
|
||||
montauk::settz(s.tz_offset_minutes);
|
||||
settings_persist_tz(st);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace settings_app
|
||||
@@ -0,0 +1,80 @@
|
||||
/*
|
||||
* helpers.cpp
|
||||
* Shared helpers for the embedded desktop settings app
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#include "settings_internal.hpp"
|
||||
|
||||
namespace settings_app {
|
||||
|
||||
bool color_eq(Color a, Color b) {
|
||||
return a.r == b.r && a.g == b.g && a.b == b.b;
|
||||
}
|
||||
|
||||
bool settings_status_visible(SettingsState* st) {
|
||||
return st->status_msg[0] &&
|
||||
(montauk::get_milliseconds() - st->status_time < 4000);
|
||||
}
|
||||
|
||||
void settings_set_status(SettingsState* st, const char* msg) {
|
||||
montauk::strncpy(st->status_msg, msg ? msg : "", (int)sizeof(st->status_msg));
|
||||
st->status_time = montauk::get_milliseconds();
|
||||
}
|
||||
|
||||
int find_swatch(const Color* palette, Color current) {
|
||||
for (int i = 0; i < SWATCH_COUNT; i++) {
|
||||
if (color_eq(palette[i], current)) return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
void draw_swatch_row(Canvas& c, int x, int y, const Color* palette, Color selected, Color accent) {
|
||||
int sel = find_swatch(palette, selected);
|
||||
for (int i = 0; i < SWATCH_COUNT; i++) {
|
||||
int sx = x + i * (SWATCH_SIZE + SWATCH_GAP);
|
||||
// Draw selection border
|
||||
if (i == sel) {
|
||||
c.fill_rounded_rect(sx - 2, y - 2, SWATCH_SIZE + 4, SWATCH_SIZE + 4, 4, accent);
|
||||
}
|
||||
c.fill_rounded_rect(sx, y, SWATCH_SIZE, SWATCH_SIZE, 3, palette[i]);
|
||||
// Thin border for light colors
|
||||
c.rect(sx, y, SWATCH_SIZE, SWATCH_SIZE, Color::from_rgb(0xCC, 0xCC, 0xCC));
|
||||
}
|
||||
}
|
||||
|
||||
void draw_radio(Canvas& c, int x, int y, bool selected, Color accent) {
|
||||
int r = 7;
|
||||
// Outer circle (simple square approximation with rounded rect)
|
||||
c.fill_rounded_rect(x, y, r * 2, r * 2, r, Color::from_rgb(0xCC, 0xCC, 0xCC));
|
||||
c.fill_rounded_rect(x + 1, y + 1, r * 2 - 2, r * 2 - 2, r - 1, colors::WHITE);
|
||||
if (selected) {
|
||||
c.fill_rounded_rect(x + 4, y + 4, r * 2 - 8, r * 2 - 8, r - 4, accent);
|
||||
}
|
||||
}
|
||||
|
||||
void draw_toggle_btn(Canvas& c, int x, int y, int bw, int bh,
|
||||
const char* label, bool active, Color accent) {
|
||||
Color bg = active ? accent : colors::WINDOW_BG;
|
||||
Color fg = active ? colors::WHITE : colors::TEXT_COLOR;
|
||||
c.fill_rounded_rect(x, y, bw, bh, 4, bg);
|
||||
if (!active) {
|
||||
c.rect(x, y, bw, bh, colors::BORDER);
|
||||
}
|
||||
int tw = text_width(label);
|
||||
int fh = system_font_height();
|
||||
c.text(x + (bw - tw) / 2, y + (bh - fh) / 2, label, fg);
|
||||
}
|
||||
|
||||
bool swatch_hit(int mx, int my, int row_x, int row_y, int* out_idx) {
|
||||
for (int i = 0; i < SWATCH_COUNT; i++) {
|
||||
int sx = row_x + i * (SWATCH_SIZE + SWATCH_GAP);
|
||||
if (mx >= sx && mx < sx + SWATCH_SIZE && my >= row_y && my < row_y + SWATCH_SIZE) {
|
||||
*out_idx = i;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace settings_app
|
||||
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* persistence.cpp
|
||||
* Settings persistence helpers for the embedded desktop settings app
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#include "settings_internal.hpp"
|
||||
|
||||
namespace settings_app {
|
||||
|
||||
static int64_t color_to_int(Color c) {
|
||||
return ((int64_t)c.r << 16) | ((int64_t)c.g << 8) | c.b;
|
||||
}
|
||||
|
||||
void settings_persist(SettingsState* st) {
|
||||
DesktopSettings& s = st->desktop->settings;
|
||||
const char* user = st->desktop->current_user;
|
||||
|
||||
montauk::toml::Doc doc;
|
||||
doc.init();
|
||||
|
||||
// Background mode
|
||||
const char* mode = s.bg_image ? "image" : (s.bg_gradient ? "gradient" : "solid");
|
||||
montauk::config::set_string(&doc, "background.mode", mode);
|
||||
|
||||
// Wallpaper path
|
||||
if (s.bg_image && s.bg_image_path[0])
|
||||
montauk::config::set_string(&doc, "wallpaper.path", s.bg_image_path);
|
||||
|
||||
// Background colors
|
||||
montauk::config::set_int(&doc, "background.solid_color", color_to_int(s.bg_solid));
|
||||
montauk::config::set_int(&doc, "background.grad_top", color_to_int(s.bg_grad_top));
|
||||
montauk::config::set_int(&doc, "background.grad_bottom", color_to_int(s.bg_grad_bottom));
|
||||
|
||||
// Appearance
|
||||
montauk::config::set_int(&doc, "appearance.panel_color", color_to_int(s.panel_color));
|
||||
montauk::config::set_int(&doc, "appearance.accent_color", color_to_int(s.accent_color));
|
||||
|
||||
// Display
|
||||
montauk::config::set_int(&doc, "display.ui_scale", s.ui_scale);
|
||||
montauk::config::set_bool(&doc, "display.clock_24h", s.clock_24h);
|
||||
montauk::config::set_bool(&doc, "display.show_shadows", s.show_shadows);
|
||||
|
||||
montauk::config::save_user(user, "desktop", &doc);
|
||||
doc.destroy();
|
||||
}
|
||||
|
||||
void settings_persist_tz(SettingsState* st) {
|
||||
montauk::toml::Doc doc;
|
||||
doc.init();
|
||||
montauk::config::set_int(&doc, "timezone.offset_minutes", st->desktop->settings.tz_offset_minutes);
|
||||
montauk::config::save("timezone", &doc);
|
||||
doc.destroy();
|
||||
}
|
||||
|
||||
} // namespace settings_app
|
||||
@@ -0,0 +1,117 @@
|
||||
/*
|
||||
* settings_internal.hpp
|
||||
* Shared declarations for the embedded desktop settings app
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "../apps_common.hpp"
|
||||
#include "../../wallpaper.hpp"
|
||||
#include <montauk/config.h>
|
||||
#include <montauk/user.h>
|
||||
|
||||
namespace settings_app {
|
||||
|
||||
struct SettingsState {
|
||||
DesktopState* desktop;
|
||||
int active_tab; // 0=Appearance, 1=Display, 2=Users, 3=About
|
||||
Montauk::SysInfo sys_info;
|
||||
uint64_t uptime_ms;
|
||||
WallpaperFileList wp_files;
|
||||
bool wp_scanned;
|
||||
|
||||
// Users tab
|
||||
montauk::user::UserInfo users[16];
|
||||
int user_count;
|
||||
int selected_user; // index into users[], or -1
|
||||
bool users_loaded;
|
||||
|
||||
char status_msg[128];
|
||||
uint64_t status_time;
|
||||
};
|
||||
|
||||
inline constexpr int SWATCH_COUNT = 8;
|
||||
inline constexpr int SWATCH_SIZE = 24;
|
||||
inline constexpr int SWATCH_GAP = 6;
|
||||
|
||||
// Background colors (light tones)
|
||||
inline const Color bg_palette[SWATCH_COUNT] = {
|
||||
Color::from_rgb(0xD0, 0xD8, 0xE8), // light blue-gray (default)
|
||||
Color::from_rgb(0xE8, 0xDD, 0xCB), // warm beige
|
||||
Color::from_rgb(0xC8, 0xE6, 0xD0), // mint green
|
||||
Color::from_rgb(0xD8, 0xD0, 0xE8), // lavender
|
||||
Color::from_rgb(0xB8, 0xBE, 0xC8), // slate
|
||||
Color::from_rgb(0xF0, 0xF0, 0xF0), // white
|
||||
Color::from_rgb(0xE8, 0xD0, 0xD8), // soft pink
|
||||
Color::from_rgb(0xE8, 0xE0, 0xC8), // light gold
|
||||
};
|
||||
|
||||
// Panel colors (dark tones)
|
||||
inline const Color panel_palette[SWATCH_COUNT] = {
|
||||
Color::from_rgb(0x2B, 0x3E, 0x50), // dark blue-gray (default)
|
||||
Color::from_rgb(0x2D, 0x2D, 0x2D), // dark charcoal
|
||||
Color::from_rgb(0x1B, 0x2A, 0x4A), // navy
|
||||
Color::from_rgb(0x1A, 0x3A, 0x3A), // dark teal
|
||||
Color::from_rgb(0x1A, 0x3A, 0x1A), // dark green
|
||||
Color::from_rgb(0x30, 0x20, 0x40), // dark purple
|
||||
Color::from_rgb(0x40, 0x1A, 0x1A), // dark red
|
||||
Color::from_rgb(0x10, 0x10, 0x10), // black
|
||||
};
|
||||
|
||||
// Accent colors
|
||||
inline const Color accent_palette[SWATCH_COUNT] = {
|
||||
Color::from_rgb(0x36, 0x7B, 0xF0), // blue (default)
|
||||
Color::from_rgb(0x00, 0x9B, 0x9B), // teal
|
||||
Color::from_rgb(0x2E, 0x9E, 0x3E), // green
|
||||
Color::from_rgb(0xE0, 0x8A, 0x20), // orange
|
||||
Color::from_rgb(0xD0, 0x3E, 0x3E), // red
|
||||
Color::from_rgb(0x7B, 0x3E, 0xB8), // purple
|
||||
Color::from_rgb(0xD0, 0x5C, 0x9E), // pink
|
||||
Color::from_rgb(0x44, 0x44, 0xCC), // indigo
|
||||
};
|
||||
|
||||
inline constexpr int TAB_BAR_H = 36;
|
||||
inline constexpr int TAB_COUNT = 4;
|
||||
inline constexpr const char* tab_labels[TAB_COUNT] = { "Appearance", "Display", "Users", "About" };
|
||||
|
||||
inline constexpr int WP_ITEM_H = 24;
|
||||
inline constexpr int USER_BTN_H = 30;
|
||||
inline constexpr int USER_BTN_W = 100;
|
||||
inline constexpr int USER_FIELD_H = 32;
|
||||
|
||||
bool color_eq(Color a, Color b);
|
||||
bool settings_status_visible(SettingsState* st);
|
||||
void settings_set_status(SettingsState* st, const char* msg);
|
||||
int find_swatch(const Color* palette, Color current);
|
||||
|
||||
void draw_swatch_row(Canvas& c, int x, int y, const Color* palette, Color selected, Color accent);
|
||||
void draw_radio(Canvas& c, int x, int y, bool selected, Color accent);
|
||||
void draw_toggle_btn(Canvas& c, int x, int y, int bw, int bh,
|
||||
const char* label, bool active, Color accent);
|
||||
bool swatch_hit(int mx, int my, int row_x, int row_y, int* out_idx);
|
||||
|
||||
void apply_ui_scale(int scale);
|
||||
|
||||
void settings_draw_appearance(Canvas& c, SettingsState* st);
|
||||
bool settings_handle_appearance_click(Window* win, SettingsState* st, int mx, int cy);
|
||||
|
||||
void settings_draw_display(Canvas& c, SettingsState* st);
|
||||
bool settings_handle_display_click(SettingsState* st, int mx, int cy);
|
||||
|
||||
void settings_draw_users(Canvas& c, SettingsState* st);
|
||||
bool settings_handle_users_click(Window* win, SettingsState* st, int mx, int cy);
|
||||
|
||||
void settings_draw_about(Canvas& c, SettingsState* st);
|
||||
|
||||
void settings_persist(SettingsState* st);
|
||||
void settings_persist_tz(SettingsState* st);
|
||||
|
||||
void users_reload(SettingsState* st);
|
||||
int user_row_height();
|
||||
|
||||
void open_add_user_dialog(SettingsState* parent);
|
||||
void open_change_pwd_dialog(SettingsState* parent);
|
||||
void open_delete_user_dialog(SettingsState* parent);
|
||||
|
||||
} // namespace settings_app
|
||||
@@ -0,0 +1,182 @@
|
||||
/*
|
||||
* users.cpp
|
||||
* Users tab for the embedded desktop settings app
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#include "settings_internal.hpp"
|
||||
|
||||
namespace settings_app {
|
||||
|
||||
void users_reload(SettingsState* st) {
|
||||
st->user_count = montauk::user::load_users(st->users, 16);
|
||||
st->users_loaded = true;
|
||||
}
|
||||
|
||||
int user_row_height() {
|
||||
return system_font_height() * 2 + 12; // Two lines of text + padding
|
||||
}
|
||||
|
||||
void settings_draw_users(Canvas& c, SettingsState* st) {
|
||||
if (!st->users_loaded) users_reload(st);
|
||||
|
||||
Color accent = st->desktop->settings.accent_color;
|
||||
Color dim = Color::from_rgb(0x88, 0x88, 0x88);
|
||||
Color card_bg = Color::from_rgb(0xF8, 0xF8, 0xF8);
|
||||
int x = 16;
|
||||
int y = 20;
|
||||
int sfh = system_font_height();
|
||||
int content_w = c.w - 2 * x;
|
||||
int row_h = user_row_height();
|
||||
|
||||
if (!st->desktop->is_admin) {
|
||||
c.text(x, y, "Admin access required to manage users.", dim);
|
||||
return;
|
||||
}
|
||||
|
||||
// User list
|
||||
for (int i = 0; i < st->user_count; i++) {
|
||||
bool sel = (i == st->selected_user);
|
||||
bool is_current = montauk::streq(st->users[i].username, st->desktop->current_user);
|
||||
|
||||
if (sel) {
|
||||
c.fill_rounded_rect(x, y, content_w, row_h, 4, accent);
|
||||
} else if (i % 2 == 0) {
|
||||
c.fill_rounded_rect(x, y, content_w, row_h, 4, card_bg);
|
||||
}
|
||||
|
||||
Color tc = sel ? colors::WHITE : colors::TEXT_COLOR;
|
||||
Color sc = sel ? Color::from_rgb(0xDD, 0xDD, 0xFF) : dim;
|
||||
|
||||
// Display name (primary line)
|
||||
int text_y = y + 4;
|
||||
c.text(x + 12, text_y, st->users[i].display_name, tc);
|
||||
|
||||
// Username (secondary line)
|
||||
char sub[48];
|
||||
if (is_current) {
|
||||
snprintf(sub, sizeof(sub), "@%s (you)", st->users[i].username);
|
||||
} else {
|
||||
snprintf(sub, sizeof(sub), "@%s", st->users[i].username);
|
||||
}
|
||||
c.text(x + 12, text_y + sfh + 2, sub, sc);
|
||||
|
||||
// Role badge
|
||||
const char* role = st->users[i].role;
|
||||
int rw = text_width(role) + 12;
|
||||
int badge_x = c.w - x - rw - 8;
|
||||
int badge_y = y + (row_h - sfh - 4) / 2;
|
||||
if (sel) {
|
||||
c.fill_rounded_rect(badge_x, badge_y, rw, sfh + 4, (sfh + 4) / 2,
|
||||
Color::from_rgb(0xFF, 0xFF, 0xFF));
|
||||
c.text(badge_x + 6, badge_y + 2, role, accent);
|
||||
} else {
|
||||
bool is_admin_role = montauk::streq(role, "admin");
|
||||
Color badge_bg = is_admin_role
|
||||
? Color::from_rgb(0xE8, 0xD8, 0xF0)
|
||||
: Color::from_rgb(0xE0, 0xE8, 0xF0);
|
||||
Color badge_fg = is_admin_role
|
||||
? Color::from_rgb(0x7B, 0x3E, 0xB8)
|
||||
: Color::from_rgb(0x36, 0x7B, 0xF0);
|
||||
c.fill_rounded_rect(badge_x, badge_y, rw, sfh + 4, (sfh + 4) / 2, badge_bg);
|
||||
c.text(badge_x + 6, badge_y + 2, role, badge_fg);
|
||||
}
|
||||
|
||||
y += row_h + 4;
|
||||
}
|
||||
|
||||
// Separator
|
||||
y += 8;
|
||||
c.hline(x, y, content_w, colors::BORDER);
|
||||
y += 16;
|
||||
|
||||
// Status message
|
||||
if (settings_status_visible(st)) {
|
||||
c.text(x, y, st->status_msg, accent);
|
||||
y += sfh + 8;
|
||||
}
|
||||
|
||||
// Action buttons
|
||||
int btn_x = x;
|
||||
|
||||
// Add User
|
||||
c.fill_rounded_rect(btn_x, y, 90, USER_BTN_H, 4, accent);
|
||||
{
|
||||
int tw = text_width("Add User");
|
||||
c.text(btn_x + (90 - tw) / 2, y + (USER_BTN_H - sfh) / 2, "Add User", colors::WHITE);
|
||||
}
|
||||
btn_x += 98;
|
||||
|
||||
// Change Password (disabled when no selection)
|
||||
{
|
||||
bool enabled = st->selected_user >= 0;
|
||||
Color bg = enabled ? accent : Color::from_rgb(0xCC, 0xCC, 0xCC);
|
||||
c.fill_rounded_rect(btn_x, y, USER_BTN_W, USER_BTN_H, 4, bg);
|
||||
int tw = text_width("Change Pwd");
|
||||
c.text(btn_x + (USER_BTN_W - tw) / 2, y + (USER_BTN_H - sfh) / 2, "Change Pwd", colors::WHITE);
|
||||
}
|
||||
btn_x += USER_BTN_W + 8;
|
||||
|
||||
// Delete (disabled when no selection)
|
||||
{
|
||||
bool enabled = st->selected_user >= 0;
|
||||
Color del_bg = enabled ? Color::from_rgb(0xD0, 0x3E, 0x3E) : Color::from_rgb(0xCC, 0xCC, 0xCC);
|
||||
c.fill_rounded_rect(btn_x, y, 70, USER_BTN_H, 4, del_bg);
|
||||
int tw = text_width("Delete");
|
||||
c.text(btn_x + (70 - tw) / 2, y + (USER_BTN_H - sfh) / 2, "Delete", colors::WHITE);
|
||||
}
|
||||
}
|
||||
|
||||
bool settings_handle_users_click(Window* win, SettingsState* st, int mx, int cy) {
|
||||
if (!st->desktop->is_admin) return false;
|
||||
|
||||
int x = 16;
|
||||
int sfh = system_font_height();
|
||||
int y = 20;
|
||||
int content_w = win->content_w - 2 * x;
|
||||
int row_h = user_row_height();
|
||||
|
||||
// User list rows
|
||||
for (int i = 0; i < st->user_count; i++) {
|
||||
if (cy >= y && cy < y + row_h && mx >= x && mx < x + content_w) {
|
||||
st->selected_user = (st->selected_user == i) ? -1 : i;
|
||||
return true;
|
||||
}
|
||||
y += row_h + 4;
|
||||
}
|
||||
|
||||
// Separator + gap
|
||||
y += 8 + 1 + 16;
|
||||
|
||||
// Status message (if shown, takes space)
|
||||
if (settings_status_visible(st)) {
|
||||
y += sfh + 8;
|
||||
}
|
||||
|
||||
// Action buttons
|
||||
int btn_x = x;
|
||||
|
||||
// Add User
|
||||
if (mx >= btn_x && mx < btn_x + 90 && cy >= y && cy < y + USER_BTN_H) {
|
||||
open_add_user_dialog(st);
|
||||
return true;
|
||||
}
|
||||
btn_x += 98;
|
||||
|
||||
// Change Password
|
||||
if (mx >= btn_x && mx < btn_x + USER_BTN_W && cy >= y && cy < y + USER_BTN_H) {
|
||||
if (st->selected_user >= 0) open_change_pwd_dialog(st);
|
||||
return true;
|
||||
}
|
||||
btn_x += USER_BTN_W + 8;
|
||||
|
||||
// Delete
|
||||
if (mx >= btn_x && mx < btn_x + 70 && cy >= y && cy < y + USER_BTN_H) {
|
||||
if (st->selected_user >= 0) open_delete_user_dialog(st);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace settings_app
|
||||
Reference in New Issue
Block a user