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 {
SplitDir::Horizontal => Axis::Row,
SplitDir::Vertical => Axis::Col,
_ => Axis::Row,
}
}
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub enum LayoutTree {
Leaf(WindowId),
Split {
dir: SplitDir,
ratio: f32,
a: Box<LayoutTree>,
b: Box<LayoutTree>,
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 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 {
LayoutTree::Leaf(id) => out.push(*id),
LayoutTree::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 {
LayoutTree::Leaf(leaf_id) => *leaf_id == id,
LayoutTree::Split { a, b, .. } => a.contains(id) || b.contains(id),
}
}
pub fn replace_leaf<F: FnOnce(WindowId) -> LayoutTree + '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) -> LayoutTree>,
) -> bool {
match self {
LayoutTree::Leaf(leaf_id) if *leaf_id == id => {
*self = f(id);
true
}
LayoutTree::Leaf(_) => false,
LayoutTree::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 {
LayoutTree::Leaf(_) => None,
LayoutTree::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 {
LayoutTree::Leaf(_) => None,
LayoutTree::Split {
dir: my_dir,
ratio,
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 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 {
Some((ratio, saved_rect, in_a))
} else {
None
}
}
}
}
pub fn equalize_all(&mut self) {
if let LayoutTree::Split { ratio, a, b, .. } = self {
*ratio = 0.5;
a.equalize_all();
b.equalize_all();
}
}
pub fn for_each_ancestor<F>(&mut self, id: WindowId, f: &mut F)
where
F: FnMut(SplitDir, &mut f32, bool, Option<LayoutRect>),
{
if let LayoutTree::Split {
dir,
ratio,
a,
b,
last_rect,
} = self
{
let in_a = a.contains(id);
let in_b = b.contains(id);
if !in_a && !in_b {
return;
}
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 {
LayoutTree::Leaf(id) => out.push((*id, area)),
LayoutTree::Split {
dir, ratio, a, b, ..
} => {
let (rect_a, rect_b) = headless_split_rect(area, *dir, *ratio);
a.collect_rects(rect_a, out);
b.collect_rects(rect_b, out);
}
}
}
pub fn swap_with_sibling(&mut self, id: WindowId) -> bool {
match self {
LayoutTree::Leaf(_) => false,
LayoutTree::Split { a, b, .. } => {
let a_is_focused_leaf = matches!(a.as_ref(), LayoutTree::Leaf(leaf) if *leaf == id);
let b_is_focused_leaf = matches!(b.as_ref(), LayoutTree::Leaf(leaf) if *leaf == id);
if a_is_focused_leaf || b_is_focused_leaf {
std::mem::swap(a, b);
return true;
}
if a.contains(id) {
return a.swap_with_sibling(id);
}
if b.contains(id) {
return b.swap_with_sibling(id);
}
false
}
}
}
pub fn remove_leaf(&mut self, id: WindowId) -> Result<WindowId, &'static str> {
if matches!(self, LayoutTree::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 {
LayoutTree::Leaf(_) => None, LayoutTree::Split { a, b, .. } => {
if matches!(a.as_ref(), LayoutTree::Leaf(leaf) if *leaf == id) {
let new_focus = first_leaf(b);
*self = *b.clone();
return Some(new_focus);
}
if matches!(b.as_ref(), LayoutTree::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
}
}
}
}
fn headless_split_rect(area: LayoutRect, dir: SplitDir, ratio: f32) -> (LayoutRect, LayoutRect) {
match dir.axis() {
Axis::Row => {
if area.h == 0 {
return (
LayoutRect::new(area.x, area.y, area.w, 0),
LayoutRect::new(area.x, area.y, area.w, 0),
);
}
let a_h = ((area.h as f32) * ratio).round() as u16;
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);
if rect_a.h >= 2 && rect_b.h > 0 {
rect_a.h -= 1;
}
(rect_a, rect_b)
}
Axis::Col => {
if area.w == 0 {
return (
LayoutRect::new(area.x, area.y, 0, area.h),
LayoutRect::new(area.x, area.y, 0, area.h),
);
}
let a_w = ((area.w as f32) * ratio).round() as u16;
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);
if rect_a.w >= 2 && rect_b.w > 0 {
rect_a.w -= 1;
}
(rect_a, rect_b)
}
}
}
#[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 {
dir: SplitDir::Horizontal,
ratio,
a: Box::new(a),
b: Box::new(b),
last_rect: None,
}
}
fn vsplit(ratio: f32, a: LayoutTree, b: LayoutTree) -> LayoutTree {
LayoutTree::Split {
dir: SplitDir::Vertical,
ratio,
a: Box::new(a),
b: Box::new(b),
last_rect: None,
}
}
fn hsplit_with_rect(ratio: f32, a: LayoutTree, b: LayoutTree, rect: LayoutRect) -> LayoutTree {
LayoutTree::Split {
dir: SplitDir::Horizontal,
ratio,
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,
a: Box::new(a),
b: Box::new(b),
last_rect: Some(rect),
}
}
#[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 (a, b) = headless_split_rect(LayoutRect::new(0, 0, 10, 0), SplitDir::Horizontal, 0.5);
assert_eq!((a.h, b.h), (0, 0), "row split of h=0 must stay 0");
let (a, b) = headless_split_rect(LayoutRect::new(0, 0, 0, 10), SplitDir::Vertical, 0.5);
assert_eq!((a.w, b.w), (0, 0), "col split of w=0 must stay 0");
}
#[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 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");
}
}
}