use crate::panes::{FloatingPanes, TiledPanes}; use std::cell::RefCell; use std::collections::BTreeMap; use std::rc::Rc; use zellij_utils::{ input::layout::{ FloatingPaneLayout, LayoutConstraint, SwapFloatingLayout, SwapTiledLayout, TiledPaneLayout, }, pane_size::{PaneGeom, Size}, }; /// Name zellij gives the tab's own layout when it inserts it as a swap layout. /// Pinned to ExactPanes of the count it was written with, so it stops applying /// as soon as a pane is added or closed. pub const BASE_LAYOUT_NAME: &str = "BASE"; #[derive(Clone, Debug, Default)] pub struct SwapLayouts { swap_tiled_layouts: Vec, swap_floating_layouts: Vec, current_floating_layout_position: usize, current_tiled_layout_position: usize, is_floating_damaged: bool, is_tiled_damaged: bool, display_area: Rc>, // includes all panes (including eg. the status bar and tab bar in the default layout) } impl SwapLayouts { pub fn new( swap_layouts: (Vec, Vec), display_area: Rc>, ) -> Self { let display_area = display_area.clone(); SwapLayouts { swap_tiled_layouts: swap_layouts.0, swap_floating_layouts: swap_layouts.1, is_floating_damaged: false, is_tiled_damaged: false, display_area, ..Default::default() } } pub fn set_base_layout(&mut self, layout: (TiledPaneLayout, Vec)) { let mut base_swap_tiled_layout = BTreeMap::new(); let mut base_swap_floating_layout = BTreeMap::new(); let tiled_panes_count = layout.0.pane_count(); let floating_panes_count = layout.1.len(); // we set ExactPanes to the current panes in the layout, because the base layout is not // intended to be progressive - i.e. to have additional panes added to it // we also don't want it to be applied for less than the expected amount of panes, because // then unintended things can happen // we still want to keep it around in case we'd like to swap layouts without adding panes base_swap_tiled_layout.insert(LayoutConstraint::ExactPanes(tiled_panes_count), layout.0); base_swap_floating_layout .insert(LayoutConstraint::ExactPanes(floating_panes_count), layout.1); self.swap_tiled_layouts .insert(0, (base_swap_tiled_layout, Some(BASE_LAYOUT_NAME.into()))); self.swap_floating_layouts .insert(0, (base_swap_floating_layout, Some(BASE_LAYOUT_NAME.into()))); self.current_tiled_layout_position = 0; self.current_floating_layout_position = 0; } pub fn set_swap_tiled_layouts(&mut self, new_swap_tiled_layouts: Vec) { self.swap_tiled_layouts = new_swap_tiled_layouts; self.current_tiled_layout_position = 0; } pub fn set_swap_floating_layouts( &mut self, new_swap_floating_layouts: Vec, ) { self.swap_floating_layouts = new_swap_floating_layouts; self.current_floating_layout_position = 0; } pub fn set_is_floating_damaged(&mut self) { self.is_floating_damaged = true; } pub fn set_is_tiled_damaged(&mut self) { self.is_tiled_damaged = true; } pub fn reset_floating_damage(&mut self) { self.is_floating_damaged = false; } pub fn is_floating_damaged(&self) -> bool { self.is_floating_damaged } pub fn is_tiled_damaged(&self) -> bool { self.is_tiled_damaged } /// Append a synthesized "flatten into one stack" layout under `name`, and /// return its position. /// /// A custom layout file only gets swap layouts from a hand-written sibling /// `.swap.kdl`, so most layouts have none and the responsive /// collapse has nothing to select. Rather than require that file, synthesize /// the one layout the collapse actually needs. /// /// Appended rather than inserted so it cannot shift the position of the BASE /// layout at 0, which is what a collapse restores back to. pub fn append_synthesized_stack_layout(&mut self, name: &str) -> usize { // MinPanes(2) because a "stack" of one pane is just that pane, and // collapsing to it would be a no-op that still counted as collapsed self.append_synthesized_layout( name, LayoutConstraint::MinPanes(2), TiledPaneLayout::single_stack(), ) } /// Append a synthesized "spread into a grid" layout, and return its position. /// /// The way back out of a collapse when the layout that was active before it /// no longer applies -- typically BASE, which is pinned to the pane count it /// was written with, so adding a single pane leaves the tab with nothing to /// expand into and stuck stacked. pub fn append_synthesized_grid_layout(&mut self, name: &str) -> usize { // NoConstraint: this is the fallback, so it must never be the thing that // fails to apply. Feasibility is still checked against the display area. self.append_synthesized_layout( name, LayoutConstraint::NoConstraint, TiledPaneLayout::single_grid(), ) } fn append_synthesized_layout( &mut self, name: &str, constraint: LayoutConstraint, layout: TiledPaneLayout, ) -> usize { if let Some(existing) = self.position_of_tiled_layout_named(name) { return existing; } // Build it into the tab's own structure rather than replacing it, so // plugin panes (status bar, tab bar) keep their pinned slots. Real swap // layouts get this from the default tab template; a runtime-built one // has to take it from BASE. let layout = match self.base_tiled_layout() { Some(base) => base.with_content_replaced_by(&layout), None => layout, }; let mut constraints = BTreeMap::new(); constraints.insert(constraint, layout); self.swap_tiled_layouts .push((constraints, Some(name.to_string()))); self.swap_tiled_layouts.len() - 1 } /// The tab's own layout, as inserted by set_base_layout. Carries the plugin /// pane slots a synthesized layout has to preserve. fn base_tiled_layout(&self) -> Option { let position = self.position_of_tiled_layout_named(BASE_LAYOUT_NAME)?; self.swap_tiled_layouts .iter() .nth(position)? .0 .values() .next() .cloned() } /// Position of the swap tiled layout with this name, if any. /// /// Used by the responsive-layout switch, which selects a layout BY NAME /// rather than by cycling. Deliberately name-based: adding a size variant to /// LayoutConstraint would change a type that crosses both the plugin /// protobuf API and the client/server contract, and the committed prost /// assets cannot be regenerated here (no protoc). pub fn position_of_tiled_layout_named(&self, name: &str) -> Option { self.swap_tiled_layouts .iter() .position(|(_constraints, layout_name)| { layout_name.as_deref().map(str::trim) == Some(name.trim()) }) } /// How many tiled swap layouts exist. Used to assert that the synthesized /// fallbacks are created once and reused, not appended on every resize. pub fn tiled_layout_count(&self) -> usize { self.swap_tiled_layouts.len() } pub fn current_tiled_layout_position(&self) -> usize { self.current_tiled_layout_position } /// Jump straight to a swap layout position, instead of cycling to it. pub fn set_tiled_layout_position(&mut self, position: usize) { if position < self.swap_tiled_layouts.len() { self.current_tiled_layout_position = position; } } /// The layout at a given position, resolved against the current pane count /// and display area. /// /// Mirrors the per-position body of swap_tiled_panes (same constraint check, /// same position_panes_in_space feasibility check) but WITHOUT the cycling, /// so a specific layout can be selected directly. pub fn tiled_layout_at( &self, position: usize, tiled_panes: &TiledPanes, ) -> Option { let swap_layout = self.swap_tiled_layouts.iter().nth(position)?; for (constraint, layout) in swap_layout.0.iter() { if self.state_fits_tiled_panes_constraint(constraint, tiled_panes) { let display_area = self.display_area.borrow(); let pane_count = tiled_panes.visible_panes_count(); let display_area = PaneGeom::from(&*display_area); if layout .position_panes_in_space(&display_area, Some(pane_count), false, true) .is_ok() { return Some(layout.clone()); } } } None } pub fn tiled_layout_info(&self) -> (Option, bool) { // (swap_layout_name, is_swap_layout_dirty) match self .swap_tiled_layouts .iter() .nth(self.current_tiled_layout_position) { Some(current_tiled_layout) => ( current_tiled_layout.1.clone().or_else(|| { Some(format!( "Layout #{}", self.current_tiled_layout_position + 1 )) }), self.is_tiled_damaged, ), None => (None, self.is_tiled_damaged), } } pub fn floating_layout_info(&self) -> (Option, bool) { // (swap_layout_name, is_swap_layout_dirty) match self .swap_floating_layouts .iter() .nth(self.current_floating_layout_position) { Some(current_floating_layout) => ( current_floating_layout.1.clone().or_else(|| { Some(format!( "Layout #{}", self.current_floating_layout_position + 1 )) }), self.is_floating_damaged, ), None => (None, self.is_floating_damaged), } } pub fn swap_floating_panes( &mut self, floating_panes: &FloatingPanes, search_backwards: bool, ) -> Option> { if self.swap_floating_layouts.is_empty() { return None; } let initial_position = self.current_floating_layout_position; macro_rules! progress_layout { () => {{ if search_backwards { if self.current_floating_layout_position == 0 { self.current_floating_layout_position = self.swap_floating_layouts.len().saturating_sub(1); } else { self.current_floating_layout_position -= 1; } } else { self.current_floating_layout_position += 1; } }}; } if !self.is_floating_damaged && self .swap_floating_layouts .iter() .nth(self.current_floating_layout_position) .is_some() { progress_layout!(); } self.is_floating_damaged = false; loop { match self .swap_floating_layouts .iter() .nth(self.current_floating_layout_position) { Some(swap_layout) => { for (constraint, layout) in swap_layout.0.iter() { if self.state_fits_floating_panes_constraint(constraint, floating_panes) { return Some(layout.clone()); }; } progress_layout!(); }, None => { self.current_floating_layout_position = 0; }, }; if self.current_floating_layout_position == initial_position { break; } } None } fn state_fits_tiled_panes_constraint( &self, constraint: &LayoutConstraint, tiled_panes: &TiledPanes, ) -> bool { match constraint { LayoutConstraint::MaxPanes(max_panes) => { tiled_panes.visible_panes_count() <= *max_panes }, LayoutConstraint::MinPanes(min_panes) => { tiled_panes.visible_panes_count() >= *min_panes }, LayoutConstraint::ExactPanes(pane_count) => { tiled_panes.visible_panes_count() == *pane_count }, LayoutConstraint::NoConstraint => true, } } fn state_fits_floating_panes_constraint( &self, constraint: &LayoutConstraint, floating_panes: &FloatingPanes, ) -> bool { match constraint { LayoutConstraint::MaxPanes(max_panes) => { floating_panes.visible_panes_count() <= *max_panes }, LayoutConstraint::MinPanes(min_panes) => { floating_panes.visible_panes_count() >= *min_panes }, LayoutConstraint::ExactPanes(pane_count) => { floating_panes.visible_panes_count() == *pane_count }, LayoutConstraint::NoConstraint => true, } } pub fn swap_tiled_panes( &mut self, tiled_panes: &TiledPanes, search_backwards: bool, ) -> Option { if self.swap_tiled_layouts.is_empty() { return None; } macro_rules! progress_layout { () => {{ if search_backwards { if self.current_tiled_layout_position == 0 { self.current_tiled_layout_position = self.swap_tiled_layouts.len().saturating_sub(1); } else { self.current_tiled_layout_position -= 1; } } else { self.current_tiled_layout_position += 1; } }}; } let initial_position = self.current_tiled_layout_position; if !self.is_tiled_damaged && self .swap_tiled_layouts .iter() .nth(self.current_tiled_layout_position) .is_some() { progress_layout!(); } self.is_tiled_damaged = false; loop { match self .swap_tiled_layouts .iter() .nth(self.current_tiled_layout_position) { Some(swap_layout) => { for (constraint, layout) in swap_layout.0.iter() { if self.state_fits_tiled_panes_constraint(constraint, tiled_panes) { let focus_layout_if_not_focused = true; let display_area = self.display_area.borrow(); // TODO: reuse the assets from position_panes_in_space here? let pane_count = tiled_panes.visible_panes_count(); let display_area = PaneGeom::from(&*display_area); if layout .position_panes_in_space( &display_area, Some(pane_count), false, focus_layout_if_not_focused, ) .is_ok() { return Some(layout.clone()); } }; } progress_layout!(); }, None => { self.current_tiled_layout_position = 0; }, }; if self.current_tiled_layout_position == initial_position { break; } } None } }