use std::rc::Rc; use anyhow::Context as _; use gpui::{ AnyElement, App, Axis, Context, InteractiveElement, IntoElement, MouseButton, ParentElement, Render, Styled, Window, div, }; use serde::{Deserialize, Serialize}; use super::{SplitPane, split::check_weights}; use crate::{ buttons::IconButton, primitives::IconName, theme::{ActiveTheme, ControlSize, TextSize}, }; pub type PaneId = u64; /// Split tree. The live model and its saved form are the same data. #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub enum PaneLayout { Pane(PaneId), Split { axis: Axis, sizes: Vec, children: Vec, }, } impl PaneLayout { /// Checks ids and splits; returns the next free pane id. pub fn validate(&self) -> anyhow::Result { let mut ids = Vec::new(); self.panes(&mut ids); let mut unique = ids.clone(); unique.sort_unstable(); unique.dedup(); anyhow::ensure!(unique.len() == ids.len(), "pane ids repeat: {ids:?}"); check_sizes(self)?; let last = *unique.last().expect("a layout holds a pane"); last.checked_add(1) .with_context(|| format!("no pane id follows {last}")) } fn panes(&self, out: &mut Vec) { match self { PaneLayout::Pane(id) => out.push(*id), PaneLayout::Split { children, .. } => { children.iter().for_each(|child| child.panes(out)) } } } /// Inserts `new` beside `target`; true if `target` was found. fn split(&mut self, target: PaneId, axis: Axis, new: PaneId) -> bool { match self { PaneLayout::Pane(id) if *id == target => { *self = PaneLayout::Split { axis, sizes: vec![0.5, 0.5], children: vec![PaneLayout::Pane(target), PaneLayout::Pane(new)], }; true } PaneLayout::Pane(_) => false, PaneLayout::Split { axis: own, sizes, children, } => { let direct = children .iter() .position(|child| *child == PaneLayout::Pane(target)); match direct { Some(ix) if *own == axis => { let total: f32 = sizes.iter().sum(); sizes.iter_mut().for_each(|size| *size /= total); let half = sizes[ix] / 2.0; sizes[ix] = half; sizes.insert(ix + 1, half); children.insert(ix + 1, PaneLayout::Pane(new)); true } _ => children .iter_mut() .any(|child| child.split(target, axis, new)), } } } } /// Removes `target`, folding splits left with one child. fn close(&mut self, target: PaneId) -> bool { let PaneLayout::Split { sizes, children, .. } = self else { return false; }; if let Some(ix) = children .iter() .position(|child| *child == PaneLayout::Pane(target)) { children.remove(ix); sizes.remove(ix); let rest: f32 = sizes.iter().sum(); if rest > 0.0 { sizes.iter_mut().for_each(|size| *size /= rest); } else { sizes.iter_mut().for_each(|size| *size = 1.0); } } else if !children.iter_mut().any(|child| child.close(target)) { return false; } if children.len() == 1 { *self = children.remove(0); } true } fn set_sizes(&mut self, path: &[usize], shares: &[f32]) { let PaneLayout::Split { sizes, children, .. } = self else { panic!("pane layout path points past a leaf"); }; match path.split_first() { None => *sizes = shares.to_vec(), Some((ix, rest)) => children[*ix].set_sizes(rest, shares), } } } type Content = Rc AnyElement>; /// Editor-style split panes: split right or down, close, focus. pub struct PaneGroup { root: PaneLayout, focused: PaneId, next: PaneId, content: Content, } impl PaneGroup { pub fn new(content: impl Fn(PaneId, &mut Window, &mut App) -> AnyElement + 'static) -> Self { Self { root: PaneLayout::Pane(1), focused: 1, next: 2, content: Rc::new(content), } } pub fn focused(&self) -> PaneId { self.focused } pub fn panes(&self) -> Vec { let mut out = Vec::new(); self.root.panes(&mut out); out } /// Opens a new pane beside `pane` and focuses it. pub fn split(&mut self, pane: PaneId, axis: Axis, cx: &mut Context) -> PaneId { let new = self.next; assert!(self.root.split(pane, axis, new), "no pane {pane} to split"); self.next = new.checked_add(1).expect("pane ids exhausted"); self.focused = new; log::info!("pane group: split {pane} -> {new} ({axis:?})"); cx.notify(); new } /// Panics on the last pane; its close button is disabled. pub fn close(&mut self, pane: PaneId, cx: &mut Context) { assert!(self.panes().len() > 1, "cannot close the last pane"); assert!(self.root.close(pane), "no pane {pane} to close"); if self.focused == pane { self.focused = self.panes()[0]; } log::info!("pane group: closed {pane}"); cx.notify(); } pub fn focus(&mut self, pane: PaneId, cx: &mut Context) { self.focused = pane; cx.notify(); } pub fn layout(&self) -> PaneLayout { self.root.clone() } /// Replaces the tree after `PaneLayout::validate` accepts it. pub fn restore(&mut self, layout: PaneLayout, cx: &mut Context) -> anyhow::Result<()> { self.next = layout.validate()?; let mut ids = Vec::new(); layout.panes(&mut ids); self.focused = ids[0]; self.root = layout; log::info!("pane group: restored {} panes", ids.len()); cx.notify(); Ok(()) } fn render_node( &self, node: &PaneLayout, path: Vec, window: &mut Window, cx: &mut Context, ) -> AnyElement { match node { PaneLayout::Pane(id) => self.render_pane(*id, window, cx), PaneLayout::Split { axis, sizes, children, } => { let min = cx.theme().pane_min().to_pixels(window.rem_size()); let key = format!("pane-split-{path:?}-{}", children.len()); let at = path.clone(); let mut split = SplitPane::new(gpui::SharedString::from(key), *axis, min) .sizes(sizes) .on_resize(cx.listener(move |group: &mut Self, shares: &[f32], _, _| { group.root.set_sizes(&at, shares) })); for (ix, child) in children.iter().enumerate() { let mut child_path = path.clone(); child_path.push(ix); split = split.pane(self.render_node(child, child_path, window, cx)); } split.into_any_element() } } } fn render_pane(&self, id: PaneId, window: &mut Window, cx: &mut Context) -> AnyElement { let theme = cx.theme(); let focused = id == self.focused; let lone = self.panes().len() == 1; let header = div() .flex() .items_center() .justify_between() .h(theme.control_height(ControlSize::Lg)) .pl_3() .pr_1() .border_b_1() .border_color(theme.colors.border) .text_size(theme.text_size(TextSize::Sm)) .text_color(if focused { theme.colors.fg } else { theme.colors.fg_subtle }) .child(gpui::SharedString::from(format!("Pane {id}"))) .child( div() .flex() .gap_0p5() .child( IconButton::new(("split-right", id), IconName::PanelRight) .size(ControlSize::Sm) .on_click(cx.listener(move |group, _, _, cx| { group.split(id, Axis::Horizontal, cx); })), ) .child( IconButton::new(("split-down", id), IconName::PanelBottom) .size(ControlSize::Sm) .on_click(cx.listener(move |group, _, _, cx| { group.split(id, Axis::Vertical, cx); })), ) .child( IconButton::new(("close", id), IconName::X) .size(ControlSize::Sm) .disabled(lone) .on_click(cx.listener(move |group, _, _, cx| group.close(id, cx))), ), ); div() .id(("pane", id)) .size_full() .flex() .flex_col() .bg(if focused { theme.colors.surface } else { theme.colors.bg }) .on_mouse_down( MouseButton::Left, cx.listener(move |group, _, _, cx| { if group.focused != id { group.focus(id, cx); } }), ) .child(header) .child( div() .flex_1() .min_h_0() .child((self.content)(id, window, cx)), ) .into_any_element() } } fn check_sizes(node: &PaneLayout) -> anyhow::Result<()> { if let PaneLayout::Split { sizes, children, .. } = node { anyhow::ensure!(children.len() >= 2, "a split needs two children"); anyhow::ensure!( sizes.len() == children.len(), "split sizes {sizes:?} miss children" ); check_weights(sizes)?; children.iter().try_for_each(check_sizes)?; } Ok(()) } impl Render for PaneGroup { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { let root = self.root.clone(); div() .size_full() .child(self.render_node(&root, Vec::new(), window, cx)) } } #[cfg(test)] mod tests { use gpui::Axis; use super::PaneLayout; fn ids(layout: &PaneLayout) -> Vec { let mut out = Vec::new(); layout.panes(&mut out); out } #[test] fn split_nests_or_extends() { let mut root = PaneLayout::Pane(1); assert!(root.split(1, Axis::Horizontal, 2)); assert!(root.split(2, Axis::Horizontal, 3)); let PaneLayout::Split { sizes, .. } = &root else { panic!("split") }; assert_eq!(sizes, &[0.5, 0.25, 0.25]); assert!(root.split(3, Axis::Vertical, 4)); assert_eq!(ids(&root), [1, 2, 3, 4]); assert!(!root.split(9, Axis::Vertical, 5)); let mut tiny = PaneLayout::Split { axis: Axis::Horizontal, sizes: vec![1e-45, 0.0], children: vec![PaneLayout::Pane(1), PaneLayout::Pane(2)], }; assert!(tiny.split(1, Axis::Horizontal, 3)); assert!(tiny.validate().is_ok()); } #[test] fn validate_refuses_bad_weights_and_exhausted_ids() { let split = |sizes: Vec| PaneLayout::Split { axis: Axis::Horizontal, sizes, children: vec![PaneLayout::Pane(1), PaneLayout::Pane(2)], }; assert_eq!(split(vec![1.0, 3.0]).validate().unwrap(), 3); assert!(split(vec![0.0, 0.0]).validate().is_err()); assert!(split(vec![-1.0, 2.0]).validate().is_err()); assert!(split(vec![f32::MAX, f32::MAX]).validate().is_err()); assert!(PaneLayout::Pane(u64::MAX).validate().is_err()); } #[test] fn close_keeps_weights_finite() { let three = |sizes: Vec| PaneLayout::Split { axis: Axis::Horizontal, sizes, children: vec![ PaneLayout::Pane(1), PaneLayout::Pane(2), PaneLayout::Pane(3), ], }; let sizes = |layout: &PaneLayout| match layout { PaneLayout::Split { sizes, .. } => sizes.clone(), PaneLayout::Pane(_) => panic!("split"), }; let mut zeros = three(vec![0.0, 0.0, 1.0]); assert!(zeros.close(3)); assert_eq!(sizes(&zeros), [1.0, 1.0]); let mut tiny = three(vec![0.0, 0.0, 1e-45]); assert!(tiny.close(3)); assert!(tiny.validate().is_ok()); let mut huge = three(vec![1e30, 1e30, 1e30]); assert!(huge.close(1)); assert_eq!(sizes(&huge), [0.5, 0.5]); let mut kept = three(vec![1.0, 3.0, 4.0]); assert!(kept.close(3)); assert_eq!(sizes(&kept), [0.25, 0.75]); } #[test] fn close_folds_single_children() { let mut root = PaneLayout::Pane(1); root.split(1, Axis::Horizontal, 2); root.split(2, Axis::Vertical, 3); assert!(root.close(3)); assert_eq!(ids(&root), [1, 2]); assert!(root.close(2)); assert_eq!(root, PaneLayout::Pane(1)); } }