//! The layout system. // Layouts have been moved from [`zellij-server`] to // [`zellij-utils`] in order to provide more helpful // error messages to the user until a more general // logging system is in place. // In case there is a logging system in place evaluate, // if [`zellij-utils`], or [`zellij-server`] is a proper // place. // If plugins should be able to depend on the layout system // then [`zellij-utils`] could be a proper place. use crate::{ data::Direction, input::{ command::RunCommand, config::{Config, ConfigError}, }, pane_size::{Dimension, PaneGeom}, setup, }; use std::str::FromStr; use super::plugins::{PluginTag, PluginsConfigError}; use serde::{Deserialize, Serialize}; use std::collections::BTreeMap; use std::vec::Vec; use std::{ fmt, ops::Not, path::{Path, PathBuf}, }; use std::{fs::File, io::prelude::*}; use url::Url; #[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Clone, Copy)] pub enum SplitDirection { Horizontal, Vertical, } impl Not for SplitDirection { type Output = Self; fn not(self) -> Self::Output { match self { SplitDirection::Horizontal => SplitDirection::Vertical, SplitDirection::Vertical => SplitDirection::Horizontal, } } } impl From for SplitDirection { fn from(direction: Direction) -> Self { match direction { Direction::Left | Direction::Right => SplitDirection::Horizontal, Direction::Down | Direction::Up => SplitDirection::Vertical, } } } #[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq)] pub enum SplitSize { #[serde(alias = "percent")] Percent(usize), // 1 to 100 #[serde(alias = "fixed")] Fixed(usize), // An absolute number of columns or rows } #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] pub enum Run { #[serde(rename = "plugin")] Plugin(RunPlugin), #[serde(rename = "command")] Command(RunCommand), EditFile(PathBuf, Option, Option), // TODO: merge this with TerminalAction::OpenFile Cwd(PathBuf), } impl Run { pub fn merge(base: &Option, other: &Option) -> Option { // This method is necessary to merge between pane_templates and their consumers // TODO: reconsider the way we parse command/edit/plugin pane_templates from layouts to prevent this // madness // TODO: handle Plugin variants once there's a need match (base, other) { (Some(Run::Command(base_run_command)), Some(Run::Command(other_run_command))) => { let mut merged = other_run_command.clone(); if merged.cwd.is_none() && base_run_command.cwd.is_some() { merged.cwd = base_run_command.cwd.clone(); } if merged.args.is_empty() && !base_run_command.args.is_empty() { merged.args = base_run_command.args.clone(); } Some(Run::Command(merged)) }, (Some(Run::Command(base_run_command)), Some(Run::Cwd(other_cwd))) => { let mut merged = base_run_command.clone(); merged.cwd = Some(other_cwd.clone()); Some(Run::Command(merged)) }, (Some(Run::Cwd(base_cwd)), Some(Run::Command(other_command))) => { let mut merged = other_command.clone(); if merged.cwd.is_none() { merged.cwd = Some(base_cwd.clone()); } Some(Run::Command(merged)) }, ( Some(Run::Command(base_run_command)), Some(Run::EditFile(file_to_edit, line_number, edit_cwd)), ) => match &base_run_command.cwd { Some(cwd) => Some(Run::EditFile( cwd.join(&file_to_edit), *line_number, Some(cwd.join(edit_cwd.clone().unwrap_or_default())), )), None => Some(Run::EditFile( file_to_edit.clone(), *line_number, edit_cwd.clone(), )), }, (Some(Run::Cwd(cwd)), Some(Run::EditFile(file_to_edit, line_number, edit_cwd))) => { let cwd = edit_cwd.clone().unwrap_or(cwd.clone()); Some(Run::EditFile( cwd.join(&file_to_edit), *line_number, Some(cwd), )) }, (Some(_base), Some(other)) => Some(other.clone()), (Some(base), _) => Some(base.clone()), (None, Some(other)) => Some(other.clone()), (None, None) => None, } } pub fn add_cwd(&mut self, cwd: &PathBuf) { match self { Run::Command(run_command) => match run_command.cwd.as_mut() { Some(run_cwd) => { *run_cwd = cwd.join(&run_cwd); }, None => { run_command.cwd = Some(cwd.clone()); }, }, Run::EditFile(path_to_file, _line_number, edit_cwd) => { match edit_cwd.as_mut() { Some(edit_cwd) => { *edit_cwd = cwd.join(&edit_cwd); }, None => { let _ = edit_cwd.insert(cwd.clone()); }, }; *path_to_file = cwd.join(&path_to_file); }, Run::Cwd(path) => { *path = cwd.join(&path); }, _ => {}, // plugins aren't yet supported } } pub fn add_args(&mut self, args: Option>) { // overrides the args of a Run::Command if they are Some // and not empty if let Some(args) = args { if let Run::Command(run_command) = self { if !args.is_empty() { run_command.args = args.clone(); } } } } pub fn add_close_on_exit(&mut self, close_on_exit: Option) { // overrides the hold_on_close of a Run::Command if it is Some // and not empty if let Some(close_on_exit) = close_on_exit { if let Run::Command(run_command) = self { run_command.hold_on_close = !close_on_exit; } } } pub fn add_start_suspended(&mut self, start_suspended: Option) { // overrides the hold_on_start of a Run::Command if they are Some // and not empty if let Some(start_suspended) = start_suspended { if let Run::Command(run_command) = self { run_command.hold_on_start = start_suspended; } } } pub fn is_same_category(first: &Option, second: &Option) -> bool { match (first, second) { (Some(Run::Plugin(..)), Some(Run::Plugin(..))) => true, (Some(Run::Command(..)), Some(Run::Command(..))) => true, (Some(Run::EditFile(..)), Some(Run::EditFile(..))) => true, (Some(Run::Cwd(..)), Some(Run::Cwd(..))) => true, _ => false, } } pub fn is_terminal(run: &Option) -> bool { match run { Some(Run::Command(..)) | Some(Run::EditFile(..)) | Some(Run::Cwd(..)) | None => true, _ => false, } } } #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Hash)] pub struct RunPlugin { #[serde(default)] pub _allow_exec_host_cmd: bool, pub location: RunPluginLocation, } #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Hash)] pub enum RunPluginLocation { File(PathBuf), Zellij(PluginTag), } impl RunPluginLocation { pub fn parse(location: &str) -> Result { let url = Url::parse(location)?; let decoded_path = percent_encoding::percent_decode_str(url.path()).decode_utf8_lossy(); match url.scheme() { "zellij" => Ok(Self::Zellij(PluginTag::new(decoded_path))), "file" => { let path = if location.starts_with("file:/") { // Path is absolute, its safe to use URL path. // // This is the case if the scheme and : delimiter are followed by a / slash decoded_path } else { // URL dep doesn't handle relative paths with `file` schema properly, // it always makes them absolute. Use raw location string instead. // // Unwrap is safe here since location is a valid URL location.strip_prefix("file:").unwrap().into() }; Ok(Self::File(PathBuf::from(path.as_ref()))) }, _ => Err(PluginsConfigError::InvalidUrlScheme(url)), } } } impl From<&RunPluginLocation> for Url { fn from(location: &RunPluginLocation) -> Self { let url = match location { RunPluginLocation::File(path) => format!( "file:{}", path.clone().into_os_string().into_string().unwrap() ), RunPluginLocation::Zellij(tag) => format!("zellij:{}", tag), }; Self::parse(&url).unwrap() } } impl fmt::Display for RunPluginLocation { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { match self { Self::File(path) => write!( f, "{}", path.clone().into_os_string().into_string().unwrap() ), Self::Zellij(tag) => write!(f, "{}", tag), } } } #[derive(Clone, Debug, PartialEq, Eq, Hash, Ord, PartialOrd, Serialize, Deserialize)] pub enum LayoutConstraint { MaxPanes(usize), MinPanes(usize), ExactPanes(usize), NoConstraint, } pub type SwapTiledLayout = (BTreeMap, Option); // Option is the swap layout name pub type SwapFloatingLayout = ( BTreeMap>, Option, ); // Option is the swap layout name #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Default)] pub struct Layout { pub tabs: Vec<(Option, TiledPaneLayout, Vec)>, pub focused_tab_index: Option, pub template: Option<(TiledPaneLayout, Vec)>, pub swap_layouts: Vec<(TiledPaneLayout, Vec)>, pub swap_tiled_layouts: Vec, pub swap_floating_layouts: Vec, } #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] pub enum PercentOrFixed { Percent(usize), // 1 to 100 Fixed(usize), // An absolute number of columns or rows } impl PercentOrFixed { pub fn to_position(&self, whole: usize) -> usize { match self { PercentOrFixed::Percent(percent) => { (whole as f64 / 100.0 * *percent as f64).ceil() as usize }, PercentOrFixed::Fixed(fixed) => { if *fixed > whole { whole } else { *fixed } }, } } } impl PercentOrFixed { pub fn is_zero(&self) -> bool { match self { PercentOrFixed::Percent(percent) => *percent == 0, PercentOrFixed::Fixed(fixed) => *fixed == 0, } } } impl FromStr for PercentOrFixed { type Err = Box; fn from_str(s: &str) -> Result { if s.chars().last() == Some('%') { let char_count = s.chars().count(); let percent_size = usize::from_str_radix(&s[..char_count.saturating_sub(1)], 10)?; if percent_size <= 100 { Ok(PercentOrFixed::Percent(percent_size)) } else { Err("Percent must be between 0 and 100".into()) } } else { let fixed_size = usize::from_str_radix(s, 10)?; Ok(PercentOrFixed::Fixed(fixed_size)) } } } #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Default)] pub struct FloatingPaneLayout { pub name: Option, pub height: Option, pub width: Option, pub x: Option, pub y: Option, pub run: Option, pub focus: Option, } impl FloatingPaneLayout { pub fn add_cwd_to_layout(&mut self, cwd: &PathBuf) { match self.run.as_mut() { Some(run) => run.add_cwd(cwd), None => { self.run = Some(Run::Cwd(cwd.clone())); }, } } } impl From<&TiledPaneLayout> for FloatingPaneLayout { fn from(pane_layout: &TiledPaneLayout) -> Self { FloatingPaneLayout { name: pane_layout.name.clone(), run: pane_layout.run.clone(), focus: pane_layout.focus, ..Default::default() } } } #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Default)] pub struct TiledPaneLayout { pub children_split_direction: SplitDirection, pub name: Option, pub children: Vec, pub split_size: Option, pub run: Option, pub borderless: bool, pub focus: Option, pub external_children_index: Option, pub children_are_stacked: bool, pub is_expanded_in_stack: bool, pub exclude_from_sync: Option, } impl TiledPaneLayout { pub fn insert_children_layout( &mut self, children_layout: &mut TiledPaneLayout, ) -> Result { // returns true if successfully inserted and false otherwise match self.external_children_index { Some(external_children_index) => { self.children .insert(external_children_index, children_layout.clone()); self.external_children_index = None; Ok(true) }, None => { for pane in self.children.iter_mut() { if pane.insert_children_layout(children_layout)? { return Ok(true); } } Ok(false) }, } } pub fn insert_children_nodes( &mut self, children_nodes: &mut Vec, ) -> Result { // returns true if successfully inserted and false otherwise match self.external_children_index { Some(external_children_index) => { children_nodes.reverse(); for child_node in children_nodes.drain(..) { self.children.insert(external_children_index, child_node); } self.external_children_index = None; Ok(true) }, None => { for pane in self.children.iter_mut() { if pane.insert_children_nodes(children_nodes)? { return Ok(true); } } Ok(false) }, } } pub fn children_block_count(&self) -> usize { let mut count = 0; if self.external_children_index.is_some() { count += 1; } for pane in &self.children { count += pane.children_block_count(); } count } pub fn pane_count(&self) -> usize { if self.children.is_empty() { 1 // self } else { let mut pane_count = 0; for child in &self.children { pane_count += child.pane_count(); } pane_count } } pub fn position_panes_in_space( &self, space: &PaneGeom, max_panes: Option, ) -> Result, &'static str> { let layouts = match max_panes { Some(max_panes) => { let mut layout_to_split = self.clone(); let pane_count_in_layout = layout_to_split.pane_count(); if max_panes > pane_count_in_layout { // the + 1 here is because this was previously an "actual" pane and will now // become just a container, so we need to account for it too // TODO: make sure this works when the `children` node has sibling nodes, // because we really should support that let children_count = (max_panes - pane_count_in_layout) + 1; let mut extra_children = vec![TiledPaneLayout::default(); children_count]; if !layout_to_split.has_focused_node() { if let Some(last_child) = extra_children.last_mut() { last_child.focus = Some(true); } } let _ = layout_to_split.insert_children_nodes(&mut extra_children); } else { layout_to_split.truncate(max_panes); } if !layout_to_split.has_focused_node() { layout_to_split.focus_deepest_pane(); } split_space(space, &layout_to_split, space)? }, None => split_space(space, self, space)?, }; for (_pane_layout, pane_geom) in layouts.iter() { if !pane_geom.is_at_least_minimum_size() { return Err("No room on screen for this layout!"); } } Ok(layouts) } pub fn extract_run_instructions(&self) -> Vec> { // the order of these run instructions is significant and needs to be the same // as the order of the "flattened" layout panes received from eg. position_panes_in_space let mut run_instructions = vec![]; if self.children.is_empty() { run_instructions.push(self.run.clone()); } for child in &self.children { let mut child_run_instructions = child.extract_run_instructions(); run_instructions.append(&mut child_run_instructions); } run_instructions } pub fn with_one_pane() -> Self { let mut default_layout = TiledPaneLayout::default(); default_layout.children = vec![TiledPaneLayout::default()]; default_layout } pub fn add_cwd_to_layout(&mut self, cwd: &PathBuf) { match self.run.as_mut() { Some(run) => run.add_cwd(cwd), None => { self.run = Some(Run::Cwd(cwd.clone())); }, } for child in self.children.iter_mut() { child.add_cwd_to_layout(cwd); } } pub fn deepest_depth(&self) -> usize { let mut deepest_child_depth = 0; for child in self.children.iter() { let child_deepest_depth = child.deepest_depth(); if child_deepest_depth > deepest_child_depth { deepest_child_depth = child_deepest_depth; } } deepest_child_depth + 1 } pub fn focus_deepest_pane(&mut self) { let mut deepest_child_index = None; let mut deepest_path = 0; for (i, child) in self.children.iter().enumerate() { let child_deepest_path = child.deepest_depth(); if child_deepest_path >= deepest_path { deepest_path = child_deepest_path; deepest_child_index = Some(i) } } match deepest_child_index { Some(deepest_child_index) => { if let Some(child) = self.children.get_mut(deepest_child_index) { child.focus_deepest_pane(); } }, None => { self.focus = Some(true); }, } } pub fn truncate(&mut self, max_panes: usize) -> usize { // returns remaining children length // if max_panes is 1, it means there's only enough panes for this node, // if max_panes is 0, this is probably the root layout being called with 0 max panes if max_panes <= 1 { while !self.children.is_empty() { // this is a special case: we're truncating a pane that was previously a logical // container but now should be an actual pane - so here we'd like to use its // deepest "non-logical" child in order to get all its attributes (eg. borderless) let first_child = self.children.remove(0); drop(std::mem::replace(self, first_child)); } self.children.clear(); } else if max_panes <= self.children.len() { self.children.truncate(max_panes); self.children.iter_mut().for_each(|l| l.children.clear()); } else { let mut remaining_panes = max_panes - self .children .iter() .filter(|c| c.children.is_empty()) .count(); for child in self.children.iter_mut() { if remaining_panes > 1 && child.children.len() > 0 { remaining_panes = remaining_panes.saturating_sub(child.truncate(remaining_panes)); } else { child.children.clear(); } } } if self.children.len() > 0 { self.children.len() } else { 1 // just me } } pub fn has_focused_node(&self) -> bool { if self.focus.map(|f| f).unwrap_or(false) { return true; }; for child in &self.children { if child.has_focused_node() { return true; } } false } } #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] pub enum LayoutParts { Tabs(Vec<(Option, Layout)>), // String is the tab name Panes(Vec), } impl LayoutParts { pub fn is_empty(&self) -> bool { match self { LayoutParts::Panes(panes) => panes.is_empty(), LayoutParts::Tabs(tabs) => tabs.is_empty(), } } pub fn insert_pane(&mut self, index: usize, layout: Layout) -> Result<(), ConfigError> { match self { LayoutParts::Panes(panes) => { panes.insert(index, layout); Ok(()) }, LayoutParts::Tabs(_tabs) => Err(ConfigError::new_layout_kdl_error( "Trying to insert a pane into a tab layout".into(), 0, 0, )), } } } impl Default for LayoutParts { fn default() -> Self { LayoutParts::Panes(vec![]) } } impl Layout { pub fn stringified_from_path_or_default( layout_path: Option<&PathBuf>, layout_dir: Option, ) -> Result<(String, String, Option<(String, String)>), ConfigError> { // (path_to_layout as String, stringified_layout, Option) match layout_path { Some(layout_path) => { // The way we determine where to look for the layout is similar to // how a path would look for an executable. // See the gh issue for more: https://github.com/zellij-org/zellij/issues/1412#issuecomment-1131559720 if layout_path.extension().is_some() || layout_path.components().count() > 1 { // We look localy! Layout::stringified_from_path(layout_path) } else { // We look in the default dir Layout::stringified_from_dir(layout_path, layout_dir.as_ref()) } }, None => Layout::stringified_from_dir( &std::path::PathBuf::from("default"), layout_dir.as_ref(), ), } } pub fn from_path_or_default( layout_path: Option<&PathBuf>, layout_dir: Option, config: Config, ) -> Result<(Layout, Config), ConfigError> { let (path_to_raw_layout, raw_layout, raw_swap_layouts) = Layout::stringified_from_path_or_default(layout_path, layout_dir)?; let layout = Layout::from_kdl( &raw_layout, path_to_raw_layout, raw_swap_layouts .as_ref() .map(|(r, f)| (r.as_str(), f.as_str())), None, )?; let config = Config::from_kdl(&raw_layout, Some(config))?; // this merges the two config, with Ok((layout, config)) } pub fn from_str( raw: &str, path_to_raw_layout: String, swap_layouts: Option<(&str, &str)>, // Option cwd: Option, ) -> Result { Layout::from_kdl(raw, path_to_raw_layout, swap_layouts, cwd) } pub fn stringified_from_dir( layout: &PathBuf, layout_dir: Option<&PathBuf>, ) -> Result<(String, String, Option<(String, String)>), ConfigError> { // (path_to_layout as String, stringified_layout, Option) match layout_dir { Some(dir) => { let layout_path = &dir.join(layout); if layout_path.with_extension("kdl").exists() { Self::stringified_from_path(layout_path) } else { Layout::stringified_from_default_assets(layout) } }, None => Layout::stringified_from_default_assets(layout), } } pub fn stringified_from_path( layout_path: &Path, ) -> Result<(String, String, Option<(String, String)>), ConfigError> { // (path_to_layout as String, stringified_layout, Option) let mut layout_file = File::open(&layout_path) .or_else(|_| File::open(&layout_path.with_extension("kdl"))) .map_err(|e| ConfigError::IoPath(e, layout_path.into()))?; let swap_layout_and_path = Layout::swap_layout_and_path(&layout_path); let mut kdl_layout = String::new(); layout_file .read_to_string(&mut kdl_layout) .map_err(|e| ConfigError::IoPath(e, layout_path.into()))?; Ok(( layout_path.as_os_str().to_string_lossy().into(), kdl_layout, swap_layout_and_path, )) } pub fn stringified_from_default_assets( path: &Path, ) -> Result<(String, String, Option<(String, String)>), ConfigError> { // (path_to_layout as String, stringified_layout, Option) // TODO: ideally these should not be hard-coded // we should load layouts by name from the config // and load them from a hashmap or some such match path.to_str() { Some("default") => Ok(( "Default layout".into(), Self::stringified_default_from_assets()?, Some(( "Default swap layout".into(), Self::stringified_default_swap_from_assets()?, )), )), Some("strider") => Ok(( "Strider layout".into(), Self::stringified_strider_from_assets()?, Some(( "Strider swap layout".into(), Self::stringified_strider_swap_from_assets()?, )), )), Some("disable-status-bar") => Ok(( "Disable Status Bar layout".into(), Self::stringified_disable_status_from_assets()?, None, )), Some("compact") => Ok(( "Compact layout".into(), Self::stringified_compact_from_assets()?, Some(( "Compact layout swap".into(), Self::stringified_compact_swap_from_assets()?, )), )), None | Some(_) => Err(ConfigError::IoPath( std::io::Error::new(std::io::ErrorKind::Other, "The layout was not found"), path.into(), )), } } pub fn stringified_default_from_assets() -> Result { Ok(String::from_utf8(setup::DEFAULT_LAYOUT.to_vec())?) } pub fn stringified_default_swap_from_assets() -> Result { Ok(String::from_utf8(setup::DEFAULT_SWAP_LAYOUT.to_vec())?) } pub fn stringified_strider_from_assets() -> Result { Ok(String::from_utf8(setup::STRIDER_LAYOUT.to_vec())?) } pub fn stringified_strider_swap_from_assets() -> Result { Ok(String::from_utf8(setup::STRIDER_SWAP_LAYOUT.to_vec())?) } pub fn stringified_disable_status_from_assets() -> Result { Ok(String::from_utf8(setup::NO_STATUS_LAYOUT.to_vec())?) } pub fn stringified_compact_from_assets() -> Result { Ok(String::from_utf8(setup::COMPACT_BAR_LAYOUT.to_vec())?) } pub fn stringified_compact_swap_from_assets() -> Result { Ok(String::from_utf8(setup::COMPACT_BAR_SWAP_LAYOUT.to_vec())?) } pub fn new_tab(&self) -> (TiledPaneLayout, Vec) { self.template.clone().unwrap_or_default() } pub fn is_empty(&self) -> bool { !self.tabs.is_empty() } // TODO: do we need both of these? pub fn has_tabs(&self) -> bool { !self.tabs.is_empty() } pub fn tabs(&self) -> Vec<(Option, TiledPaneLayout, Vec)> { // String is the tab name self.tabs.clone() } pub fn focused_tab_index(&self) -> Option { self.focused_tab_index } fn swap_layout_and_path(path: &Path) -> Option<(String, String)> { // Option let mut swap_layout_path = PathBuf::from(path); swap_layout_path.set_extension("swap.kdl"); match File::open(&swap_layout_path) { Ok(mut stringified_swap_layout_file) => { let mut swap_kdl_layout = String::new(); match stringified_swap_layout_file.read_to_string(&mut swap_kdl_layout) { Ok(..) => Some(( swap_layout_path.as_os_str().to_string_lossy().into(), swap_kdl_layout, )), Err(e) => { log::warn!( "Failed to read swap layout file: {}. Error: {:?}", swap_layout_path.as_os_str().to_string_lossy(), e ); None }, } }, Err(e) => { log::warn!( "Failed to read swap layout file: {}. Error: {:?}", swap_layout_path.as_os_str().to_string_lossy(), e ); None }, } } } fn split_space( space_to_split: &PaneGeom, layout: &TiledPaneLayout, total_space_to_split: &PaneGeom, ) -> Result, &'static str> { let mut pane_positions = Vec::new(); let sizes: Vec> = if layout.children_are_stacked { let index_of_expanded_pane = layout.children.iter().position(|p| p.is_expanded_in_stack); let mut sizes: Vec> = layout .children .iter() .map(|_part| Some(SplitSize::Fixed(1))) .collect(); if let Some(index_of_expanded_pane) = index_of_expanded_pane { *sizes.get_mut(index_of_expanded_pane).unwrap() = None; } else if let Some(last_size) = sizes.last_mut() { *last_size = None; } sizes } else { layout.children.iter().map(|part| part.split_size).collect() }; let mut split_geom = Vec::new(); let ( mut current_position, split_dimension_space, inherited_dimension, total_split_dimension_space, ) = match layout.children_split_direction { SplitDirection::Vertical => ( space_to_split.x, space_to_split.cols, space_to_split.rows, total_space_to_split.cols, ), SplitDirection::Horizontal => ( space_to_split.y, space_to_split.rows, space_to_split.cols, total_space_to_split.rows, ), }; let min_size_for_panes = sizes.iter().fold(0, |acc, size| match size { Some(SplitSize::Percent(_)) | None => acc + 1, // TODO: minimum height/width as relevant here Some(SplitSize::Fixed(fixed)) => acc + fixed, }); if min_size_for_panes > split_dimension_space.as_usize() { return Err("Not enough room for panes"); // TODO: use error infra } let flex_parts = sizes.iter().filter(|s| s.is_none()).count(); let total_fixed_size = sizes.iter().fold(0, |acc, s| { if let Some(SplitSize::Fixed(fixed)) = s { acc + fixed } else { acc } }); let mut total_pane_size = 0; for (&size, _part) in sizes.iter().zip(&*layout.children) { let mut split_dimension = match size { Some(SplitSize::Percent(percent)) => Dimension::percent(percent as f64), Some(SplitSize::Fixed(size)) => Dimension::fixed(size), None => { let free_percent = if let Some(p) = split_dimension_space.as_percent() { p - sizes .iter() .map(|&s| match s { Some(SplitSize::Percent(ip)) => ip as f64, _ => 0.0, }) .sum::() } else { panic!("Implicit sizing within fixed-size panes is not supported"); }; Dimension::percent(free_percent / flex_parts as f64) }, }; split_dimension.adjust_inner( total_split_dimension_space .as_usize() .saturating_sub(total_fixed_size), ); total_pane_size += split_dimension.as_usize(); let geom = match layout.children_split_direction { SplitDirection::Vertical => PaneGeom { x: current_position, y: space_to_split.y, cols: split_dimension, rows: inherited_dimension, is_stacked: layout.children_are_stacked, }, SplitDirection::Horizontal => PaneGeom { x: space_to_split.x, y: current_position, cols: inherited_dimension, rows: split_dimension, is_stacked: layout.children_are_stacked, }, }; split_geom.push(geom); current_position += split_dimension.as_usize(); } if total_pane_size < split_dimension_space.as_usize() { // add extra space from rounding errors to the last pane let increase_by = split_dimension_space.as_usize() - total_pane_size; if let Some(last_geom) = split_geom.last_mut() { match layout.children_split_direction { SplitDirection::Vertical => last_geom.cols.increase_inner(increase_by), SplitDirection::Horizontal => last_geom.rows.increase_inner(increase_by), } } } else if total_pane_size > split_dimension_space.as_usize() { // remove extra space from rounding errors to the last pane let decrease_by = total_pane_size - split_dimension_space.as_usize(); if let Some(last_geom) = split_geom.last_mut() { match layout.children_split_direction { SplitDirection::Vertical => last_geom.cols.decrease_inner(decrease_by), SplitDirection::Horizontal => last_geom.rows.decrease_inner(decrease_by), } } } for (i, part) in layout.children.iter().enumerate() { let part_position_and_size = split_geom.get(i).unwrap(); if !part.children.is_empty() { let mut part_positions = split_space(part_position_and_size, part, total_space_to_split)?; pane_positions.append(&mut part_positions); } else { let part = part.clone(); pane_positions.push((part, *part_position_and_size)); } } if pane_positions.is_empty() { let layout = layout.clone(); pane_positions.push((layout, space_to_split.clone())); } Ok(pane_positions) } impl Default for SplitDirection { fn default() -> Self { SplitDirection::Horizontal } } impl FromStr for SplitDirection { type Err = Box; fn from_str(s: &str) -> Result { match s { "vertical" | "Vertical" => Ok(SplitDirection::Vertical), "horizontal" | "Horizontal" => Ok(SplitDirection::Horizontal), _ => Err("split direction must be either vertical or horizontal".into()), } } } impl FromStr for SplitSize { type Err = Box; fn from_str(s: &str) -> Result { if s.chars().last() == Some('%') { let char_count = s.chars().count(); let percent_size = usize::from_str_radix(&s[..char_count.saturating_sub(1)], 10)?; if percent_size > 0 && percent_size <= 100 { Ok(SplitSize::Percent(percent_size)) } else { Err("Percent must be between 0 and 100".into()) } } else { let fixed_size = usize::from_str_radix(s, 10)?; Ok(SplitSize::Fixed(fixed_size)) } } } // The unit test location. #[path = "./unit/layout_test.rs"] #[cfg(test)] mod layout_test;