Files
Ely-GPUI-Components/src/layout/panes.rs
T

426 lines
14 KiB
Rust

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<f32>,
children: Vec<PaneLayout>,
},
}
impl PaneLayout {
/// Checks ids and splits; returns the next free pane id.
pub fn validate(&self) -> anyhow::Result<PaneId> {
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<PaneId>) {
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<dyn Fn(PaneId, &mut Window, &mut App) -> 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<PaneId> {
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<Self>) -> 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<Self>) {
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>) {
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<Self>) -> 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<usize>,
window: &mut Window,
cx: &mut Context<Self>,
) -> 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<Self>) -> 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<Self>) -> 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<u64> {
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<f32>| 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<f32>| 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));
}
}