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
+33
View File
@@ -11,6 +11,7 @@
#include <montauk/heap.h>
#include <gui/gui.hpp>
#include <gui/truetype.hpp>
#include <gui/mtk/widgets.hpp>
extern "C" {
#include <string.h>
@@ -107,6 +108,25 @@ struct DisplayRow {
static constexpr int MAX_DISPLAY_ROWS = MAX_TOTAL_DEVS + NUM_CATEGORIES;
// ============================================================================
// Scroll metrics (pixel-space) — shared by render and input handling so the
// draggable MTK scrollbar and the row layout stay in sync.
// ============================================================================
struct ScrollMetrics {
int list_y; // top of the scrollable list area
int list_h; // viewport height (view extent)
int total_h; // total content height (content extent)
int max_scroll_px; // maximum pixel scroll offset
int scroll_px; // current pixel scroll offset (derived from scroll_y)
};
// Bounds of the toolbar "Refresh" button — shared by render and hit-testing.
inline Rect refresh_button_rect() {
constexpr int btn_w = 80, btn_h = 26;
return { 8, (TOOLBAR_H - btn_h) / 2, btn_w, btn_h };
}
// ============================================================================
// Disk detail window
// ============================================================================
@@ -142,6 +162,14 @@ struct DevExplorerState {
int last_click_row;
uint64_t last_click_ms;
// Draggable MTK scrollbar interaction state
int mouse_x, mouse_y; // last known pointer position (for hover)
bool sb_hover; // pointer is over the scrollbar track
bool sb_dragging; // thumb is being dragged
int sb_drag_offset; // pointer offset from thumb top when drag began
bool btn_hover; // pointer is over the Refresh button
DiskDetailState detail;
};
@@ -152,6 +180,7 @@ struct DevExplorerState {
extern int g_win_w, g_win_h;
extern DevExplorerState g_state;
extern TrueTypeFont* g_font;
extern mtk::Theme g_theme;
// ============================================================================
// Function declarations — render.cpp
@@ -165,6 +194,10 @@ void render(uint32_t* pixels);
int build_display_rows(DevExplorerState* de, DisplayRow* rows);
int append_printer_devices(Montauk::DevInfo* out, int max_count);
ScrollMetrics compute_scroll_metrics(DevExplorerState* de,
const DisplayRow* rows, int row_count);
void set_scroll_from_px(DevExplorerState* de, const DisplayRow* rows,
int row_count, int target_px);
// ============================================================================
// Function declarations — diskdetail.cpp
+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(); }
+20 -42
View File
@@ -49,12 +49,11 @@ static void render_toolbar(uint32_t* px) {
px_fill(px, g_win_w, g_win_h, 0, 0, g_win_w, TOOLBAR_H, TOOLBAR_BG);
px_hline(px, g_win_w, g_win_h, 0, TOOLBAR_H - 1, g_win_w, BORDER_COLOR);
// "Refresh" button
int btn_w = 80, btn_h = 26;
int btn_x = 8;
int btn_y = (TOOLBAR_H - btn_h) / 2;
px_button(px, g_win_w, g_win_h, btn_x, btn_y, btn_w, btn_h,
"Refresh", ACCENT_COLOR, WHITE, 4);
// "Refresh" button (MTK primary button)
Canvas canvas(px, g_win_w, g_win_h);
mtk::WidgetState btn_state = mtk::widget_state(false, de.btn_hover, true);
mtk::draw_button(canvas, refresh_button_rect(), "Refresh",
mtk::BUTTON_PRIMARY, btn_state, g_theme);
// Device count on right
char count_str[24];
@@ -71,34 +70,11 @@ static void render_list(uint32_t* px) {
DisplayRow rows[MAX_DISPLAY_ROWS];
int row_count = build_display_rows(&de, rows);
int list_y = TOOLBAR_H;
int list_h = g_win_h - list_y;
ScrollMetrics m = compute_scroll_metrics(&de, rows, row_count);
int list_y = m.list_y;
int list_h = m.list_h;
if (list_h < 1) return;
// Compute total content height
int total_h = 0;
for (int i = 0; i < row_count; i++)
total_h += (rows[i].type == ROW_CATEGORY) ? CAT_H : ITEM_H;
int max_scroll_px = total_h - list_h;
if (max_scroll_px < 0) max_scroll_px = 0;
// Compute scroll pixel offset
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 (scroll_px > max_scroll_px) {
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;
}
}
if (de.scroll_y < 0) de.scroll_y = 0;
if (de.selected_row >= row_count) de.selected_row = row_count - 1;
// Draw rows
@@ -175,16 +151,18 @@ static void render_list(uint32_t* px) {
cur_y += row_h;
}
// Scrollbar
if (total_h > list_h) {
int sb_x = g_win_w - 6;
int thumb_h = (list_h * list_h) / total_h;
if (thumb_h < 20) thumb_h = 20;
int thumb_y = list_y + (scroll_px * (list_h - thumb_h)) / max_scroll_px;
px_fill(px, g_win_w, g_win_h, sb_x, list_y, 4, list_h,
Color::from_rgb(0xE0, 0xE0, 0xE0));
px_fill(px, g_win_w, g_win_h, sb_x, thumb_y, 4, thumb_h,
Color::from_rgb(0xAA, 0xAA, 0xAA));
// Draggable MTK scrollbar
Rect viewport = { 0, list_y, g_win_w, list_h };
Rect track = mtk::scrollbar_track_rect(viewport);
Rect thumb = mtk::scrollbar_thumb_rect(track, m.total_h, list_h, m.scroll_px);
if (!track.empty() && !thumb.empty()) {
bool hovered = track.contains(de.mouse_x, de.mouse_y);
mtk::Theme theme{};
Color thumb_col = mtk::scrollbar_thumb_color(theme, hovered, de.sb_dragging);
px_fill(px, g_win_w, g_win_h, track.x, track.y, track.w, track.h,
colors::SCROLLBAR_BG);
px_fill(px, g_win_w, g_win_h, thumb.x, thumb.y, thumb.w, thumb.h,
thumb_col);
}
}