feature(wm): dynamic dock with persistence, drag-to-reorder, and file pinning

This commit is contained in:
boreddevnl 2026-04-21 16:41:33 +02:00
parent 3893276974
commit 987a96e2e8
2 changed files with 640 additions and 157 deletions

View file

@ -469,7 +469,7 @@ bool explorer_clipboard_has_content(void) {
static bool explorer_copy_recursive(const char *src_path, const char *dest_path) {
if (vfs_is_directory(src_path)) {
if (!vfs_mkdir(dest_path)) return false;
if (!vfs_mkdir(dest_path) && !vfs_is_directory(dest_path)) return false;
int capacity = 64;
vfs_dirent_t *files = (vfs_dirent_t*)kmalloc(capacity * sizeof(vfs_dirent_t));
if (!files) return false;
@ -1761,10 +1761,11 @@ static void explorer_perform_move_internal(Window *win, const char *source_path,
explorer_strcat(origin_path, ".origin");
vfs_delete(origin_path);
}
if (!vfs_rename(source_path, dest_path)) {
if (explorer_copy_recursive(source_path, dest_path)) {
explorer_delete_permanently(source_path);
}
}
explorer_refresh_all();
}

View file

@ -62,7 +62,9 @@ static bool str_eq(const char *s1, const char *s2) {
static int mx = 400, my = 300;
static int prev_mx = 400, prev_my = 300;
static bool start_menu_open = false;
static char *start_menu_pending_app = NULL;
static int pending_dock_click_index = -1;
static int dock_drag_source_index = -1;
static bool dock_drag_active = false;
static int pending_desktop_icon_click = -1;
// Desktop Context Menu
@ -71,6 +73,12 @@ static int desktop_menu_x = 0;
static int desktop_menu_y = 0;
static int desktop_menu_target_icon = -1;
// Dock Context Menu
static bool dock_menu_visible = false;
static int dock_menu_x = 0;
static int dock_menu_y = 0;
static int dock_menu_target_index = -1;
// Desktop Dialog State
static int desktop_dialog_state = 0;
static char desktop_dialog_input[64];
@ -433,19 +441,6 @@ void wm_refresh_desktop(void) {
force_redraw = true;
}
static void create_desktop_shortcut(const char *app_name) {
char path[128] = "/root/Desktop/";
int p = 14;
int n = 0; while(app_name[n]) path[p++] = app_name[n++];
const char *ext = ".shortcut";
int e = 0; while(ext[e]) path[p++] = ext[e++];
path[p] = 0;
FAT32_FileHandle *fh = fat32_open(path, "w");
if (fh) fat32_close(fh);
refresh_desktop_icons();
}
int wm_get_desktop_icon_count(void) {
return desktop_icon_count;
}
@ -566,11 +561,67 @@ static void draw_dock_taskman(int x, int y);
static void draw_dock_word(int x, int y);
static void draw_dock_browser(int x, int y);
static void draw_filled_circle(int cx, int cy, int r, uint32_t color);
static bool thumb_cache_is_failed(const char *path);
static uint32_t* thumb_cache_lookup(const char *path);
static uint32_t* thumb_cache_decode(const char *path);
#define DOCK_ITEM_SIZE 48
#define DOCK_ITEM_SPACING 10
#define DOCK_HEIGHT 60
#define DOCK_VERTICAL_MARGIN 6
#define DOCK_BG_PADDING 12
#define DOCK_ICON_COUNT 12
#define DOCK_ICON_SIZE 48
#define DOCK_ICON_PIXELS (DOCK_ICON_SIZE * DOCK_ICON_SIZE)
#define DOCK_ICON_BASE_PATH "/Library/images/icons/colloid/"
#define MAX_DOCK_ITEMS 32
#define DOCK_LABEL_MAX 64
#define DOCK_CONFIG_PATH "/Library/Dock/dock.cfg"
enum {
DOCK_SLOT_FILES = 0,
DOCK_SLOT_SETTINGS = 1,
DOCK_SLOT_NOTEPAD = 2,
DOCK_SLOT_CALCULATOR = 3,
DOCK_SLOT_GRAPHER = 4,
DOCK_SLOT_TERMINAL = 5,
DOCK_SLOT_MINESWEEPER = 6,
DOCK_SLOT_PAINT = 7,
DOCK_SLOT_BROWSER = 8,
DOCK_SLOT_TASKMAN = 9,
DOCK_SLOT_CLOCK = 10,
DOCK_SLOT_WORD = 11,
};
typedef struct {
char label[DOCK_LABEL_MAX];
char target[FAT32_MAX_PATH];
int icon_slot;
} dock_item_t;
typedef struct {
const char *label;
const char *target;
int icon_slot;
} dock_default_item_t;
static dock_item_t dock_items[MAX_DOCK_ITEMS];
static int dock_item_count = 0;
static const dock_default_item_t dock_default_items[] = {
{"Files", "/root", DOCK_SLOT_FILES},
{"Browser", "/bin/browser.elf", DOCK_SLOT_BROWSER},
{"Settings", "/bin/settings.elf", DOCK_SLOT_SETTINGS},
{"Notepad", "/bin/notepad.elf", DOCK_SLOT_NOTEPAD},
{"Calculator", "/bin/calculator.elf", DOCK_SLOT_CALCULATOR},
{"Grapher", "/bin/grapher.elf", DOCK_SLOT_GRAPHER},
{"Terminal", "/bin/terminal.elf", DOCK_SLOT_TERMINAL},
{"Minesweeper", "/bin/minesweeper.elf", DOCK_SLOT_MINESWEEPER},
{"Paint", "/bin/paint.elf", DOCK_SLOT_PAINT},
{"Task Manager", "/bin/taskman.elf", DOCK_SLOT_TASKMAN},
{"Clock", "/bin/clock.elf", DOCK_SLOT_CLOCK},
{"BoredWord", "/bin/boredword.elf", DOCK_SLOT_WORD},
};
typedef enum {
DOCK_ICON_UNTRIED = 0,
@ -751,24 +802,422 @@ static void draw_dock_icon_slot_png(int x, int y, int slot_index) {
(void)wm_draw_dock_icon_scaled(x, y, DOCK_ICON_SIZE, slot_index);
}
static bool draw_icon_path_scaled(int x, int y, int size, const char *path) {
uint32_t *icon = NULL;
if (size <= 0 || !path || !path[0]) return false;
icon = thumb_cache_lookup(path);
if (!icon && !thumb_cache_is_failed(path)) {
icon = thumb_cache_decode(path);
}
if (!icon) return false;
int src_max = 47;
for (int ty = 0; ty < size; ty++) {
int sy = (size > 1) ? (ty * src_max) / (size - 1) : 0;
if (sy < 0) sy = 0;
if (sy > src_max) sy = src_max;
for (int tx = 0; tx < size; tx++) {
int sx = (size > 1) ? (tx * src_max) / (size - 1) : 0;
if (sx < 0) sx = 0;
if (sx > src_max) sx = src_max;
uint32_t src = icon[sy * 48 + sx];
uint32_t a = (src >> 24) & 0xFF;
if (a == 0) continue;
if (a == 255) {
put_pixel(x + tx, y + ty, 0xFF000000 | (src & 0x00FFFFFF));
} else {
uint32_t dst = graphics_get_pixel(x + tx, y + ty);
put_pixel(x + tx, y + ty, blend_src_over_dst(dst, src));
}
}
}
return true;
}
static bool dock_resolve_shortcut_target(const char *shortcut_path, char *out_target, int out_target_size) {
if (!shortcut_path || !out_target || out_target_size <= 1) return false;
FAT32_FileHandle *fh = fat32_open(shortcut_path, "r");
if (!fh) return false;
int len = fat32_read(fh, out_target, out_target_size - 1);
fat32_close(fh);
if (len <= 0) return false;
while (len > 0 && (out_target[len - 1] == '\n' || out_target[len - 1] == '\r')) {
len--;
}
out_target[len] = 0;
return len > 0;
}
static bool dock_draw_metadata_icon(int x, int y, const dock_item_t *item) {
if (!item || !item->target[0]) return false;
char elf_target[FAT32_MAX_PATH];
const char *metadata_target = NULL;
if (str_ends_with(item->target, ".elf")) {
metadata_target = item->target;
} else if (str_ends_with(item->target, ".shortcut")) {
if (dock_resolve_shortcut_target(item->target, elf_target, sizeof(elf_target)) &&
str_ends_with(elf_target, ".elf")) {
metadata_target = elf_target;
}
}
if (!metadata_target) return false;
char icon_path[BOREDOS_APP_METADATA_MAX_IMAGE_PATH];
if (!app_metadata_get_primary_image(metadata_target, icon_path, sizeof(icon_path))) {
return false;
}
return draw_icon_path_scaled(x, y, DOCK_ICON_SIZE, icon_path);
}
static void dock_copy_text(char *dst, int dst_size, const char *src) {
if (!dst || dst_size <= 0) return;
int i = 0;
if (src) {
while (src[i] && i < dst_size - 1) {
dst[i] = src[i];
i++;
}
}
dst[i] = 0;
}
static int dock_parse_int(const char *s) {
if (!s) return 0;
int v = 0;
while (*s >= '0' && *s <= '9') {
v = (v * 10) + (*s - '0');
s++;
}
return v;
}
static void dock_write_int(FAT32_FileHandle *fh, int v) {
char tmp[16];
int n = 0;
if (v == 0) {
tmp[n++] = '0';
} else {
char rev[16];
int r = 0;
while (v > 0 && r < 15) {
rev[r++] = '0' + (v % 10);
v /= 10;
}
while (r > 0) tmp[n++] = rev[--r];
}
if (n > 0) fat32_write(fh, tmp, n);
}
static void dock_label_from_target(const char *target, char *out_label, int out_size) {
const char *name = target;
if (!target || !target[0]) {
dock_copy_text(out_label, out_size, "Item");
return;
}
int i = 0;
while (target[i]) {
if (target[i] == '/') name = &target[i + 1];
i++;
}
if (!name || !name[0]) {
dock_copy_text(out_label, out_size, "Files");
return;
}
dock_copy_text(out_label, out_size, name);
int len = (int)k_strlen(out_label);
if (len > 4 && str_ends_with(out_label, ".elf")) {
out_label[len - 4] = 0;
} else if (len > 9 && str_ends_with(out_label, ".shortcut")) {
out_label[len - 9] = 0;
}
}
static int dock_icon_slot_for_target(const char *target, const char *label) {
(void)label;
if (!target || !target[0]) return DOCK_SLOT_FILES;
if (str_eq(target, "/") != 0 || str_eq(target, "/root") != 0 || fat32_is_directory(target)) {
return DOCK_SLOT_FILES;
}
if (str_ends_with(target, ".elf")) return DOCK_SLOT_TERMINAL;
if (str_ends_with(target, ".shortcut")) {
char shortcut_target[FAT32_MAX_PATH];
if (dock_resolve_shortcut_target(target, shortcut_target, sizeof(shortcut_target)) &&
str_ends_with(shortcut_target, ".elf")) {
return DOCK_SLOT_TERMINAL;
}
return DOCK_SLOT_NOTEPAD;
}
return DOCK_SLOT_NOTEPAD;
}
static bool dock_insert_item(int index, const char *label, const char *target, int icon_slot) {
if (dock_item_count >= MAX_DOCK_ITEMS) return false;
if (index < 0) index = 0;
if (index > dock_item_count) index = dock_item_count;
if (icon_slot < 0 || icon_slot >= DOCK_ICON_COUNT) icon_slot = DOCK_SLOT_FILES;
for (int i = dock_item_count; i > index; i--) dock_items[i] = dock_items[i - 1];
dock_copy_text(dock_items[index].label, DOCK_LABEL_MAX, label);
dock_copy_text(dock_items[index].target, FAT32_MAX_PATH, target);
dock_items[index].icon_slot = icon_slot;
dock_item_count++;
return true;
}
static void dock_remove_item(int index) {
if (index < 0 || index >= dock_item_count) return;
for (int i = index; i < dock_item_count - 1; i++) dock_items[i] = dock_items[i + 1];
dock_item_count--;
}
static void dock_move_item(int from_index, int to_index) {
if (from_index < 0 || from_index >= dock_item_count) return;
if (to_index < 0) to_index = 0;
if (to_index > dock_item_count) to_index = dock_item_count;
if (to_index > from_index) to_index--;
if (to_index == from_index) return;
dock_item_t temp = dock_items[from_index];
if (to_index > from_index) {
for (int i = from_index; i < to_index; i++) dock_items[i] = dock_items[i + 1];
} else {
for (int i = from_index; i > to_index; i--) dock_items[i] = dock_items[i - 1];
}
dock_items[to_index] = temp;
}
static int dock_find_item_by_target(const char *target) {
if (!target) return -1;
for (int i = 0; i < dock_item_count; i++) {
if (str_eq(dock_items[i].target, target) != 0) return i;
}
return -1;
}
static int dock_total_width(void) {
if (dock_item_count <= 0) return 0;
return (dock_item_count * DOCK_ITEM_SIZE) + ((dock_item_count - 1) * DOCK_ITEM_SPACING);
}
static bool dock_rect_contains(int x, int y, int w, int h, int px, int py) {
return px >= x && px < x + w && py >= y && py < y + h;
}
static void dock_get_geometry(int sw, int sh, int *dock_x, int *dock_y, int *dock_bg_x, int *dock_bg_w) {
int total_w = dock_total_width();
if (dock_x) *dock_x = (sw - total_w) / 2;
if (dock_y) *dock_y = sh - DOCK_HEIGHT - DOCK_VERTICAL_MARGIN;
if (dock_bg_x) *dock_bg_x = (sw - total_w) / 2 - DOCK_BG_PADDING;
if (dock_bg_w) *dock_bg_w = total_w + (DOCK_BG_PADDING * 2);
}
static bool dock_point_in_bounds(int x, int y, int sw, int sh) {
int dock_y, dock_bg_x, dock_bg_w;
dock_get_geometry(sw, sh, NULL, &dock_y, &dock_bg_x, &dock_bg_w);
return dock_item_count > 0 && dock_rect_contains(dock_bg_x, dock_y, dock_bg_w, DOCK_HEIGHT, x, y);
}
static int dock_item_index_at_point(int x, int y, int sw, int sh) {
if (!dock_point_in_bounds(x, y, sw, sh)) return -1;
int dock_x;
dock_get_geometry(sw, sh, &dock_x, NULL, NULL, NULL);
for (int i = 0; i < dock_item_count; i++) {
int ix = dock_x + i * (DOCK_ITEM_SIZE + DOCK_ITEM_SPACING);
if (dock_rect_contains(ix, sh - DOCK_HEIGHT - DOCK_VERTICAL_MARGIN + 6, DOCK_ITEM_SIZE, DOCK_ITEM_SIZE, x, y)) {
return i;
}
}
return -1;
}
static int dock_insertion_index_from_x(int x, int y, int sw, int sh) {
if (!dock_point_in_bounds(x, y, sw, sh)) return -1;
int dock_x;
dock_get_geometry(sw, sh, &dock_x, NULL, NULL, NULL);
for (int i = 0; i < dock_item_count; i++) {
int ix = dock_x + i * (DOCK_ITEM_SIZE + DOCK_ITEM_SPACING);
if (x < ix + (DOCK_ITEM_SIZE / 2)) return i;
}
return dock_item_count;
}
static void dock_save_config(void) {
fat32_mkdir("/root");
fat32_mkdir("/Library/Dock/dock.cfg");
FAT32_FileHandle *fh = fat32_open(DOCK_CONFIG_PATH, "w");
if (!fh) return;
const char *header = "v1\n";
fat32_write(fh, header, (int)k_strlen(header));
for (int i = 0; i < dock_item_count; i++) {
dock_write_int(fh, dock_items[i].icon_slot);
fat32_write(fh, "|", 1);
fat32_write(fh, dock_items[i].label, (int)k_strlen(dock_items[i].label));
fat32_write(fh, "|", 1);
fat32_write(fh, dock_items[i].target, (int)k_strlen(dock_items[i].target));
fat32_write(fh, "\n", 1);
}
fat32_close(fh);
}
static void dock_seed_defaults(void) {
dock_item_count = 0;
int default_count = (int)(sizeof(dock_default_items) / sizeof(dock_default_items[0]));
for (int i = 0; i < default_count; i++) {
dock_insert_item(dock_item_count, dock_default_items[i].label,
dock_default_items[i].target, dock_default_items[i].icon_slot);
}
}
static void dock_load_config(void) {
dock_item_count = 0;
FAT32_FileHandle *fh = fat32_open(DOCK_CONFIG_PATH, "r");
if (!fh) {
dock_seed_defaults();
dock_save_config();
return;
}
uint32_t size = fh->size;
if (size == 0 || size > 16384) {
fat32_close(fh);
dock_seed_defaults();
dock_save_config();
return;
}
char *buffer = (char*)kmalloc(size + 1);
if (!buffer) {
fat32_close(fh);
dock_seed_defaults();
return;
}
int total = 0;
while (total < (int)size) {
int chunk = fat32_read(fh, buffer + total, (int)size - total);
if (chunk <= 0) break;
total += chunk;
}
fat32_close(fh);
buffer[total] = 0;
char *cursor = buffer;
while (*cursor) {
char *line = cursor;
while (*cursor && *cursor != '\n' && *cursor != '\r') cursor++;
if (*cursor) {
*cursor = 0;
cursor++;
while (*cursor == '\n' || *cursor == '\r') cursor++;
}
if (!line[0] || str_eq(line, "v1") != 0 || line[0] == '#') continue;
char *sep1 = line;
while (*sep1 && *sep1 != '|') sep1++;
if (*sep1 != '|') continue;
*sep1 = 0;
char *label = sep1 + 1;
char *sep2 = label;
while (*sep2 && *sep2 != '|') sep2++;
if (*sep2 != '|') continue;
*sep2 = 0;
char *target = sep2 + 1;
if (!label[0] || !target[0]) continue;
int icon_slot = dock_parse_int(line);
if (icon_slot < 0 || icon_slot >= DOCK_ICON_COUNT) {
icon_slot = dock_icon_slot_for_target(target, label);
}
if (!dock_insert_item(dock_item_count, label, target, icon_slot)) break;
}
kfree(buffer);
if (dock_item_count == 0) {
dock_seed_defaults();
dock_save_config();
}
}
static bool dock_can_pin_path(const char *path) {
if (!path || !path[0]) return false;
if (path[0] != '/') return false;
if (fat32_is_directory(path)) return true;
if (str_ends_with(path, ".elf")) return true;
if (str_ends_with(path, ".shortcut")) return true;
return false;
}
static bool dock_launch_shortcut_path(const char *shortcut_path) {
FAT32_FileHandle *fh = fat32_open(shortcut_path, "r");
if (!fh) return false;
char buf[FAT32_MAX_PATH];
int len = fat32_read(fh, buf, FAT32_MAX_PATH - 1);
fat32_close(fh);
if (len <= 0) return false;
while (len > 0 && (buf[len - 1] == '\n' || buf[len - 1] == '\r')) len--;
buf[len] = 0;
if (!buf[0]) return false;
explorer_open_target(buf);
return true;
}
static void dock_launch_item(int index) {
if (index < 0 || index >= dock_item_count) return;
const char *target = dock_items[index].target;
if (!target || !target[0]) return;
if (str_ends_with(target, ".shortcut")) {
if (dock_launch_shortcut_path(target)) return;
}
explorer_open_target(target);
}
static bool dock_item_is_protected(int index) {
if (index < 0 || index >= dock_item_count) return false;
const dock_item_t *item = &dock_items[index];
return (str_eq(item->label, "Files") != 0) && (str_eq(item->target, "/root") != 0);
}
static void draw_scaled_icon(int x, int y, void (*draw_fn)(int, int)) {
// 48x48 buffer for the dock icon
uint32_t icon_buf[48 * 48];
// Clear to magenta (transparent key color)
for (int i = 0; i < 48 * 48; i++) icon_buf[i] = 0xFFFF00FF;
// Redirect graphics to our buffer
graphics_set_render_target(icon_buf, 48, 48);
// Draw at 0,0 in the buffer
draw_fn(0, 0);
// Restore graphics to screen
graphics_set_render_target(NULL, 0, 0);
// Calculate centered x,y in the 80x80 cell
// (80-32)/2 = 24.
int dx = x + 24, dy = y + 12;
// Blit scaled down (nearest neighbor 48x48 -> 32x32 downsample, ratio = 1.5)
for (int ty = 0; ty < 32; ty++) {
for (int tx = 0; tx < 32; tx++) {
int src_x = tx * 48 / 32;
@ -1593,7 +2042,6 @@ void draw_window(Window *win) {
}
}
// Draw Mouse Cursor (classic outlined arrow)
void draw_cursor(int x, int y) {
// '.' transparent, 'w' white outline, 'b' black fill
static const char cursor_bitmap[CURSOR_H][CURSOR_W + 1] = {
@ -1929,15 +2377,25 @@ static void wm_paint_region(int y_start, int y_end, DirtyRect dirty, int pass) {
draw_string(20, 108, "Restart", COLOR_DARK_TEXT);
}
int dock_h = 60, dock_y = sh - dock_h - 6;
if (dock_y < cy + ch && dock_y + dock_h > cy) {
int d_item_sz = 48, d_space = 10, d_total_w = 12 * (d_item_sz + d_space);
int d_bg_x = (sw - d_total_w) / 2 - 12, d_bg_w = d_total_w + 24;
draw_rounded_rect_blurred(d_bg_x, dock_y, d_bg_w, dock_h, 18, COLOR_DOCK_BG, 1, 180);
int dx = (sw - d_total_w) / 2, dy = dock_y + 6;
for (int i = 0; i < DOCK_ICON_COUNT; i++) {
draw_dock_icon_slot_png(dx, dy, i);
dx += d_item_sz + d_space;
int dock_y, dock_x, dock_bg_x, dock_bg_w;
dock_get_geometry(sw, sh, &dock_x, &dock_y, &dock_bg_x, &dock_bg_w);
if (dock_item_count > 0 && dock_y < cy + ch && dock_y + DOCK_HEIGHT > cy) {
draw_rounded_rect_blurred(dock_bg_x, dock_y, dock_bg_w, DOCK_HEIGHT, 18, COLOR_DOCK_BG, 1, 180);
int dx = dock_x;
int dy = dock_y + 6;
for (int i = 0; i < dock_item_count; i++) {
if (!dock_draw_metadata_icon(dx, dy, &dock_items[i])) {
draw_dock_icon_slot_png(dx, dy, dock_items[i].icon_slot);
}
dx += DOCK_ITEM_SIZE + DOCK_ITEM_SPACING;
}
}
if (dock_menu_visible) {
int d_mw = 140, d_mh = 25;
if (dock_menu_y < cy + ch && dock_menu_y + d_mh > cy) {
draw_rounded_rect_filled(dock_menu_x, dock_menu_y, d_mw, d_mh, 8, COLOR_DARK_PANEL);
draw_string(dock_menu_x + 10, dock_menu_y + 8, "Remove from Dock", COLOR_TRAFFIC_RED);
}
}
@ -1979,11 +2437,19 @@ static void wm_paint_region(int y_start, int y_end, DirtyRect dirty, int pass) {
}
if (msg_box_visible) {
int mw = 320, mh = 100, m_x = (sw - mw)/2, m_y = (sh - mh)/2;
ttf_font_t *_ttf = graphics_get_current_ttf();
int title_w = _ttf ? font_manager_get_string_width(_ttf, msg_box_title) : (int)(k_strlen(msg_box_title) * 8);
int text_w = _ttf ? font_manager_get_string_width(_ttf, msg_box_text) : (int)(k_strlen(msg_box_text) * 8);
int content_w = (title_w > text_w) ? title_w : text_w;
int padding = 30; // horizontal padding on each side
int mw = content_w + padding * 2;
if (mw < 160) mw = 160; // minimum width
int mh = 100;
int m_x = (sw - mw)/2, m_y = (sh - mh)/2;
if (m_y < cy + ch && m_y + mh > cy) {
draw_rounded_rect_filled(m_x, m_y, mw, mh, 8, COLOR_DARK_PANEL);
draw_string(m_x + 15, m_y + 10, msg_box_title, COLOR_DARK_TEXT);
draw_string(m_x + 10, m_y + 40, msg_box_text, COLOR_DARK_TEXT);
draw_string(m_x + 15, m_y + 40, msg_box_text, COLOR_DARK_TEXT);
draw_rounded_rect_filled(m_x + mw/2 - 30, m_y + 70, 60, 20, 4, COLOR_DARK_BORDER);
draw_string(m_x + mw/2 - 10, m_y + 75, "OK", COLOR_WHITE);
}
@ -2033,12 +2499,12 @@ void wm_paint(void) {
dirty = graphics_get_dirty_rect();
menubar_dirty_pending = false;
}
if (dirty.active) {
int d_h = 60, d_y = sh - d_h - 6, d_total_w = 11 * (48 + 10);
int d_bg_x = (sw - d_total_w) / 2 - 12, d_bg_w = d_total_w + 24;
if (dirty.active && dock_item_count > 0) {
int d_x, d_y, d_bg_x, d_bg_w;
dock_get_geometry(sw, sh, &d_x, &d_y, &d_bg_x, &d_bg_w);
if (!(dirty.x >= d_bg_x + d_bg_w || dirty.x + dirty.w <= d_bg_x ||
dirty.y >= d_y + d_h || dirty.y + dirty.h <= d_y)) {
graphics_mark_dirty(d_bg_x - 10, d_y - 10, d_bg_w + 20, d_h + 20);
dirty.y >= d_y + DOCK_HEIGHT || dirty.y + dirty.h <= d_y)) {
graphics_mark_dirty(d_bg_x - 10, d_y - 10, d_bg_w + 20, DOCK_HEIGHT + 20);
dirty = graphics_get_dirty_rect();
}
}
@ -2256,6 +2722,28 @@ void wm_handle_click(int x, int y) {
return;
}
if (dock_menu_visible) {
int menu_w = 140;
int menu_h = 25;
if (rect_contains(dock_menu_x, dock_menu_y, menu_w, menu_h, x, y)) {
if (dock_menu_target_index >= 0 && dock_menu_target_index < dock_item_count) {
if (dock_item_is_protected(dock_menu_target_index)) {
wm_show_message("Dock", "Unable to remove the Files app from the dock.");
} else {
dock_remove_item(dock_menu_target_index);
dock_save_config();
}
}
}
dock_menu_visible = false;
dock_menu_target_index = -1;
pending_dock_click_index = -1;
dock_drag_active = false;
dock_drag_source_index = -1;
force_redraw = true;
return;
}
// Handle Desktop Context Menu Click
if (desktop_menu_visible) {
int menu_w = 140;
@ -2500,7 +2988,13 @@ void wm_handle_click(int x, int y) {
// Handle right click (context menu or special actions)
void wm_handle_right_click(int x, int y) {
desktop_menu_visible = false; // Close if open
desktop_menu_visible = false;
dock_menu_visible = false;
dock_menu_target_index = -1;
int sw = get_screen_width();
int sh = get_screen_height();
// Find topmost window at click location
Window *topmost = NULL;
int topmost_z = -1;
@ -2525,6 +3019,20 @@ void wm_handle_right_click(int x, int y) {
}
}
} else {
int dock_item = dock_item_index_at_point(x, y, sw, sh);
if (dock_item != -1) {
dock_menu_visible = true;
dock_menu_x = x;
dock_menu_y = y;
dock_menu_target_index = dock_item;
force_redraw = true;
return;
}
if (dock_point_in_bounds(x, y, sw, sh)) {
force_redraw = true;
return;
}
// Desktop Right Click
desktop_menu_visible = true;
desktop_menu_x = x;
@ -2598,35 +3106,24 @@ static void wm_handle_mouse_internal(int dx, int dy, uint8_t buttons, int dz) {
if (left && !prev_left) {
drag_start_x = mx;
drag_start_y = my;
int dock_h = 60;
int dock_y = sh - dock_h - 6;
int dock_item_size = 48;
int dock_spacing = 10;
int total_dock_width = 12 * (dock_item_size + dock_spacing);
int dock_bg_x = (sw - total_dock_width) / 2 - 12;
int dock_bg_w = total_dock_width + 24;
if (rect_contains(dock_bg_x, dock_y, dock_bg_w, dock_h, mx, my)) {
int dock_x = (sw - total_dock_width) / 2;
// Check which dock item was clicked
int item_x = mx - dock_x;
if (item_x >= 0) {
int item = item_x / (dock_item_size + dock_spacing);
if (item == 0) start_menu_pending_app = "Files";
else if (item == 1) start_menu_pending_app = "Settings";
else if (item == 2) start_menu_pending_app = "Notepad";
else if (item == 3) start_menu_pending_app = "Calculator";
else if (item == 4) start_menu_pending_app = "Grapher";
else if (item == 5) start_menu_pending_app = "Terminal";
else if (item == 6) start_menu_pending_app = "Minesweeper";
else if (item == 7) start_menu_pending_app = "Paint";
else if (item == 8) start_menu_pending_app = "Browser";
else if (item == 9) start_menu_pending_app = "Task Manager";
else if (item == 10) start_menu_pending_app = "Clock";
else if (item == 11) start_menu_pending_app = "Word Processor";
if (dock_menu_visible) {
pending_dock_click_index = -1;
dock_drag_active = false;
dock_drag_source_index = -1;
wm_handle_click(mx, my);
prev_left = left;
prev_right = right;
return;
}
} else {
int dock_item = dock_item_index_at_point(mx, my, sw, sh);
if (dock_item != -1) {
pending_dock_click_index = dock_item;
dock_drag_active = false;
dock_drag_source_index = -1;
dock_menu_visible = false;
} else if (!dock_point_in_bounds(mx, my, sw, sh)) {
wm_handle_click(mx, my);
}
} else if (right && !prev_right) {
@ -2671,19 +3168,13 @@ static void wm_handle_mouse_internal(int dx, int dy, uint8_t buttons, int dz) {
if (dist_y < 0) dist_y = -dist_y;
if (dist_x >= 5 || dist_y >= 5) {
// Check for Start Menu Drag
if (start_menu_pending_app) {
// Start dragging app from start menu
if (pending_dock_click_index != -1 && pending_dock_click_index < dock_item_count) {
dock_drag_active = true;
dock_drag_source_index = pending_dock_click_index;
pending_dock_click_index = -1;
is_dragging_file = true;
drag_icon_type = 2;
// Construct special path for app drag
char *p = drag_file_path;
const char *prefix = "::APP::";
while(*prefix) *p++ = *prefix++;
char *n = start_menu_pending_app;
while(*n) *p++ = *n++;
*p = 0;
start_menu_pending_app = NULL;
drag_file_path[0] = 0;
}
if (pending_desktop_icon_click != -1) {
@ -2745,72 +3236,10 @@ static void wm_handle_mouse_internal(int dx, int dy, uint8_t buttons, int dz) {
force_redraw = true;
}
// Handle Dock App Click (Mouse Up without Drag)
if (start_menu_pending_app) {
// Launch App
if (str_starts_with(start_menu_pending_app, "Files")) {
explorer_open_directory("/root");
} else if (str_starts_with(start_menu_pending_app, "Notepad")) {
Window *existing = wm_find_window_by_title_locked("Notepad");
if (existing) {
wm_bring_to_front_locked(existing);
} else {
process_create_elf("/bin/notepad.elf", NULL, false, -1);
}
} else if (str_starts_with(start_menu_pending_app, "Editor")) {
Window *existing = wm_find_window_by_title_locked("Txtedit");
if (existing) wm_bring_to_front_locked(existing);
else process_create_elf("/bin/txtedit.elf", NULL, false, -1);
} else if (str_starts_with(start_menu_pending_app, "Word Processor")) {
Window *existing = wm_find_window_by_title_locked("Word Processor");
if (existing) wm_bring_to_front_locked(existing);
else process_create_elf("/bin/boredword.elf", NULL, false, -1);
} else if (str_starts_with(start_menu_pending_app, "Terminal")) {
process_create_elf("/bin/terminal.elf", NULL, false, -1);
} else if (str_starts_with(start_menu_pending_app, "Grapher")) {
Window *existing = wm_find_window_by_title_locked("Grapher");
if (existing) wm_bring_to_front_locked(existing);
else process_create_elf("/bin/grapher.elf", NULL, false, -1);
} else if (str_starts_with(start_menu_pending_app, "Calculator")) {
Window *existing = wm_find_window_by_title_locked("Calculator");
if (existing) {
wm_bring_to_front_locked(existing);
} else {
process_create_elf("/bin/calculator.elf", NULL, false, -1);
}
} else if (str_starts_with(start_menu_pending_app, "Minesweeper")) {
Window *existing = wm_find_window_by_title_locked("Minesweeper");
if (existing) wm_bring_to_front_locked(existing);
else process_create_elf("/bin/minesweeper.elf", NULL, false, -1);
} else if (str_starts_with(start_menu_pending_app, "Settings")) {
Window *existing = wm_find_window_by_title_locked("Settings");
if (existing) wm_bring_to_front_locked(existing);
else process_create_elf("/bin/settings.elf", NULL, false, -1);
} else if (str_starts_with(start_menu_pending_app, "Paint")) {
Window *existing = wm_find_window_by_title_locked("Paint");
if (existing) wm_bring_to_front_locked(existing);
else process_create_elf("/bin/paint.elf", NULL, false, -1);
} else if (str_starts_with(start_menu_pending_app, "Clock")) {
Window *existing = wm_find_window_by_title_locked("Clock");
if (existing) wm_bring_to_front_locked(existing);
else process_create_elf("/bin/clock.elf", NULL, false, -1);
} else if (str_starts_with(start_menu_pending_app, "Browser")) {
Window *existing = wm_find_window_by_title_locked("Web Browser");
if (existing) wm_bring_to_front_locked(existing);
else process_create_elf("/bin/browser.elf", NULL, false, -1);
} else if (str_starts_with(start_menu_pending_app, "About")) {
process_create_elf("/bin/about.elf", NULL, false, -1);
} else if (str_starts_with(start_menu_pending_app, "Task Manager")) {
Window *existing = wm_find_window_by_title_locked("Task Manager");
if (existing) wm_bring_to_front_locked(existing);
else process_create_elf("/bin/taskman.elf", NULL, false, -1);
} else if (str_starts_with(start_menu_pending_app, "Shutdown")) {
k_shutdown();
} else if (str_starts_with(start_menu_pending_app, "Restart")) {
k_reboot();
}
start_menu_pending_app = NULL;
// Handle Dock App Click (mouse up without dragging)
if (pending_dock_click_index != -1 && !dock_drag_active) {
dock_launch_item(pending_dock_click_index);
pending_dock_click_index = -1;
force_redraw = true;
}
@ -2895,6 +3324,39 @@ static void wm_handle_mouse_internal(int dx, int dy, uint8_t buttons, int dz) {
if (is_dragging_file) {
// Drop logic
bool drop_handled = false;
if (dock_drag_active && dock_drag_source_index >= 0 && dock_drag_source_index < dock_item_count) {
int insert_idx = dock_insertion_index_from_x(mx, my, sw, sh);
if (insert_idx != -1) {
dock_move_item(dock_drag_source_index, insert_idx);
dock_save_config();
}
drop_handled = true;
}
if (!drop_handled && dock_point_in_bounds(mx, my, sw, sh)) {
int insert_idx = dock_insertion_index_from_x(mx, my, sw, sh);
if (insert_idx == -1) insert_idx = dock_item_count;
if (dock_can_pin_path(drag_file_path)) {
int existing_idx = dock_find_item_by_target(drag_file_path);
if (existing_idx != -1) {
dock_move_item(existing_idx, insert_idx);
} else if (dock_item_count < MAX_DOCK_ITEMS) {
char label[DOCK_LABEL_MAX];
dock_label_from_target(drag_file_path, label, sizeof(label));
int icon_slot = dock_icon_slot_for_target(drag_file_path, label);
dock_insert_item(insert_idx, label, drag_file_path, icon_slot);
} else {
wm_show_message("Dock", "Dock is full.");
}
dock_save_config();
} else {
wm_show_message("Dock", "Only folders, .elf and .shortcut can be pinned.");
}
drop_handled = true;
}
Window *drop_win = NULL;
int topmost_z = -1;
@ -2908,7 +3370,7 @@ static void wm_handle_mouse_internal(int dx, int dy, uint8_t buttons, int dz) {
}
}
if (drop_win) {
if (!drop_handled && drop_win) {
char target_path[256];
bool is_dir;
// Check if dropped on a folder inside this explorer
@ -2922,11 +3384,9 @@ static void wm_handle_mouse_internal(int dx, int dy, uint8_t buttons, int dz) {
if (str_starts_with(drag_file_path, "/root/Desktop/")) {
refresh_desktop_icons();
}
} else {
} else if (!drop_handled) {
// Dropped on Desktop (or elsewhere)
if (drag_file_path[0] == ':' && drag_file_path[1] == ':' && drag_file_path[2] == 'A') {
create_desktop_shortcut(drag_file_path + 7);
} else {
{
bool from_desktop = (drag_file_path[0]=='/' && drag_file_path[1]=='D' && drag_file_path[2]=='e');
bool dropped_on_target = false;
for (int i = 0; i < desktop_icon_count; i++) {
@ -3089,6 +3549,8 @@ static void wm_handle_mouse_internal(int dx, int dy, uint8_t buttons, int dz) {
if (is_dragging_file) {
is_dragging_file = false;
dock_drag_active = false;
dock_drag_source_index = -1;
force_redraw = true;
}
}
@ -3407,6 +3869,9 @@ void wm_init(void) {
refresh_desktop_icons();
log_ok("Desktop icons refreshed");
dock_load_config();
log_ok("Dock config loaded");
// Initialize z-indices
win_explorer.z_index = 1;
@ -3464,9 +3929,26 @@ void wm_timer_tick(void) {
}
}
static volatile bool index_rebuild_queued = false;
static void index_rebuild_wrapper(void *arg) {
(void)arg;
file_index_build();
lumos_index_built = file_index_is_valid();
index_rebuild_queued = false;
}
void wm_notify_fs_change(void) {
periodic_refresh_pending = true;
file_index_invalidate_cache();
lumos_index_built = false;
// Kick off a background index rebuild so Lumos reflects changes immediately.
// The flag prevents flooding the work queue when many files change at once
// (e.g. during a bulk folder move that triggers one notification per file).
if (!index_rebuild_queued) {
index_rebuild_queued = true;
work_queue_submit(index_rebuild_wrapper, NULL);
}
}