diff options
| author | user <user@clank> | 2026-08-11 00:12:10 +0200 |
|---|---|---|
| committer | user <user@clank> | 2026-08-11 00:12:10 +0200 |
| commit | 393748bb2fb0ad741f2d51cfe0f8db75cf2ad7c3 (patch) | |
| tree | 09a651d4640e18398df90cdac0857e8602bcd5a0 /zellij-server/src/tab/mod.rs | |
| parent | sl0p: refresh snapshots for the compact scroll indicator (diff) | |
| download | sl0ppij-393748bb2fb0ad741f2d51cfe0f8db75cf2ad7c3.tar.gz sl0ppij-393748bb2fb0ad741f2d51cfe0f8db75cf2ad7c3.tar.xz sl0ppij-393748bb2fb0ad741f2d51cfe0f8db75cf2ad7c3.zip | |
sl0p: collapse to a swap layout when the terminal gets narrow
Splitting a window into thirds is great until the window is 70 columns wide
and each pane gets 23. `responsive_min_cols` collapses the tab into the swap
layout named by `responsive_layout` when it gets narrower than that, and
restores the previously active layout when it grows back.
Selects a swap layout BY NAME rather than adding a size-aware
LayoutConstraint: that type crosses both the plugin protobuf API and the
client/server contract, and the committed prost assets cannot be regenerated
here (no protoc).
Restoring uses a 10-column hysteresis band. Without a dead band, dragging a
window edge that lands exactly on the threshold flips the layout on every
motion event, and every flip resizes every PTY in the tab.
"Did the user rearrange panes by hand" is judged from the damaged flag as
sampled BEFORE each resize, not the live one: resize_whole_tab's own
auto-layout pass sets that flag itself and clears it again only if it found a
layout that fits, so on a terminal too narrow for any of them it latches true
although nothing was rearranged. And once collapsed, restore stays reachable
regardless of the flag, so damage occurring while collapsed cannot strand the
tab in the narrow layout.
Diffstat (limited to 'zellij-server/src/tab/mod.rs')
| -rw-r--r-- | zellij-server/src/tab/mod.rs | 165 |
1 files changed, 136 insertions, 29 deletions
diff --git a/zellij-server/src/tab/mod.rs b/zellij-server/src/tab/mod.rs index bc8337e1..2d8c00c8 100644 --- a/zellij-server/src/tab/mod.rs +++ b/zellij-server/src/tab/mod.rs @@ -271,6 +271,11 @@ pub(crate) struct Tab { /// the live layout acts as the preview. Undone when the drag moves to a /// different target. drag_preview_swapped_with: HashMap<ClientId, PaneId>, + /// Swap layout position to restore when the terminal grows back, set while + /// the tab is collapsed into the responsive layout. None = not collapsed. + responsive_restore_position: Option<usize>, + responsive_min_cols: usize, + responsive_layout_name: String, 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 @@ -1083,6 +1088,9 @@ impl Tab { frame_clicks: HashMap::new(), tiled_pane_being_dragged: HashMap::new(), drag_preview_swapped_with: HashMap::new(), + responsive_restore_position: None, + responsive_min_cols: 0, + responsive_layout_name: String::from("stacked"), connected_clients_in_app, web_server_ip, web_server_port, @@ -1990,6 +1998,41 @@ impl Tab { .with_context(|| format!("failed to apply cached resizes"))?; Ok(()) } + /// Apply a resolved swap layout to the existing panes. + /// + /// Extracted from relayout_tiled_panes so the responsive-layout switch + /// applies layouts through exactly the same path rather than a lookalike. + fn apply_tiled_layout_candidate(&mut self, layout_candidate: &TiledPaneLayout) { + let application_res = LayoutApplier::new( + &self.viewport, + &self.senders, + &self.sixel_image_store, + &self.kitty_image_store, + &self.link_handler, + &self.terminal_emulator_colors, + &self.terminal_emulator_color_codes, + &self.character_cell_size, + &self.connected_clients_in_app, + &self.style, + &self.display_area, + &mut self.tiled_panes, + &mut self.floating_panes, + self.pane_frame_style, + &mut self.focus_pane_id, + &self.os_api, + self.debug, + self.arrow_fonts, + self.styled_underlines, + self.osc8_hyperlinks, + self.explicitly_disable_kitty_keyboard_protocol, + None, + ) + .apply_tiled_panes_layout_to_existing_panes(layout_candidate); + if application_res.is_err() { + self.swap_layouts.set_is_tiled_damaged(); + application_res.non_fatal(); + } + } fn relayout_tiled_panes(&mut self, search_backwards: bool) -> Result<()> { if self.tiled_panes.fullscreen_is_active() { self.tiled_panes.unset_fullscreen(); @@ -1999,35 +2042,7 @@ impl Tab { .swap_layouts .swap_tiled_panes(&self.tiled_panes, search_backwards) { - let application_res = LayoutApplier::new( - &self.viewport, - &self.senders, - &self.sixel_image_store, - &self.kitty_image_store, - &self.link_handler, - &self.terminal_emulator_colors, - &self.terminal_emulator_color_codes, - &self.character_cell_size, - &self.connected_clients_in_app, - &self.style, - &self.display_area, - &mut self.tiled_panes, - &mut self.floating_panes, - self.pane_frame_style, - &mut self.focus_pane_id, - &self.os_api, - self.debug, - self.arrow_fonts, - self.styled_underlines, - self.osc8_hyperlinks, - self.explicitly_disable_kitty_keyboard_protocol, - None, - ) - .apply_tiled_panes_layout_to_existing_panes(&layout_candidate); - if application_res.is_err() { - self.swap_layouts.set_is_tiled_damaged(); - application_res.non_fatal(); - } + self.apply_tiled_layout_candidate(&layout_candidate); } else { self.swap_layouts.set_is_tiled_damaged(); } @@ -5080,9 +5095,90 @@ impl Tab { let selectable_tiled_panes = self.tiled_panes.get_panes().filter(|(_, p)| p.selectable()); selectable_tiled_panes.count() > 0 } + /// Collapse into the configured swap layout when the terminal is too narrow, + /// and restore the previous one when it grows back. + /// + /// Hysteresis: collapsing happens below `responsive_min_cols`, but restoring + /// only above `responsive_min_cols + RESPONSIVE_HYSTERESIS_COLS`. Without a + /// gap, dragging a terminal edge that lands exactly on the threshold flips + /// the layout on every motion event, and each flip resizes every PTY. + /// + /// Does nothing once the user has manually rearranged panes + /// (`is_tiled_damaged`), on the same principle as auto_layout: an explicit + /// arrangement outranks an automatic one. + fn apply_responsive_layout( + &mut self, + new_size: Size, + was_damaged_before_resize: bool, + ) -> Result<()> { + const RESPONSIVE_HYSTERESIS_COLS: usize = 10; + if self.responsive_min_cols == 0 { + return Ok(()); + } + // NB: deliberately the flag as sampled at the TOP of resize_whole_tab, + // not the live `self.swap_layouts.is_tiled_damaged()`. + // + // By the time we run, resize_whole_tab's own auto_layout pass has + // already set the flag as bookkeeping and then cleared it only if it + // found a layout that fits. On a terminal too narrow for any of the wide + // layouts it can find none, which leaves the flag true even though the + // user never rearranged anything -- and that is exactly the case this + // feature exists to handle. Sampling before the resize keeps the flag + // meaning "the user rearranged panes by hand" and nothing else. + // + // The restore_position exemption matters for the same reason: once we + // have collapsed, a later resize that damages the layout must not strand + // the user in the narrow layout forever, so restore stays reachable. + if was_damaged_before_resize && self.responsive_restore_position.is_none() { + return Ok(()); + } + let too_narrow = new_size.cols < self.responsive_min_cols; + let wide_again = + new_size.cols >= self.responsive_min_cols + RESPONSIVE_HYSTERESIS_COLS; + + let target_position = match (self.responsive_restore_position, too_narrow, wide_again) { + // collapse: remember where we were, go to the responsive layout + (None, true, _) => { + let Some(position) = self + .swap_layouts + .position_of_tiled_layout_named(&self.responsive_layout_name) + else { + return Ok(()); + }; + self.responsive_restore_position = + Some(self.swap_layouts.current_tiled_layout_position()); + Some(position) + }, + // restore: back to whatever was active before collapsing + (Some(restore_to), _, true) => { + self.responsive_restore_position = None; + Some(restore_to) + }, + _ => None, + }; + + let Some(target_position) = target_position else { + return Ok(()); + }; + self.swap_layouts.set_tiled_layout_position(target_position); + if let Some(layout) = self + .swap_layouts + .tiled_layout_at(target_position, &self.tiled_panes) + { + self.dissolve_stack_lists_for_classic_mutation(); + self.apply_tiled_layout_candidate(&layout); + self.tiled_panes.reapply_pane_frames(); + self.set_should_clear_display_before_rendering(); + } + Ok(()) + } pub fn resize_whole_tab(&mut self, new_screen_size: Size) -> Result<()> { let err_context = || format!("failed to resize whole tab (id {})", self.id); self.size = new_screen_size; + // Sample this BEFORE the auto_layout pass below mutates it, so the + // responsive switch can tell a genuine manual rearrangement from this + // function's own bookkeeping. See apply_responsive_layout. + let was_tiled_damaged = self.swap_layouts.is_tiled_damaged(); // If a tiled pane is fullscreen, exit fullscreen first so that *all* // tiled panes (including the currently hidden ones) participate in the // resize/relayout. We re-enter fullscreen on the same pane after the @@ -5147,6 +5243,11 @@ impl Tab { } } self.resize_all_stack_list_hidden_members(); + // after the geometry settles, decide whether this size warrants + // collapsing into (or restoring from) the responsive layout. + // was_tiled_damaged is sampled at the top of this function, before the + // auto_layout pass above overwrites it -- see apply_responsive_layout. + self.apply_responsive_layout(new_screen_size, was_tiled_damaged)?; Ok(()) } pub fn resize(&mut self, client_id: ClientId, strategy: ResizeStrategy) -> Result<()> { @@ -7562,6 +7663,12 @@ impl Tab { }) .unwrap_or(false) } + /// Threshold + target layout for the responsive collapse. Set from config + /// at tab creation and on reconfigure. + pub fn update_responsive_layout(&mut self, min_cols: usize, layout_name: String) { + self.responsive_min_cols = min_cols; + self.responsive_layout_name = layout_name; + } pub fn update_focus_follows_mouse(&mut self, focus_follows_mouse: bool) { self.focus_follows_mouse = focus_follows_mouse; } |
