use std::collections::BTreeMap; use std::path::PathBuf; use crate::{ input::layout::PluginUserConfiguration, input::layout::{ FloatingPaneLayout, Layout, PercentOrFixed, Run, RunPluginOrAlias, SplitDirection, SplitSize, SwapFloatingLayout, SwapTiledLayout, TiledPaneLayout, }, pane_size::{Constraint, PaneGeom}, }; const INDENT: &str = " "; const DOUBLE_INDENT: &str = " "; const TRIPLE_INDENT: &str = " "; fn indent(s: &str, prefix: &str) -> String { let mut result = String::new(); for line in s.lines() { if line.chars().any(|c| !c.is_whitespace()) { result.push_str(prefix); result.push_str(line); } result.push('\n'); } result } #[derive(Default, Debug, Clone)] pub struct GlobalLayoutManifest { pub global_cwd: Option, pub default_shell: Option, pub default_layout: Box, pub tabs: Vec<(String, TabLayoutManifest)>, } #[derive(Default, Debug, Clone)] pub struct TabLayoutManifest { pub tiled_panes: Vec, pub floating_panes: Vec, pub is_focused: bool, pub hide_floating_panes: bool, } #[derive(Default, Debug, Clone)] pub struct PaneLayoutManifest { pub geom: PaneGeom, pub run: Option, pub cwd: Option, pub is_borderless: bool, pub title: Option, pub is_focused: bool, pub pane_contents: Option, } pub fn serialize_session_layout( global_layout_manifest: GlobalLayoutManifest, ) -> Result<(String, BTreeMap), &'static str> { // BTreeMap is the pane contents and their file names let mut kdl_string = String::from("layout {\n"); let mut pane_contents = BTreeMap::new(); stringify_global_cwd(&global_layout_manifest.global_cwd, &mut kdl_string); if let Err(e) = stringify_multiple_tabs( global_layout_manifest.tabs, &mut pane_contents, &mut kdl_string, ) { return Err(e); } stringify_new_tab_template( global_layout_manifest.default_layout.template, &mut pane_contents, &mut kdl_string, ); stringify_swap_tiled_layouts( global_layout_manifest.default_layout.swap_tiled_layouts, &mut pane_contents, &mut kdl_string, ); stringify_swap_floating_layouts( global_layout_manifest.default_layout.swap_floating_layouts, &mut pane_contents, &mut kdl_string, ); kdl_string.push_str("}"); Ok((kdl_string, pane_contents)) } fn stringify_tab( tab_name: String, is_focused: bool, hide_floating_panes: bool, tiled_panes: &Vec, floating_panes: &Vec, pane_contents: &mut BTreeMap, ) -> Option { let mut kdl_string = String::new(); match get_tiled_panes_layout_from_panegeoms(tiled_panes, None) { Some(tiled_panes_layout) => { let floating_panes_layout = get_floating_panes_layout_from_panegeoms(floating_panes); let tiled_panes = if &tiled_panes_layout.children_split_direction != &SplitDirection::default() || tiled_panes_layout.children_are_stacked { vec![tiled_panes_layout] } else { tiled_panes_layout.children }; let mut tab_attributes = vec![format!("name=\"{}\"", tab_name,)]; if is_focused { tab_attributes.push(format!("focus=true")); } if hide_floating_panes { tab_attributes.push(format!("hide_floating_panes=true")); } kdl_string.push_str(&kdl_string_from_tab( &tiled_panes, &floating_panes_layout, tab_attributes, None, pane_contents, )); Some(kdl_string) }, None => { return None; }, } } /// Redundant with `geoms_to_kdl_tab` fn kdl_string_from_tab( tiled_panes: &Vec, floating_panes: &Vec, node_attributes: Vec, node_name: Option, pane_contents: &mut BTreeMap, ) -> String { let mut kdl_string = if node_attributes.is_empty() { format!("{} {{\n", node_name.unwrap_or_else(|| "tab".to_owned())) } else { format!( "{} {} {{\n", node_name.unwrap_or_else(|| "tab".to_owned()), node_attributes.join(" ") ) }; for tiled_pane_layout in tiled_panes { let ignore_size = false; let sub_kdl_string = kdl_string_from_tiled_pane(&tiled_pane_layout, ignore_size, pane_contents); kdl_string.push_str(&indent(&sub_kdl_string, INDENT)); } if !floating_panes.is_empty() { kdl_string.push_str(&indent("floating_panes {\n", INDENT)); for floating_pane_layout in floating_panes { let sub_kdl_string = kdl_string_from_floating_pane(&floating_pane_layout, pane_contents); kdl_string.push_str(&indent(&sub_kdl_string, DOUBLE_INDENT)); } kdl_string.push_str(&indent("}\n", INDENT)); } kdl_string.push_str("}\n"); kdl_string } /// Pane declaration and recursion fn kdl_string_from_tiled_pane( layout: &TiledPaneLayout, ignore_size: bool, pane_contents: &mut BTreeMap, ) -> String { let (command, args) = extract_command_and_args(&layout.run); let (plugin, plugin_config) = extract_plugin_and_config(&layout.run); let (edit, _line_number) = extract_edit_and_line_number(&layout.run); let cwd = layout.run.as_ref().and_then(|r| r.get_cwd()); let has_children = layout.external_children_index.is_some() || !layout.children.is_empty(); let mut kdl_string = stringify_pane_title_and_attributes( &command, &edit, &layout.name, cwd, layout.focus, &layout.pane_initial_contents, pane_contents, has_children, ); stringify_tiled_layout_attributes(&layout, ignore_size, &mut kdl_string); let has_child_attributes = !layout.children.is_empty() || layout.external_children_index.is_some() || !args.is_empty() || plugin.is_some() || command.is_some(); if has_child_attributes { kdl_string.push_str(" {\n"); stringify_args(args, &mut kdl_string); stringify_start_suspended(&command, &mut kdl_string); stringify_plugin(plugin, plugin_config, &mut kdl_string); if layout.children.is_empty() && layout.external_children_index.is_some() { kdl_string.push_str(&indent(&"children\n", INDENT)); } for (i, pane) in layout.children.iter().enumerate() { if Some(i) == layout.external_children_index { kdl_string.push_str(&indent(&"children\n", INDENT)); } else { let ignore_size = layout.children_are_stacked; let sub_kdl_string = kdl_string_from_tiled_pane(&pane, ignore_size, pane_contents); kdl_string.push_str(&indent(&sub_kdl_string, INDENT)); } } kdl_string.push_str("}\n"); } else { kdl_string.push_str("\n"); } kdl_string } fn extract_command_and_args(layout_run: &Option) -> (Option, Vec) { match layout_run { Some(Run::Command(run_command)) => ( Some(run_command.command.display().to_string()), run_command.args.clone(), ), _ => (None, vec![]), } } fn extract_plugin_and_config( layout_run: &Option, ) -> (Option, Option) { match &layout_run { Some(Run::Plugin(run_plugin_or_alias)) => match run_plugin_or_alias { RunPluginOrAlias::RunPlugin(run_plugin) => ( Some(run_plugin.location.display()), Some(run_plugin.configuration.clone()), ), RunPluginOrAlias::Alias(plugin_alias) => { let name = plugin_alias.name.clone(); let configuration = plugin_alias .run_plugin .as_ref() .map(|run_plugin| run_plugin.configuration.clone()); (Some(name), configuration) }, }, _ => (None, None), } } fn extract_edit_and_line_number(layout_run: &Option) -> (Option, Option) { match &layout_run { // TODO: line number in layouts? Some(Run::EditFile(path, line_number, _cwd)) => { (Some(path.display().to_string()), line_number.clone()) }, _ => (None, None), } } fn stringify_pane_title_and_attributes( command: &Option, edit: &Option, name: &Option, cwd: Option, focus: Option, initial_pane_contents: &Option, pane_contents: &mut BTreeMap, has_children: bool, ) -> String { let mut kdl_string = match (&command, &edit) { (Some(command), _) => format!("pane command=\"{}\"", command), (None, Some(edit)) => format!("pane edit=\"{}\"", edit), (None, None) => format!("pane"), }; if let Some(name) = name { kdl_string.push_str(&format!(" name=\"{}\"", name)); } if let Some(cwd) = cwd { let path = cwd.display().to_string(); if !path.is_empty() && !has_children { kdl_string.push_str(&format!(" cwd=\"{}\"", path)); } } if focus.unwrap_or(false) { kdl_string.push_str(&" focus=true"); } if let Some(initial_pane_contents) = initial_pane_contents.as_ref() { if command.is_none() && edit.is_none() { let file_name = format!("initial_contents_{}", pane_contents.keys().len() + 1); kdl_string.push_str(&format!(" contents_file=\"{}\"", file_name)); pane_contents.insert(file_name.to_string(), initial_pane_contents.clone()); } } kdl_string } fn stringify_args(args: Vec, kdl_string: &mut String) { if !args.is_empty() { let args = args .iter() .map(|a| format!("\"{}\"", a)) .collect::>() .join(" "); kdl_string.push_str(&indent(&format!("args {}\n", args), INDENT)); } } fn stringify_plugin( plugin: Option, plugin_config: Option, kdl_string: &mut String, ) { if let Some(plugin) = plugin { if let Some(plugin_config) = plugin_config.and_then(|p| if p.inner().is_empty() { None } else { Some(p) }) { kdl_string.push_str(&indent( &format!("plugin location=\"{}\" {{\n", plugin), INDENT, )); for (config_key, config_value) in plugin_config.inner() { kdl_string.push_str(&indent( &format!("{} \"{}\"\n", config_key, config_value), INDENT, )); } kdl_string.push_str(&indent("}\n", INDENT)); } else { kdl_string.push_str(&indent( &format!("plugin location=\"{}\"\n", plugin), INDENT, )); } } } fn stringify_tiled_layout_attributes( layout: &TiledPaneLayout, ignore_size: bool, kdl_string: &mut String, ) { if !ignore_size { match layout.split_size { Some(SplitSize::Fixed(size)) => kdl_string.push_str(&format!(" size={size}")), Some(SplitSize::Percent(size)) => kdl_string.push_str(&format!(" size=\"{size}%\"")), None => (), }; } if layout.borderless { kdl_string.push_str(&" borderless=true"); } if layout.children_are_stacked { kdl_string.push_str(&" stacked=true"); } if layout.is_expanded_in_stack { kdl_string.push_str(&" expanded=true"); } if layout.children_split_direction != SplitDirection::default() { let direction = match layout.children_split_direction { SplitDirection::Horizontal => "horizontal", SplitDirection::Vertical => "vertical", }; kdl_string.push_str(&format!(" split_direction=\"{direction}\"")); } } fn stringify_floating_layout_attributes(layout: &FloatingPaneLayout, kdl_string: &mut String) { match layout.height { Some(PercentOrFixed::Fixed(fixed_height)) => { kdl_string.push_str(&indent(&format!("height {}\n", fixed_height), INDENT)); }, Some(PercentOrFixed::Percent(percent)) => { kdl_string.push_str(&indent(&format!("height \"{}%\"\n", percent), INDENT)); }, None => {}, } match layout.width { Some(PercentOrFixed::Fixed(fixed_width)) => { kdl_string.push_str(&indent(&format!("width {}\n", fixed_width), INDENT)); }, Some(PercentOrFixed::Percent(percent)) => { kdl_string.push_str(&indent(&format!("width \"{}%\"\n", percent), INDENT)); }, None => {}, } match layout.x { Some(PercentOrFixed::Fixed(fixed_x)) => { kdl_string.push_str(&indent(&format!("x {}\n", fixed_x), INDENT)); }, Some(PercentOrFixed::Percent(percent)) => { kdl_string.push_str(&indent(&format!("x \"{}%\"\n", percent), INDENT)); }, None => {}, } match layout.y { Some(PercentOrFixed::Fixed(fixed_y)) => { kdl_string.push_str(&indent(&format!("y {}\n", fixed_y), INDENT)); }, Some(PercentOrFixed::Percent(percent)) => { kdl_string.push_str(&indent(&format!("y \"{}%\"\n", percent), INDENT)); }, None => {}, } } fn stringify_start_suspended(command: &Option, kdl_string: &mut String) { if command.is_some() { kdl_string.push_str(&indent(&"start_suspended true\n", INDENT)); } } fn stringify_global_cwd(global_cwd: &Option, kdl_string: &mut String) { if let Some(global_cwd) = global_cwd { kdl_string.push_str(&indent( &format!("cwd \"{}\"\n", global_cwd.display()), INDENT, )); } } fn stringify_new_tab_template( new_tab_template: Option<(TiledPaneLayout, Vec)>, pane_contents: &mut BTreeMap, kdl_string: &mut String, ) { if let Some((tiled_panes, floating_panes)) = new_tab_template { let tiled_panes = if &tiled_panes.children_split_direction != &SplitDirection::default() { vec![tiled_panes] } else { tiled_panes.children }; kdl_string.push_str(&indent( &kdl_string_from_tab( &tiled_panes, &floating_panes, vec![], Some(String::from("new_tab_template")), pane_contents, ), INDENT, )); } } fn stringify_swap_tiled_layouts( swap_tiled_layouts: Vec, pane_contents: &mut BTreeMap, kdl_string: &mut String, ) { for swap_tiled_layout in swap_tiled_layouts { let swap_tiled_layout_name = swap_tiled_layout.1; match &swap_tiled_layout_name { Some(name) => kdl_string.push_str(&indent( &format!("swap_tiled_layout name=\"{}\" {{\n", name), INDENT, )), None => kdl_string.push_str(&indent("swap_tiled_layout {\n", INDENT)), }; for (layout_constraint, tiled_panes_layout) in swap_tiled_layout.0 { let tiled_panes_layout = if &tiled_panes_layout.children_split_direction != &SplitDirection::default() { vec![tiled_panes_layout] } else { tiled_panes_layout.children }; kdl_string.push_str(&indent( &kdl_string_from_tab( &tiled_panes_layout, &vec![], vec![layout_constraint.to_string()], None, pane_contents, ), DOUBLE_INDENT, )); } kdl_string.push_str(&indent("}", INDENT)); } } fn stringify_swap_floating_layouts( swap_floating_layouts: Vec, pane_contents: &mut BTreeMap, kdl_string: &mut String, ) { for swap_floating_layout in swap_floating_layouts { let swap_floating_layout_name = swap_floating_layout.1; match &swap_floating_layout_name { Some(name) => kdl_string.push_str(&indent( &format!("swap_floating_layout name=\"{}\" {{\n", name), INDENT, )), None => kdl_string.push_str(&indent("swap_floating_layout {\n", INDENT)), }; for (layout_constraint, floating_panes_layout) in swap_floating_layout.0 { let has_floating_panes = !floating_panes_layout.is_empty(); if has_floating_panes { kdl_string.push_str(&indent( &format!("floating_panes {} {{\n", layout_constraint), DOUBLE_INDENT, )); } else { kdl_string.push_str(&indent( &format!("floating_panes {}\n", layout_constraint), DOUBLE_INDENT, )); } for floating_pane_layout in floating_panes_layout { let sub_kdl_string = kdl_string_from_floating_pane(&floating_pane_layout, pane_contents); kdl_string.push_str(&indent(&sub_kdl_string, TRIPLE_INDENT)); } if has_floating_panes { kdl_string.push_str(&indent("}\n", DOUBLE_INDENT)); } } kdl_string.push_str(&indent("}", INDENT)); } } fn stringify_multiple_tabs( tabs: Vec<(String, TabLayoutManifest)>, pane_contents: &mut BTreeMap, kdl_string: &mut String, ) -> Result<(), &'static str> { for (tab_name, tab_layout_manifest) in tabs { let tiled_panes = tab_layout_manifest.tiled_panes; let floating_panes = tab_layout_manifest.floating_panes; let hide_floating_panes = tab_layout_manifest.hide_floating_panes; let stringified = stringify_tab( tab_name.clone(), tab_layout_manifest.is_focused, hide_floating_panes, &tiled_panes, &floating_panes, pane_contents, ); match stringified { Some(stringified) => { kdl_string.push_str(&indent(&stringified, INDENT)); }, None => { return Err("Failed to stringify tab"); }, } } Ok(()) } fn kdl_string_from_floating_pane( layout: &FloatingPaneLayout, pane_contents: &mut BTreeMap, ) -> String { let (command, args) = extract_command_and_args(&layout.run); let (plugin, plugin_config) = extract_plugin_and_config(&layout.run); let (edit, _line_number) = extract_edit_and_line_number(&layout.run); let cwd = layout.run.as_ref().and_then(|r| r.get_cwd()); let has_children = false; let mut kdl_string = stringify_pane_title_and_attributes( &command, &edit, &layout.name, cwd, layout.focus, &layout.pane_initial_contents, pane_contents, has_children, ); kdl_string.push_str(" {\n"); stringify_start_suspended(&command, &mut kdl_string); stringify_floating_layout_attributes(&layout, &mut kdl_string); stringify_args(args, &mut kdl_string); stringify_plugin(plugin, plugin_config, &mut kdl_string); kdl_string.push_str("}\n"); kdl_string } fn tiled_pane_layout_from_manifest( manifest: Option<&PaneLayoutManifest>, split_size: Option, ) -> TiledPaneLayout { let (run, borderless, is_expanded_in_stack, name, focus, pane_initial_contents) = manifest .map(|g| { let mut run = g.run.clone(); if let Some(cwd) = &g.cwd { if let Some(run) = run.as_mut() { run.add_cwd(cwd); } else { run = Some(Run::Cwd(cwd.clone())); } } ( run, g.is_borderless, g.geom.is_stacked && g.geom.rows.inner > 1, g.title.clone(), Some(g.is_focused), g.pane_contents.clone(), ) }) .unwrap_or((None, false, false, None, None, None)); TiledPaneLayout { split_size, run, borderless, is_expanded_in_stack, name, focus, pane_initial_contents, ..Default::default() } } /// Tab-level parsing fn get_tiled_panes_layout_from_panegeoms( geoms: &Vec, split_size: Option, ) -> Option { let (children_split_direction, splits) = match get_splits(&geoms) { Some(x) => x, None => { return Some(tiled_pane_layout_from_manifest( geoms.iter().next(), split_size, )) }, }; let mut children = Vec::new(); let mut remaining_geoms = geoms.clone(); let mut new_geoms = Vec::new(); let mut new_constraints = Vec::new(); for i in 1..splits.len() { let (v_min, v_max) = (splits[i - 1], splits[i]); let subgeoms: Vec; (subgeoms, remaining_geoms) = match children_split_direction { SplitDirection::Horizontal => remaining_geoms .clone() .into_iter() .partition(|g| g.geom.y + g.geom.rows.as_usize() <= v_max), SplitDirection::Vertical => remaining_geoms .clone() .into_iter() .partition(|g| g.geom.x + g.geom.cols.as_usize() <= v_max), }; match get_domain_constraint(&subgeoms, &children_split_direction, (v_min, v_max)) { Some(constraint) => { new_geoms.push(subgeoms); new_constraints.push(constraint); }, None => { return None; }, } } let new_split_sizes = get_split_sizes(&new_constraints); for (subgeoms, subsplit_size) in new_geoms.iter().zip(new_split_sizes) { match get_tiled_panes_layout_from_panegeoms(&subgeoms, subsplit_size) { Some(child) => { children.push(child); }, None => { return None; }, } } let children_are_stacked = children_split_direction == SplitDirection::Horizontal && new_geoms .iter() .all(|c| c.iter().all(|c| c.geom.is_stacked)); Some(TiledPaneLayout { children_split_direction, split_size, children, children_are_stacked, ..Default::default() }) } fn get_floating_panes_layout_from_panegeoms( manifests: &Vec, ) -> Vec { manifests .iter() .map(|m| { let mut run = m.run.clone(); if let Some(cwd) = &m.cwd { run.as_mut().map(|r| r.add_cwd(cwd)); } FloatingPaneLayout { name: m.title.clone(), height: Some(m.geom.rows.into()), width: Some(m.geom.cols.into()), x: Some(PercentOrFixed::Fixed(m.geom.x)), y: Some(PercentOrFixed::Fixed(m.geom.y)), run, focus: Some(m.is_focused), already_running: false, pane_initial_contents: m.pane_contents.clone(), } }) .collect() } fn get_x_lims(geoms: &Vec) -> Option<(usize, usize)> { match ( geoms.iter().map(|g| g.geom.x).min(), geoms .iter() .map(|g| g.geom.x + g.geom.cols.as_usize()) .max(), ) { (Some(x_min), Some(x_max)) => Some((x_min, x_max)), _ => None, } } fn get_y_lims(geoms: &Vec) -> Option<(usize, usize)> { match ( geoms.iter().map(|g| g.geom.y).min(), geoms .iter() .map(|g| g.geom.y + g.geom.rows.as_usize()) .max(), ) { (Some(y_min), Some(y_max)) => Some((y_min, y_max)), _ => None, } } /// Returns the `SplitDirection` as well as the values, on the axis /// perpendicular the `SplitDirection`, for which there is a split spanning /// the max_cols or max_rows of the domain. The values are ordered /// increasingly and contains the boundaries of the domain. fn get_splits(geoms: &Vec) -> Option<(SplitDirection, Vec)> { if geoms.len() == 1 { return None; } let (x_lims, y_lims) = match (get_x_lims(&geoms), get_y_lims(&geoms)) { (Some(x_lims), Some(y_lims)) => (x_lims, y_lims), _ => return None, }; let mut direction = SplitDirection::default(); let mut splits = match direction { SplitDirection::Vertical => get_col_splits(&geoms, &x_lims, &y_lims), SplitDirection::Horizontal => get_row_splits(&geoms, &x_lims, &y_lims), }; if splits.len() <= 2 { // ie only the boundaries are present and no real split has been found direction = !direction; splits = match direction { SplitDirection::Vertical => get_col_splits(&geoms, &x_lims, &y_lims), SplitDirection::Horizontal => get_row_splits(&geoms, &x_lims, &y_lims), }; } if splits.len() <= 2 { // ie no real split has been found in both directions None } else { Some((direction, splits)) } } /// Returns a vector containing the abscisse (x) of the cols that split the /// domain including the boundaries, ie the min and max abscisse values. fn get_col_splits( // geoms: &Vec<(PaneGeom, Option>)>, geoms: &Vec, (_, x_max): &(usize, usize), (y_min, y_max): &(usize, usize), ) -> Vec { let max_rows = y_max - y_min; let mut splits = Vec::new(); let mut sorted_geoms = geoms.clone(); sorted_geoms.sort_by_key(|g| g.geom.x); for x in sorted_geoms.iter().map(|g| g.geom.x) { if splits.contains(&x) { continue; } if sorted_geoms .iter() .filter(|g| g.geom.x == x) .map(|g| g.geom.rows.as_usize()) .sum::() == max_rows { splits.push(x); }; } splits.push(*x_max); // Necessary as `g.x` is from the upper-left corner splits } /// Returns a vector containing the coordinate (y) of the rows that split the /// domain including the boundaries, ie the min and max coordinate values. fn get_row_splits( geoms: &Vec, (x_min, x_max): &(usize, usize), (_, y_max): &(usize, usize), ) -> Vec { let max_cols = x_max - x_min; let mut splits = Vec::new(); let mut sorted_geoms = geoms.clone(); sorted_geoms.sort_by_key(|g| g.geom.y); for y in sorted_geoms.iter().map(|g| g.geom.y) { if splits.contains(&y) { continue; } if sorted_geoms .iter() .filter(|g| g.geom.y == y) .map(|g| g.geom.cols.as_usize()) .sum::() == max_cols { splits.push(y); }; } splits.push(*y_max); // Necessary as `g.y` is from the upper-left corner splits } /// Get the constraint of the domain considered, base on the rows or columns, /// depending on the split direction provided. fn get_domain_constraint( // geoms: &Vec<(PaneGeom, Option>)>, geoms: &Vec, split_direction: &SplitDirection, (v_min, v_max): (usize, usize), ) -> Option { match split_direction { SplitDirection::Horizontal => get_domain_row_constraint(&geoms, (v_min, v_max)), SplitDirection::Vertical => get_domain_col_constraint(&geoms, (v_min, v_max)), } } // fn get_domain_col_constraint(geoms: &Vec<(PaneGeom, Option>)>, (x_min, x_max): (usize, usize)) -> Constraint { fn get_domain_col_constraint( geoms: &Vec, (x_min, x_max): (usize, usize), ) -> Option { let mut percent = 0.0; let mut x = x_min; while x != x_max { // we only look at one (ie the last) geom that has value `x` for `g.x` let geom = geoms.iter().filter(|g| g.geom.x == x).last(); match geom { Some(geom) => { if let Some(size) = geom.geom.cols.as_percent() { percent += size; } x += geom.geom.cols.as_usize(); }, None => { return None; }, } } if percent == 0.0 { Some(Constraint::Fixed(x_max - x_min)) } else { Some(Constraint::Percent(percent)) } } // fn get_domain_row_constraint(geoms: &Vec<(PaneGeom, Option>)>, (y_min, y_max): (usize, usize)) -> Constraint { fn get_domain_row_constraint( geoms: &Vec, (y_min, y_max): (usize, usize), ) -> Option { let mut percent = 0.0; let mut y = y_min; while y != y_max { // we only look at one (ie the last) geom that has value `y` for `g.y` let geom = geoms.iter().filter(|g| g.geom.y == y).last(); match geom { Some(geom) => { if let Some(size) = geom.geom.rows.as_percent() { percent += size; } y += geom.geom.rows.as_usize(); }, None => { return None; }, } } if percent == 0.0 { Some(Constraint::Fixed(y_max - y_min)) } else { Some(Constraint::Percent(percent)) } } /// Returns split sizes for all the children of a `TiledPaneLayout` based on /// their constraints. fn get_split_sizes(constraints: &Vec) -> Vec> { let mut split_sizes = Vec::new(); let max_percent = constraints .iter() .filter_map(|c| match c { Constraint::Percent(size) => Some(size), _ => None, }) .sum::(); for constraint in constraints { let split_size = match constraint { Constraint::Fixed(size) => Some(SplitSize::Fixed(*size)), Constraint::Percent(size) => { if size == &max_percent { None } else { Some(SplitSize::Percent((100.0 * size / max_percent) as usize)) } }, }; split_sizes.push(split_size); } split_sizes } #[cfg(test)] mod tests { use super::*; use crate::pane_size::Dimension; use expect_test::expect; use serde_json::Value; use std::collections::HashMap; const PANEGEOMS_JSON: &[&[&str]] = &[ &[ r#"{ "x": 0, "y": 1, "rows": { "constraint": "Percent(100.0)", "inner": 43 }, "cols": { "constraint": "Percent(100.0)", "inner": 211 }, "is_stacked": false }"#, r#"{ "x": 0, "y": 0, "rows": { "constraint": "Fixed(1)", "inner": 1 }, "cols": { "constraint": "Percent(100.0)", "inner": 211 }, "is_stacked": false }"#, r#"{ "x": 0, "y": 44, "rows": { "constraint": "Fixed(2)", "inner": 2 }, "cols": { "constraint": "Percent(100.0)", "inner": 211 }, "is_stacked": false }"#, ], &[ r#"{ "x": 0, "y": 0, "rows": { "constraint": "Percent(100.0)", "inner": 26 }, "cols": { "constraint": "Percent(100.0)", "inner": 211 }, "is_stacked": false }"#, r#"{ "x": 0, "y": 26, "rows": { "constraint": "Fixed(20)", "inner": 20 }, "cols": { "constraint": "Fixed(50)", "inner": 50 }, "is_stacked": false }"#, r#"{ "x": 50, "y": 26, "rows": { "constraint": "Fixed(20)", "inner": 20 }, "cols": { "constraint": "Percent(100.0)", "inner": 161 }, "is_stacked": false }"#, ], &[ r#"{ "x": 0, "y": 0, "rows": { "constraint": "Fixed(10)", "inner": 10 }, "cols": { "constraint": "Percent(50.0)", "inner": 106 }, "is_stacked": false }"#, r#"{ "x": 106, "y": 0, "rows": { "constraint": "Fixed(10)", "inner": 10 }, "cols": { "constraint": "Percent(50.0)", "inner": 105 }, "is_stacked": false }"#, r#"{ "x": 0, "y": 10, "rows": { "constraint": "Percent(100.0)", "inner": 26 }, "cols": { "constraint": "Fixed(40)", "inner": 40 }, "is_stacked": false }"#, r#"{ "x": 40, "y": 10, "rows": { "constraint": "Percent(100.0)", "inner": 26 }, "cols": { "constraint": "Percent(100.0)", "inner": 131 }, "is_stacked": false }"#, r#"{ "x": 171, "y": 10, "rows": { "constraint": "Percent(100.0)", "inner": 26 }, "cols": { "constraint": "Fixed(40)", "inner": 40 }, "is_stacked": false }"#, r#"{ "x": 0, "y": 36, "rows": { "constraint": "Fixed(10)", "inner": 10 }, "cols": { "constraint": "Percent(50.0)", "inner": 106 }, "is_stacked": false }"#, r#"{ "x": 106, "y": 36, "rows": { "constraint": "Fixed(10)", "inner": 10 }, "cols": { "constraint": "Percent(50.0)", "inner": 105 }, "is_stacked": false }"#, ], &[ r#"{ "x": 0, "y": 0, "rows": { "constraint": "Percent(30.0)", "inner": 11 }, "cols": { "constraint": "Percent(35.0)", "inner": 74 }, "is_stacked": false }"#, r#"{ "x": 0, "y": 11, "rows": { "constraint": "Percent(30.0)", "inner": 11 }, "cols": { "constraint": "Percent(35.0)", "inner": 74 }, "is_stacked": false }"#, r#"{ "x": 0, "y": 22, "rows": { "constraint": "Percent(40.0)", "inner": 14 }, "cols": { "constraint": "Percent(35.0)", "inner": 74 }, "is_stacked": false }"#, r#"{ "x": 74, "y": 0, "rows": { "constraint": "Percent(100.0)", "inner": 36 }, "cols": { "constraint": "Percent(35.0)", "inner": 74 }, "is_stacked": false }"#, r#"{ "x": 0, "y": 36, "rows": { "constraint": "Fixed(10)", "inner": 10 }, "cols": { "constraint": "Percent(70.0)", "inner": 148 }, "is_stacked": false }"#, r#"{ "x": 148, "y": 0, "rows": { "constraint": "Percent(100.0)", "inner": 46 }, "cols": { "constraint": "Percent(30.0)", "inner": 63 }, "is_stacked": false }"#, ], &[ r#"{ "x": 0, "y": 0, "rows": { "constraint": "Fixed(5)", "inner": 5 }, "cols": { "constraint": "Percent(100.0)", "inner": 211 }, "is_stacked": false }"#, r#"{ "x": 0, "y": 5, "rows": { "constraint": "Percent(100.0)", "inner": 36 }, "cols": { "constraint": "Fixed(20)", "inner": 20 }, "is_stacked": false }"#, r#"{ "x": 20, "y": 5, "rows": { "constraint": "Percent(100.0)", "inner": 36 }, "cols": { "constraint": "Percent(50.0)", "inner": 86 }, "is_stacked": false }"#, r#"{ "x": 106, "y": 5, "rows": { "constraint": "Percent(100.0)", "inner": 36 }, "cols": { "constraint": "Percent(50.0)", "inner": 85 }, "is_stacked": false }"#, r#"{ "x": 191, "y": 5, "rows": { "constraint": "Percent(100.0)", "inner": 36 }, "cols": { "constraint": "Fixed(20)", "inner": 20 }, "is_stacked": false }"#, r#"{ "x": 0, "y": 41, "rows": { "constraint": "Fixed(5)", "inner": 5 }, "cols": { "constraint": "Percent(100.0)", "inner": 211 }, "is_stacked": false }"#, ], ]; #[test] fn geoms() { let geoms = PANEGEOMS_JSON[0] .iter() .map(|pg| parse_panegeom_from_json(pg)) .map(|geom| PaneLayoutManifest { geom, ..Default::default() }) .collect(); let tab_layout_manifest = TabLayoutManifest { tiled_panes: geoms, ..Default::default() }; let global_layout_manifest = GlobalLayoutManifest { tabs: vec![("Tab #1".to_owned(), tab_layout_manifest)], ..Default::default() }; // let kdl = kdl_string_from_panegeoms(&geoms); let kdl = serialize_session_layout(global_layout_manifest).unwrap(); expect![[r#"layout { tab name="Tab #1" { pane size=1 pane pane size=2 } }"#]] .assert_eq(&kdl.0); let geoms = PANEGEOMS_JSON[1] .iter() .map(|pg| parse_panegeom_from_json(pg)) .map(|geom| PaneLayoutManifest { geom, ..Default::default() }) .collect(); let tab_layout_manifest = TabLayoutManifest { tiled_panes: geoms, ..Default::default() }; let global_layout_manifest = GlobalLayoutManifest { tabs: vec![("Tab #1".to_owned(), tab_layout_manifest)], ..Default::default() }; let kdl = serialize_session_layout(global_layout_manifest).unwrap(); expect![[r#"layout { tab name="Tab #1" { pane pane size=20 split_direction="vertical" { pane size=50 pane } } }"#]] .assert_eq(&kdl.0); let geoms = PANEGEOMS_JSON[2] .iter() .map(|pg| parse_panegeom_from_json(pg)) .map(|geom| PaneLayoutManifest { geom, ..Default::default() }) .collect(); let tab_layout_manifest = TabLayoutManifest { tiled_panes: geoms, ..Default::default() }; let global_layout_manifest = GlobalLayoutManifest { tabs: vec![("Tab #1".to_owned(), tab_layout_manifest)], ..Default::default() }; let kdl = serialize_session_layout(global_layout_manifest).unwrap(); expect![[r#"layout { tab name="Tab #1" { pane size=10 split_direction="vertical" { pane size="50%" pane size="50%" } pane split_direction="vertical" { pane size=40 pane pane size=40 } pane size=10 split_direction="vertical" { pane size="50%" pane size="50%" } } }"#]] .assert_eq(&kdl.0); let geoms = PANEGEOMS_JSON[3] .iter() .map(|pg| parse_panegeom_from_json(pg)) .map(|geom| PaneLayoutManifest { geom, ..Default::default() }) .collect(); let tab_layout_manifest = TabLayoutManifest { tiled_panes: geoms, ..Default::default() }; let global_layout_manifest = GlobalLayoutManifest { tabs: vec![("Tab #1".to_owned(), tab_layout_manifest)], ..Default::default() }; let kdl = serialize_session_layout(global_layout_manifest).unwrap(); expect![[r#"layout { tab name="Tab #1" { pane split_direction="vertical" { pane size="70%" { pane split_direction="vertical" { pane size="50%" { pane size="30%" pane size="30%" pane size="40%" } pane size="50%" } pane size=10 } pane size="30%" } } }"#]] .assert_eq(&kdl.0); let geoms = PANEGEOMS_JSON[4] .iter() .map(|pg| parse_panegeom_from_json(pg)) .map(|geom| PaneLayoutManifest { geom, ..Default::default() }) .collect(); let tab_layout_manifest = TabLayoutManifest { tiled_panes: geoms, ..Default::default() }; let global_layout_manifest = GlobalLayoutManifest { tabs: vec![("Tab #1".to_owned(), tab_layout_manifest)], ..Default::default() }; let kdl = serialize_session_layout(global_layout_manifest).unwrap(); expect![[r#"layout { tab name="Tab #1" { pane size=5 pane split_direction="vertical" { pane size=20 pane size="50%" pane size="50%" pane size=20 } pane size=5 } }"#]] .assert_eq(&kdl.0); } // utility functions fn parse_panegeom_from_json(data_str: &str) -> PaneGeom { // // Expects this input // // r#"{ "x": 0, "y": 1, "rows": { "constraint": "Percent(100.0)", "inner": 43 }, "cols": { "constraint": "Percent(100.0)", "inner": 211 }, "is_stacked": false }"#, // let data: HashMap = serde_json::from_str(data_str).unwrap(); PaneGeom { x: data["x"].to_string().parse().unwrap(), y: data["y"].to_string().parse().unwrap(), rows: get_dim(&data["rows"]), cols: get_dim(&data["cols"]), is_stacked: data["is_stacked"].to_string().parse().unwrap(), } } fn get_dim(dim_hm: &Value) -> Dimension { let constr_str = dim_hm["constraint"].to_string(); let dim = if constr_str.contains("Fixed") { let value = &constr_str[7..constr_str.len() - 2]; Dimension::fixed(value.parse().unwrap()) } else if constr_str.contains("Percent") { let value = &constr_str[9..constr_str.len() - 2]; let mut dim = Dimension::percent(value.parse().unwrap()); dim.set_inner(dim_hm["inner"].to_string().parse().unwrap()); dim } else { panic!("Constraint is nor a percent nor fixed"); }; dim } }