diff options
| author | user <user@clank> | 2026-08-11 14:14:53 +0200 |
|---|---|---|
| committer | user <user@clank> | 2026-08-11 14:14:53 +0200 |
| commit | 9441f1b177c1e08f4ceae76ef70d444103d15efa (patch) | |
| tree | bf59f9655fc32e46cd3f6c15e599f5b6dbc10281 /zellij-server/src/tab/mod.rs | |
| parent | sl0p: add `arrangement` to build a tree for N panes (diff) | |
| download | sl0ppij-9441f1b177c1e08f4ceae76ef70d444103d15efa.tar.gz sl0ppij-9441f1b177c1e08f4ceae76ef70d444103d15efa.tar.xz sl0ppij-9441f1b177c1e08f4ceae76ef70d444103d15efa.zip | |
sl0p: cap how many rows the stacked-pane picker takes
A stack reserves one row per member plus a padding row, so a deep stack spends
most of the screen listing itself. `stack_list_max_rows` caps that; the list
windows around the focused member and reports the rest as "+N more".
The indicator goes in the padding row the list already reserves, so it costs
no extra row and never displaces a real entry. Entries keep their real rank
rather than renumbering from 1, otherwise the numbering stops meaning anything
once the window has scrolled. The window slides on ordinary focus movement,
which is also why this needs no new keybinding -- Action is a protobuf enum
and the prost assets cannot be regenerated here (no protoc).
A fixed cap rather than hover-to-expand on purpose: reserved_top_rows feeds
pane geometry, so growing the list on hover would resize the pane's PTY and
reflow whatever is running in it on every mouse move. A real expand/collapse
has to be an overlay that reserves nothing, which is a much larger change to
the render path.
reserved_top_rows and the renderer both go through stack_list_window(); if
they disagreed the list would draw into rows the geometry never reserved, i.e.
over the pane beneath it.
Diffstat (limited to 'zellij-server/src/tab/mod.rs')
| -rw-r--r-- | zellij-server/src/tab/mod.rs | 82 |
1 files changed, 73 insertions, 9 deletions
diff --git a/zellij-server/src/tab/mod.rs b/zellij-server/src/tab/mod.rs index cf413e63..2c75c425 100644 --- a/zellij-server/src/tab/mod.rs +++ b/zellij-server/src/tab/mod.rs @@ -53,7 +53,10 @@ use crate::{ panes::nested_session_modal::GuestModalShortcuts, panes::sixel::SixelImageStore, panes::{FloatingPanes, TiledPanes}, - panes::{LinkHandler, PaneId, PluginPane, TerminalPane, EMPTY_TERMINAL_CHARACTER}, + panes::{ + LinkHandler, PaneId, PluginPane, TerminalCharacter, TerminalPane, + EMPTY_TERMINAL_CHARACTER, + }, plugins::PluginInstruction, pty::{ClientTabIndexOrPaneId, PtyInstruction, VteBytes}, thread_bus::ThreadSenders, @@ -277,6 +280,8 @@ pub(crate) struct Tab { responsive_restore_position: Option<usize>, responsive_min_cols: usize, responsive_layout_name: String, + /// Cap on picker rows; see stack_list_window. 0 = uncapped. + stack_list_max_rows: usize, currently_marking_pane_group: Rc<RefCell<HashMap<ClientId, bool>>>, connected_clients_in_app: Rc<RefCell<HashMap<ClientId, bool>>>, // bool -> is_web_client // the below are the configured values - the ones that will be set if and when the web server @@ -1092,6 +1097,7 @@ impl Tab { responsive_restore_position: None, responsive_min_cols: 0, responsive_layout_name: String::from("stacked"), + stack_list_max_rows: 0, connected_clients_in_app, web_server_ip, web_server_port, @@ -1256,17 +1262,41 @@ impl Tab { } } } + /// Where the picker's visible window starts, and how many entries it shows. + /// + /// `stack_list_max_rows` caps the height; the window slides to keep the + /// currently visible member on screen, so ordinary focus movement scrolls + /// the list instead of needing its own binding. + /// + /// Both the reserved-row arithmetic and the renderer go through this. If + /// they ever disagree the list draws into rows the geometry did not reserve, + /// i.e. over the pane below it. + fn stack_list_window(&self, member_count: usize, visible_rank: usize) -> (usize, usize) { + let cap = self.stack_list_max_rows; + if cap == 0 || member_count <= cap { + return (0, member_count); + } + let start = visible_rank + .saturating_sub(cap / 2) + .min(member_count.saturating_sub(cap)); + (start, cap) + } fn refresh_or_dissolve_stack_list(&mut self, stack_list_id: StackListId) { - let (member_count, visible) = match self.stack_lists.get(&stack_list_id) { - Some(list) => (list.members.len(), list.visible), + let (member_count, visible, visible_rank) = match self.stack_lists.get(&stack_list_id) { + Some(list) => ( + list.members.len(), + list.visible, + list.rank_of(&list.visible).unwrap_or(0), + ), None => return, }; if member_count <= 1 { self.dissolve_stack_list(stack_list_id); } else { + let (_start, shown) = self.stack_list_window(member_count, visible_rank); self.reserved_top_rows .borrow_mut() - .insert(visible, member_count + 1); + .insert(visible, shown + 1); self.tiled_panes.reapply_pane_frames(); self.resize_stack_list_hidden_members(stack_list_id); } @@ -1718,11 +1748,25 @@ impl Tab { .max() .unwrap_or(0); let stack_is_focused = active_panes.values().any(|p_id| *p_id == list.visible); - for (rank, member) in list.members.iter().enumerate() { + 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); + let hidden_count = list.members.len().saturating_sub(shown_count); + for (row, (rank, member)) in list + .members + .iter() + .enumerate() + .skip(window_start) + .take(shown_count) + .enumerate() + { let mut header_geom = rect; header_geom.x = entry_x; header_geom.cols.set_inner(entry_cols); - header_geom.y = rect.y + rank; + // `row` is the position on screen, `rank` stays the member's real + // position in the stack, so the numbering keeps meaning something + // once the window has scrolled away from the top + header_geom.y = rect.y + row; header_geom.rows = Dimension::fixed(1); header_geom.stacked = None; let pane_box = if *member == list.visible { @@ -1768,12 +1812,27 @@ impl Tab { )?; } } + // The row after the entries is already part of the reserved budget, + // so the "+N hidden" indicator costs nothing extra and never steals a + // row from a real entry. let mut padding_row = Vec::with_capacity(entry_cols); - for _ in 0..entry_cols { + if hidden_count > 0 { + let indicator = format!("+{} more", hidden_count); + let indicator_start = entry_cols.saturating_sub(indicator.width()) / 2; + for _ in 0..indicator_start { + padding_row.push(EMPTY_TERMINAL_CHARACTER); + } + for character in indicator.chars() { + padding_row.push(TerminalCharacter::new_styled( + character, + EMPTY_TERMINAL_CHARACTER.styles, + )); + } + } + for _ in padding_row.len()..entry_cols { padding_row.push(EMPTY_TERMINAL_CHARACTER); } - let padding_chunk = - CharacterChunk::new(padding_row, entry_x, rect.y + list.members.len()); + let padding_chunk = CharacterChunk::new(padding_row, entry_x, rect.y + shown_count); output.add_character_chunks_to_multiple_clients( vec![padding_chunk], connected_clients.iter().copied(), @@ -7677,6 +7736,11 @@ impl Tab { self.responsive_min_cols = min_cols; self.responsive_layout_name = layout_name; } + /// Cap on stacked-pane picker rows. Takes effect on the next list refresh, + /// which is what recomputes the reserved rows. + pub fn update_stack_list_max_rows(&mut self, max_rows: usize) { + self.stack_list_max_rows = max_rows; + } pub fn update_focus_follows_mouse(&mut self, focus_follows_mouse: bool) { self.focus_follows_mouse = focus_follows_mouse; } |
