diff options
| author | user <user@clank> | 2026-08-11 14:38:16 +0200 |
|---|---|---|
| committer | user <user@clank> | 2026-08-12 19:34:19 +0200 |
| commit | 6a319134e6899917d39881b1abb494a9b7f4e103 (patch) | |
| tree | 048b1bbabea3a3589e5e682d09817f3d1e7151c4 | |
| parent | sl0p: cap how many rows the stacked-pane picker takes (diff) | |
| download | sl0ppi-6a319134e6899917d39881b1abb494a9b7f4e103.tar.gz sl0ppi-6a319134e6899917d39881b1abb494a9b7f4e103.tar.xz sl0ppi-6a319134e6899917d39881b1abb494a9b7f4e103.zip | |
sl0p: fix clicking a scrolled stack entry selecting the wrong pane
Regression from the picker row cap. The row -> member mapping lives in TWO
places: the renderer, and the hit-test that resolves mouse events to a pane.
Capping made the renderer draw a WINDOW of the members, so screen row 0 is
members[window_start], but the hit-test still resolved row N to members[N].
Any click or hover on a capped stack whose window had scrolled therefore
landed on the wrong pane -- with 8 members focused on the last, clicking the
top row selected member 0 instead of member 5.
Both sites now go through stack_list_window(). Clicks on the "+N more"
indicator resolve to no member rather than to a bogus one.
Caught by asking how a user reaches the panes past the cutoff, not by the
tests: the previous commit tested the window arithmetic and the rendering, but
nothing covered this second consumer of the same mapping.
| -rw-r--r-- | sl0ppij/zellij-server/src/tab/mod.rs | 17 | ||||
| -rw-r--r-- | sl0ppij/zellij-server/src/tab/unit/tab_integration_tests.rs | 61 |
2 files changed, 76 insertions, 2 deletions
diff --git a/sl0ppij/zellij-server/src/tab/mod.rs b/sl0ppij/zellij-server/src/tab/mod.rs index 2c75c42..7f13246 100644 --- a/sl0ppij/zellij-server/src/tab/mod.rs +++ b/sl0ppij/zellij-server/src/tab/mod.rs @@ -1170,8 +1170,21 @@ impl Tab { if point.line() < geom.y as isize { return pane_id; } - let rank = (point.line() - geom.y as isize) as usize; - list.members.get(rank).copied().unwrap_or(pane_id) + // Screen row, NOT the member's rank: the picker draws a window of the + // members (see stack_list_window), so row 0 is members[window_start] + // once the list has scrolled. Resolving this against the raw rank picks + // the wrong pane for every capped, scrolled stack. + let row = (point.line() - geom.y as isize) as usize; + let visible_rank = list.rank_of(&list.visible).unwrap_or(0); + let (window_start, shown_count) = self.stack_list_window(list.members.len(), visible_rank); + if row >= shown_count { + // the "+N more" indicator row, which is not a member + return pane_id; + } + list.members + .get(window_start + row) + .copied() + .unwrap_or(pane_id) } pub fn suppressed_stack_list_members(&self) -> impl Iterator<Item = (&PaneId, &Box<dyn Pane>)> { self.suppressed_panes diff --git a/sl0ppij/zellij-server/src/tab/unit/tab_integration_tests.rs b/sl0ppij/zellij-server/src/tab/unit/tab_integration_tests.rs index a036bf6..b22b38f 100644 --- a/sl0ppij/zellij-server/src/tab/unit/tab_integration_tests.rs +++ b/sl0ppij/zellij-server/src/tab/unit/tab_integration_tests.rs @@ -16075,3 +16075,64 @@ fn sl0p_stack_list_uncapped_by_default_shows_every_member() { ); } } + +#[test] +fn sl0p_clicking_a_scrolled_stack_entry_selects_the_pane_it_shows() { + // REGRESSION: the picker's row -> member mapping exists in TWO places -- + // the renderer, and this hit-test used to resolve mouse events. Capping the + // list made the renderer draw a WINDOW of members, so screen row 0 is no + // longer members[0]. A hit-test that still assumes row N == member N picks + // the wrong pane the moment the window scrolls. + let size = Size { + cols: 121, + rows: 40, + }; + let client_id = 1; + let mut tab = create_new_tab(size, ModeInfo::default()); + let mut members = vec![PaneId::Terminal(1)]; + for pid in 2..=8 { + tab.new_pane( + PaneId::Terminal(pid), + None, + None, + false, + true, + NewPanePlacement::default(), + Some(client_id), + None, + ) + .unwrap(); + members.push(PaneId::Terminal(pid)); + } + + // register the stack list directly: we are testing the row arithmetic, not + // the stack machinery that would normally build this + let visible = members[7]; + tab.stack_lists.insert( + 0, + super::StackList { + members: members.clone(), + visible, + }, + ); + for member in &members { + tab.stack_list_of_member.insert(*member, 0); + } + tab.update_stack_list_max_rows(3); + + // 8 members, cap 3, focused on rank 7 -> window clamps to ranks 5,6,7 + let (window_start, shown) = tab.stack_list_window(members.len(), 7); + assert_eq!((window_start, shown), (5, 3), "precondition: window position"); + + let geom = tab.tiled_panes.get_pane(visible).unwrap().current_geom(); + for row in 0..shown { + let point = Position::new((geom.y + row) as i32, 0); + assert_eq!( + tab.stack_list_member_at_point(visible, &point), + members[window_start + row], + "row {} shows member {}, so clicking it must select that pane", + row, + window_start + row + ); + } +} |
