aboutsummaryrefslogtreecommitdiff
path: root/src/layout.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/layout.rs')
-rw-r--r--src/layout.rs215
1 files changed, 171 insertions, 44 deletions
diff --git a/src/layout.rs b/src/layout.rs
index c46a0753..5ce72250 100644
--- a/src/layout.rs
+++ b/src/layout.rs
@@ -132,7 +132,7 @@ impl TileLayout {
/// Collect all split boundaries for mouse drag resize.
pub fn splits(&self, area: Rect) -> Vec<SplitBorder> {
let mut result = Vec::new();
- collect_splits(&self.root, area, vec![], &mut result);
+ collect_splits(&self.root, area, &mut Vec::new(), &mut result);
result
}
@@ -162,13 +162,9 @@ impl TileLayout {
direction: Direction,
ratio: f32,
) -> Option<PaneId> {
- if !self.pane_ids().contains(&target) {
- return None;
- }
+ let node = find_pane_mut(&mut self.root, target)?;
let new_id = PaneId::alloc();
- let placeholder = PaneId::from_raw(0);
- let old = std::mem::replace(&mut self.root, Node::Pane(placeholder));
- self.root = split_at(old, target, direction, new_id, valid_split_ratio(ratio));
+ *node = split_node(target, direction, new_id, valid_split_ratio(ratio));
Some(new_id)
}
@@ -187,13 +183,13 @@ impl TileLayout {
return false;
}
let ids = self.pane_ids();
- if !ids.contains(&target) || ids.contains(&moved) {
+ if ids.contains(&moved) {
return false;
}
-
- let placeholder = PaneId::from_raw(0);
- let old = std::mem::replace(&mut self.root, Node::Pane(placeholder));
- self.root = split_at(old, target, direction, moved, valid_split_ratio(ratio));
+ let Some(node) = find_pane_mut(&mut self.root, target) else {
+ return false;
+ };
+ *node = split_node(target, direction, moved, valid_split_ratio(ratio));
if focus {
self.set_focus(moved);
}
@@ -506,7 +502,7 @@ fn collect_panes(node: &Node, area: Rect, focus: PaneId, result: &mut Vec<PaneIn
}
}
-fn collect_splits(node: &Node, area: Rect, path: Vec<bool>, result: &mut Vec<SplitBorder>) {
+fn collect_splits(node: &Node, area: Rect, path: &mut Vec<bool>, result: &mut Vec<SplitBorder>) {
if let Node::Split {
direction,
ratio,
@@ -526,12 +522,12 @@ fn collect_splits(node: &Node, area: Rect, path: Vec<bool>, result: &mut Vec<Spl
area,
path: path.clone(),
});
- let mut lp = path.clone();
- lp.push(false);
- collect_splits(first, a, lp, result);
- let mut rp = path;
- rp.push(true);
- collect_splits(second, b, rp, result);
+ path.push(false);
+ collect_splits(first, a, path, result);
+ path.pop();
+ path.push(true);
+ collect_splits(second, b, path, result);
+ path.pop();
}
}
@@ -587,32 +583,22 @@ fn swap_pane_ids(node: &mut Node, first: PaneId, second: PaneId) {
}
}
-fn split_at(
- node: Node,
- target: PaneId,
- direction: Direction,
- new_id: PaneId,
- split_ratio: f32,
-) -> Node {
+fn find_pane_mut(node: &mut Node, target: PaneId) -> Option<&mut Node> {
match node {
- Node::Pane(id) if id == target => Node::Split {
- direction,
- ratio: split_ratio,
- first: Box::new(Node::Pane(id)),
- second: Box::new(Node::Pane(new_id)),
- },
- Node::Pane(_) => node,
- Node::Split {
- direction: d,
- ratio,
- first,
- second,
- } => Node::Split {
- direction: d,
- ratio,
- first: Box::new(split_at(*first, target, direction, new_id, split_ratio)),
- second: Box::new(split_at(*second, target, direction, new_id, split_ratio)),
- },
+ Node::Pane(id) if *id == target => Some(node),
+ Node::Pane(_) => None,
+ Node::Split { first, second, .. } => {
+ find_pane_mut(first, target).or_else(|| find_pane_mut(second, target))
+ }
+ }
+}
+
+fn split_node(target: PaneId, direction: Direction, new_id: PaneId, split_ratio: f32) -> Node {
+ Node::Split {
+ direction,
+ ratio: split_ratio,
+ first: Box::new(Node::Pane(target)),
+ second: Box::new(Node::Pane(new_id)),
}
}
@@ -713,6 +699,147 @@ fn split_rect(area: Rect, direction: Direction, ratio: f32) -> (Rect, Rect) {
mod tests {
use super::*;
+ #[test]
+ fn split_paths_preserve_preorder_geometry_and_resize_targets() {
+ let ids = [
+ PaneId::alloc(),
+ PaneId::alloc(),
+ PaneId::alloc(),
+ PaneId::alloc(),
+ ];
+ let mut layout = TileLayout::from_saved(
+ Node::Split {
+ direction: Direction::Horizontal,
+ ratio: 0.5,
+ first: Box::new(split_node(ids[0], Direction::Vertical, ids[1], 0.5)),
+ second: Box::new(split_node(ids[2], Direction::Vertical, ids[3], 0.25)),
+ },
+ ids[0],
+ );
+ let area = Rect::new(3, 7, 120, 80);
+ let splits = layout.splits(area);
+ assert_eq!(splits.len(), 3);
+ for (split, (path, direction, pos, rect)) in splits.iter().zip([
+ (vec![], Direction::Horizontal, 63, area),
+ (
+ vec![false],
+ Direction::Vertical,
+ 47,
+ Rect::new(3, 7, 60, 80),
+ ),
+ (
+ vec![true],
+ Direction::Vertical,
+ 27,
+ Rect::new(63, 7, 60, 80),
+ ),
+ ]) {
+ assert_eq!(
+ (split.path.clone(), split.direction, split.pos, split.area),
+ (path, direction, pos, rect)
+ );
+ }
+ assert!(layout.set_ratio_at(&splits[2].path, 0.5));
+ let resized = layout.splits(area);
+ assert_eq!(resized[1].pos, splits[1].pos);
+ assert_eq!(resized[2].pos, 47);
+ }
+
+ #[test]
+ fn rejected_splits_and_insertions_preserve_layout_and_focus_history() {
+ let (mut layout, root) = TileLayout::new();
+ let second = layout.split_focused(Direction::Horizontal);
+ let absent = PaneId::alloc();
+ let before = pane_rects(&layout);
+ let splits = split_snapshot(&layout);
+ assert!(layout
+ .split_pane(absent, Direction::Vertical, 0.5)
+ .is_none());
+ for (target, moved) in [(absent, PaneId::alloc()), (root, second), (root, root)] {
+ assert!(!layout.insert_pane_near(target, moved, Direction::Vertical, 0.3, true));
+ }
+ assert_eq!(pane_rects(&layout), before);
+ assert_eq!(split_snapshot(&layout), splits);
+ assert_eq!(layout.focused(), second);
+ assert_eq!(layout.prev_focus, Some(root));
+ assert!(layout.close_focused());
+ assert_eq!(layout.focused(), root);
+ }
+
+ #[test]
+ fn splitting_a_deep_leaf_preserves_other_branches_and_focus() {
+ let (mut layout, root) = TileLayout::new();
+ let right = layout.split_pane(root, Direction::Horizontal, 0.6).unwrap();
+ let bottom_left = layout.split_pane(root, Direction::Vertical, 0.4).unwrap();
+ let right_rect = pane_rect(&layout, right);
+ let focus = layout.focused();
+ let new = layout
+ .split_pane(bottom_left, Direction::Horizontal, f32::NAN)
+ .unwrap();
+ assert_eq!(layout.pane_ids(), [root, bottom_left, new, right]);
+ assert_eq!(pane_rect(&layout, right), right_rect);
+ assert_eq!(layout.focused(), focus);
+ assert_eq!(
+ split_snapshot(&layout),
+ [
+ (Direction::Horizontal, 0.6),
+ (Direction::Vertical, 0.4),
+ (Direction::Horizontal, 0.5)
+ ]
+ );
+ }
+
+ #[test]
+ #[ignore = "manual BSP allocation and traversal scaling profile"]
+ fn bsp_layout_profile() {
+ use std::hint::black_box;
+ use std::time::{Duration, Instant};
+ fn build(count: usize, balanced: bool) -> TileLayout {
+ let (mut layout, root) = TileLayout::new();
+ let mut leaves = std::collections::VecDeque::from([root]);
+ for _ in 1..count {
+ let target = leaves.pop_front().unwrap();
+ let new = layout
+ .split_pane(target, Direction::Horizontal, 0.5)
+ .unwrap();
+ if balanced {
+ leaves.push_back(target);
+ }
+ leaves.push_back(new);
+ }
+ layout
+ }
+ fn measure(mut operation: impl FnMut()) -> f64 {
+ for _ in 0..32 {
+ operation();
+ }
+ let mut samples = Vec::new();
+ for _ in 0..7 {
+ let start = Instant::now();
+ let mut iterations = 0;
+ while start.elapsed() < Duration::from_millis(20) {
+ operation();
+ iterations += 1;
+ }
+ samples.push(start.elapsed().as_secs_f64() * 1e6 / f64::from(iterations));
+ }
+ samples.sort_by(f64::total_cmp);
+ samples[3]
+ }
+ for count in [1, 15, 128, 512] {
+ for balanced in [true, false] {
+ let layout = build(count, balanced);
+ let splits = measure(|| {
+ black_box(layout.splits(Rect::new(0, 0, 120, 40)));
+ });
+ let build = measure(|| {
+ black_box(build(count, balanced));
+ });
+ println!("bsp panes={count} balanced={balanced} splits_us={splits:.3} build_us={build:.3}");
+ }
+ }
+ }
+
fn pane(id: u32) -> PaneId {
PaneId::from_raw(id)
}