aboutsummaryrefslogtreecommitdiffstats
path: root/zellij-server/src/tab
diff options
context:
space:
mode:
authorAram Drevekenin <aram@poor.dev>2026-07-04 16:27:08 +0200
committerGitHub <noreply@github.com>2026-07-04 16:27:08 +0200
commit82c4a24d701ecf9a48aa01bcc5c0bb3882747fe7 (patch)
tree906e2f207dc1f5bf01112d7889663a2ce0a4cf2f /zellij-server/src/tab
parentReduce compilation time (#5321) (diff)
downloadsl0ppij-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.rs879
-rw-r--r--zellij-server/src/tab/mouse_handler.rs13
-rw-r--r--zellij-server/src/tab/unit/layout_applier_tests.rs8
-rw-r--r--zellij-server/src/tab/unit/tab_integration_tests.rs9
-rw-r--r--zellij-server/src/tab/unit/tab_tests.rs3
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(&current_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(&current_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,