pub type WindowId = usize;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[non_exhaustive]
pub struct LayoutRect {
pub x: u16,
pub y: u16,
pub w: u16,
pub h: u16,
}
impl LayoutRect {
pub fn new(x: u16, y: u16, w: u16, h: u16) -> Self {
Self { x, y, w, h }
}
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct Tab {
pub layout: LayoutTree,
pub focused_window: WindowId,
}
impl Tab {
pub fn new(layout: LayoutTree, focused_window: WindowId) -> Self {
Self {
layout,
focused_window,
}
}
}
impl Default for Tab {
fn default() -> Self {
Self {
layout: LayoutTree::Leaf(0),
focused_window: 0,
}
}
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct Window {
pub slot: usize,
pub last_rect: Option<LayoutRect>,
}
impl Window {
pub fn new(slot: usize) -> Self {
Self {
slot,
last_rect: None,
}
}
}
impl Default for Window {
fn default() -> Self {
Self::new(0)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum SplitDir {
Horizontal,
Vertical,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Axis {
Row,
Col,
}
impl SplitDir {
pub fn axis(self) -> Axis {
#[allow(unreachable_patterns)]
match self {
Self::Horizontal => Axis::Row,
Self::Vertical => Axis::Col,
_ => Axis::Row,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum Fixed {
First(u16),
Second(u16),
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub enum LayoutTree {
Leaf(WindowId),
Split {
dir: SplitDir,
ratio: f32,
fixed: Option<Fixed>,
a: Box<Self>,
b: Box<Self>,
last_rect: Option<LayoutRect>,
},
}
impl Default for LayoutTree {
fn default() -> Self {
Self::Leaf(0)
}
}
impl LayoutTree {
pub fn new(id: WindowId) -> Self {
Self::Leaf(id)
}
pub fn split(dir: SplitDir, ratio: f32, a: Self, b: Self) -> Self {
Self::Split {
dir,
ratio,
fixed: None,
a: Box::new(a),
b: Box::new(b),
last_rect: None,
}
}
pub fn split_fixed(dir: SplitDir, ratio: f32, fixed: Fixed, a: Self, b: Self) -> Self {
Self::Split {
dir,
ratio,
fixed: Some(fixed),
a: Box::new(a),
b: Box::new(b),
last_rect: None,
}
}
pub fn leaves(&self) -> Vec<WindowId> {
let mut out = Vec::new();
self.collect_leaves(&mut out);
out
}
fn collect_leaves(&self, out: &mut Vec<WindowId>) {
match self {
Self::Leaf(id) => out.push(*id),
Self::Split { a, b, .. } => {
a.collect_leaves(out);
b.collect_leaves(out);
}
}
}
pub fn next_leaf(&self, id: WindowId) -> Option<WindowId> {
let leaves = self.leaves();
let pos = leaves.iter().position(|&l| l == id)?;
Some(leaves[(pos + 1) % leaves.len()])
}
pub fn prev_leaf(&self, id: WindowId) -> Option<WindowId> {
let leaves = self.leaves();
let pos = leaves.iter().position(|&l| l == id)?;
let len = leaves.len();
Some(leaves[(pos + len - 1) % len])
}
pub fn contains(&self, id: WindowId) -> bool {
match self {
Self::Leaf(leaf_id) => *leaf_id == id,
Self::Split { a, b, .. } => a.contains(id) || b.contains(id),
}
}
pub fn replace_leaf<F: FnOnce(WindowId) -> Self + 'static>(
&mut self,
id: WindowId,
f: F,
) -> bool {
self.replace_leaf_boxed(id, Box::new(f))
}
fn replace_leaf_boxed(&mut self, id: WindowId, f: Box<dyn FnOnce(WindowId) -> Self>) -> bool {
match self {
Self::Leaf(leaf_id) if *leaf_id == id => {
*self = f(id);
true
}
Self::Leaf(_) => false,
Self::Split { a, b, .. } => {
if a.contains(id) {
a.replace_leaf_boxed(id, f)
} else {
b.replace_leaf_boxed(id, f)
}
}
}
}
pub fn neighbor_below(&self, id: WindowId) -> Option<WindowId> {
self.neighbor_direction(id, NavDir::Below)
}
pub fn neighbor_above(&self, id: WindowId) -> Option<WindowId> {
self.neighbor_direction(id, NavDir::Above)
}
pub fn neighbor_left(&self, id: WindowId) -> Option<WindowId> {
self.neighbor_direction(id, NavDir::Left)
}
pub fn neighbor_right(&self, id: WindowId) -> Option<WindowId> {
self.neighbor_direction(id, NavDir::Right)
}
fn neighbor_direction(&self, id: WindowId, dir: NavDir) -> Option<WindowId> {
match self {
Self::Leaf(_) => None,
Self::Split {
dir: split_dir,
a,
b,
..
} => {
let active_split = match dir {
NavDir::Below | NavDir::Above => SplitDir::Horizontal,
NavDir::Left | NavDir::Right => SplitDir::Vertical,
};
let forward = matches!(dir, NavDir::Below | NavDir::Right);
if *split_dir == active_split {
if a.contains(id) {
if forward {
let inner = a.neighbor_direction(id, dir);
if inner.is_some() {
return inner;
}
Some(first_leaf(b))
} else {
a.neighbor_direction(id, dir)
}
} else if b.contains(id) {
if forward {
b.neighbor_direction(id, dir)
} else {
let inner = b.neighbor_direction(id, dir);
if inner.is_some() {
return inner;
}
Some(last_leaf(a))
}
} else {
None
}
} else {
if a.contains(id) {
a.neighbor_direction(id, dir)
} else if b.contains(id) {
b.neighbor_direction(id, dir)
} else {
None
}
}
}
}
}
pub fn enclosing_split_mut(
&mut self,
id: WindowId,
dir: SplitDir,
) -> Option<(&mut f32, Option<LayoutRect>, bool)> {
match self {
Self::Leaf(_) => None,
Self::Split {
dir: my_dir,
ratio,
fixed,
a,
b,
last_rect,
} => {
let in_a = a.contains(id);
let in_b = b.contains(id);
if !in_a && !in_b {
return None;
}
let my_dir = *my_dir;
let saved_rect = *last_rect;
let is_fixed = fixed.is_some();
let inner = if in_a {
a.enclosing_split_mut(id, dir)
} else {
b.enclosing_split_mut(id, dir)
};
if inner.is_some() {
return inner;
}
if my_dir == dir && !is_fixed {
Some((ratio, saved_rect, in_a))
} else {
None
}
}
}
}
pub fn equalize_all(&mut self, pinned: &[WindowId]) {
if let Self::Split {
ratio, fixed, a, b, ..
} = self
{
let touches_pinned =
fixed.is_some() || is_pinned_leaf(a, pinned) || is_pinned_leaf(b, pinned);
if !touches_pinned {
*ratio = 0.5;
}
a.equalize_all(pinned);
b.equalize_all(pinned);
}
}
pub fn only(&mut self, keep: WindowId, pinned: &[WindowId]) -> Vec<WindowId> {
if !self.contains(keep) {
return Vec::new();
}
let mut removed = Vec::new();
self.prune_to(keep, pinned, &mut removed);
removed
}
fn prune_to(&mut self, keep: WindowId, pinned: &[WindowId], removed: &mut Vec<WindowId>) {
let Self::Split { a, b, .. } = self else {
return;
};
let a_keeps = a.retains_any(keep, pinned);
let b_keeps = b.retains_any(keep, pinned);
match (a_keeps, b_keeps) {
(true, true) => {
a.prune_to(keep, pinned, removed);
b.prune_to(keep, pinned, removed);
}
(true, false) => {
b.collect_leaves(removed);
let mut survivor = std::mem::replace(a.as_mut(), Self::Leaf(keep));
survivor.prune_to(keep, pinned, removed);
*self = survivor;
}
(false, true) => {
a.collect_leaves(removed);
let mut survivor = std::mem::replace(b.as_mut(), Self::Leaf(keep));
survivor.prune_to(keep, pinned, removed);
*self = survivor;
}
(false, false) => {}
}
}
fn retains_any(&self, keep: WindowId, pinned: &[WindowId]) -> bool {
match self {
Self::Leaf(id) => *id == keep || pinned.contains(id),
Self::Split { a, b, .. } => a.retains_any(keep, pinned) || b.retains_any(keep, pinned),
}
}
pub fn for_each_ancestor<F>(&mut self, id: WindowId, f: &mut F)
where
F: FnMut(SplitDir, &mut f32, bool, Option<LayoutRect>),
{
if let Self::Split {
dir,
ratio,
fixed,
a,
b,
last_rect,
} = self
{
let in_a = a.contains(id);
let in_b = b.contains(id);
if !in_a && !in_b {
return;
}
if fixed.is_none() {
f(*dir, ratio, in_a, *last_rect);
}
if in_a {
a.for_each_ancestor(id, f);
} else {
b.for_each_ancestor(id, f);
}
}
}
pub fn window_rects(&self, area: LayoutRect) -> Vec<(WindowId, LayoutRect)> {
let mut out = Vec::new();
self.collect_rects(area, &mut out);
out
}
fn collect_rects(&self, area: LayoutRect, out: &mut Vec<(WindowId, LayoutRect)>) {
match self {
Self::Leaf(id) => out.push((*id, area)),
Self::Split {
dir,
ratio,
fixed,
a,
b,
..
} => {
let geo = split_geometry(area, *dir, *ratio, *fixed);
a.collect_rects(geo.a, out);
b.collect_rects(geo.b, out);
}
}
}
pub fn swap_with_sibling(&mut self, id: WindowId, pinned: &[WindowId]) -> bool {
if pinned.contains(&id) {
return false;
}
self.swap_with_sibling_inner(id, pinned)
}
fn swap_with_sibling_inner(&mut self, id: WindowId, pinned: &[WindowId]) -> bool {
match self {
Self::Leaf(_) => false,
Self::Split { a, b, .. } => {
let a_is_focused_leaf = matches!(a.as_ref(), Self::Leaf(leaf) if *leaf == id);
let b_is_focused_leaf = matches!(b.as_ref(), Self::Leaf(leaf) if *leaf == id);
if a_is_focused_leaf || b_is_focused_leaf {
let sibling_pinned = if a_is_focused_leaf {
contains_pinned(b, pinned)
} else {
contains_pinned(a, pinned)
};
if sibling_pinned {
return false;
}
std::mem::swap(a, b);
return true;
}
if a.contains(id) {
return a.swap_with_sibling_inner(id, pinned);
}
if b.contains(id) {
return b.swap_with_sibling_inner(id, pinned);
}
false
}
}
}
pub fn remove_leaf(&mut self, id: WindowId) -> Result<WindowId, &'static str> {
if matches!(self, Self::Leaf(_)) {
return Err("E444: Cannot close last window");
}
match self.try_remove_leaf(id) {
Some(focus) => Ok(focus),
None => Err("E444: Cannot close last window"),
}
}
fn try_remove_leaf(&mut self, id: WindowId) -> Option<WindowId> {
match self {
Self::Leaf(_) => None, Self::Split { a, b, .. } => {
if matches!(a.as_ref(), Self::Leaf(leaf) if *leaf == id) {
let new_focus = first_leaf(b);
*self = *b.clone();
return Some(new_focus);
}
if matches!(b.as_ref(), Self::Leaf(leaf) if *leaf == id) {
let new_focus = last_leaf(a);
*self = *a.clone();
return Some(new_focus);
}
if a.contains(id) {
return a.try_remove_leaf(id);
}
if b.contains(id) {
return b.try_remove_leaf(id);
}
None
}
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub struct SplitGeometry {
pub a: LayoutRect,
pub b: LayoutRect,
pub separator: Option<LayoutRect>,
}
pub fn split_geometry(
area: LayoutRect,
dir: SplitDir,
ratio: f32,
fixed: Option<Fixed>,
) -> SplitGeometry {
match dir.axis() {
Axis::Row => {
if area.h == 0 {
let empty = LayoutRect::new(area.x, area.y, area.w, 0);
return SplitGeometry {
a: empty,
b: empty,
separator: None,
};
}
let a_h = first_child_cells(area.h, ratio, fixed);
let a_h = a_h.clamp(1, area.h.saturating_sub(1).max(1));
let b_h = area.h.saturating_sub(a_h);
let mut rect_a = LayoutRect::new(area.x, area.y, area.w, a_h);
let rect_b = LayoutRect::new(area.x, area.y + a_h, area.w, b_h);
let separator = if rect_a.h >= 2 && rect_b.h > 0 {
rect_a.h -= 1;
Some(LayoutRect::new(rect_a.x, rect_a.y + rect_a.h, rect_a.w, 1))
} else {
None
};
SplitGeometry {
a: rect_a,
b: rect_b,
separator,
}
}
Axis::Col => {
if area.w == 0 {
let empty = LayoutRect::new(area.x, area.y, 0, area.h);
return SplitGeometry {
a: empty,
b: empty,
separator: None,
};
}
let a_w = first_child_cells(area.w, ratio, fixed);
let a_w = a_w.clamp(1, area.w.saturating_sub(1).max(1));
let b_w = area.w.saturating_sub(a_w);
let mut rect_a = LayoutRect::new(area.x, area.y, a_w, area.h);
let rect_b = LayoutRect::new(area.x + a_w, area.y, b_w, area.h);
let separator = if rect_a.w >= 2 && rect_b.w > 0 {
rect_a.w -= 1;
Some(LayoutRect::new(rect_a.x + rect_a.w, rect_a.y, 1, rect_a.h))
} else {
None
};
SplitGeometry {
a: rect_a,
b: rect_b,
separator,
}
}
}
}
fn first_child_cells(len: u16, ratio: f32, fixed: Option<Fixed>) -> u16 {
match fixed {
Some(Fixed::First(n)) => n.saturating_add(1),
Some(Fixed::Second(n)) => len.saturating_sub(n),
_ => ((len as f32) * ratio).round() as u16,
}
}
fn is_pinned_leaf(tree: &LayoutTree, pinned: &[WindowId]) -> bool {
matches!(tree, LayoutTree::Leaf(id) if pinned.contains(id))
}
fn contains_pinned(tree: &LayoutTree, pinned: &[WindowId]) -> bool {
match tree {
LayoutTree::Leaf(id) => pinned.contains(id),
LayoutTree::Split { a, b, .. } => contains_pinned(a, pinned) || contains_pinned(b, pinned),
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum NavDir {
Below,
Above,
Left,
Right,
}
fn first_leaf(tree: &LayoutTree) -> WindowId {
match tree {
LayoutTree::Leaf(id) => *id,
LayoutTree::Split { a, .. } => first_leaf(a),
}
}
fn last_leaf(tree: &LayoutTree) -> WindowId {
match tree {
LayoutTree::Leaf(id) => *id,
LayoutTree::Split { b, .. } => last_leaf(b),
}
}
#[cfg(test)]
mod tests {
use super::*;
fn leaf(id: WindowId) -> LayoutTree {
LayoutTree::Leaf(id)
}
fn hsplit(ratio: f32, a: LayoutTree, b: LayoutTree) -> LayoutTree {
LayoutTree::split(SplitDir::Horizontal, ratio, a, b)
}
fn vsplit(ratio: f32, a: LayoutTree, b: LayoutTree) -> LayoutTree {
LayoutTree::split(SplitDir::Vertical, ratio, a, b)
}
fn hsplit_with_rect(ratio: f32, a: LayoutTree, b: LayoutTree, rect: LayoutRect) -> LayoutTree {
LayoutTree::Split {
dir: SplitDir::Horizontal,
ratio,
fixed: None,
a: Box::new(a),
b: Box::new(b),
last_rect: Some(rect),
}
}
fn vsplit_with_rect(ratio: f32, a: LayoutTree, b: LayoutTree, rect: LayoutRect) -> LayoutTree {
LayoutTree::Split {
dir: SplitDir::Vertical,
ratio,
fixed: None,
a: Box::new(a),
b: Box::new(b),
last_rect: Some(rect),
}
}
fn root_ratio(t: &LayoutTree) -> f32 {
match t {
LayoutTree::Split { ratio, .. } => *ratio,
LayoutTree::Leaf(_) => panic!("expected a split at the root"),
}
}
#[test]
fn layout_rect_new_roundtrips_fields() {
let r = LayoutRect::new(1, 2, 80, 24);
assert_eq!(r.x, 1);
assert_eq!(r.y, 2);
assert_eq!(r.w, 80);
assert_eq!(r.h, 24);
}
#[test]
fn layout_rect_default_is_zero() {
let r = LayoutRect::default();
assert_eq!(r, LayoutRect::new(0, 0, 0, 0));
}
#[test]
fn headless_split_zero_size_parent_does_not_overflow() {
let geo = split_geometry(
LayoutRect::new(0, 0, 10, 0),
SplitDir::Horizontal,
0.5,
None,
);
assert_eq!((geo.a.h, geo.b.h), (0, 0), "row split of h=0 must stay 0");
assert_eq!(
geo.separator, None,
"no separator fits in a zero-height row"
);
let geo = split_geometry(LayoutRect::new(0, 0, 0, 10), SplitDir::Vertical, 0.5, None);
assert_eq!((geo.a.w, geo.b.w), (0, 0), "col split of w=0 must stay 0");
assert_eq!(geo.separator, None, "no separator fits in a zero-width col");
}
#[test]
fn tab_new_and_default() {
let t = Tab::new(LayoutTree::Leaf(5), 5);
assert_eq!(t.focused_window, 5);
assert_eq!(t.layout.leaves(), vec![5]);
let d = Tab::default();
assert_eq!(d.focused_window, 0);
assert_eq!(d.layout.leaves(), vec![0]);
}
#[test]
fn window_new_and_default() {
let w = Window::new(3);
assert_eq!(w.slot, 3);
assert!(w.last_rect.is_none());
let d = Window::default();
assert_eq!(d.slot, 0);
}
#[test]
fn layout_tree_new_creates_leaf() {
let t = LayoutTree::new(7);
assert_eq!(t.leaves(), vec![7]);
}
#[test]
fn layout_tree_default_is_leaf_zero() {
let t = LayoutTree::default();
assert_eq!(t.leaves(), vec![0]);
}
#[test]
fn leaves_single_leaf() {
let tree = leaf(0);
assert_eq!(tree.leaves(), vec![0]);
}
#[test]
fn leaves_two_leaf_split() {
let tree = hsplit(0.5, leaf(0), leaf(1));
assert_eq!(tree.leaves(), vec![0, 1]);
}
#[test]
fn leaves_nested_horizontal_splits() {
let tree = hsplit(0.5, leaf(0), hsplit(0.5, leaf(1), leaf(2)));
assert_eq!(tree.leaves(), vec![0, 1, 2]);
}
#[test]
fn leaves_nested_left_split() {
let tree = hsplit(0.5, hsplit(0.5, leaf(0), leaf(1)), leaf(2));
assert_eq!(tree.leaves(), vec![0, 1, 2]);
}
#[test]
fn contains_returns_true_for_present_id() {
let tree = hsplit(0.5, leaf(0), leaf(1));
assert!(tree.contains(0));
assert!(tree.contains(1));
assert!(!tree.contains(2));
}
#[test]
fn replace_leaf_on_single_leaf() {
let mut tree = leaf(0);
let replaced = tree.replace_leaf(0, |_| leaf(99));
assert!(replaced);
assert_eq!(tree.leaves(), vec![99]);
}
#[test]
fn replace_leaf_in_split_left() {
let mut tree = hsplit(0.5, leaf(0), leaf(1));
let replaced = tree.replace_leaf(0, |id| hsplit(0.5, leaf(id + 10), leaf(id)));
assert!(replaced);
assert_eq!(tree.leaves(), vec![10, 0, 1]);
}
#[test]
fn replace_leaf_not_found_returns_false() {
let mut tree = hsplit(0.5, leaf(0), leaf(1));
let replaced = tree.replace_leaf(99, |_| leaf(99));
assert!(!replaced);
assert_eq!(tree.leaves(), vec![0, 1]);
}
#[test]
fn neighbor_below_two_leaf() {
let tree = hsplit(0.5, leaf(0), leaf(1));
assert_eq!(tree.neighbor_below(0), Some(1));
assert_eq!(tree.neighbor_below(1), None);
}
#[test]
fn neighbor_above_two_leaf() {
let tree = hsplit(0.5, leaf(0), leaf(1));
assert_eq!(tree.neighbor_above(0), None);
assert_eq!(tree.neighbor_above(1), Some(0));
}
#[test]
fn neighbor_below_three_leaf_nested_bottom() {
let tree = hsplit(0.5, leaf(0), hsplit(0.5, leaf(1), leaf(2)));
assert_eq!(tree.neighbor_below(0), Some(1));
assert_eq!(tree.neighbor_below(1), Some(2));
assert_eq!(tree.neighbor_below(2), None);
}
#[test]
fn neighbor_above_three_leaf_nested_bottom() {
let tree = hsplit(0.5, leaf(0), hsplit(0.5, leaf(1), leaf(2)));
assert_eq!(tree.neighbor_above(0), None);
assert_eq!(tree.neighbor_above(1), Some(0));
assert_eq!(tree.neighbor_above(2), Some(1));
}
#[test]
fn neighbor_below_three_leaf_nested_top() {
let tree = hsplit(0.5, hsplit(0.5, leaf(0), leaf(1)), leaf(2));
assert_eq!(tree.neighbor_below(0), Some(1));
assert_eq!(tree.neighbor_below(1), Some(2));
assert_eq!(tree.neighbor_below(2), None);
}
#[test]
fn neighbor_above_three_leaf_nested_top() {
let tree = hsplit(0.5, hsplit(0.5, leaf(0), leaf(1)), leaf(2));
assert_eq!(tree.neighbor_above(0), None);
assert_eq!(tree.neighbor_above(1), Some(0));
assert_eq!(tree.neighbor_above(2), Some(1));
}
#[test]
fn remove_leaf_only_leaf_errors() {
let mut tree = leaf(0);
assert!(tree.remove_leaf(0).is_err());
}
#[test]
fn remove_leaf_collapses_parent_keeps_sibling() {
let mut tree = hsplit(0.5, leaf(0), leaf(1));
let focus = tree.remove_leaf(0).unwrap();
assert_eq!(focus, 1);
assert_eq!(tree.leaves(), vec![1]);
}
#[test]
fn remove_leaf_b_side_collapses_to_a() {
let mut tree = hsplit(0.5, leaf(0), leaf(1));
let focus = tree.remove_leaf(1).unwrap();
assert_eq!(focus, 0);
assert_eq!(tree.leaves(), vec![0]);
}
#[test]
fn remove_leaf_nested_middle() {
let mut tree = hsplit(0.5, leaf(0), hsplit(0.5, leaf(1), leaf(2)));
let focus = tree.remove_leaf(1).unwrap();
assert_eq!(focus, 2);
assert_eq!(tree.leaves(), vec![0, 2]);
}
#[test]
fn neighbor_left_in_vertical_split() {
let tree = vsplit(0.5, leaf(0), leaf(1));
assert_eq!(tree.neighbor_left(0), None);
assert_eq!(tree.neighbor_left(1), Some(0));
}
#[test]
fn neighbor_right_in_vertical_split() {
let tree = vsplit(0.5, leaf(0), leaf(1));
assert_eq!(tree.neighbor_right(0), Some(1));
assert_eq!(tree.neighbor_right(1), None);
}
#[test]
fn neighbor_left_no_op_in_horizontal_split() {
let tree = hsplit(0.5, leaf(0), leaf(1));
assert_eq!(tree.neighbor_left(0), None);
assert_eq!(tree.neighbor_left(1), None);
assert_eq!(tree.neighbor_right(0), None);
assert_eq!(tree.neighbor_right(1), None);
}
#[test]
fn neighbor_left_three_leaf_vertical() {
let tree = vsplit(0.5, leaf(0), vsplit(0.5, leaf(1), leaf(2)));
assert_eq!(tree.neighbor_left(0), None);
assert_eq!(tree.neighbor_left(1), Some(0));
assert_eq!(tree.neighbor_left(2), Some(1));
}
#[test]
fn neighbor_right_three_leaf_vertical() {
let tree = vsplit(0.5, leaf(0), vsplit(0.5, leaf(1), leaf(2)));
assert_eq!(tree.neighbor_right(0), Some(1));
assert_eq!(tree.neighbor_right(1), Some(2));
assert_eq!(tree.neighbor_right(2), None);
}
#[test]
fn next_leaf_cycles_through_all_leaves() {
let tree = vsplit(0.5, leaf(0), hsplit(0.5, leaf(1), leaf(2)));
assert_eq!(tree.next_leaf(0), Some(1));
assert_eq!(tree.next_leaf(1), Some(2));
assert_eq!(tree.next_leaf(2), Some(0));
}
#[test]
fn prev_leaf_wraps_around() {
let tree = vsplit(0.5, leaf(0), hsplit(0.5, leaf(1), leaf(2)));
assert_eq!(tree.prev_leaf(0), Some(2));
assert_eq!(tree.prev_leaf(1), Some(0));
assert_eq!(tree.prev_leaf(2), Some(1));
}
#[test]
fn next_leaf_single_leaf_wraps_to_self() {
let tree = leaf(0);
assert_eq!(tree.next_leaf(0), Some(0));
}
#[test]
fn next_prev_returns_none_for_unknown_id() {
let tree = vsplit(0.5, leaf(0), leaf(1));
assert_eq!(tree.next_leaf(99), None);
assert_eq!(tree.prev_leaf(99), None);
}
#[test]
fn enclosing_split_mut_returns_innermost() {
let outer_rect = LayoutRect::new(0, 0, 80, 40);
let inner_rect = LayoutRect::new(0, 20, 80, 20);
let mut tree = hsplit_with_rect(
0.4,
leaf(0),
hsplit_with_rect(0.6, leaf(1), leaf(2), inner_rect),
outer_rect,
);
let result = tree.enclosing_split_mut(1, SplitDir::Horizontal);
assert!(result.is_some(), "should find enclosing horizontal split");
let (ratio, rect, in_a) = result.unwrap();
assert!(
(*ratio - 0.6).abs() < 1e-5,
"innermost split ratio should be 0.6, got {ratio}"
);
assert_eq!(
rect,
Some(inner_rect),
"should return inner rect, not outer"
);
assert!(in_a, "id=1 is in the 'a' side of the inner split");
}
#[test]
fn enclosing_split_mut_skips_wrong_dir() {
let mut tree = vsplit(0.5, leaf(0), leaf(1));
let result = tree.enclosing_split_mut(0, SplitDir::Horizontal);
assert!(
result.is_none(),
"should not match a Vertical split for Horizontal dir"
);
}
#[test]
fn enclosing_split_mut_returns_none_for_only_leaf() {
let mut tree = leaf(0);
let result = tree.enclosing_split_mut(0, SplitDir::Horizontal);
assert!(result.is_none(), "single leaf has no enclosing split");
}
#[test]
fn equalize_all_resets_nested_splits_to_half() {
let mut tree = hsplit(0.3, leaf(0), hsplit(0.7, leaf(1), leaf(2)));
tree.equalize_all(&[]);
fn check_all_half(t: &LayoutTree) {
if let LayoutTree::Split { ratio, a, b, .. } = t {
assert!(
(ratio - 0.5).abs() < 1e-5,
"ratio should be 0.5, got {ratio}"
);
check_all_half(a);
check_all_half(b);
}
}
check_all_half(&tree);
}
#[test]
fn for_each_ancestor_visits_outermost_first() {
let outer_rect = LayoutRect::new(0, 0, 80, 24);
let inner_rect = LayoutRect::new(24, 0, 56, 24);
let mut tree = vsplit_with_rect(
0.3,
leaf(0),
hsplit_with_rect(0.7, leaf(1), leaf(2), inner_rect),
outer_rect,
);
let mut visited_dirs: Vec<SplitDir> = Vec::new();
let mut visited_ratios: Vec<f32> = Vec::new();
tree.for_each_ancestor(1, &mut |dir, ratio, _in_a, _rect| {
visited_dirs.push(dir);
visited_ratios.push(*ratio);
});
assert_eq!(
visited_dirs,
vec![SplitDir::Vertical, SplitDir::Horizontal],
"outermost (Vertical) should be visited first"
);
assert!(
(visited_ratios[0] - 0.3).abs() < 1e-5,
"outer ratio should be 0.3"
);
assert!(
(visited_ratios[1] - 0.7).abs() < 1e-5,
"inner ratio should be 0.7"
);
}
#[test]
fn swap_with_sibling_swaps_two_leaves() {
let mut tree = hsplit(0.5, leaf(0), leaf(1));
let swapped = tree.swap_with_sibling(0, &[]);
assert!(swapped, "swap should succeed in a two-leaf split");
assert_eq!(tree.leaves(), vec![1, 0], "leaves should be swapped");
}
#[test]
fn swap_with_sibling_in_nested_split_swaps_at_focused_parent() {
let mut tree = hsplit(0.5, leaf(0), vsplit(0.5, leaf(1), leaf(2)));
let swapped = tree.swap_with_sibling(1, &[]);
assert!(swapped, "swap should succeed");
assert_eq!(
tree.leaves(),
vec![0, 2, 1],
"inner leaves should be swapped"
);
}
#[test]
fn swap_with_sibling_returns_false_for_only_leaf() {
let mut tree = leaf(0);
let swapped = tree.swap_with_sibling(0, &[]);
assert!(!swapped, "single leaf has no sibling to swap with");
}
#[test]
fn swap_with_sibling_refuses_when_the_moving_leaf_is_pinned() {
let mut tree = vsplit(0.5, leaf(9), leaf(0));
let swapped = tree.swap_with_sibling(9, &[9]);
assert!(!swapped, "a pinned leaf must not move");
assert_eq!(tree.leaves(), vec![9, 0], "tree must be untouched");
}
#[test]
fn swap_with_sibling_refuses_when_the_sibling_is_pinned() {
let mut tree = vsplit(0.5, leaf(9), leaf(0));
let swapped = tree.swap_with_sibling(0, &[9]);
assert!(!swapped, "must not swap a pinned sibling out of place");
assert_eq!(tree.leaves(), vec![9, 0], "tree must be untouched");
}
#[test]
fn swap_with_sibling_refuses_when_the_sibling_subtree_holds_a_pin() {
let mut tree = vsplit(0.5, leaf(0), vsplit(0.5, leaf(9), leaf(1)));
let swapped = tree.swap_with_sibling(0, &[9]);
assert!(!swapped, "a pin anywhere in the sibling blocks the swap");
assert_eq!(tree.leaves(), vec![0, 9, 1]);
}
#[test]
fn swap_with_sibling_still_works_beside_an_unrelated_pin() {
let mut tree = vsplit(0.5, leaf(9), vsplit(0.5, leaf(0), leaf(1)));
let swapped = tree.swap_with_sibling(0, &[9]);
assert!(swapped, "the pin is not on either side of this swap");
assert_eq!(tree.leaves(), vec![9, 1, 0]);
}
#[test]
fn equalize_all_leaves_a_pinned_leaf_at_its_size() {
let area = LayoutRect::new(0, 0, 80, 24);
let mut tree = LayoutTree::split_fixed(
SplitDir::Vertical,
0.9,
Fixed::First(30),
leaf(9),
hsplit(0.8, leaf(1), leaf(2)),
);
let dock_before = tree.window_rects(area)[0].1;
tree.equalize_all(&[9]);
let after = tree.window_rects(area);
assert_eq!(after[0].0, 9);
assert_eq!(after[0].1, dock_before, "pinned dock must keep its rect");
assert!((root_ratio(&tree) - 0.9).abs() < 1e-5);
if let LayoutTree::Split { b, .. } = &tree {
assert!(
(root_ratio(b) - 0.5).abs() < 1e-5,
"ordinary splits under a pin still equalize"
);
} else {
panic!("root should still be a split");
}
}
#[test]
fn equalize_all_protects_a_ratio_split_next_to_a_pinned_leaf() {
let mut tree = vsplit(0.2, leaf(9), leaf(0));
tree.equalize_all(&[9]);
assert!(
(root_ratio(&tree) - 0.2).abs() < 1e-5,
"a split with a pinned child keeps its ratio"
);
}
#[test]
fn only_collapses_to_the_kept_leaf_when_nothing_is_pinned() {
let mut tree = hsplit(0.5, leaf(0), vsplit(0.5, leaf(1), leaf(2)));
let mut removed = tree.only(1, &[]);
removed.sort_unstable();
assert_eq!(removed, vec![0, 2]);
assert_eq!(tree.leaves(), vec![1]);
}
#[test]
fn only_retains_pinned_leaves_and_their_arrangement() {
let mut tree = vsplit(
0.5,
leaf(9),
hsplit(0.5, hsplit(0.5, leaf(1), leaf(2)), leaf(8)),
);
let removed = tree.only(1, &[9, 8]);
assert_eq!(removed, vec![2]);
assert_eq!(
tree.leaves(),
vec![9, 1, 8],
"dock stays left of the kept window, quickfix stays below it"
);
}
#[test]
fn only_on_a_single_leaf_is_a_no_op() {
let mut tree = leaf(0);
assert!(tree.only(0, &[]).is_empty());
assert_eq!(tree.leaves(), vec![0]);
}
#[test]
fn only_with_an_absent_keep_changes_nothing() {
let mut tree = hsplit(0.5, leaf(0), leaf(1));
assert!(tree.only(99, &[]).is_empty());
assert_eq!(tree.leaves(), vec![0, 1]);
}
#[test]
fn only_keeping_a_pinned_leaf_drops_the_rest() {
let mut tree = vsplit(0.5, leaf(9), hsplit(0.5, leaf(0), leaf(1)));
let mut removed = tree.only(9, &[9]);
removed.sort_unstable();
assert_eq!(removed, vec![0, 1]);
assert_eq!(tree.leaves(), vec![9]);
}
#[test]
fn only_preserves_the_geometry_of_the_surviving_split() {
let mut tree = vsplit(0.25, leaf(9), hsplit(0.5, leaf(0), leaf(1)));
tree.only(0, &[9]);
assert!(
(root_ratio(&tree) - 0.25).abs() < 1e-5,
"the split joining the retained leaves keeps its ratio"
);
}
#[test]
fn fixed_first_renders_exact_cells_along_a_vertical_split() {
let tree =
LayoutTree::split_fixed(SplitDir::Vertical, 0.5, Fixed::First(30), leaf(0), leaf(1));
let rects = tree.window_rects(LayoutRect::new(0, 0, 80, 24));
assert_eq!(rects[0].1, LayoutRect::new(0, 0, 30, 24));
assert_eq!(rects[1].1, LayoutRect::new(31, 0, 49, 24));
}
#[test]
fn fixed_second_renders_exact_cells_along_a_vertical_split() {
let tree =
LayoutTree::split_fixed(SplitDir::Vertical, 0.5, Fixed::Second(30), leaf(0), leaf(1));
let rects = tree.window_rects(LayoutRect::new(0, 0, 80, 24));
assert_eq!(
rects[1].1.w, 30,
"Second(30) must render 30, like First(30)"
);
assert_eq!(rects[0].1, LayoutRect::new(0, 0, 49, 24));
assert_eq!(rects[1].1, LayoutRect::new(50, 0, 30, 24));
}
#[test]
fn fixed_first_and_second_render_the_same_size_on_both_axes() {
let area = LayoutRect::new(0, 0, 80, 24);
let along = |dir: SplitDir, r: LayoutRect| match dir.axis() {
Axis::Col => r.w,
Axis::Row => r.h,
};
for dir in [SplitDir::Vertical, SplitDir::Horizontal] {
for n in [1u16, 2, 10, 20] {
let first = LayoutTree::split_fixed(dir, 0.5, Fixed::First(n), leaf(0), leaf(1));
let second = LayoutTree::split_fixed(dir, 0.5, Fixed::Second(n), leaf(0), leaf(1));
assert_eq!(
along(dir, first.window_rects(area)[0].1),
n,
"First({n}) on {dir:?} must render {n}"
);
assert_eq!(
along(dir, second.window_rects(area)[1].1),
n,
"Second({n}) on {dir:?} must render {n}"
);
}
}
}
#[test]
fn fixed_second_renders_exact_cells_along_a_horizontal_split() {
let tree = LayoutTree::split_fixed(
SplitDir::Horizontal,
0.5,
Fixed::Second(10),
leaf(0),
leaf(1),
);
let rects = tree.window_rects(LayoutRect::new(0, 0, 80, 24));
assert_eq!(rects[0].1, LayoutRect::new(0, 0, 80, 13));
assert_eq!(rects[1].1, LayoutRect::new(0, 14, 80, 10));
}
#[test]
fn fixed_wins_over_ratio() {
let ratio_only = LayoutTree::split(SplitDir::Vertical, 0.5, leaf(0), leaf(1));
let fixed =
LayoutTree::split_fixed(SplitDir::Vertical, 0.5, Fixed::First(20), leaf(0), leaf(1));
let area = LayoutRect::new(0, 0, 80, 24);
assert_eq!(ratio_only.window_rects(area)[0].1.w, 39);
assert_eq!(fixed.window_rects(area)[0].1.w, 20);
}
#[test]
fn fixed_is_independent_of_the_parent_size() {
let tree =
LayoutTree::split_fixed(SplitDir::Vertical, 0.5, Fixed::First(30), leaf(0), leaf(1));
for total in [60u16, 80, 120, 200] {
let rects = tree.window_rects(LayoutRect::new(0, 0, total, 24));
assert_eq!(rects[0].1.w, 30, "dock width must not track the parent");
assert_eq!(rects[1].1.w, total - 31);
}
}
#[test]
fn fixed_renders_the_requested_size_when_no_separator_is_carved() {
let area = LayoutRect::new(0, 0, 2, 2);
for dir in [SplitDir::Vertical, SplitDir::Horizontal] {
let first = LayoutTree::split_fixed(dir, 0.5, Fixed::First(1), leaf(0), leaf(1));
let second = LayoutTree::split_fixed(dir, 0.5, Fixed::Second(1), leaf(0), leaf(1));
for tree in [first, second] {
let rects = tree.window_rects(area);
let (a, b) = (rects[0].1, rects[1].1);
let (a_len, b_len) = match dir.axis() {
Axis::Col => (a.w, b.w),
Axis::Row => (a.h, b.h),
};
assert_eq!((a_len, b_len), (1, 1), "{dir:?}: both children render 1");
}
}
let area = LayoutRect::new(0, 0, 3, 24);
let first =
LayoutTree::split_fixed(SplitDir::Vertical, 0.5, Fixed::First(1), leaf(0), leaf(1));
let rects = first.window_rects(area);
assert_eq!(rects[0].1, LayoutRect::new(0, 0, 1, 24));
assert_eq!(rects[1].1, LayoutRect::new(2, 0, 1, 24));
let second =
LayoutTree::split_fixed(SplitDir::Vertical, 0.5, Fixed::Second(1), leaf(0), leaf(1));
let rects = second.window_rects(area);
assert_eq!(rects[0].1, LayoutRect::new(0, 0, 1, 24));
assert_eq!(rects[1].1, LayoutRect::new(2, 0, 1, 24));
}
#[test]
fn oversized_fixed_clamps_to_leave_the_sibling_one_cell() {
let area = LayoutRect::new(0, 0, 80, 24);
let first =
LayoutTree::split_fixed(SplitDir::Vertical, 0.5, Fixed::First(200), leaf(0), leaf(1));
let rects = first.window_rects(area);
assert_eq!(rects[0].1.w, 78);
assert_eq!(rects[1].1.w, 1);
let second = LayoutTree::split_fixed(
SplitDir::Vertical,
0.5,
Fixed::Second(200),
leaf(0),
leaf(1),
);
let rects = second.window_rects(area);
assert_eq!(rects[0].1.w, 1);
assert_eq!(rects[1].1.w, 79);
assert_eq!(
rects[0].1.w + rects[1].1.w,
80,
"no cells may be lost or invented"
);
}
#[test]
fn fixed_on_a_degenerate_area_does_not_underflow() {
for dir in [SplitDir::Vertical, SplitDir::Horizontal] {
for fixed in [Fixed::First(u16::MAX), Fixed::Second(u16::MAX)] {
for area in [
LayoutRect::new(0, 0, 0, 0),
LayoutRect::new(0, 0, 1, 1),
LayoutRect::new(0, 0, 2, 2),
] {
let tree = LayoutTree::split_fixed(dir, 0.5, fixed, leaf(0), leaf(1));
let rects = tree.window_rects(area);
let (a, b) = (rects[0].1, rects[1].1);
assert!(a.w <= area.w && b.w <= area.w, "child wider than parent");
assert!(a.h <= area.h && b.h <= area.h, "child taller than parent");
}
}
}
}
#[test]
fn fixed_nested_under_an_ordinary_split() {
let tree = hsplit(
0.5,
LayoutTree::split_fixed(SplitDir::Vertical, 0.5, Fixed::Second(20), leaf(0), leaf(9)),
leaf(1),
);
let rects = tree.window_rects(LayoutRect::new(0, 0, 80, 24));
assert_eq!(rects.len(), 3);
let dock = rects.iter().find(|(id, _)| *id == 9).unwrap().1;
assert_eq!(dock.w, 20, "nested fixed child keeps its exact width");
}
#[test]
fn mixed_layout_navigation() {
let tree = hsplit(0.5, vsplit(0.5, leaf(0), leaf(1)), leaf(2));
assert_eq!(tree.neighbor_right(0), Some(1));
assert_eq!(tree.neighbor_left(1), Some(0));
assert_eq!(tree.neighbor_right(1), None);
assert_eq!(tree.neighbor_left(0), None);
assert_eq!(tree.neighbor_below(0), Some(2));
assert_eq!(tree.neighbor_below(1), Some(2));
assert_eq!(tree.neighbor_below(2), None);
assert_eq!(tree.neighbor_above(2), Some(1));
assert_eq!(tree.neighbor_above(0), None);
assert_eq!(tree.neighbor_above(1), None);
assert_eq!(tree.next_leaf(0), Some(1));
assert_eq!(tree.next_leaf(1), Some(2));
assert_eq!(tree.next_leaf(2), Some(0));
assert_eq!(tree.prev_leaf(0), Some(2));
assert_eq!(tree.prev_leaf(2), Some(1));
}
#[test]
fn window_rects_single_leaf_gets_full_area() {
let tree = leaf(0);
let area = LayoutRect::new(0, 0, 80, 23);
let rects = tree.window_rects(area);
assert_eq!(rects, vec![(0, area)]);
}
#[test]
fn window_rects_vsplit_two_side_by_side() {
let tree = vsplit(0.5, leaf(0), leaf(1));
let area = LayoutRect::new(0, 0, 80, 23);
let rects = tree.window_rects(area);
assert_eq!(rects.len(), 2);
let (id_a, ra) = rects[0];
let (id_b, rb) = rects[1];
assert_eq!(id_a, 0);
assert_eq!(id_b, 1);
assert_eq!(
ra.w + 1 + rb.w,
80,
"widths + separator must sum to parent width"
);
assert_eq!(ra.h, 23);
assert_eq!(rb.h, 23);
assert_eq!(rb.x, ra.x + ra.w + 1);
}
#[test]
fn window_rects_hsplit_stacked() {
let tree = hsplit(0.5, leaf(0), leaf(1));
let area = LayoutRect::new(0, 0, 80, 23);
let rects = tree.window_rects(area);
assert_eq!(rects.len(), 2);
let (_, ra) = rects[0];
let (_, rb) = rects[1];
assert_eq!(
ra.h + 1 + rb.h,
23,
"heights + separator must sum to parent height"
);
assert_eq!(ra.w, 80);
assert_eq!(rb.w, 80);
assert_eq!(rb.y, ra.y + ra.h + 1);
}
#[test]
fn window_rects_nested_vsplit_inside_vsplit() {
let tree = vsplit(0.5, leaf(0), vsplit(0.5, leaf(1), leaf(2)));
let area = LayoutRect::new(0, 0, 80, 23);
let rects = tree.window_rects(area);
assert_eq!(rects.len(), 3);
let total_w: u16 = rects.iter().map(|(_, r)| r.w).sum::<u16>() + 2;
assert_eq!(total_w, 80, "all window widths + 2 separators == 80");
}
#[test]
fn remove_leaf_error_contains_e444() {
let mut tree = leaf(0);
let err = tree.remove_leaf(0).unwrap_err();
assert!(err.contains("E444"), "error must mention E444, got: {err}");
}
#[test]
fn enclosing_split_mut_ratio_update_persists() {
let mut tree = hsplit(0.5, leaf(0), leaf(1));
{
let (ratio, _, _) = tree.enclosing_split_mut(0, SplitDir::Horizontal).unwrap();
*ratio = 0.75;
}
if let LayoutTree::Split { ratio, .. } = &tree {
assert!((*ratio - 0.75).abs() < 1e-5, "ratio should now be 0.75");
} else {
panic!("tree should still be a Split");
}
}
}
#[cfg(test)]
mod fixed_sizing_sweep {
use super::*;
#[test]
fn fixed_renders_requested_size_and_never_exceeds_parent() {
let mut asym = Vec::new();
for len in 0u16..40 {
for n in 0u16..45 {
for dir in [SplitDir::Vertical, SplitDir::Horizontal] {
let area = match dir.axis() {
Axis::Col => LayoutRect::new(0, 0, len, 10),
Axis::Row => LayoutRect::new(0, 0, 10, len),
};
let ext = |r: LayoutRect| match dir.axis() {
Axis::Col => r.w,
Axis::Row => r.h,
};
let fa = split_geometry(area, dir, 0.5, Some(Fixed::First(n))).a;
let sb = split_geometry(area, dir, 0.5, Some(Fixed::Second(n))).b;
let (first, second) = (ext(fa), ext(sb));
let meaningful = len >= 3 && n >= 1 && n < len - 1;
if meaningful && first != second {
asym.push((len, n, format!("{dir:?}"), first, second));
}
if meaningful {
assert_eq!(first, n, "First({n}) on len {len} rendered {first}");
assert_eq!(second, n, "Second({n}) on len {len} rendered {second}");
}
assert!(first <= len && second <= len, "child exceeds parent");
}
}
}
assert!(
asym.is_empty(),
"First/Second render differently in {} cases, e.g. {:?}",
asym.len(),
&asym[..asym.len().min(6)]
);
}
#[test]
fn split_geometry_children_and_separator_tile_the_parent() {
let area = LayoutRect::new(3, 5, 40, 20);
let fixings = [
None,
Some(Fixed::First(10)),
Some(Fixed::Second(10)),
Some(Fixed::First(1)),
Some(Fixed::Second(1)),
];
for dir in [SplitDir::Vertical, SplitDir::Horizontal] {
for fixed in fixings {
let geo = split_geometry(area, dir, 0.5, fixed);
let sep = geo
.separator
.unwrap_or_else(|| panic!("{dir:?}/{fixed:?} must fit a separator"));
match dir.axis() {
Axis::Col => {
assert_eq!(geo.a.x, area.x, "a starts at the parent's left edge");
assert_eq!(sep.x, geo.a.x + geo.a.w, "separator abuts a's right edge");
assert_eq!(sep.w, 1, "separator is one column");
assert_eq!(geo.b.x, sep.x + 1, "b starts right after the separator");
assert_eq!(
geo.b.x + geo.b.w,
area.x + area.w,
"b reaches the right edge"
);
assert_eq!((sep.y, sep.h), (area.y, area.h), "separator spans the rows");
}
Axis::Row => {
assert_eq!(geo.a.y, area.y, "a starts at the parent's top edge");
assert_eq!(sep.y, geo.a.y + geo.a.h, "separator abuts a's bottom edge");
assert_eq!(sep.h, 1, "separator is one row");
assert_eq!(geo.b.y, sep.y + 1, "b starts right after the separator");
assert_eq!(
geo.b.y + geo.b.h,
area.y + area.h,
"b reaches the bottom edge"
);
assert_eq!(
(sep.x, sep.w),
(area.x, area.w),
"separator spans the columns"
);
}
}
}
}
}
#[test]
fn split_geometry_reports_no_separator_when_none_is_drawn() {
let geo = split_geometry(LayoutRect::new(0, 0, 1, 10), SplitDir::Vertical, 0.5, None);
assert_eq!(geo.separator, None);
let geo = split_geometry(
LayoutRect::new(0, 0, 10, 1),
SplitDir::Horizontal,
0.5,
None,
);
assert_eq!(geo.separator, None);
}
#[test]
fn enclosing_split_mut_skips_fixed_splits() {
let inner = LayoutTree::Split {
dir: SplitDir::Vertical,
ratio: 0.5,
fixed: Some(Fixed::First(20)),
a: Box::new(LayoutTree::Leaf(0)),
b: Box::new(LayoutTree::Leaf(1)),
last_rect: Some(LayoutRect::new(20, 0, 60, 24)),
};
let mut tree = LayoutTree::Split {
dir: SplitDir::Vertical,
ratio: 0.25,
fixed: None,
a: Box::new(LayoutTree::Leaf(9)),
b: Box::new(inner),
last_rect: Some(LayoutRect::new(0, 0, 80, 24)),
};
let (ratio, rect, in_a) = tree
.enclosing_split_mut(0, SplitDir::Vertical)
.expect("the outer ratio split is still resizable");
assert!(
(*ratio - 0.25).abs() < 1e-5,
"must be the OUTER split's ratio"
);
assert_eq!(rect, Some(LayoutRect::new(0, 0, 80, 24)));
assert!(!in_a, "leaf 0 lives in the outer split's `b` branch");
}
#[test]
fn enclosing_split_mut_returns_none_for_a_lone_fixed_split() {
let mut tree = LayoutTree::Split {
dir: SplitDir::Vertical,
ratio: 0.5,
fixed: Some(Fixed::First(20)),
a: Box::new(LayoutTree::Leaf(0)),
b: Box::new(LayoutTree::Leaf(1)),
last_rect: Some(LayoutRect::new(0, 0, 80, 24)),
};
assert!(tree.enclosing_split_mut(0, SplitDir::Vertical).is_none());
assert!(tree.enclosing_split_mut(1, SplitDir::Vertical).is_none());
}
#[test]
fn for_each_ancestor_skips_fixed_splits() {
let inner = LayoutTree::Split {
dir: SplitDir::Vertical,
ratio: 0.7,
fixed: Some(Fixed::First(20)),
a: Box::new(LayoutTree::Leaf(1)),
b: Box::new(LayoutTree::Leaf(2)),
last_rect: Some(LayoutRect::new(0, 12, 80, 12)),
};
let mut tree = LayoutTree::Split {
dir: SplitDir::Horizontal,
ratio: 0.3,
fixed: None,
a: Box::new(LayoutTree::Leaf(0)),
b: Box::new(inner),
last_rect: Some(LayoutRect::new(0, 0, 80, 24)),
};
let mut seen = Vec::new();
tree.for_each_ancestor(1, &mut |dir, ratio, _in_a, _rect| {
seen.push((dir, *ratio));
*ratio = 0.9;
});
assert_eq!(seen.len(), 1, "only the non-fixed ancestor is visited");
assert_eq!(seen[0].0, SplitDir::Horizontal);
let LayoutTree::Split { b, .. } = &tree else {
panic!("expected a split at the root");
};
let LayoutTree::Split { ratio, .. } = b.as_ref() else {
panic!("expected a split at b");
};
assert!((*ratio - 0.7).abs() < 1e-5, "fixed split's ratio untouched");
}
}