//! The 10 layout presets (DOCS/MAIN.md §8.2). A preset maps a list of surface //! ids (one per slot, in order) to a LayoutNode. `slot_count` says how many //! panels the preset needs. use spacesh_proto::ids::SurfaceId; use spacesh_proto::layout::{LayoutNode, Orient}; /// Known preset ids and their panel counts. pub fn slot_count(preset_id: &str) -> Option { Some(match preset_id { "1" => 1, "2lr" => 2, // 2↔ "2tb" => 2, // 2↕ "2+1" => 3, "1+2" => 3, "3" => 3, "2x2" => 4, "4" => 4, // single row of 4 "2x3" => 6, "2x4" => 8, _ => return None, }) } fn leaf(id: &SurfaceId) -> LayoutNode { LayoutNode::leaf(id.clone()) } fn even(n: usize) -> Vec { vec![1.0 / n as f32; n] } fn row(ids: &[SurfaceId]) -> LayoutNode { LayoutNode::Split { orient: Orient::H, ratios: even(ids.len()), children: ids.iter().map(leaf).collect() } } fn col(children: Vec) -> LayoutNode { LayoutNode::Split { orient: Orient::V, ratios: even(children.len()), children } } fn rown(children: Vec) -> LayoutNode { LayoutNode::Split { orient: Orient::H, ratios: even(children.len()), children } } /// Build the preset tree from exactly `slot_count(preset_id)` ids. /// Returns None for an unknown id or wrong id count. pub fn build(preset_id: &str, ids: &[SurfaceId]) -> Option { if slot_count(preset_id)? != ids.len() { return None; } Some(match preset_id { "1" => leaf(&ids[0]), "2lr" => row(&ids), "2tb" => col(vec![leaf(&ids[0]), leaf(&ids[1])]), // left big column over... 2 stacked on the right. "2+1" => rown(vec![leaf(&ids[0]), col(vec![leaf(&ids[1]), leaf(&ids[2])])]), // one big on the left, 2 stacked on the right (mirror naming kept simple). "1+2" => rown(vec![col(vec![leaf(&ids[0]), leaf(&ids[1])]), leaf(&ids[2])]), "3" => row(&ids), "2x2" => col(vec![row(&ids[0..2]), row(&ids[2..4])]), "4" => row(&ids), "2x3" => col(vec![row(&ids[0..3]), row(&ids[3..6])]), "2x4" => col(vec![row(&ids[0..4]), row(&ids[4..8])]), _ => return None, }) } #[cfg(test)] mod tests { use super::*; use crate::ops::leaves; fn ids(n: usize) -> Vec { (0..n).map(|i| SurfaceId(format!("s_{i}"))).collect() } #[test] fn all_presets_have_counts() { for p in ["1","2lr","2tb","2+1","1+2","3","2x2","4","2x3","2x4"] { assert!(slot_count(p).is_some(), "missing count for {p}"); } assert!(slot_count("nope").is_none()); } #[test] fn build_uses_all_ids_in_order() { for p in ["1","2lr","2tb","2+1","1+2","3","2x2","4","2x3","2x4"] { let n = slot_count(p).unwrap(); let tree = build(p, &ids(n)).unwrap(); assert_eq!(leaves(&tree), ids(n), "preset {p} must place all ids in order"); } } #[test] fn build_rejects_wrong_id_count() { assert!(build("2x2", &ids(3)).is_none()); assert!(build("bogus", &ids(1)).is_none()); } #[test] fn grid_2x2_is_two_rows() { let tree = build("2x2", &ids(4)).unwrap(); match tree { LayoutNode::Split { orient: Orient::V, children, .. } => { assert_eq!(children.len(), 2); for r in &children { matches!(r, LayoutNode::Split { orient: Orient::H, .. }); } } _ => panic!("2x2 should be a vertical split of two horizontal rows"), } } }