fix: fix scrolling cutoff for last entry in devexplorer

This commit is contained in:
2026-06-13 17:13:15 +02:00
parent 25f9d6e401
commit ee660a64f9
3 changed files with 68 additions and 54 deletions
+2 -2
View File
@@ -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;
+43 -49
View File
@@ -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;
+23 -3
View File
@@ -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);
}