Files
MontaukOS/programs/src/desktop/apps/filemanager/render.cpp
T

591 lines
24 KiB
C++

/*
* render.cpp
* Drawing code for the embedded desktop file manager
* Copyright (c) 2026 Daniel Hammer
*/
#include "filemanager_internal.hpp"
namespace filemanager {
static bool label_break_char(char ch) {
return ch == ' ' || ch == '-' || ch == '_' || ch == '/';
}
static void copy_label_range(char* dst, int dst_max, const char* src, int start, int end) {
if (!dst || dst_max <= 0) return;
while (start < end && src[start] == ' ') start++;
while (end > start && src[end - 1] == ' ') end--;
int out = 0;
for (int i = start; i < end && out < dst_max - 1; i++) {
dst[out++] = src[i];
}
dst[out] = '\0';
}
static int fitted_prefix_len(const char* src, int max_w, int max_len) {
char tmp[128];
int best = 0;
int limit = max_len < 127 ? max_len : 127;
for (int len = 1; len <= limit; len++) {
copy_label_range(tmp, sizeof(tmp), src, 0, len);
if (text_width(tmp) > max_w) break;
best = len;
}
return best > 0 ? best : 1;
}
static void fit_label_with_ellipsis(char* dst, int dst_max, const char* src, int max_w) {
montauk::strncpy(dst, src, dst_max);
if (text_width(dst) <= max_w) return;
int ellipsis_w = text_width("...");
int len = montauk::slen(dst);
while (len > 0) {
dst[len] = '\0';
if (text_width(dst) + ellipsis_w <= max_w) break;
len--;
}
if (len > dst_max - 4) len = dst_max - 4;
if (len < 0) len = 0;
dst[len] = '.';
dst[len + 1] = '.';
dst[len + 2] = '.';
dst[len + 3] = '\0';
}
static void build_grid_label_lines(const char* name, int max_w,
char* line1, int line1_max,
char* line2, int line2_max) {
line1[0] = '\0';
line2[0] = '\0';
if (text_width(name) <= max_w) {
montauk::strncpy(line1, name, line1_max);
return;
}
int nlen = montauk::slen(name);
int break_end = -1;
int break_next = -1;
for (int i = 0; i < nlen - 1; i++) {
if (!label_break_char(name[i])) continue;
int end = (name[i] == ' ') ? i : i + 1;
int next = i + 1;
char candidate[128];
copy_label_range(candidate, sizeof(candidate), name, 0, end);
if (candidate[0] != '\0' && text_width(candidate) <= max_w) {
break_end = end;
break_next = next;
}
}
if (break_end < 0) {
break_end = fitted_prefix_len(name, max_w, nlen);
break_next = break_end;
}
copy_label_range(line1, line1_max, name, 0, break_end);
fit_label_with_ellipsis(line2, line2_max, name + break_next, max_w);
}
static void draw_centered_grid_label(Canvas& c, int cell_x, int y, const char* label, Color color) {
int tw = text_width(label);
int tx = cell_x + (FM_GRID_CELL_W - tw) / 2;
if (tx < cell_x + 2) tx = cell_x + 2;
c.text(tx, y, label, color);
}
int filemanager_grid_row_height(FileManagerState* fm, int row, int cols) {
if (!fm || cols <= 0 || row < 0) return FM_GRID_CELL_H;
int start = row * cols;
int end = start + cols;
if (end > fm->entry_count) end = fm->entry_count;
int max_w = FM_GRID_CELL_W - 4;
for (int i = start; i < end; i++) {
if (text_width(fm->entry_names[i]) > max_w) {
return FM_GRID_CELL_H_WRAPPED;
}
}
return FM_GRID_CELL_H;
}
int filemanager_grid_row_y(FileManagerState* fm, int row, int cols) {
int y = 0;
for (int r = 0; r < row; r++) {
y += filemanager_grid_row_height(fm, r, cols);
}
return y;
}
int filemanager_grid_content_height(FileManagerState* fm, int cols) {
if (!fm || cols <= 0) return 0;
int rows = (fm->entry_count + cols - 1) / cols;
int h = 0;
for (int r = 0; r < rows; r++) {
h += filemanager_grid_row_height(fm, r, cols);
}
return h;
}
int filemanager_grid_index_at(FileManagerState* fm, int cols, int x, int rel_y) {
if (!fm || cols <= 0 || x < 0 || rel_y < 0) return -1;
int col = x / FM_GRID_CELL_W;
if (col < 0 || col >= cols) return -1;
int rows = (fm->entry_count + cols - 1) / cols;
int row_y = 0;
for (int row = 0; row < rows; row++) {
int row_h = filemanager_grid_row_height(fm, row, cols);
if (rel_y < row_y + row_h) {
int idx = row * cols + col;
return idx < fm->entry_count ? idx : -1;
}
row_y += row_h;
}
return -1;
}
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
&& !filemanager_is_computer_view(fm) && !filemanager_is_virtual_view(fm)
&& fm->entry_types[fm->selected] != FM_ENTRY_DRIVE;
bool has_clip = fm->clipboard_has_data && !filemanager_is_computer_view(fm)
&& !filemanager_is_virtual_view(fm);
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,
!filemanager_is_computer_view(fm) && !filemanager_is_virtual_view(fm) },
{ 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 (filemanager_is_computer_view(fm)) pathbar_text = "Computer";
else if (filemanager_is_virtual_view(fm)) pathbar_text = filemanager_virtual_view_label(fm->virtual_view);
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 content_h = filemanager_grid_content_height(fm, cols);
// 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 row_h = filemanager_grid_row_height(fm, row, cols);
int cell_y = list_y + filemanager_grid_row_y(fm, row, cols) - fm->scrollbar.scroll_offset;
// Skip if entirely off-screen
if (cell_y + row_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 + row_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] == FM_ENTRY_SPECIAL_DIR) sfi = fm->drive_indices[i];
else if (ds && fm->entry_types[i] == FM_ENTRY_DIR) 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] == FM_ENTRY_VIRTUAL_APP
&& i < fm->virtual_entry_count
&& fm->virtual_entries[i].icon_lg.pixels) {
c.icon(icon_x, icon_y, fm->virtual_entries[i].icon_lg);
} else if (ds && fm->entry_types[i] == FM_ENTRY_SYSTEM_CONFIGURATION_ROOT
&& ds->icon_system_configuration_lg.pixels) {
c.icon(icon_x, icon_y, ds->icon_system_configuration_lg);
} else if (ds && fm->entry_types[i] == FM_ENTRY_APPS_ROOT && ds->icon_apps_lg.pixels) {
c.icon(icon_x, icon_y, ds->icon_apps_lg);
} else if (ds && fm->entry_types[i] == FM_ENTRY_HOME && ds->icon_home_folder_lg.pixels) {
c.icon(icon_x, icon_y, ds->icon_home_folder_lg);
} else if (ds && fm->entry_types[i] == FM_ENTRY_DRIVE && ds->icon_drive_lg.pixels) {
c.icon(icon_x, icon_y, ds->icon_drive_lg);
} else if (ds && fm->entry_types[i] == FM_ENTRY_DIR && ds->icon_folder_lg.pixels) {
c.icon(icon_x, icon_y, ds->icon_folder_lg);
} else if (ds && fm->entry_types[i] == FM_ENTRY_EXEC && 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 {
// Prefer two grid label rows before truncating.
int max_w = FM_GRID_CELL_W - 4;
const char* name = fm->entry_names[i];
char line1[128];
char line2[128];
build_grid_label_lines(name, max_w, line1, sizeof(line1), line2, sizeof(line2));
int ty = icon_y + FM_GRID_ICON + 2;
int fh = system_font_height();
if (ty >= list_y && ty + fh <= c.h) {
draw_centered_grid_label(c, cell_x, ty, line1, colors::TEXT_COLOR);
}
if (line2[0] != '\0' && ty + fh >= list_y && ty + fh * 2 <= c.h) {
draw_centered_grid_label(c, cell_x, ty + fh, line2, 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] == FM_ENTRY_SPECIAL_DIR) sfi_sm = fm->drive_indices[i];
else if (ds && fm->entry_types[i] == FM_ENTRY_DIR) 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] == FM_ENTRY_VIRTUAL_APP
&& i < fm->virtual_entry_count
&& fm->virtual_entries[i].icon_sm.pixels) {
c.icon(ico_x, ico_y, fm->virtual_entries[i].icon_sm);
} else if (ds && fm->entry_types[i] == FM_ENTRY_SYSTEM_CONFIGURATION_ROOT
&& ds->icon_system_configuration.pixels) {
c.icon(ico_x, ico_y, ds->icon_system_configuration);
} else if (ds && fm->entry_types[i] == FM_ENTRY_APPS_ROOT && ds->icon_apps.pixels) {
c.icon(ico_x, ico_y, ds->icon_apps);
} else if (ds && fm->entry_types[i] == FM_ENTRY_HOME && ds->icon_home_folder.pixels) {
c.icon(ico_x, ico_y, ds->icon_home_folder);
} else if (ds && fm->entry_types[i] == FM_ENTRY_DRIVE && ds->icon_drive.pixels) {
c.icon(ico_x, ico_y, ds->icon_drive);
} else if (ds && fm->entry_types[i] == FM_ENTRY_DIR && ds->icon_folder.pixels) {
c.icon(ico_x, ico_y, ds->icon_folder);
} else if (ds && fm->entry_types[i] == FM_ENTRY_EXEC && 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) {
// Truncate name to fit the name column width
const char* name = fm->entry_names[i];
int name_max_w = size_col_x - tx - 8;
if (name_max_w < 20) name_max_w = c.w - FM_SCROLLBAR_W - tx - 8;
char label[64];
montauk::strncpy(label, name, 63);
if (text_width(label) > name_max_w && montauk::slen(name) > 3) {
int ellipsis_w = text_width("...");
int final_len = montauk::slen(name);
while (final_len > 1) {
label[final_len] = '\0';
int w = text_width(label);
if (w + ellipsis_w <= name_max_w) break;
final_len--;
}
if (final_len > 60) final_len = 60;
label[final_len] = '.';
label[final_len + 1] = '.';
label[final_len + 2] = '.';
label[final_len + 3] = '\0';
}
c.text(tx, ty, label, 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] == FM_ENTRY_VIRTUAL_APP) {
type_str = fm->virtual_view == FM_VIRTUAL_VIEW_SYSTEM_CONFIGURATION
? "Applet"
: "App";
}
else if (fm->entry_types[i] == FM_ENTRY_APPS_ROOT) type_str = "Apps";
else if (fm->entry_types[i] == FM_ENTRY_SYSTEM_CONFIGURATION_ROOT) type_str = "Settings";
else if (fm->entry_types[i] == FM_ENTRY_HOME) type_str = "Home";
else if (fm->entry_types[i] == FM_ENTRY_DRIVE) type_str = "Drive";
else if (fm->entry_types[i] == FM_ENTRY_DIR) type_str = "Dir";
else if (fm->entry_types[i] == FM_ENTRY_EXEC) 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) {
mtk::Theme theme = mtk::make_theme(colors::ACCENT);
theme.border = colors::BORDER;
theme.text = colors::TEXT_COLOR;
mtk::ContextMenuItem items[8];
filemanager_build_context_items(fm, items, 8);
mtk::draw_context_menu(c, filemanager_context_state(fm), items,
fm->ctx_item_count, theme);
}
}
} // namespace filemanager