From ee660a64f9cd8c2ebf0c7cc6a973bdd8f48d1e39 Mon Sep 17 00:00:00 2001 From: Daniel Hammer Date: Sat, 13 Jun 2026 17:13:15 +0200 Subject: [PATCH] fix: fix scrolling cutoff for last entry in devexplorer --- programs/src/devexplorer/devexplorer.h | 4 +- programs/src/devexplorer/main.cpp | 92 ++++++++++++-------------- programs/src/devexplorer/render.cpp | 26 +++++++- 3 files changed, 68 insertions(+), 54 deletions(-) diff --git a/programs/src/devexplorer/devexplorer.h b/programs/src/devexplorer/devexplorer.h index 435d4e5..c72dcf7 100644 --- a/programs/src/devexplorer/devexplorer.h +++ b/programs/src/devexplorer/devexplorer.h @@ -118,7 +118,7 @@ struct ScrollMetrics { 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) + int scroll_px; // current pixel scroll offset (clamped scroll_y) }; // Bounds of the toolbar "Refresh" button — shared by render and hit-testing. @@ -156,7 +156,7 @@ struct DevExplorerState { int dev_count; bool collapsed[NUM_CATEGORIES]; int selected_row; - int scroll_y; + int scroll_y; // vertical scroll offset, in pixels (not rows) uint64_t last_poll_ms; int last_click_row; diff --git a/programs/src/devexplorer/main.cpp b/programs/src/devexplorer/main.cpp index 5b4bc3b..2398709 100644 --- a/programs/src/devexplorer/main.cpp +++ b/programs/src/devexplorer/main.cpp @@ -86,47 +86,23 @@ ScrollMetrics compute_scroll_metrics(DevExplorerState* de, if (max_scroll_px < 0) max_scroll_px = 0; m.max_scroll_px = max_scroll_px; + // scroll_y is a pixel offset. Clamping to max_scroll_px (rather than a row + // boundary) lets the final scroll position sit the last row flush against + // the bottom edge, so a trailing category like Printers is never cut off. if (de->scroll_y < 0) de->scroll_y = 0; - if (de->scroll_y > row_count) de->scroll_y = row_count; + if (de->scroll_y > max_scroll_px) de->scroll_y = max_scroll_px; - 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; + m.scroll_px = de->scroll_y; 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. +// Stores a target pixel offset (from the dragged thumb) directly. scroll_y is +// pixel-space; compute_scroll_metrics clamps it to the content bounds. 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; + (void)rows; + (void)row_count; + de->scroll_y = target_px < 0 ? 0 : target_px; } // ============================================================================ @@ -155,9 +131,25 @@ static bool handle_list_click(int mx, int my) { DisplayRow rows[MAX_DISPLAY_ROWS]; int row_count = build_display_rows(&de, rows); + // Locate the first visible row for the current pixel scroll offset, mirroring + // render_list() so click targets line up with what is drawn. The first row + // may be partially scrolled above the viewport (negative top offset). + int scroll_px = de.scroll_y; + int first_visible = 0, row_top = 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 > scroll_px) { first_visible = i; row_top = acc; break; } + acc += rh; + first_visible = i + 1; + row_top = acc; + } + } + uint64_t now = montauk::get_milliseconds(); - int cur_y = list_y; - for (int i = de.scroll_y; i < row_count; i++) { + int cur_y = list_y - (scroll_px - row_top); + for (int i = first_visible; i < row_count; i++) { int row_h = (rows[i].type == ROW_CATEGORY) ? CAT_H : ITEM_H; if (my >= cur_y && my < cur_y + row_h) { if (rows[i].type == ROW_CATEGORY) { @@ -215,9 +207,9 @@ static bool handle_main_mouse(const Montauk::WinEvent& ev) { 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. + // Mouse wheel scrolls by roughly one row per notch (scroll_y is pixels). if (ev.mouse.scroll != 0) { - de.scroll_y += ev.mouse.scroll; + de.scroll_y += ev.mouse.scroll * ITEM_H; if (de.scroll_y < 0) de.scroll_y = 0; changed = true; } @@ -291,21 +283,23 @@ static bool handle_key(const Montauk::KeyEvent& key) { if (key.scancode == 0x48) { // Up if (de.selected_row <= 0) de.selected_row = 0; else de.selected_row--; - if (de.selected_row < de.scroll_y) - de.scroll_y = de.selected_row; + // Scroll up just enough to bring the selected row's top into view. + int sel_top = 0; + for (int i = 0; i < de.selected_row && i < row_count; i++) + sel_top += (rows[i].type == ROW_CATEGORY) ? CAT_H : ITEM_H; + if (sel_top < de.scroll_y) + de.scroll_y = sel_top; } else if (key.scancode == 0x50) { // Down if (de.selected_row < row_count - 1) de.selected_row++; + // Scroll down just enough to bring the selected row's bottom into view. + int sel_top = 0; + for (int i = 0; i < de.selected_row && i < row_count; i++) + sel_top += (rows[i].type == ROW_CATEGORY) ? CAT_H : ITEM_H; + int sel_h = (rows[de.selected_row].type == ROW_CATEGORY) ? CAT_H : ITEM_H; int list_h = g_win_h - TOOLBAR_H; - int cur_h = 0, last_visible = de.scroll_y; - for (int i = de.scroll_y; i < row_count; i++) { - int rh = (rows[i].type == ROW_CATEGORY) ? CAT_H : ITEM_H; - if (cur_h + rh > list_h) break; - cur_h += rh; - last_visible = i; - } - if (de.selected_row > last_visible) - de.scroll_y += (de.selected_row - last_visible); + if (sel_top + sel_h > de.scroll_y + list_h) + de.scroll_y = sel_top + sel_h - list_h; } else if (key.scancode == 0x4B) { // Left — collapse if (de.selected_row >= 0 && de.selected_row < row_count) { int cat = rows[de.selected_row].category; diff --git a/programs/src/devexplorer/render.cpp b/programs/src/devexplorer/render.cpp index b23058f..308089c 100644 --- a/programs/src/devexplorer/render.cpp +++ b/programs/src/devexplorer/render.cpp @@ -77,9 +77,27 @@ static void render_list(uint32_t* px) { if (de.selected_row >= row_count) de.selected_row = row_count - 1; + // Locate the first visible row for the current pixel scroll offset. The top + // row may be partially scrolled above the viewport (its top is then above + // list_y); render() draws the toolbar after the list so that overhang is + // masked. Clamping the scroll offset in pixels (not whole rows) lets the + // final row sit flush with the bottom edge, so the last category is never + // cut off. + int scroll_px = m.scroll_px; + int first_visible = 0, row_top = 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 > scroll_px) { first_visible = i; row_top = acc; break; } + acc += rh; + first_visible = i + 1; + row_top = acc; + } + } + // Draw rows - int first_visible = de.scroll_y; - int cur_y = list_y; + int cur_y = list_y - (scroll_px - row_top); Color menu_hover = Color::from_rgb(0xD0, 0xE0, 0xF8); for (int i = first_visible; i < row_count; i++) { @@ -168,7 +186,9 @@ static void render_list(uint32_t* px) { void render(uint32_t* pixels) { px_fill(pixels, g_win_w, g_win_h, 0, 0, g_win_w, g_win_h, BG_COLOR); - render_toolbar(pixels); + // List first, toolbar last: a partially-scrolled top row can extend above + // the list area, and drawing the toolbar afterward masks that overhang. render_list(pixels); + render_toolbar(pixels); }