diff options
| author | Aram Drevekenin <aram@poor.dev> | 2026-07-04 16:27:08 +0200 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-07-04 16:27:08 +0200 |
| commit | 82c4a24d701ecf9a48aa01bcc5c0bb3882747fe7 (patch) | |
| tree | 906e2f207dc1f5bf01112d7889663a2ce0a4cf2f /zellij-server/src/tab | |
| parent | Reduce compilation time (#5321) (diff) | |
| download | sl0ppij-82c4a24d701ecf9a48aa01bcc5c0bb3882747fe7.tar.gz sl0ppij-82c4a24d701ecf9a48aa01bcc5c0bb3882747fe7.tar.xz sl0ppij-82c4a24d701ecf9a48aa01bcc5c0bb3882747fe7.zip | |
Stacked lists (#5331)
* initial implementation
* improve ui
* fix: stacked resize interaction
* title list boundaries fix
* fix BEL
* mouse parity
* float/embed + bulk-op features
* fix in-place bug
* improve ui
* config live reload
* improve ui
* fix issues, add exit status
* fix for serialization
* fix tests
* integration tests
* comments
* refactor: duplicate title logic
* refactor: consolidate logic
* fix: flaky tests
* add a stacked swap layout
* rustfmt
* fix e2e
* add pr
Diffstat (limited to 'zellij-server/src/tab')
| -rw-r--r-- | zellij-server/src/tab/mod.rs | 879 | ||||
| -rw-r--r-- | zellij-server/src/tab/mouse_handler.rs | 13 | ||||
| -rw-r--r-- | zellij-server/src/tab/unit/layout_applier_tests.rs | 8 | ||||
| -rw-r--r-- | zellij-server/src/tab/unit/tab_integration_tests.rs | 9 | ||||
| -rw-r--r-- | zellij-server/src/tab/unit/tab_tests.rs | 3 |
5 files changed, 893 insertions, 19 deletions
diff --git a/zellij-server/src/tab/mod.rs b/zellij-server/src/tab/mod.rs index a2601966..18f3e0a7 100644 --- a/zellij-server/src/tab/mod.rs +++ b/zellij-server/src/tab/mod.rs @@ -13,6 +13,7 @@ pub use mouse_handler::{MouseEffect, MouseHandler, PaneEdge, PaneResizeState}; use std::env::temp_dir; use std::net::IpAddr; use std::path::PathBuf; +use unicode_width::UnicodeWidthStr; use uuid::Uuid; use zellij_utils::data::PaneContents; use zellij_utils::data::{ @@ -33,7 +34,10 @@ use crate::screen::{CopyOptions, ScreenInstruction}; use crate::ui::hint_text::{ held_hint_variants, hover_hint_variants, resize_hint_variants, HintExitStatus, }; -use crate::ui::{loading_indication::LoadingIndication, pane_boundaries_frame::FrameParams}; +use crate::ui::{ + loading_indication::LoadingIndication, pane_boundaries_frame::FrameParams, + pane_contents_and_ui::PaneContentsAndUi, +}; use layout_applier::LayoutApplier; use swap_layouts::SwapLayouts; @@ -70,7 +74,7 @@ use zellij_utils::{ options::PaneFrameStyle, parse_keys, }, - pane_size::{Offset, PaneGeom, Size, SizeInPixels, Viewport}, + pane_size::{Dimension, Offset, PaneGeom, Size, SizeInPixels, Viewport}, }; #[macro_export] @@ -151,6 +155,26 @@ const MAX_PENDING_VTE_EVENTS: usize = 7000; type HoldForCommand = Option<RunCommand>; pub type SuppressedPanes = HashMap<PaneId, (bool, Box<dyn Pane>)>; // bool => is scrollback editor +pub type StackListId = usize; + +#[derive(Debug, Clone)] +pub struct StackList { + pub members: Vec<PaneId>, + pub visible: PaneId, +} + +impl StackList { + pub fn rank_of(&self, pane_id: &PaneId) -> Option<usize> { + self.members.iter().position(|p| p == pane_id) + } +} + +enum StackListRemoval { + NotRemoved, + DissolvedInPlace, + Removed(Option<Box<dyn Pane>>), +} + enum BufferedTabInstruction { SetPaneSelectable(PaneId, bool), HandlePtyBytes(u32, VteBytes), @@ -168,6 +192,13 @@ pub(crate) struct Tab { tiled_panes: TiledPanes, floating_panes: FloatingPanes, suppressed_panes: SuppressedPanes, + stacked_pane_list: Rc<RefCell<bool>>, + reserved_top_rows: Rc<RefCell<HashMap<PaneId, usize>>>, + stack_lists: HashMap<StackListId, StackList>, + stack_list_of_member: HashMap<PaneId, StackListId>, + stack_list_parked_pairs: HashMap<PaneId, PaneId>, + next_stack_list_id: StackListId, + stack_list_session_is_mirrored: bool, max_panes: Option<usize>, viewport: Rc<RefCell<Viewport>>, // includes all non-UI panes display_area: Rc<RefCell<Size>>, // includes all panes (including eg. the status bar and tab bar in the default layout) @@ -628,6 +659,9 @@ pub trait Pane { fn start_loading_indication(&mut self, _loading_indication: LoadingIndication) {} // only relevant for plugins fn progress_animation_offset(&mut self) {} // only relevant for plugins fn current_title(&self) -> String; + fn stack_list_entry_label(&self) -> String { + self.current_title() + } fn custom_title(&self) -> Option<String>; fn has_explicit_title(&self) -> bool { false @@ -745,6 +779,7 @@ impl Tab { display_area: Size, character_cell_size: Rc<RefCell<Option<SizeInPixels>>>, stacked_resize: Rc<RefCell<bool>>, + stacked_pane_list: Rc<RefCell<bool>>, sixel_image_store: Rc<RefCell<SixelImageStore>>, os_api: Box<dyn ServerOsApi>, senders: ThreadSenders, @@ -800,6 +835,8 @@ impl Tab { let display_area = Rc::new(RefCell::new(display_area)); let connected_clients = Rc::new(RefCell::new(connected_clients)); let mode_info = Rc::new(RefCell::new(HashMap::new())); + let reserved_top_rows: Rc<RefCell<HashMap<PaneId, usize>>> = + Rc::new(RefCell::new(HashMap::new())); let tiled_panes = TiledPanes::new( display_area.clone(), @@ -809,6 +846,8 @@ impl Tab { mode_info.clone(), character_cell_size.clone(), stacked_resize.clone(), + stacked_pane_list.clone(), + reserved_top_rows.clone(), session_is_mirrored, pane_frame_style, default_mode_info.clone(), @@ -842,6 +881,13 @@ impl Tab { tiled_panes, floating_panes, suppressed_panes: HashMap::new(), + stacked_pane_list, + reserved_top_rows, + stack_lists: HashMap::new(), + stack_list_of_member: HashMap::new(), + stack_list_parked_pairs: HashMap::new(), + next_stack_list_id: 0, + stack_list_session_is_mirrored: session_is_mirrored, name: name.clone(), prev_name: name, default_name, @@ -908,6 +954,645 @@ impl Tab { } } + pub fn stacked_pane_list_is_active(&self) -> bool { + *self.stacked_pane_list.borrow() + } + pub fn pane_is_stack_list_member(&self, pane_id: &PaneId) -> bool { + self.stack_list_of_member.contains_key(pane_id) + } + pub fn has_stack_lists(&self) -> bool { + !self.stack_lists.is_empty() + } + fn stack_list_id_of_member(&self, pane_id: &PaneId) -> Option<StackListId> { + self.stack_list_of_member.get(pane_id).copied() + } + pub fn pane_is_hidden_stack_list_member(&self, pane_id: &PaneId) -> bool { + self.stack_list_id_of_member(pane_id) + .and_then(|stack_list_id| self.stack_lists.get(&stack_list_id)) + .map(|list| list.visible != *pane_id) + .unwrap_or(false) + } + pub fn focus_hidden_stack_list_member(&mut self, pane_id: PaneId, client_id: ClientId) -> bool { + self.swap_in_hidden_stack_list_member(pane_id, Some(client_id)) + } + fn swap_in_hidden_stack_list_member( + &mut self, + pane_id: PaneId, + client_id: Option<ClientId>, + ) -> bool { + if !self.pane_is_hidden_stack_list_member(&pane_id) { + return false; + } + match self.stack_list_id_of_member(&pane_id) { + Some(stack_list_id) => { + self.select_stack_list_member(stack_list_id, pane_id, client_id); + true + }, + None => false, + } + } + fn stack_list_member_at_point(&self, pane_id: PaneId, point: &Position) -> PaneId { + let Some(stack_list_id) = self.stack_list_id_of_member(&pane_id) else { + return pane_id; + }; + let Some(list) = self.stack_lists.get(&stack_list_id) else { + return pane_id; + }; + let Some(pane) = self.tiled_panes.get_pane(pane_id) else { + return pane_id; + }; + let geom = pane.current_geom(); + 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) + } + pub fn suppressed_stack_list_members(&self) -> impl Iterator<Item = (&PaneId, &Box<dyn Pane>)> { + self.suppressed_panes + .iter() + .filter(|(pane_id, _)| self.stack_list_of_member.contains_key(pane_id)) + .map(|(pane_id, (_, pane))| (pane_id, pane)) + } + pub fn stack_list_serialization_geoms(&self) -> HashMap<PaneId, PaneGeom> { + let mut synthetic_geoms = HashMap::new(); + if self.stack_lists.is_empty() { + return synthetic_geoms; + } + let mut next_synthetic_stack_id = self + .tiled_panes + .get_panes() + .filter_map(|(_, pane)| pane.position_and_size().stacked) + .max() + .map(|highest_stack_id| highest_stack_id + 1) + .unwrap_or(0); + for list in self.stack_lists.values() { + let Some(visible_pane) = self.tiled_panes.get_pane(list.visible) else { + continue; + }; + let full_rect = visible_pane.position_and_size(); + let collapsed_member_count = list.members.len().saturating_sub(1); + let mut running_y = full_rect.y; + for member in &list.members { + let mut member_geom = full_rect; + member_geom.stacked = Some(next_synthetic_stack_id); + member_geom.y = running_y; + if *member == list.visible { + member_geom.rows.set_inner( + full_rect + .rows + .as_usize() + .saturating_sub(collapsed_member_count), + ); + } else { + member_geom.rows = Dimension::fixed(1); + member_geom.logical_position = self + .suppressed_panes + .get(member) + .and_then(|(_, pane)| pane.position_and_size().logical_position); + } + running_y += member_geom.rows.as_usize(); + synthetic_geoms.insert(*member, member_geom); + } + next_synthetic_stack_id += 1; + } + synthetic_geoms + } + pub fn hidden_stack_list_members_for_serialization( + &self, + ) -> Vec<(PaneId, &Box<dyn Pane>, PaneGeom)> { + let synthetic_geoms = self.stack_list_serialization_geoms(); + let mut hidden_members = vec![]; + for list in self.stack_lists.values() { + for member in &list.members { + if *member == list.visible { + continue; + } + let Some(member_geom) = synthetic_geoms.get(member) else { + continue; + }; + let resolved_pane = self + .stack_list_parked_pairs + .get(member) + .and_then(|parked_pid| self.suppressed_panes.get(parked_pid)) + .filter(|(is_scrollback_editor, _)| *is_scrollback_editor) + .or_else(|| self.suppressed_panes.get(member)); + if let Some((_, pane)) = resolved_pane { + hidden_members.push((pane.pid(), pane, *member_geom)); + } + } + } + hidden_members + } + fn pane_parked_by(&self, id: &PaneId) -> Option<PaneId> { + self.suppressed_panes + .get(id) + .map(|(_, parked)| parked.pid()) + .filter(|parked_pid| parked_pid != id) + } + fn adopt_parked_pair(&mut self, member: PaneId) { + if let Some(parked_pid) = self.stack_list_parked_pairs.remove(&member) { + if let Some(parked_entry) = self.suppressed_panes.remove(&parked_pid) { + self.suppressed_panes.insert(member, parked_entry); + } + } + } + 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), + None => return, + }; + if member_count <= 1 { + self.dissolve_stack_list(stack_list_id); + } else { + self.reserved_top_rows + .borrow_mut() + .insert(visible, member_count); + self.tiled_panes.reapply_pane_frames(); + self.resize_stack_list_hidden_members(stack_list_id); + } + } + fn select_stack_list_member( + &mut self, + stack_list_id: StackListId, + target_member: PaneId, + client_id: Option<ClientId>, + ) { + let current_visible = match self.stack_lists.get(&stack_list_id) { + Some(list) => list.visible, + None => return, + }; + if current_visible == target_member { + return; + } + let pane_parked_by_current_visible = self.pane_parked_by(¤t_visible); + let target_box = match self.suppressed_panes.remove(&target_member) { + Some((_is_scrollback_editor, pane)) => pane, + None => { + log::error!( + "stack-list member {:?} not found in suppressed panes", + target_member + ); + return; + }, + }; + let removed_visible = self.tiled_panes.replace_pane(current_visible, target_box); + if let Some(removed_visible) = removed_visible { + self.insert_suppressed_pane(current_visible, (false, removed_visible)); + if let Some(parked_pid) = pane_parked_by_current_visible { + self.stack_list_parked_pairs + .insert(current_visible, parked_pid); + } + } + self.adopt_parked_pair(target_member); + if let Some(list) = self.stack_lists.get_mut(&stack_list_id) { + list.visible = target_member; + } + self.reserved_top_rows.borrow_mut().remove(¤t_visible); + if let Some(client_id) = client_id { + self.tiled_panes.focus_pane(target_member, client_id); + } + self.refresh_or_dissolve_stack_list(stack_list_id); + self.set_force_render(); + self.set_should_clear_display_before_rendering(); + } + fn move_focus_within_stack_list(&mut self, client_id: ClientId, down: bool) -> bool { + let active_pane_id = match self.tiled_panes.get_active_pane_id(client_id) { + Some(id) => id, + None => return false, + }; + let stack_list_id = match self.stack_list_id_of_member(&active_pane_id) { + Some(id) => id, + None => return false, + }; + let target_member = { + let list = match self.stack_lists.get(&stack_list_id) { + Some(l) => l, + None => return false, + }; + if list.visible != active_pane_id { + return false; + } + let rank = match list.rank_of(&active_pane_id) { + Some(r) => r, + None => return false, + }; + let target_rank = if down { + rank + 1 + } else if rank == 0 { + return false; + } else { + rank - 1 + }; + match list.members.get(target_rank) { + Some(member) => *member, + None => return false, + } + }; + self.select_stack_list_member(stack_list_id, target_member, Some(client_id)); + true + } + fn close_stack_list_member(&mut self, id: PaneId, exit_status: Option<i32>) -> bool { + match self.remove_stack_list_member(id) { + StackListRemoval::Removed(removed) => { + if let (Some(exit_status), Some(mut removed)) = (exit_status, removed) { + removed.update_exit_status(exit_status); + } + true + }, + StackListRemoval::DissolvedInPlace | StackListRemoval::NotRemoved => false, + } + } + fn remove_stack_list_member(&mut self, id: PaneId) -> StackListRemoval { + let stack_list_id = match self.stack_list_id_of_member(&id) { + Some(sid) => sid, + None => return StackListRemoval::NotRemoved, + }; + let is_visible = self + .stack_lists + .get(&stack_list_id) + .map(|l| l.visible == id) + .unwrap_or(false); + let removed = if is_visible { + let (members, rank) = { + let list = match self.stack_lists.get(&stack_list_id) { + Some(l) => l, + None => return StackListRemoval::NotRemoved, + }; + (list.members.clone(), list.rank_of(&id).unwrap_or(0)) + }; + let promote_target = members + .iter() + .skip(rank + 1) + .next() + .copied() + .or_else(|| members[..rank].iter().last().copied()); + let promote_target = match promote_target { + Some(target) => target, + None => { + self.dissolve_stack_list(stack_list_id); + self.reserved_top_rows.borrow_mut().remove(&id); + return StackListRemoval::DissolvedInPlace; + }, + }; + let target_box = match self.suppressed_panes.remove(&promote_target) { + Some((_is_scrollback_editor, pane)) => pane, + None => { + log::error!("stack-list promote target not found in suppressed panes"); + return StackListRemoval::NotRemoved; + }, + }; + let removed_visible = self.tiled_panes.replace_pane(id, target_box); + self.adopt_parked_pair(promote_target); + self.stack_list_of_member.remove(&id); + self.reserved_top_rows.borrow_mut().remove(&id); + if let Some(list) = self.stack_lists.get_mut(&stack_list_id) { + list.members.retain(|m| *m != id); + list.visible = promote_target; + } + self.refresh_or_dissolve_stack_list(stack_list_id); + removed_visible + } else { + let removed = self.suppressed_panes.remove(&id).map(|(_, p)| p); + let substitute_member = self + .stack_list_parked_pairs + .remove(&id) + .filter(|parked_pid| self.suppressed_panes.contains_key(parked_pid)); + self.stack_list_of_member.remove(&id); + self.reserved_top_rows.borrow_mut().remove(&id); + if let Some(substitute_member) = substitute_member { + self.stack_list_of_member + .insert(substitute_member, stack_list_id); + if let Some(list) = self.stack_lists.get_mut(&stack_list_id) { + for member in list.members.iter_mut() { + if *member == id { + *member = substitute_member; + } + } + } + } else if let Some(list) = self.stack_lists.get_mut(&stack_list_id) { + list.members.retain(|m| *m != id); + } + self.refresh_or_dissolve_stack_list(stack_list_id); + removed + }; + self.set_force_render(); + self.set_should_clear_display_before_rendering(); + StackListRemoval::Removed(removed) + } + fn track_stack_list_member_id_swap(&mut self, old_id: PaneId, new_id: PaneId) { + let stack_list_id = match self.stack_list_id_of_member(&old_id) { + Some(sid) => sid, + None => return, + }; + self.stack_list_of_member.remove(&old_id); + self.stack_list_of_member.insert(new_id, stack_list_id); + let mut swapped_visible_member = false; + if let Some(list) = self.stack_lists.get_mut(&stack_list_id) { + for member in list.members.iter_mut() { + if *member == old_id { + *member = new_id; + } + } + if list.visible == old_id { + list.visible = new_id; + swapped_visible_member = true; + } + } + { + let mut reserved = self.reserved_top_rows.borrow_mut(); + if let Some(reserved_rows) = reserved.remove(&old_id) { + reserved.insert(new_id, reserved_rows); + } + } + if swapped_visible_member { + self.refresh_or_dissolve_stack_list(stack_list_id); + self.set_force_render(); + self.set_should_clear_display_before_rendering(); + } + } + fn dissolve_stack_list(&mut self, stack_list_id: StackListId) { + if let Some(list) = self.stack_lists.get(&stack_list_id) { + let members = list.members.clone(); + self.drop_stack_list_ledger(stack_list_id, &members); + self.tiled_panes.reapply_pane_frames(); + } + } + fn drop_stack_list_ledger(&mut self, stack_list_id: StackListId, members: &[PaneId]) { + self.stack_lists.remove(&stack_list_id); + let mut reserved = self.reserved_top_rows.borrow_mut(); + for member in members { + self.stack_list_of_member.remove(member); + self.stack_list_parked_pairs.remove(member); + reserved.remove(member); + } + } + fn degroupify(&mut self, stack_list_id: StackListId) { + let (members, visible) = match self.stack_lists.get(&stack_list_id) { + Some(list) => (list.members.clone(), list.visible), + None => return, + }; + let n = members.len(); + if n == 0 || !self.tiled_panes.panes_contain(&visible) { + self.drop_stack_list_ledger(stack_list_id, &members); + return; + } + let geoms = self.tiled_panes.stack_panes(visible, n); + if geoms.len() != n { + log::error!("degroupify: could not compute classic stack geoms"); + return; + } + for (i, member) in members.iter().enumerate() { + let mut geom = geoms[i]; + if *member == visible { + geom.logical_position = self + .tiled_panes + .get_pane(*member) + .and_then(|p| p.position_and_size().logical_position); + self.tiled_panes.set_geom_for_pane_with_id(member, geom); + } else if let Some((_is_scrollback_editor, mut pane)) = + self.suppressed_panes.remove(member) + { + geom.logical_position = pane.position_and_size().logical_position; + pane.set_geom(geom); + self.tiled_panes.add_pane_with_existing_geom(*member, pane); + self.adopt_parked_pair(*member); + } + } + self.drop_stack_list_ledger(stack_list_id, &members); + self.tiled_panes.expand_pane_in_stack(visible); + self.tiled_panes.reapply_pane_frames(); + self.set_force_render(); + self.set_should_clear_display_before_rendering(); + } + // the classic layout solver cannot see suppressed stack-list members; dissolve + // lists into in-grid stacks first, groupify_all re-forms them on mode entry + fn degroupify_all(&mut self) { + let ids: Vec<StackListId> = self.stack_lists.keys().copied().collect(); + for id in ids { + self.degroupify(id); + } + } + fn dissolve_stack_lists_for_classic_mutation(&mut self) { + if self.stacked_pane_list_is_active() { + self.degroupify_all(); + } + } + pub fn sync_stacked_pane_list_mode(&mut self) { + if self.stacked_pane_list_is_active() { + self.groupify_all(); + } else if self.has_stack_lists() { + self.degroupify_all(); + } + } + // the inverse of degroupify_all: adopts classic in-grid stacks into stack + // lists, suppressing all but the visible member of each + fn groupify_all(&mut self) { + if self.tiled_panes.fullscreen_is_active() { + return; + } + let mut stacks: HashMap<usize, Vec<(usize, PaneId, bool)>> = HashMap::new(); + for (id, pane) in self.tiled_panes.get_panes() { + let geom = pane.position_and_size(); + if let Some(stack_id) = geom.stacked { + let is_expanded = !geom.rows.is_fixed(); + stacks + .entry(stack_id) + .or_default() + .push((geom.y, *id, is_expanded)); + } + } + let mut converted_any = false; + for (_stack_id, mut members) in stacks { + if members.len() < 2 { + continue; + } + members.sort_by_key(|(y, _, _)| *y); + let representative = members[0].1; + let rect = match self.tiled_panes.position_and_size_of_stack(&representative) { + Some(r) => r, + None => continue, + }; + let visible = members + .iter() + .find(|(_, _, expanded)| *expanded) + .map(|(_, id, _)| *id) + .unwrap_or_else(|| members.last().map(|(_, id, _)| *id).unwrap()); + let ordered_ids: Vec<PaneId> = members.iter().map(|(_, id, _)| *id).collect(); + let logical_position = self + .tiled_panes + .get_pane(visible) + .and_then(|p| p.position_and_size().logical_position); + for (_, id, _) in &members { + if *id == visible { + continue; + } + if let Some(pane) = self.tiled_panes.extract_pane(*id) { + let pane_parked_by_member = self.pane_parked_by(id); + self.insert_suppressed_pane(*id, (false, pane)); + if let Some(parked_pid) = pane_parked_by_member { + self.stack_list_parked_pairs.insert(*id, parked_pid); + } + } + } + if let Some(vpane) = self.tiled_panes.get_pane_mut(visible) { + let mut g = rect; + g.stacked = None; + g.logical_position = logical_position; + vpane.set_geom(g); + } + let stack_list_id = self.next_stack_list_id; + self.next_stack_list_id += 1; + for id in &ordered_ids { + self.stack_list_of_member.insert(*id, stack_list_id); + } + self.stack_lists.insert( + stack_list_id, + StackList { + members: ordered_ids, + visible, + }, + ); + self.refresh_or_dissolve_stack_list(stack_list_id); + converted_any = true; + } + if converted_any { + self.set_force_render(); + self.set_should_clear_display_before_rendering(); + } + } + fn resize_all_stack_list_hidden_members(&mut self) { + let ids: Vec<StackListId> = self.stack_lists.keys().copied().collect(); + for id in ids { + self.resize_stack_list_hidden_members(id); + } + } + fn resize_stack_list_hidden_members(&mut self, stack_list_id: StackListId) { + let (visible_geom, visible_offset, hidden_members) = { + let list = match self.stack_lists.get(&stack_list_id) { + Some(l) => l, + None => return, + }; + let (geom, offset) = match self.tiled_panes.get_pane(list.visible) { + Some(p) => (p.position_and_size(), p.get_content_offset()), + None => return, + }; + let hidden: Vec<PaneId> = list + .members + .iter() + .copied() + .filter(|m| *m != list.visible) + .collect(); + (geom, offset, hidden) + }; + for member in hidden_members { + if let Some((_is_scrollback_editor, pane)) = self.suppressed_panes.get_mut(&member) { + pane.set_geom(visible_geom); + pane.set_content_offset(visible_offset); + let _ = resize_pty!(pane, self.os_api, self.senders, self.character_cell_size); + } + } + } + fn render_stack_list_headers( + &mut self, + output: &mut Output, + client_id_override: Option<ClientId>, + ) -> Result<()> { + if self.stack_lists.is_empty() || self.tiled_panes.fullscreen_is_active() { + return Ok(()); + } + let mut connected_clients: HashSet<ClientId> = + { self.connected_clients.borrow().iter().copied().collect() }; + if let Some(override_id) = client_id_override { + connected_clients.insert(override_id); + } + if connected_clients.is_empty() { + return Ok(()); + } + let multiple_users_exist_in_session = { self.connected_clients_in_app.borrow().len() > 1 }; + let should_draw_pane_frames = self.pane_frame_style.draws_full_frames(); + let session_is_mirrored = self.stack_list_session_is_mirrored; + let current_pane_group: HashMap<ClientId, Vec<PaneId>> = + { self.current_pane_group.borrow().clone_inner() }; + let active_panes: HashMap<ClientId, PaneId> = connected_clients + .iter() + .filter_map(|c| self.tiled_panes.get_active_pane_id(*c).map(|p| (*c, p))) + .collect(); + let client_modes: Vec<(ClientId, InputMode)> = { + let mode_info = self.mode_info.borrow(); + connected_clients + .iter() + .map(|c| { + let mode = mode_info.get(c).unwrap_or(&self.default_mode_info).mode; + (*c, mode) + }) + .collect() + }; + let lists: Vec<StackList> = self.stack_lists.values().cloned().collect(); + for list in lists { + let rect = match self.tiled_panes.get_pane(list.visible) { + Some(p) => p.position_and_size(), + None => continue, + }; + let widest_member_title = list + .members + .iter() + .filter_map(|member| { + if *member == list.visible { + self.tiled_panes.get_pane(*member).map(|p| &**p) + } else { + self.suppressed_panes.get(member).map(|(_, p)| &**p) + } + }) + .map(|pane| pane.stack_list_entry_label().width()) + .max() + .unwrap_or(0); + for (rank, member) in list.members.iter().enumerate() { + let mut header_geom = rect; + header_geom.y = rect.y + rank; + header_geom.rows = Dimension::fixed(1); + header_geom.stacked = None; + let pane_box = if *member == list.visible { + self.tiled_panes.get_pane_mut(*member) + } else { + self.suppressed_panes.get_mut(member).map(|(_, p)| p) + }; + let pane_box = match pane_box { + Some(p) => p, + None => continue, + }; + let mut pane_contents_and_ui = PaneContentsAndUi::new( + pane_box, + output, + self.style, + &active_panes, + multiple_users_exist_in_session, + None, + false, + false, + should_draw_pane_frames, + &self.mouse_hover_pane_id, + current_pane_group.clone(), + false, + false, + ); + pane_contents_and_ui.set_frame_geom_override(Some(header_geom)); + pane_contents_and_ui + .set_stack_list_entry(Some(widest_member_title), *member == list.visible); + for (client_id, client_mode) in &client_modes { + pane_contents_and_ui.render_pane_frame( + *client_id, + *client_mode, + session_is_mirrored, + false, + true, + )?; + } + } + } + Ok(()) + } + pub fn apply_layout( &mut self, layout: TiledPaneLayout, @@ -1078,7 +1763,7 @@ impl Tab { } else { let selectable_tiled_panes = self.tiled_panes.get_panes().filter(|(_, p)| p.selectable()); - if selectable_tiled_panes.count() > 1 { + if selectable_tiled_panes.count() + self.suppressed_stack_list_members().count() > 1 { self.swap_layouts.tiled_layout_info() } else { // no layout for single pane @@ -1127,6 +1812,7 @@ impl Tab { if self.tiled_panes.fullscreen_is_active() { self.tiled_panes.unset_fullscreen(); } + self.dissolve_stack_lists_for_classic_mutation(); if let Some(layout_candidate) = self .swap_layouts .swap_tiled_panes(&self.tiled_panes, search_backwards) @@ -1522,7 +2208,9 @@ impl Tab { } } } else if let Some(focused_pane_id) = self.tiled_panes.focused_pane_id(client_id) { - if self.get_selectable_tiled_panes().count() <= 1 { + if self.get_selectable_tiled_panes().count() <= 1 + && !self.pane_is_stack_list_member(&focused_pane_id) + { // don't close the only pane on screen... return Ok(()); } @@ -1564,7 +2252,9 @@ impl Tab { self.add_tiled_pane(floating_pane_to_embed, pane_id, false, client_id)?; } } else if self.tiled_panes.panes_contain(&pane_id) { - if self.get_selectable_tiled_panes().count() <= 1 { + if self.get_selectable_tiled_panes().count() <= 1 + && !self.pane_is_stack_list_member(&pane_id) + { log::error!("Cannot float the last tiled pane..."); // don't close the only pane on screen... return Ok(()); @@ -1572,6 +2262,10 @@ impl Tab { if let Some(embedded_pane_to_float) = self.extract_pane(pane_id, true) { self.add_floating_pane(embedded_pane_to_float, pane_id, None, true, client_id)?; } + } else if self.pane_is_hidden_stack_list_member(&pane_id) { + if let Some(hidden_member_to_float) = self.extract_pane(pane_id, true) { + self.add_floating_pane(hidden_member_to_float, pane_id, None, true, client_id)?; + } } Ok(()) } @@ -2236,6 +2930,10 @@ impl Tab { }; match replaced_pane { Some(replaced_pane) => { + self.track_stack_list_member_id_swap( + replaced_pane.pid(), + PaneId::Terminal(pid), + ); self.insert_scrollback_editor_replaced_pane(replaced_pane, pid); self.get_active_pane(client_id) .with_context(|| format!("no active pane found for client {client_id}")) @@ -2304,6 +3002,10 @@ impl Tab { self.character_cell_size ) .non_fatal(); + self.track_stack_list_member_id_swap( + pane_id_to_replace, + PaneId::Terminal(pid), + ); self.insert_scrollback_editor_replaced_pane(replaced_pane, pid); }, None => { @@ -2367,6 +3069,12 @@ impl Tab { self.tiled_panes .replace_pane(old_pane_id, Box::new(new_pane)) }; + if replaced_pane.is_some() { + self.track_stack_list_member_id_swap( + old_pane_id, + PaneId::Terminal(new_pane_id), + ); + } if close_replaced_pane { if let Some(pid) = replaced_pane.as_ref().map(|p| p.pid()) { self.senders @@ -2437,6 +3145,9 @@ impl Tab { self.tiled_panes .replace_pane(old_pane_id, Box::new(new_pane)) }; + if replaced_pane.is_some() { + self.track_stack_list_member_id_swap(old_pane_id, PaneId::Plugin(plugin_pid)); + } if close_replaced_pane { drop(replaced_pane); } else { @@ -2473,6 +3184,7 @@ impl Tab { pane_to_replace_with: Box<dyn Pane>, completion_tx: Option<NotificationEnd>, ) { + let replacement_pane_id = pane_to_replace_with.pid(); let mut replaced_pane = if self.floating_panes.panes_contain(&pane_id_to_replace) { self.floating_panes .replace_pane(pane_id_to_replace, pane_to_replace_with) @@ -2481,6 +3193,9 @@ impl Tab { self.tiled_panes .replace_pane(pane_id_to_replace, pane_to_replace_with) }; + if replaced_pane.is_some() { + self.track_stack_list_member_id_swap(pane_id_to_replace, replacement_pane_id); + } if let Some(replaced_pane) = replaced_pane.take() { let pane_id = replaced_pane.pid(); let _ = self @@ -2537,6 +3252,7 @@ impl Tab { if self.tiled_panes.fullscreen_is_active() { self.toggle_active_pane_fullscreen(client_id); } + self.dissolve_stack_lists_for_classic_mutation(); if self.tiled_panes.can_split_pane_horizontally(client_id) { if let PaneId::Terminal(term_pid) = pid { let next_terminal_position = self.get_next_terminal_position(); @@ -2604,6 +3320,7 @@ impl Tab { if self.tiled_panes.fullscreen_is_active() { self.toggle_active_pane_fullscreen(client_id); } + self.dissolve_stack_lists_for_classic_mutation(); if self.tiled_panes.can_split_pane_vertically(client_id) { if let PaneId::Terminal(term_pid) = pid { let next_terminal_position = self.get_next_terminal_position(); @@ -2732,6 +3449,7 @@ impl Tab { .tiled_panes .get_panes() .chain(self.floating_panes.get_panes()) + .chain(self.suppressed_stack_list_members()) .filter(|(_, pane)| pane.has_bell()) .map(|(pane_id, _)| *pane_id) .collect(); @@ -2781,6 +3499,7 @@ impl Tab { .tiled_panes .get_panes() .chain(self.floating_panes.get_panes()) + .chain(self.suppressed_stack_list_members()) .filter(|(_, pane)| pane.has_bell()) .map(|(pane_id, _)| *pane_id) .collect(); @@ -3073,7 +3792,13 @@ impl Tab { // returns true if a UI update should be triggered (eg. when closing a command pane with // ctrl-c) let mut should_trigger_ui_change = false; - let pane_ids = self.get_static_and_floating_pane_ids(); + let mut pane_ids = self.get_static_and_floating_pane_ids(); + pane_ids.extend( + self.stack_list_of_member + .keys() + .filter(|member| self.pane_is_hidden_stack_list_member(member)) + .copied(), + ); for pane_id in pane_ids { let ui_change_triggered = self .write_to_pane_id( @@ -3393,9 +4118,14 @@ impl Tab { if self.floating_panes.panes_are_visible() { return; } + self.dissolve_stack_lists_for_classic_mutation(); self.tiled_panes.toggle_active_pane_fullscreen(client_id); } pub fn toggle_pane_fullscreen(&mut self, pane_id: PaneId) { + if self.pane_is_hidden_stack_list_member(&pane_id) { + self.make_hidden_stack_list_member_visible(pane_id); + } + self.dissolve_stack_lists_for_classic_mutation(); if self.tiled_panes.panes_contain(&pane_id) { self.tiled_panes.toggle_pane_fullscreen(pane_id); } else { @@ -3460,6 +4190,12 @@ impl Tab { pub fn set_force_render(&mut self) { self.tiled_panes.set_force_render(); self.floating_panes.set_force_render(); + for member in self.stack_list_of_member.keys() { + if let Some((_is_scrollback_editor, pane)) = self.suppressed_panes.get_mut(member) { + pane.set_should_render(true); + pane.render_full_viewport(); + } + } } pub fn set_should_clear_display_before_rendering(&mut self) { self.should_clear_display_before_rendering = true; @@ -3515,6 +4251,9 @@ impl Tab { if connected_clients.is_empty() || !self.tiled_panes.has_active_panes() { return Ok(()); } + if self.stacked_pane_list_is_active() { + self.groupify_all(); + } self.update_active_panes_in_pty_thread() .with_context(err_context)?; @@ -3537,6 +4276,8 @@ impl Tab { &self.mouse_help_text_visible, ) .with_context(err_context)?; + self.render_stack_list_headers(output, client_id_override) + .with_context(err_context)?; if (self.floating_panes.panes_are_visible() && self.floating_panes.has_active_panes()) || self.floating_panes.has_pinned_panes() { @@ -3572,7 +4313,7 @@ impl Tab { hide_cursor, ); if self.should_clear_display_before_rendering { - let clear_display = "\u{1b}[2J"; + let clear_display = "\u{1b}[m\u{1b}[2J"; output.add_pre_vte_instruction_to_multiple_clients( connected_clients.iter().copied(), clear_display, @@ -3707,7 +4448,11 @@ impl Tab { PaneId::Terminal(_) => true, }) .count(); - tiled_panes_count + floating_panes_count + 1 + let hidden_stack_list_members_count = self + .suppressed_stack_list_members() + .filter(|(pane_id, _)| matches!(pane_id, PaneId::Terminal(_))) + .count(); + tiled_panes_count + floating_panes_count + hidden_stack_list_members_count + 1 } pub fn has_selectable_panes(&self) -> bool { let selectable_tiled_panes = self.tiled_panes.get_panes().filter(|(_, p)| p.selectable()); @@ -3768,6 +4513,7 @@ impl Tab { self.tiled_panes.toggle_pane_fullscreen(pane_id); } } + self.resize_all_stack_list_hidden_members(); Ok(()) } pub fn resize(&mut self, client_id: ClientId, strategy: ResizeStrategy) -> Result<()> { @@ -3782,6 +4528,7 @@ impl Tab { self.set_force_render(); // we force render here to make sure the panes under the floating pane render and don't leave "garbage" in case of a decrease } } else { + self.dissolve_stack_lists_for_classic_mutation(); match self.tiled_panes.resize_active_pane(client_id, &strategy) { Ok(_) => { self.swap_layouts.set_is_tiled_damaged(); @@ -3896,6 +4643,11 @@ impl Tab { self.focus_pane_down_fullscreen(client_id); return Ok(true); } + if self.stacked_pane_list_is_active() + && self.move_focus_within_stack_list(client_id, true) + { + return Ok(true); + } Ok(self.tiled_panes.move_focus_down(client_id)) } } @@ -3918,6 +4670,11 @@ impl Tab { self.focus_pane_up_fullscreen(client_id); return Ok(true); } + if self.stacked_pane_list_is_active() + && self.move_focus_within_stack_list(client_id, false) + { + return Ok(true); + } Ok(self.tiled_panes.move_focus_up(client_id)) } } @@ -3955,6 +4712,7 @@ impl Tab { self.floating_panes .move_active_pane(search_backwards, &mut self.os_api, client_id); } else { + self.dissolve_stack_lists_for_classic_mutation(); self.tiled_panes .move_active_pane(search_backwards, client_id); } @@ -3970,6 +4728,7 @@ impl Tab { if self.floating_panes.panes_are_visible() { self.floating_panes.move_pane(search_backwards, pane_id); } else { + self.dissolve_stack_lists_for_classic_mutation(); self.tiled_panes.move_pane(search_backwards, pane_id); } } @@ -3985,6 +4744,7 @@ impl Tab { self.floating_panes .move_active_pane(search_backwards, &mut self.os_api, client_id); } else { + self.dissolve_stack_lists_for_classic_mutation(); self.tiled_panes .move_active_pane(search_backwards, client_id); } @@ -4001,6 +4761,7 @@ impl Tab { if self.tiled_panes.fullscreen_is_active() { return; } + self.dissolve_stack_lists_for_classic_mutation(); self.tiled_panes.move_active_pane_down(client_id); } } @@ -4016,6 +4777,7 @@ impl Tab { if self.tiled_panes.fullscreen_is_active() { return; } + self.dissolve_stack_lists_for_classic_mutation(); self.tiled_panes.move_pane_down(pane_id); } } @@ -4031,6 +4793,7 @@ impl Tab { if self.tiled_panes.fullscreen_is_active() { return; } + self.dissolve_stack_lists_for_classic_mutation(); self.tiled_panes.move_active_pane_up(client_id); } } @@ -4046,6 +4809,7 @@ impl Tab { if self.tiled_panes.fullscreen_is_active() { return; } + self.dissolve_stack_lists_for_classic_mutation(); self.tiled_panes.move_pane_up(pane_id); } } @@ -4061,6 +4825,7 @@ impl Tab { if self.tiled_panes.fullscreen_is_active() { return; } + self.dissolve_stack_lists_for_classic_mutation(); self.tiled_panes.move_active_pane_right(client_id); } } @@ -4076,6 +4841,7 @@ impl Tab { if self.tiled_panes.fullscreen_is_active() { return; } + self.dissolve_stack_lists_for_classic_mutation(); self.tiled_panes.move_pane_right(pane_id); } } @@ -4091,6 +4857,7 @@ impl Tab { if self.tiled_panes.fullscreen_is_active() { return; } + self.dissolve_stack_lists_for_classic_mutation(); self.tiled_panes.move_active_pane_left(client_id); } } @@ -4106,11 +4873,13 @@ impl Tab { if self.tiled_panes.fullscreen_is_active() { return; } + self.dissolve_stack_lists_for_classic_mutation(); self.tiled_panes.move_pane_left(pane_id); } } fn close_down_to_max_terminals(&mut self) -> Result<()> { if let Some(max_panes) = self.max_panes { + self.dissolve_stack_lists_for_classic_mutation(); let terminals = self.get_tiled_pane_ids(); for &pid in terminals.iter().skip(max_panes - 1) { self.senders @@ -4226,6 +4995,25 @@ impl Tab { ignore_suppressed_panes: bool, exit_status: Option<i32>, ) { + let id_parks_a_different_pane = self.pane_parked_by(&id).is_some(); + if !ignore_suppressed_panes + && !id_parks_a_different_pane + && self.pane_is_stack_list_member(&id) + { + if self.close_stack_list_member(id, exit_status) { + let _ = self.senders.send_to_plugin(PluginInstruction::Update(vec![( + None, + None, + Event::PaneClosed(id.into()), + )])); + let _ = + self.senders + .send_to_screen(ScreenInstruction::NotifyPaneClosedToSubscribers { + pane_id: id.into(), + }); + return; + } + } // we need to ignore suppressed panes when we toggle a pane to be floating/embedded(tiled) // this is because in that case, while we do use this logic, we're not actually closing the // pane, we're moving it @@ -4271,6 +5059,7 @@ impl Tab { // confusing let _ = self.relayout_tiled_panes(false); } + self.resize_all_stack_list_hidden_members(); closed_pane }; if let Some(exit_status) = exit_status { @@ -4295,6 +5084,17 @@ impl Tab { id: PaneId, dont_swap_if_suppressed: bool, ) -> Option<Box<dyn Pane>> { + let id_parks_a_different_pane = self.pane_parked_by(&id).is_some(); + if self.pane_is_stack_list_member(&id) + && (dont_swap_if_suppressed || !id_parks_a_different_pane) + { + if let StackListRemoval::Removed(removed) = self.remove_stack_list_member(id) { + return removed.map(|mut pane| { + pane.reset_logical_position(); + pane + }); + } + } if !dont_swap_if_suppressed && self.suppressed_panes.contains_key(&id) { // this is done for the scrollback editor return match self.replace_pane_with_suppressed_pane(id) { @@ -4349,6 +5149,7 @@ impl Tab { // confusing let _ = self.relayout_tiled_panes(false); } + self.resize_all_stack_list_hidden_members(); // we do this so that the logical index will not affect ordering in the target tab if let Some(closed_pane) = closed_pane.as_mut() { closed_pane.reset_logical_position(); @@ -4413,6 +5214,9 @@ impl Tab { } else { self.tiled_panes.replace_pane(pane_id, suppressed_pane) }; + if replaced_pane.is_some() { + self.track_stack_list_member_id_swap(pane_id, suppressed_pane_id); + } if let Some(suppressed_pane) = self .floating_panes .get_pane(suppressed_pane_id) @@ -4636,6 +5440,7 @@ impl Tab { completion_tx: Option<NotificationEnd>, ) -> Result<()> { if let PaneId::Terminal(_terminal_pane_id) = pane_id { + self.make_hidden_stack_list_member_visible(pane_id); let mut file = temp_dir(); file.push(format!("{}.dump", Uuid::new_v4())); self.dump_terminal_screen(Some(String::from(file.to_string_lossy())), pane_id, true) @@ -4660,6 +5465,7 @@ impl Tab { completion_tx: Option<NotificationEnd>, ) -> Result<()> { if let PaneId::Terminal(_terminal_pane_id) = pane_id { + self.make_hidden_stack_list_member_visible(pane_id); let mut file = temp_dir(); file.push(format!("{}.dump", Uuid::new_v4())); self.dump_with_ansi_terminal_screen( @@ -4978,16 +5784,24 @@ impl Tab { geom_to_compare_against.contains(point) }; - if search_selectable { - Ok(self - .get_selectable_tiled_panes() + let found_pane_id = if search_selectable { + self.get_selectable_tiled_panes() .find(|(_, p)| pane_contains_point(p, point, &stacked_pane_ids_under_flexible_pane)) - .map(|(&id, _)| id)) + .map(|(&id, _)| id) } else { - Ok(self - .get_tiled_panes() + self.get_tiled_panes() .find(|(_, p)| pane_contains_point(p, point, &stacked_pane_ids_under_flexible_pane)) - .map(|(&id, _)| id)) + .map(|(&id, _)| id) + }; + let resolved_pane_id = found_pane_id.map(|id| self.stack_list_member_at_point(id, point)); + if search_selectable { + Ok(resolved_pane_id.filter(|id| { + self.get_pane_with_id(*id) + .map(|p| p.selectable()) + .unwrap_or(false) + })) + } else { + Ok(resolved_pane_id) } } @@ -5521,6 +6335,9 @@ impl Tab { client_id: ClientId, ) -> Result<()> { // TODO: should error if pane is not selectable + if self.focus_hidden_stack_list_member(pane_id, client_id) { + return Ok(()); + } self.tiled_panes .focus_pane_if_exists(pane_id, client_id) .map(|_| self.hide_floating_panes()) @@ -5556,6 +6373,7 @@ impl Tab { }; if let Some(replaced_pane) = replaced_pane { let is_scrollback_editor = false; + self.track_stack_list_member_id_swap(replaced_pane.pid(), pane_id); self.insert_suppressed_pane( pane_id, (is_scrollback_editor, replaced_pane), @@ -5574,6 +6392,10 @@ impl Tab { }) } pub fn focus_suppressed_pane_for_all_clients(&mut self, pane_id: PaneId) { + if self.make_hidden_stack_list_member_visible(pane_id) { + self.tiled_panes.focus_pane_for_all_clients(pane_id); + return; + } match self.suppressed_panes.remove(&pane_id) { Some(pane) => { self.show_floating_panes(); @@ -5595,8 +6417,14 @@ impl Tab { self.insert_suppressed_pane(pane_id, (false, pane)); } } + fn make_hidden_stack_list_member_visible(&mut self, pane_id: PaneId) -> bool { + self.swap_in_hidden_stack_list_member(pane_id, None) + } pub fn unsuppress_pane(&mut self, pane_id: PaneId, should_float_if_hidden: bool) { // removes a pane from being suppressed (hidden) but does not focus it + if self.make_hidden_stack_list_member_visible(pane_id) { + return; + } match self .suppressed_panes .extract_if(|_key, (_, pane)| pane.pid() == pane_id) @@ -5618,6 +6446,9 @@ impl Tab { } pub fn unsuppress_or_expand_pane(&mut self, pane_id: PaneId, should_float_if_hidden: bool) { // removes a pane from being suppressed (hidden) but does not focus it + if self.make_hidden_stack_list_member_visible(pane_id) { + return; + } match self .suppressed_panes .extract_if(|_key, (_, pane)| pane.pid() == pane_id) @@ -5767,6 +6598,7 @@ impl Tab { if self.tiled_panes.fullscreen_is_active() { self.tiled_panes.unset_fullscreen(); } + self.dissolve_stack_lists_for_classic_mutation(); let should_auto_layout = self.auto_layout && !self.swap_layouts.is_tiled_damaged() && !without_relayout; if self.tiled_panes.has_room_for_new_pane() { @@ -5779,6 +6611,7 @@ impl Tab { } else { self.tiled_panes.insert_pane(pane_id, pane, client_id); } + self.tiled_panes.reapply_pane_frames(); self.set_should_clear_display_before_rendering(); if let Some(client_id) = client_id { self.tiled_panes.focus_pane(pane_id, client_id); @@ -5802,6 +6635,7 @@ impl Tab { if self.tiled_panes.fullscreen_is_active() { self.tiled_panes.unset_fullscreen(); } + self.dissolve_stack_lists_for_classic_mutation(); self.tiled_panes .add_pane_to_stack_of_pane_id(pane_id, pane, root_pane_id); self.set_should_clear_display_before_rendering(); @@ -5819,6 +6653,7 @@ impl Tab { if self.tiled_panes.fullscreen_is_active() { self.tiled_panes.unset_fullscreen(); } + self.dissolve_stack_lists_for_classic_mutation(); self.tiled_panes .add_pane_to_stack_of_active_pane(pane_id, pane, client_id); self.tiled_panes.focus_pane(pane_id, client_id); @@ -5891,6 +6726,9 @@ impl Tab { } pub fn resize_pane_with_id(&mut self, strategy: ResizeStrategy, pane_id: PaneId) -> Result<()> { let err_context = || format!("unable to resize pane"); + if self.pane_is_stack_list_member(&pane_id) { + self.dissolve_stack_lists_for_classic_mutation(); + } if self.floating_panes.panes_contain(&pane_id) { let successfully_resized = self .floating_panes @@ -6017,7 +6855,10 @@ impl Tab { } } pub fn extract_suppressed_panes(&mut self) -> SuppressedPanes { - self.suppressed_panes.drain().collect() + let stack_list_members = &self.stack_list_of_member; + self.suppressed_panes + .extract_if(|key, _| !stack_list_members.contains_key(key)) + .collect() } pub fn add_suppressed_panes(&mut self, mut suppressed_panes: SuppressedPanes) { for (pane_id, suppressed_pane_entry) in suppressed_panes.drain() { @@ -6037,6 +6878,7 @@ impl Tab { } } pub fn has_room_for_stack(&mut self, root_pane_id: PaneId, stack_size: usize) -> bool { + self.dissolve_stack_lists_for_classic_mutation(); if self.floating_panes.panes_contain(&root_pane_id) || self.suppressed_panes.contains_key(&root_pane_id) { @@ -6063,6 +6905,7 @@ impl Tab { // nothing to do return; } + self.dissolve_stack_lists_for_classic_mutation(); self.swap_layouts.set_is_tiled_damaged(); // TODO: verify we can do all the below first if self.pane_is_stacked(root_pane_id) { for pane in panes_to_stack.drain(..) { @@ -6345,6 +7188,9 @@ impl Tab { if self.tiled_panes.fullscreen_is_active() { return; } + if !self.floating_panes.panes_contain(&pane_id) { + self.dissolve_stack_lists_for_classic_mutation(); + } match direction { Some(Direction::Left) => { if self.floating_panes.panes_contain(&pane_id) { @@ -6403,6 +7249,7 @@ impl Tab { if self.floating_panes.panes_contain(&pane_id) { self.floating_panes.move_pane(search_backwards, pane_id); } else { + self.dissolve_stack_lists_for_classic_mutation(); self.tiled_panes.move_pane(search_backwards, pane_id); } } diff --git a/zellij-server/src/tab/mouse_handler.rs b/zellij-server/src/tab/mouse_handler.rs index 61718864..d04b511c 100644 --- a/zellij-server/src/tab/mouse_handler.rs +++ b/zellij-server/src/tab/mouse_handler.rs @@ -417,9 +417,10 @@ impl MouseHandler { client_id: ClientId, ) -> Option<ClickedPaneDetails> { let is_floating = tab.floating_panes.panes_contain(&pane_id); + let is_hidden_stack_list_member = tab.pane_is_hidden_stack_list_member(&pane_id); let pane = Self::get_pane_at(tab, position, false).ok()??; - let on_frame = pane.position_is_on_frame(position); + let on_frame = !is_hidden_stack_list_member && pane.position_is_on_frame(position); let frame_intercepted = on_frame && pane.intercept_mouse_event_on_frame(event, client_id); let edge = if on_frame { pane.get_edge_at_position(position) @@ -1053,6 +1054,10 @@ impl MouseHandler { return Self::execute_update_hover(tab, Some(pane_id), Some(position), client_id); } + if tab.pane_is_hidden_stack_list_member(&pane_id) { + return Self::execute_update_hover(tab, Some(pane_id), Some(position), client_id); + } + let active_pane_id = tab.get_active_pane_id(client_id); if active_pane_id == Some(pane_id) { return Self::execute_update_hover(tab, Some(pane_id), Some(position), client_id); @@ -1543,7 +1548,9 @@ impl MouseHandler { } } if let Some(clicked_pane) = tab.get_pane_id_at(point, true).with_context(err_context)? { - tab.tiled_panes.focus_pane(clicked_pane, client_id); + if !tab.focus_hidden_stack_list_member(clicked_pane, client_id) { + tab.tiled_panes.focus_pane(clicked_pane, client_id); + } tab.set_pane_active_at(clicked_pane); if tab.floating_panes.panes_are_visible() { tab.hide_floating_panes(); @@ -1737,7 +1744,7 @@ impl MouseHandler { .get_pane_id_at(point, search_selectable) .with_context(err_context)? { - Ok(tab.tiled_panes.get_pane_mut(pane_id)) + Ok(tab.get_pane_with_id_mut(pane_id)) } else { Ok(None) } diff --git a/zellij-server/src/tab/unit/layout_applier_tests.rs b/zellij-server/src/tab/unit/layout_applier_tests.rs index 33ad8ee4..f67a9a17 100644 --- a/zellij-server/src/tab/unit/layout_applier_tests.rs +++ b/zellij-server/src/tab/unit/layout_applier_tests.rs @@ -187,6 +187,8 @@ fn create_layout_applier_fixtures( let connected_clients_set = Rc::new(RefCell::new(HashSet::from([client_id]))); let mode_info = Rc::new(RefCell::new(HashMap::new())); let stacked_resize = Rc::new(RefCell::new(false)); + let stacked_pane_list = Rc::new(RefCell::new(false)); + let reserved_top_rows = Rc::new(RefCell::new(HashMap::new())); let session_is_mirrored = true; let draw_pane_frames = PaneFrameStyle::Full; let default_mode_info = ModeInfo::default(); @@ -199,6 +201,8 @@ fn create_layout_applier_fixtures( mode_info.clone(), character_cell_size.clone(), stacked_resize, + stacked_pane_list, + reserved_top_rows, session_is_mirrored, draw_pane_frames, default_mode_info.clone(), @@ -317,6 +321,8 @@ fn create_layout_applier_fixtures_with_receivers( let connected_clients_set = Rc::new(RefCell::new(HashSet::from([client_id]))); let mode_info = Rc::new(RefCell::new(HashMap::new())); let stacked_resize = Rc::new(RefCell::new(false)); + let stacked_pane_list = Rc::new(RefCell::new(false)); + let reserved_top_rows = Rc::new(RefCell::new(HashMap::new())); let session_is_mirrored = true; let draw_pane_frames = PaneFrameStyle::Full; let default_mode_info = ModeInfo::default(); @@ -329,6 +335,8 @@ fn create_layout_applier_fixtures_with_receivers( mode_info.clone(), character_cell_size.clone(), stacked_resize, + stacked_pane_list, + reserved_top_rows, session_is_mirrored, draw_pane_frames, default_mode_info.clone(), diff --git a/zellij-server/src/tab/unit/tab_integration_tests.rs b/zellij-server/src/tab/unit/tab_integration_tests.rs index 5b623b00..35a2e9bd 100644 --- a/zellij-server/src/tab/unit/tab_integration_tests.rs +++ b/zellij-server/src/tab/unit/tab_integration_tests.rs @@ -246,6 +246,7 @@ fn create_new_tab(size: Size, default_mode: ModeInfo) -> Tab { size, character_cell_info, stacked_resize, + Rc::new(RefCell::new(false)), sixel_image_store, os_api, senders, @@ -334,6 +335,7 @@ fn create_new_tab_without_pane_frames(size: Size, default_mode: ModeInfo) -> Tab size, character_cell_info, stacked_resize, + Rc::new(RefCell::new(false)), sixel_image_store, os_api, senders, @@ -437,6 +439,7 @@ fn create_new_tab_with_swap_layouts( size, character_cell_info, stacked_resize, + Rc::new(RefCell::new(false)), sixel_image_store, os_api, senders, @@ -545,6 +548,7 @@ fn create_new_tab_with_os_api( size, character_cell_info, stacked_resize, + Rc::new(RefCell::new(false)), sixel_image_store, os_api, senders, @@ -635,6 +639,7 @@ fn create_new_tab_with_layout(size: Size, default_mode: ModeInfo, layout: &str) size, character_cell_info, stacked_resize, + Rc::new(RefCell::new(false)), sixel_image_store, os_api, senders, @@ -739,6 +744,7 @@ fn create_new_tab_with_mock_pty_writer( size, character_cell_info, stacked_resize, + Rc::new(RefCell::new(false)), sixel_image_store, os_api, senders, @@ -834,6 +840,7 @@ fn create_new_tab_with_sixel_support( size, character_cell_size, stacked_resize, + Rc::new(RefCell::new(false)), sixel_image_store, os_api, senders, @@ -12727,6 +12734,7 @@ fn create_new_tab_with_plugin_receiver( size, character_cell_info, stacked_resize, + Rc::new(RefCell::new(false)), sixel_image_store, os_api, senders, @@ -14512,6 +14520,7 @@ fn create_new_tab_with_server_receiver( size, character_cell_info, stacked_resize, + Rc::new(RefCell::new(false)), sixel_image_store, os_api, senders, diff --git a/zellij-server/src/tab/unit/tab_tests.rs b/zellij-server/src/tab/unit/tab_tests.rs index 0d18897e..75a6695b 100644 --- a/zellij-server/src/tab/unit/tab_tests.rs +++ b/zellij-server/src/tab/unit/tab_tests.rs @@ -188,6 +188,7 @@ fn create_new_tab(size: Size, stacked_resize: bool) -> Tab { size, character_cell_info, stacked_resize, + Rc::new(RefCell::new(false)), sixel_image_store, os_api, senders, @@ -275,6 +276,7 @@ fn create_new_tab_with_layout(size: Size, layout: TiledPaneLayout) -> Tab { size, character_cell_info, stacked_resize, + Rc::new(RefCell::new(false)), sixel_image_store, os_api, senders, @@ -368,6 +370,7 @@ fn create_new_tab_with_cell_size( size, character_cell_size, stacked_resize, + Rc::new(RefCell::new(false)), sixel_image_store, os_api, senders, |
