feat: filesystem and Files app support for >64 file entries, 64-bit sizes, async ops, GUI toolkit improvements to devexplorer app

This commit is contained in:
2026-06-07 09:33:46 +02:00
parent 6554ef7e15
commit 3d620673c0
27 changed files with 1110 additions and 293 deletions
+163 -26
View File
@@ -16,6 +16,7 @@ int g_win_h = INIT_H;
DevExplorerState g_state;
TrueTypeFont* g_font = nullptr;
mtk::Theme g_theme;
static void refresh_devices(DevExplorerState* de) {
if (de == nullptr) return;
@@ -63,6 +64,71 @@ int build_display_rows(DevExplorerState* de, DisplayRow* rows) {
return count;
}
// ============================================================================
// Scroll metrics
// ============================================================================
// Computes pixel-space scroll geometry and clamps de->scroll_y so the list can
// never scroll past its content. Shared by render.cpp and the input handlers.
ScrollMetrics compute_scroll_metrics(DevExplorerState* de,
const DisplayRow* rows, int row_count) {
ScrollMetrics m;
m.list_y = TOOLBAR_H;
m.list_h = g_win_h - m.list_y;
if (m.list_h < 0) m.list_h = 0;
int total_h = 0;
for (int i = 0; i < row_count; i++)
total_h += (rows[i].type == ROW_CATEGORY) ? CAT_H : ITEM_H;
m.total_h = total_h;
int max_scroll_px = total_h - m.list_h;
if (max_scroll_px < 0) max_scroll_px = 0;
m.max_scroll_px = max_scroll_px;
if (de->scroll_y < 0) de->scroll_y = 0;
if (de->scroll_y > row_count) de->scroll_y = row_count;
int scroll_px = 0;
for (int i = 0; i < de->scroll_y && i < row_count; i++)
scroll_px += (rows[i].type == ROW_CATEGORY) ? CAT_H : ITEM_H;
// If the first visible row would push content past the end, snap scroll_y
// back to the furthest row that keeps the list fully populated.
if (scroll_px > max_scroll_px) {
de->scroll_y = 0;
int acc = 0;
for (int i = 0; i < row_count; i++) {
int rh = (rows[i].type == ROW_CATEGORY) ? CAT_H : ITEM_H;
if (acc + rh > max_scroll_px) break;
acc += rh;
de->scroll_y = i + 1;
}
scroll_px = acc;
}
m.scroll_px = scroll_px;
return m;
}
// Maps a target pixel offset (from the dragged thumb) back to the nearest row
// index, keeping scroll_y row-aligned like the keyboard and wheel navigation.
void set_scroll_from_px(DevExplorerState* de, const DisplayRow* rows,
int row_count, int target_px) {
if (target_px < 0) target_px = 0;
int acc = 0;
int best_k = 0;
int best_diff = target_px; // distance for scroll_y == 0
for (int i = 0; i < row_count; i++) {
int rh = (rows[i].type == ROW_CATEGORY) ? CAT_H : ITEM_H;
acc += rh;
int diff = gui_abs(acc - target_px);
if (diff < best_diff) { best_diff = diff; best_k = i + 1; }
}
de->scroll_y = best_k;
}
// ============================================================================
// Toolbar hit testing
// ============================================================================
@@ -70,10 +136,7 @@ int build_display_rows(DevExplorerState* de, DisplayRow* rows) {
static bool handle_toolbar_click(int mx, int my) {
if (my >= TOOLBAR_H) return false;
int btn_w = 80, btn_h = 26;
int btn_x = 8;
int btn_y = (TOOLBAR_H - btn_h) / 2;
if (mx >= btn_x && mx < btn_x + btn_w && my >= btn_y && my < btn_y + btn_h) {
if (refresh_button_rect().contains(mx, my)) {
g_state.last_poll_ms = 0; // force refresh
return true;
}
@@ -127,6 +190,92 @@ static bool handle_list_click(int mx, int my) {
return true;
}
// ============================================================================
// Mouse handling (wheel, draggable scrollbar, clicks)
// ============================================================================
static bool handle_main_mouse(const Montauk::WinEvent& ev) {
auto& de = g_state;
int mx = ev.mouse.x;
int my = ev.mouse.y;
bool changed = false;
bool left_down = (ev.mouse.buttons & 1) != 0;
bool just_clicked = (ev.mouse.buttons & 1) && !(ev.mouse.prev_buttons & 1);
bool just_released = !(ev.mouse.buttons & 1) && (ev.mouse.prev_buttons & 1);
de.mouse_x = mx;
de.mouse_y = my;
DisplayRow rows[MAX_DISPLAY_ROWS];
int row_count = build_display_rows(&de, rows);
ScrollMetrics m = compute_scroll_metrics(&de, rows, row_count);
Rect viewport = { 0, m.list_y, g_win_w, m.list_h };
Rect track = mtk::scrollbar_track_rect(viewport);
Rect thumb = mtk::scrollbar_thumb_rect(track, m.total_h, m.list_h, m.scroll_px);
// Mouse wheel scrolls by rows.
if (ev.mouse.scroll != 0) {
de.scroll_y += ev.mouse.scroll;
if (de.scroll_y < 0) de.scroll_y = 0;
changed = true;
}
// Release ends any active drag.
if (just_released && de.sb_dragging) {
de.sb_dragging = false;
changed = true;
}
// Active drag follows the pointer even if it leaves the track horizontally.
if (de.sb_dragging && left_down && !track.empty() && !thumb.empty()) {
int new_top = my - de.sb_drag_offset;
int target_px = mtk::scrollbar_offset_from_thumb_top(
track, m.total_h, m.list_h, thumb.h, new_top);
set_scroll_from_px(&de, rows, row_count, target_px);
changed = true;
}
// Hover highlight on the thumb; only redraw when it changes. The track is
// only interactive when the list actually scrolls (thumb is present).
bool scrollable = !track.empty() && !thumb.empty();
bool hover_now = scrollable && track.contains(mx, my);
if (hover_now != de.sb_hover) {
de.sb_hover = hover_now;
changed = true;
}
// Hover highlight on the Refresh button.
bool btn_hover_now = refresh_button_rect().contains(mx, my);
if (btn_hover_now != de.btn_hover) {
de.btn_hover = btn_hover_now;
changed = true;
}
if (just_clicked) {
if (scrollable && thumb.contains(mx, my)) {
// Grab the thumb.
de.sb_dragging = true;
de.sb_drag_offset = my - thumb.y;
changed = true;
} else if (scrollable && track.contains(mx, my)) {
// Click on the track: center the thumb under the pointer and drag.
de.sb_dragging = true;
de.sb_drag_offset = thumb.h / 2;
int new_top = my - de.sb_drag_offset;
int target_px = mtk::scrollbar_offset_from_thumb_top(
track, m.total_h, m.list_h, thumb.h, new_top);
set_scroll_from_px(&de, rows, row_count, target_px);
changed = true;
} else if (handle_toolbar_click(mx, my) || handle_list_click(mx, my)) {
changed = true;
}
}
return changed;
}
// ============================================================================
// Key handling
// ============================================================================
@@ -205,15 +354,15 @@ extern "C" void _start() {
for (int i = 0; i < NUM_CATEGORIES; i++)
g_state.collapsed[i] = false;
// Load font
{
TrueTypeFont* f = (TrueTypeFont*)montauk::malloc(sizeof(TrueTypeFont));
if (f) {
montauk::memset(f, 0, sizeof(TrueTypeFont));
if (!f->init("0:/fonts/Roboto-Medium.ttf")) { montauk::mfree(f); f = nullptr; }
}
g_font = f;
}
// Load fonts. fonts::init() loads the shared system font (Roboto-Medium at
// UI_SIZE 18) which both the device list (via g_font) and the MTK widgets
// (button, scrollbar) render with, so point g_font at it to avoid a second
// copy of the same face.
gui::fonts::init();
g_font = gui::fonts::system_font;
// Theme for MTK widgets, built once from the user's accent color.
g_theme = mtk::make_theme();
// Initial device poll
refresh_devices(&g_state);
@@ -290,20 +439,8 @@ extern "C" void _start() {
// Mouse
if (ev.type == 1) {
int mx = ev.mouse.x;
int my = ev.mouse.y;
bool clicked = (ev.mouse.buttons & 1) && !(ev.mouse.prev_buttons & 1);
if (ev.mouse.scroll != 0) {
g_state.scroll_y += ev.mouse.scroll;
if (g_state.scroll_y < 0) g_state.scroll_y = 0;
if (handle_main_mouse(ev))
redraw_main = true;
}
if (clicked) {
if (handle_toolbar_click(mx, my) || handle_list_click(mx, my))
redraw_main = true;
}
}
if (redraw_main) { render(pixels); win.present(); }