aboutsummaryrefslogtreecommitdiffstats
path: root/sl0ppij/zellij-server/src
diff options
context:
space:
mode:
authoruser <user@clank>2026-08-11 14:38:16 +0200
committeruser <user@clank>2026-08-12 19:34:19 +0200
commit6a319134e6899917d39881b1abb494a9b7f4e103 (patch)
tree048b1bbabea3a3589e5e682d09817f3d1e7151c4 /sl0ppij/zellij-server/src
parentsl0p: cap how many rows the stacked-pane picker takes (diff)
downloadsl0ppi-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.
Diffstat (limited to 'sl0ppij/zellij-server/src')
-rw-r--r--sl0ppij/zellij-server/src/tab/mod.rs17
-rw-r--r--sl0ppij/zellij-server/src/tab/unit/tab_integration_tests.rs61
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
+ );
+ }
+}