1pub type WindowId = usize;
34
35#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
44#[non_exhaustive]
45pub struct LayoutRect {
46 pub x: u16,
48 pub y: u16,
50 pub w: u16,
52 pub h: u16,
54}
55
56impl LayoutRect {
57 pub fn new(x: u16, y: u16, w: u16, h: u16) -> Self {
59 Self { x, y, w, h }
60 }
61}
62
63#[derive(Debug, Clone)]
72#[non_exhaustive]
73pub struct Tab {
74 pub layout: LayoutTree,
76 pub focused_window: WindowId,
78}
79
80impl Tab {
81 pub fn new(layout: LayoutTree, focused_window: WindowId) -> Self {
83 Self {
84 layout,
85 focused_window,
86 }
87 }
88}
89
90impl Default for Tab {
91 fn default() -> Self {
92 Self {
93 layout: LayoutTree::Leaf(0),
94 focused_window: 0,
95 }
96 }
97}
98
99#[derive(Debug, Clone)]
103#[non_exhaustive]
104pub struct Window {
105 pub slot: usize,
107 pub last_rect: Option<LayoutRect>,
111}
112
113impl Window {
114 pub fn new(slot: usize) -> Self {
119 Self {
120 slot,
121 last_rect: None,
122 }
123 }
124}
125
126impl Default for Window {
127 fn default() -> Self {
128 Self::new(0)
129 }
130}
131
132#[derive(Debug, Clone, Copy, PartialEq, Eq)]
136#[non_exhaustive]
137pub enum SplitDir {
138 Horizontal,
140 Vertical,
142}
143
144#[derive(Debug, Clone, Copy, PartialEq, Eq)]
150pub enum Axis {
151 Row,
153 Col,
155}
156
157impl SplitDir {
158 pub fn axis(self) -> Axis {
167 #[allow(unreachable_patterns)]
171 match self {
172 Self::Horizontal => Axis::Row,
173 Self::Vertical => Axis::Col,
174 _ => Axis::Row,
175 }
176 }
177}
178
179#[derive(Debug, Clone, Copy, PartialEq, Eq)]
196#[non_exhaustive]
197pub enum Fixed {
198 First(u16),
200 Second(u16),
202}
203
204#[derive(Debug, Clone)]
221#[non_exhaustive]
222pub enum LayoutTree {
223 Leaf(WindowId),
225 Split {
227 dir: SplitDir,
229 ratio: f32,
235 fixed: Option<Fixed>,
240 a: Box<Self>,
242 b: Box<Self>,
244 last_rect: Option<LayoutRect>,
248 },
249}
250
251impl Default for LayoutTree {
252 fn default() -> Self {
253 Self::Leaf(0)
254 }
255}
256
257impl LayoutTree {
258 pub fn new(id: WindowId) -> Self {
260 Self::Leaf(id)
261 }
262
263 pub fn split(dir: SplitDir, ratio: f32, a: Self, b: Self) -> Self {
280 Self::Split {
281 dir,
282 ratio,
283 fixed: None,
284 a: Box::new(a),
285 b: Box::new(b),
286 last_rect: None,
287 }
288 }
289
290 pub fn split_fixed(dir: SplitDir, ratio: f32, fixed: Fixed, a: Self, b: Self) -> Self {
315 Self::Split {
316 dir,
317 ratio,
318 fixed: Some(fixed),
319 a: Box::new(a),
320 b: Box::new(b),
321 last_rect: None,
322 }
323 }
324
325 pub fn leaves(&self) -> Vec<WindowId> {
342 let mut out = Vec::new();
343 self.collect_leaves(&mut out);
344 out
345 }
346
347 fn collect_leaves(&self, out: &mut Vec<WindowId>) {
348 match self {
349 Self::Leaf(id) => out.push(*id),
350 Self::Split { a, b, .. } => {
351 a.collect_leaves(out);
352 b.collect_leaves(out);
353 }
354 }
355 }
356
357 pub fn next_leaf(&self, id: WindowId) -> Option<WindowId> {
362 let leaves = self.leaves();
363 let pos = leaves.iter().position(|&l| l == id)?;
364 Some(leaves[(pos + 1) % leaves.len()])
365 }
366
367 pub fn prev_leaf(&self, id: WindowId) -> Option<WindowId> {
372 let leaves = self.leaves();
373 let pos = leaves.iter().position(|&l| l == id)?;
374 let len = leaves.len();
375 Some(leaves[(pos + len - 1) % len])
376 }
377
378 pub fn contains(&self, id: WindowId) -> bool {
380 match self {
381 Self::Leaf(leaf_id) => *leaf_id == id,
382 Self::Split { a, b, .. } => a.contains(id) || b.contains(id),
383 }
384 }
385
386 pub fn replace_leaf<F: FnOnce(WindowId) -> Self + 'static>(
389 &mut self,
390 id: WindowId,
391 f: F,
392 ) -> bool {
393 self.replace_leaf_boxed(id, Box::new(f))
394 }
395
396 fn replace_leaf_boxed(&mut self, id: WindowId, f: Box<dyn FnOnce(WindowId) -> Self>) -> bool {
397 match self {
398 Self::Leaf(leaf_id) if *leaf_id == id => {
399 *self = f(id);
400 true
401 }
402 Self::Leaf(_) => false,
403 Self::Split { a, b, .. } => {
404 if a.contains(id) {
408 a.replace_leaf_boxed(id, f)
409 } else {
410 b.replace_leaf_boxed(id, f)
411 }
412 }
413 }
414 }
415
416 pub fn neighbor_below(&self, id: WindowId) -> Option<WindowId> {
424 self.neighbor_direction(id, NavDir::Below)
425 }
426
427 pub fn neighbor_above(&self, id: WindowId) -> Option<WindowId> {
429 self.neighbor_direction(id, NavDir::Above)
430 }
431
432 pub fn neighbor_left(&self, id: WindowId) -> Option<WindowId> {
435 self.neighbor_direction(id, NavDir::Left)
436 }
437
438 pub fn neighbor_right(&self, id: WindowId) -> Option<WindowId> {
441 self.neighbor_direction(id, NavDir::Right)
442 }
443
444 fn neighbor_direction(&self, id: WindowId, dir: NavDir) -> Option<WindowId> {
455 match self {
456 Self::Leaf(_) => None,
457 Self::Split {
458 dir: split_dir,
459 a,
460 b,
461 ..
462 } => {
463 let active_split = match dir {
465 NavDir::Below | NavDir::Above => SplitDir::Horizontal,
466 NavDir::Left | NavDir::Right => SplitDir::Vertical,
467 };
468 let forward = matches!(dir, NavDir::Below | NavDir::Right);
470
471 if *split_dir == active_split {
472 if a.contains(id) {
473 if forward {
474 let inner = a.neighbor_direction(id, dir);
476 if inner.is_some() {
477 return inner;
478 }
479 Some(first_leaf(b))
481 } else {
482 a.neighbor_direction(id, dir)
484 }
485 } else if b.contains(id) {
486 if forward {
487 b.neighbor_direction(id, dir)
489 } else {
490 let inner = b.neighbor_direction(id, dir);
492 if inner.is_some() {
493 return inner;
494 }
495 Some(last_leaf(a))
497 }
498 } else {
499 None
500 }
501 } else {
502 if a.contains(id) {
504 a.neighbor_direction(id, dir)
505 } else if b.contains(id) {
506 b.neighbor_direction(id, dir)
507 } else {
508 None
509 }
510 }
511 }
512 }
513 }
514
515 pub fn enclosing_split_mut(
528 &mut self,
529 id: WindowId,
530 dir: SplitDir,
531 ) -> Option<(&mut f32, Option<LayoutRect>, bool)> {
532 match self {
533 Self::Leaf(_) => None,
534 Self::Split {
535 dir: my_dir,
536 ratio,
537 fixed,
538 a,
539 b,
540 last_rect,
541 } => {
542 let in_a = a.contains(id);
543 let in_b = b.contains(id);
544 if !in_a && !in_b {
545 return None;
546 }
547
548 let my_dir = *my_dir;
549 let saved_rect = *last_rect;
550 let is_fixed = fixed.is_some();
551
552 let inner = if in_a {
554 a.enclosing_split_mut(id, dir)
555 } else {
556 b.enclosing_split_mut(id, dir)
557 };
558 if inner.is_some() {
559 return inner;
560 }
561
562 if my_dir == dir && !is_fixed {
565 Some((ratio, saved_rect, in_a))
566 } else {
567 None
568 }
569 }
570 }
571 }
572
573 pub fn equalize_all(&mut self, pinned: &[WindowId]) {
602 if let Self::Split {
603 ratio, fixed, a, b, ..
604 } = self
605 {
606 let touches_pinned =
607 fixed.is_some() || is_pinned_leaf(a, pinned) || is_pinned_leaf(b, pinned);
608 if !touches_pinned {
609 *ratio = 0.5;
610 }
611 a.equalize_all(pinned);
612 b.equalize_all(pinned);
613 }
614 }
615
616 pub fn only(&mut self, keep: WindowId, pinned: &[WindowId]) -> Vec<WindowId> {
644 if !self.contains(keep) {
645 return Vec::new();
646 }
647 let mut removed = Vec::new();
648 self.prune_to(keep, pinned, &mut removed);
649 removed
650 }
651
652 fn prune_to(&mut self, keep: WindowId, pinned: &[WindowId], removed: &mut Vec<WindowId>) {
656 let Self::Split { a, b, .. } = self else {
657 return;
658 };
659 let a_keeps = a.retains_any(keep, pinned);
660 let b_keeps = b.retains_any(keep, pinned);
661 match (a_keeps, b_keeps) {
662 (true, true) => {
663 a.prune_to(keep, pinned, removed);
664 b.prune_to(keep, pinned, removed);
665 }
666 (true, false) => {
667 b.collect_leaves(removed);
668 let mut survivor = std::mem::replace(a.as_mut(), Self::Leaf(keep));
669 survivor.prune_to(keep, pinned, removed);
670 *self = survivor;
671 }
672 (false, true) => {
673 a.collect_leaves(removed);
674 let mut survivor = std::mem::replace(b.as_mut(), Self::Leaf(keep));
675 survivor.prune_to(keep, pinned, removed);
676 *self = survivor;
677 }
678 (false, false) => {}
681 }
682 }
683
684 fn retains_any(&self, keep: WindowId, pinned: &[WindowId]) -> bool {
686 match self {
687 Self::Leaf(id) => *id == keep || pinned.contains(id),
688 Self::Split { a, b, .. } => a.retains_any(keep, pinned) || b.retains_any(keep, pinned),
689 }
690 }
691
692 pub fn for_each_ancestor<F>(&mut self, id: WindowId, f: &mut F)
700 where
701 F: FnMut(SplitDir, &mut f32, bool, Option<LayoutRect>),
702 {
703 if let Self::Split {
704 dir,
705 ratio,
706 fixed,
707 a,
708 b,
709 last_rect,
710 } = self
711 {
712 let in_a = a.contains(id);
713 let in_b = b.contains(id);
714 if !in_a && !in_b {
715 return;
716 }
717 if fixed.is_none() {
719 f(*dir, ratio, in_a, *last_rect);
720 }
721 if in_a {
722 a.for_each_ancestor(id, f);
723 } else {
724 b.for_each_ancestor(id, f);
725 }
726 }
727 }
728
729 pub fn window_rects(&self, area: LayoutRect) -> Vec<(WindowId, LayoutRect)> {
777 let mut out = Vec::new();
778 self.collect_rects(area, &mut out);
779 out
780 }
781
782 fn collect_rects(&self, area: LayoutRect, out: &mut Vec<(WindowId, LayoutRect)>) {
783 match self {
784 Self::Leaf(id) => out.push((*id, area)),
785 Self::Split {
786 dir,
787 ratio,
788 fixed,
789 a,
790 b,
791 ..
792 } => {
793 let geo = split_geometry(area, *dir, *ratio, *fixed);
794 a.collect_rects(geo.a, out);
795 b.collect_rects(geo.b, out);
796 }
797 }
798 }
799
800 pub fn swap_with_sibling(&mut self, id: WindowId, pinned: &[WindowId]) -> bool {
813 if pinned.contains(&id) {
814 return false;
815 }
816 self.swap_with_sibling_inner(id, pinned)
817 }
818
819 fn swap_with_sibling_inner(&mut self, id: WindowId, pinned: &[WindowId]) -> bool {
820 match self {
821 Self::Leaf(_) => false,
822 Self::Split { a, b, .. } => {
823 let a_is_focused_leaf = matches!(a.as_ref(), Self::Leaf(leaf) if *leaf == id);
824 let b_is_focused_leaf = matches!(b.as_ref(), Self::Leaf(leaf) if *leaf == id);
825 if a_is_focused_leaf || b_is_focused_leaf {
826 let sibling_pinned = if a_is_focused_leaf {
828 contains_pinned(b, pinned)
829 } else {
830 contains_pinned(a, pinned)
831 };
832 if sibling_pinned {
833 return false;
834 }
835 std::mem::swap(a, b);
836 return true;
837 }
838 if a.contains(id) {
840 return a.swap_with_sibling_inner(id, pinned);
841 }
842 if b.contains(id) {
843 return b.swap_with_sibling_inner(id, pinned);
844 }
845 false
846 }
847 }
848 }
849
850 pub fn remove_leaf(&mut self, id: WindowId) -> Result<WindowId, &'static str> {
863 if matches!(self, Self::Leaf(_)) {
864 return Err("E444: Cannot close last window");
865 }
866 match self.try_remove_leaf(id) {
867 Some(focus) => Ok(focus),
868 None => Err("E444: Cannot close last window"),
869 }
870 }
871
872 fn try_remove_leaf(&mut self, id: WindowId) -> Option<WindowId> {
876 match self {
877 Self::Leaf(_) => None, Self::Split { a, b, .. } => {
879 if matches!(a.as_ref(), Self::Leaf(leaf) if *leaf == id) {
881 let new_focus = first_leaf(b);
882 *self = *b.clone();
884 return Some(new_focus);
885 }
886 if matches!(b.as_ref(), Self::Leaf(leaf) if *leaf == id) {
888 let new_focus = last_leaf(a);
889 *self = *a.clone();
891 return Some(new_focus);
892 }
893 if a.contains(id) {
895 return a.try_remove_leaf(id);
896 }
897 if b.contains(id) {
899 return b.try_remove_leaf(id);
900 }
901 None
902 }
903 }
904 }
905}
906
907#[derive(Debug, Clone, Copy, PartialEq, Eq)]
916#[non_exhaustive]
917pub struct SplitGeometry {
918 pub a: LayoutRect,
920 pub b: LayoutRect,
922 pub separator: Option<LayoutRect>,
926}
927
928pub fn split_geometry(
969 area: LayoutRect,
970 dir: SplitDir,
971 ratio: f32,
972 fixed: Option<Fixed>,
973) -> SplitGeometry {
974 match dir.axis() {
975 Axis::Row => {
976 if area.h == 0 {
979 let empty = LayoutRect::new(area.x, area.y, area.w, 0);
980 return SplitGeometry {
981 a: empty,
982 b: empty,
983 separator: None,
984 };
985 }
986 let a_h = first_child_cells(area.h, ratio, fixed);
988 let a_h = a_h.clamp(1, area.h.saturating_sub(1).max(1));
989 let b_h = area.h.saturating_sub(a_h);
990 let mut rect_a = LayoutRect::new(area.x, area.y, area.w, a_h);
991 let rect_b = LayoutRect::new(area.x, area.y + a_h, area.w, b_h);
992 let separator = if rect_a.h >= 2 && rect_b.h > 0 {
994 rect_a.h -= 1;
995 Some(LayoutRect::new(rect_a.x, rect_a.y + rect_a.h, rect_a.w, 1))
996 } else {
997 None
998 };
999 SplitGeometry {
1000 a: rect_a,
1001 b: rect_b,
1002 separator,
1003 }
1004 }
1005 Axis::Col => {
1006 if area.w == 0 {
1008 let empty = LayoutRect::new(area.x, area.y, 0, area.h);
1009 return SplitGeometry {
1010 a: empty,
1011 b: empty,
1012 separator: None,
1013 };
1014 }
1015 let a_w = first_child_cells(area.w, ratio, fixed);
1017 let a_w = a_w.clamp(1, area.w.saturating_sub(1).max(1));
1018 let b_w = area.w.saturating_sub(a_w);
1019 let mut rect_a = LayoutRect::new(area.x, area.y, a_w, area.h);
1020 let rect_b = LayoutRect::new(area.x + a_w, area.y, b_w, area.h);
1021 let separator = if rect_a.w >= 2 && rect_b.w > 0 {
1023 rect_a.w -= 1;
1024 Some(LayoutRect::new(rect_a.x + rect_a.w, rect_a.y, 1, rect_a.h))
1025 } else {
1026 None
1027 };
1028 SplitGeometry {
1029 a: rect_a,
1030 b: rect_b,
1031 separator,
1032 }
1033 }
1034 }
1035}
1036
1037fn first_child_cells(len: u16, ratio: f32, fixed: Option<Fixed>) -> u16 {
1062 match fixed {
1063 Some(Fixed::First(n)) => n.saturating_add(1),
1064 Some(Fixed::Second(n)) => len.saturating_sub(n),
1065 _ => ((len as f32) * ratio).round() as u16,
1066 }
1067}
1068
1069fn is_pinned_leaf(tree: &LayoutTree, pinned: &[WindowId]) -> bool {
1071 matches!(tree, LayoutTree::Leaf(id) if pinned.contains(id))
1072}
1073
1074fn contains_pinned(tree: &LayoutTree, pinned: &[WindowId]) -> bool {
1076 match tree {
1077 LayoutTree::Leaf(id) => pinned.contains(id),
1078 LayoutTree::Split { a, b, .. } => contains_pinned(a, pinned) || contains_pinned(b, pinned),
1079 }
1080}
1081
1082#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1084enum NavDir {
1085 Below,
1086 Above,
1087 Left,
1088 Right,
1089}
1090
1091fn first_leaf(tree: &LayoutTree) -> WindowId {
1093 match tree {
1094 LayoutTree::Leaf(id) => *id,
1095 LayoutTree::Split { a, .. } => first_leaf(a),
1096 }
1097}
1098
1099fn last_leaf(tree: &LayoutTree) -> WindowId {
1101 match tree {
1102 LayoutTree::Leaf(id) => *id,
1103 LayoutTree::Split { b, .. } => last_leaf(b),
1104 }
1105}
1106
1107#[cfg(test)]
1110mod tests {
1111 use super::*;
1112
1113 fn leaf(id: WindowId) -> LayoutTree {
1114 LayoutTree::Leaf(id)
1115 }
1116
1117 fn hsplit(ratio: f32, a: LayoutTree, b: LayoutTree) -> LayoutTree {
1118 LayoutTree::split(SplitDir::Horizontal, ratio, a, b)
1119 }
1120
1121 fn vsplit(ratio: f32, a: LayoutTree, b: LayoutTree) -> LayoutTree {
1122 LayoutTree::split(SplitDir::Vertical, ratio, a, b)
1123 }
1124
1125 fn hsplit_with_rect(ratio: f32, a: LayoutTree, b: LayoutTree, rect: LayoutRect) -> LayoutTree {
1126 LayoutTree::Split {
1127 dir: SplitDir::Horizontal,
1128 ratio,
1129 fixed: None,
1130 a: Box::new(a),
1131 b: Box::new(b),
1132 last_rect: Some(rect),
1133 }
1134 }
1135
1136 fn vsplit_with_rect(ratio: f32, a: LayoutTree, b: LayoutTree, rect: LayoutRect) -> LayoutTree {
1137 LayoutTree::Split {
1138 dir: SplitDir::Vertical,
1139 ratio,
1140 fixed: None,
1141 a: Box::new(a),
1142 b: Box::new(b),
1143 last_rect: Some(rect),
1144 }
1145 }
1146
1147 fn root_ratio(t: &LayoutTree) -> f32 {
1149 match t {
1150 LayoutTree::Split { ratio, .. } => *ratio,
1151 LayoutTree::Leaf(_) => panic!("expected a split at the root"),
1152 }
1153 }
1154
1155 #[test]
1158 fn layout_rect_new_roundtrips_fields() {
1159 let r = LayoutRect::new(1, 2, 80, 24);
1160 assert_eq!(r.x, 1);
1161 assert_eq!(r.y, 2);
1162 assert_eq!(r.w, 80);
1163 assert_eq!(r.h, 24);
1164 }
1165
1166 #[test]
1167 fn layout_rect_default_is_zero() {
1168 let r = LayoutRect::default();
1169 assert_eq!(r, LayoutRect::new(0, 0, 0, 0));
1170 }
1171
1172 #[test]
1173 fn headless_split_zero_size_parent_does_not_overflow() {
1174 let geo = split_geometry(
1177 LayoutRect::new(0, 0, 10, 0),
1178 SplitDir::Horizontal,
1179 0.5,
1180 None,
1181 );
1182 assert_eq!((geo.a.h, geo.b.h), (0, 0), "row split of h=0 must stay 0");
1183 assert_eq!(
1184 geo.separator, None,
1185 "no separator fits in a zero-height row"
1186 );
1187 let geo = split_geometry(LayoutRect::new(0, 0, 0, 10), SplitDir::Vertical, 0.5, None);
1189 assert_eq!((geo.a.w, geo.b.w), (0, 0), "col split of w=0 must stay 0");
1190 assert_eq!(geo.separator, None, "no separator fits in a zero-width col");
1191 }
1192
1193 #[test]
1196 fn tab_new_and_default() {
1197 let t = Tab::new(LayoutTree::Leaf(5), 5);
1198 assert_eq!(t.focused_window, 5);
1199 assert_eq!(t.layout.leaves(), vec![5]);
1200
1201 let d = Tab::default();
1202 assert_eq!(d.focused_window, 0);
1203 assert_eq!(d.layout.leaves(), vec![0]);
1204 }
1205
1206 #[test]
1209 fn window_new_and_default() {
1210 let w = Window::new(3);
1211 assert_eq!(w.slot, 3);
1212 assert!(w.last_rect.is_none());
1213
1214 let d = Window::default();
1215 assert_eq!(d.slot, 0);
1216 }
1217
1218 #[test]
1221 fn layout_tree_new_creates_leaf() {
1222 let t = LayoutTree::new(7);
1223 assert_eq!(t.leaves(), vec![7]);
1224 }
1225
1226 #[test]
1227 fn layout_tree_default_is_leaf_zero() {
1228 let t = LayoutTree::default();
1229 assert_eq!(t.leaves(), vec![0]);
1230 }
1231
1232 #[test]
1235 fn leaves_single_leaf() {
1236 let tree = leaf(0);
1237 assert_eq!(tree.leaves(), vec![0]);
1238 }
1239
1240 #[test]
1241 fn leaves_two_leaf_split() {
1242 let tree = hsplit(0.5, leaf(0), leaf(1));
1243 assert_eq!(tree.leaves(), vec![0, 1]);
1244 }
1245
1246 #[test]
1247 fn leaves_nested_horizontal_splits() {
1248 let tree = hsplit(0.5, leaf(0), hsplit(0.5, leaf(1), leaf(2)));
1250 assert_eq!(tree.leaves(), vec![0, 1, 2]);
1251 }
1252
1253 #[test]
1254 fn leaves_nested_left_split() {
1255 let tree = hsplit(0.5, hsplit(0.5, leaf(0), leaf(1)), leaf(2));
1256 assert_eq!(tree.leaves(), vec![0, 1, 2]);
1257 }
1258
1259 #[test]
1262 fn contains_returns_true_for_present_id() {
1263 let tree = hsplit(0.5, leaf(0), leaf(1));
1264 assert!(tree.contains(0));
1265 assert!(tree.contains(1));
1266 assert!(!tree.contains(2));
1267 }
1268
1269 #[test]
1272 fn replace_leaf_on_single_leaf() {
1273 let mut tree = leaf(0);
1274 let replaced = tree.replace_leaf(0, |_| leaf(99));
1275 assert!(replaced);
1276 assert_eq!(tree.leaves(), vec![99]);
1277 }
1278
1279 #[test]
1280 fn replace_leaf_in_split_left() {
1281 let mut tree = hsplit(0.5, leaf(0), leaf(1));
1282 let replaced = tree.replace_leaf(0, |id| hsplit(0.5, leaf(id + 10), leaf(id)));
1283 assert!(replaced);
1284 assert_eq!(tree.leaves(), vec![10, 0, 1]);
1285 }
1286
1287 #[test]
1288 fn replace_leaf_not_found_returns_false() {
1289 let mut tree = hsplit(0.5, leaf(0), leaf(1));
1290 let replaced = tree.replace_leaf(99, |_| leaf(99));
1291 assert!(!replaced);
1292 assert_eq!(tree.leaves(), vec![0, 1]);
1293 }
1294
1295 #[test]
1298 fn neighbor_below_two_leaf() {
1299 let tree = hsplit(0.5, leaf(0), leaf(1));
1300 assert_eq!(tree.neighbor_below(0), Some(1));
1301 assert_eq!(tree.neighbor_below(1), None);
1302 }
1303
1304 #[test]
1305 fn neighbor_above_two_leaf() {
1306 let tree = hsplit(0.5, leaf(0), leaf(1));
1307 assert_eq!(tree.neighbor_above(0), None);
1308 assert_eq!(tree.neighbor_above(1), Some(0));
1309 }
1310
1311 #[test]
1312 fn neighbor_below_three_leaf_nested_bottom() {
1313 let tree = hsplit(0.5, leaf(0), hsplit(0.5, leaf(1), leaf(2)));
1315 assert_eq!(tree.neighbor_below(0), Some(1));
1316 assert_eq!(tree.neighbor_below(1), Some(2));
1317 assert_eq!(tree.neighbor_below(2), None);
1318 }
1319
1320 #[test]
1321 fn neighbor_above_three_leaf_nested_bottom() {
1322 let tree = hsplit(0.5, leaf(0), hsplit(0.5, leaf(1), leaf(2)));
1323 assert_eq!(tree.neighbor_above(0), None);
1324 assert_eq!(tree.neighbor_above(1), Some(0));
1325 assert_eq!(tree.neighbor_above(2), Some(1));
1326 }
1327
1328 #[test]
1329 fn neighbor_below_three_leaf_nested_top() {
1330 let tree = hsplit(0.5, hsplit(0.5, leaf(0), leaf(1)), leaf(2));
1331 assert_eq!(tree.neighbor_below(0), Some(1));
1332 assert_eq!(tree.neighbor_below(1), Some(2));
1333 assert_eq!(tree.neighbor_below(2), None);
1334 }
1335
1336 #[test]
1337 fn neighbor_above_three_leaf_nested_top() {
1338 let tree = hsplit(0.5, hsplit(0.5, leaf(0), leaf(1)), leaf(2));
1339 assert_eq!(tree.neighbor_above(0), None);
1340 assert_eq!(tree.neighbor_above(1), Some(0));
1341 assert_eq!(tree.neighbor_above(2), Some(1));
1342 }
1343
1344 #[test]
1347 fn remove_leaf_only_leaf_errors() {
1348 let mut tree = leaf(0);
1349 assert!(tree.remove_leaf(0).is_err());
1350 }
1351
1352 #[test]
1353 fn remove_leaf_collapses_parent_keeps_sibling() {
1354 let mut tree = hsplit(0.5, leaf(0), leaf(1));
1355 let focus = tree.remove_leaf(0).unwrap();
1356 assert_eq!(focus, 1);
1357 assert_eq!(tree.leaves(), vec![1]);
1358 }
1359
1360 #[test]
1361 fn remove_leaf_b_side_collapses_to_a() {
1362 let mut tree = hsplit(0.5, leaf(0), leaf(1));
1363 let focus = tree.remove_leaf(1).unwrap();
1364 assert_eq!(focus, 0);
1365 assert_eq!(tree.leaves(), vec![0]);
1366 }
1367
1368 #[test]
1369 fn remove_leaf_nested_middle() {
1370 let mut tree = hsplit(0.5, leaf(0), hsplit(0.5, leaf(1), leaf(2)));
1372 let focus = tree.remove_leaf(1).unwrap();
1373 assert_eq!(focus, 2);
1374 assert_eq!(tree.leaves(), vec![0, 2]);
1375 }
1376
1377 #[test]
1380 fn neighbor_left_in_vertical_split() {
1381 let tree = vsplit(0.5, leaf(0), leaf(1));
1382 assert_eq!(tree.neighbor_left(0), None);
1383 assert_eq!(tree.neighbor_left(1), Some(0));
1384 }
1385
1386 #[test]
1387 fn neighbor_right_in_vertical_split() {
1388 let tree = vsplit(0.5, leaf(0), leaf(1));
1389 assert_eq!(tree.neighbor_right(0), Some(1));
1390 assert_eq!(tree.neighbor_right(1), None);
1391 }
1392
1393 #[test]
1394 fn neighbor_left_no_op_in_horizontal_split() {
1395 let tree = hsplit(0.5, leaf(0), leaf(1));
1396 assert_eq!(tree.neighbor_left(0), None);
1397 assert_eq!(tree.neighbor_left(1), None);
1398 assert_eq!(tree.neighbor_right(0), None);
1399 assert_eq!(tree.neighbor_right(1), None);
1400 }
1401
1402 #[test]
1403 fn neighbor_left_three_leaf_vertical() {
1404 let tree = vsplit(0.5, leaf(0), vsplit(0.5, leaf(1), leaf(2)));
1405 assert_eq!(tree.neighbor_left(0), None);
1406 assert_eq!(tree.neighbor_left(1), Some(0));
1407 assert_eq!(tree.neighbor_left(2), Some(1));
1408 }
1409
1410 #[test]
1411 fn neighbor_right_three_leaf_vertical() {
1412 let tree = vsplit(0.5, leaf(0), vsplit(0.5, leaf(1), leaf(2)));
1413 assert_eq!(tree.neighbor_right(0), Some(1));
1414 assert_eq!(tree.neighbor_right(1), Some(2));
1415 assert_eq!(tree.neighbor_right(2), None);
1416 }
1417
1418 #[test]
1421 fn next_leaf_cycles_through_all_leaves() {
1422 let tree = vsplit(0.5, leaf(0), hsplit(0.5, leaf(1), leaf(2)));
1423 assert_eq!(tree.next_leaf(0), Some(1));
1424 assert_eq!(tree.next_leaf(1), Some(2));
1425 assert_eq!(tree.next_leaf(2), Some(0));
1426 }
1427
1428 #[test]
1429 fn prev_leaf_wraps_around() {
1430 let tree = vsplit(0.5, leaf(0), hsplit(0.5, leaf(1), leaf(2)));
1431 assert_eq!(tree.prev_leaf(0), Some(2));
1432 assert_eq!(tree.prev_leaf(1), Some(0));
1433 assert_eq!(tree.prev_leaf(2), Some(1));
1434 }
1435
1436 #[test]
1437 fn next_leaf_single_leaf_wraps_to_self() {
1438 let tree = leaf(0);
1439 assert_eq!(tree.next_leaf(0), Some(0));
1440 }
1441
1442 #[test]
1443 fn next_prev_returns_none_for_unknown_id() {
1444 let tree = vsplit(0.5, leaf(0), leaf(1));
1445 assert_eq!(tree.next_leaf(99), None);
1446 assert_eq!(tree.prev_leaf(99), None);
1447 }
1448
1449 #[test]
1452 fn enclosing_split_mut_returns_innermost() {
1453 let outer_rect = LayoutRect::new(0, 0, 80, 40);
1455 let inner_rect = LayoutRect::new(0, 20, 80, 20);
1456 let mut tree = hsplit_with_rect(
1457 0.4,
1458 leaf(0),
1459 hsplit_with_rect(0.6, leaf(1), leaf(2), inner_rect),
1460 outer_rect,
1461 );
1462 let result = tree.enclosing_split_mut(1, SplitDir::Horizontal);
1463 assert!(result.is_some(), "should find enclosing horizontal split");
1464 let (ratio, rect, in_a) = result.unwrap();
1465 assert!(
1466 (*ratio - 0.6).abs() < 1e-5,
1467 "innermost split ratio should be 0.6, got {ratio}"
1468 );
1469 assert_eq!(
1470 rect,
1471 Some(inner_rect),
1472 "should return inner rect, not outer"
1473 );
1474 assert!(in_a, "id=1 is in the 'a' side of the inner split");
1475 }
1476
1477 #[test]
1478 fn enclosing_split_mut_skips_wrong_dir() {
1479 let mut tree = vsplit(0.5, leaf(0), leaf(1));
1480 let result = tree.enclosing_split_mut(0, SplitDir::Horizontal);
1481 assert!(
1482 result.is_none(),
1483 "should not match a Vertical split for Horizontal dir"
1484 );
1485 }
1486
1487 #[test]
1488 fn enclosing_split_mut_returns_none_for_only_leaf() {
1489 let mut tree = leaf(0);
1490 let result = tree.enclosing_split_mut(0, SplitDir::Horizontal);
1491 assert!(result.is_none(), "single leaf has no enclosing split");
1492 }
1493
1494 #[test]
1495 fn equalize_all_resets_nested_splits_to_half() {
1496 let mut tree = hsplit(0.3, leaf(0), hsplit(0.7, leaf(1), leaf(2)));
1497 tree.equalize_all(&[]);
1498 fn check_all_half(t: &LayoutTree) {
1499 if let LayoutTree::Split { ratio, a, b, .. } = t {
1500 assert!(
1501 (ratio - 0.5).abs() < 1e-5,
1502 "ratio should be 0.5, got {ratio}"
1503 );
1504 check_all_half(a);
1505 check_all_half(b);
1506 }
1507 }
1508 check_all_half(&tree);
1509 }
1510
1511 #[test]
1512 fn for_each_ancestor_visits_outermost_first() {
1513 let outer_rect = LayoutRect::new(0, 0, 80, 24);
1514 let inner_rect = LayoutRect::new(24, 0, 56, 24);
1515 let mut tree = vsplit_with_rect(
1516 0.3,
1517 leaf(0),
1518 hsplit_with_rect(0.7, leaf(1), leaf(2), inner_rect),
1519 outer_rect,
1520 );
1521 let mut visited_dirs: Vec<SplitDir> = Vec::new();
1522 let mut visited_ratios: Vec<f32> = Vec::new();
1523 tree.for_each_ancestor(1, &mut |dir, ratio, _in_a, _rect| {
1524 visited_dirs.push(dir);
1525 visited_ratios.push(*ratio);
1526 });
1527 assert_eq!(
1528 visited_dirs,
1529 vec![SplitDir::Vertical, SplitDir::Horizontal],
1530 "outermost (Vertical) should be visited first"
1531 );
1532 assert!(
1533 (visited_ratios[0] - 0.3).abs() < 1e-5,
1534 "outer ratio should be 0.3"
1535 );
1536 assert!(
1537 (visited_ratios[1] - 0.7).abs() < 1e-5,
1538 "inner ratio should be 0.7"
1539 );
1540 }
1541
1542 #[test]
1545 fn swap_with_sibling_swaps_two_leaves() {
1546 let mut tree = hsplit(0.5, leaf(0), leaf(1));
1547 let swapped = tree.swap_with_sibling(0, &[]);
1548 assert!(swapped, "swap should succeed in a two-leaf split");
1549 assert_eq!(tree.leaves(), vec![1, 0], "leaves should be swapped");
1550 }
1551
1552 #[test]
1553 fn swap_with_sibling_in_nested_split_swaps_at_focused_parent() {
1554 let mut tree = hsplit(0.5, leaf(0), vsplit(0.5, leaf(1), leaf(2)));
1555 let swapped = tree.swap_with_sibling(1, &[]);
1556 assert!(swapped, "swap should succeed");
1557 assert_eq!(
1558 tree.leaves(),
1559 vec![0, 2, 1],
1560 "inner leaves should be swapped"
1561 );
1562 }
1563
1564 #[test]
1565 fn swap_with_sibling_returns_false_for_only_leaf() {
1566 let mut tree = leaf(0);
1567 let swapped = tree.swap_with_sibling(0, &[]);
1568 assert!(!swapped, "single leaf has no sibling to swap with");
1569 }
1570
1571 #[test]
1572 fn swap_with_sibling_refuses_when_the_moving_leaf_is_pinned() {
1573 let mut tree = vsplit(0.5, leaf(9), leaf(0));
1574 let swapped = tree.swap_with_sibling(9, &[9]);
1575 assert!(!swapped, "a pinned leaf must not move");
1576 assert_eq!(tree.leaves(), vec![9, 0], "tree must be untouched");
1577 }
1578
1579 #[test]
1580 fn swap_with_sibling_refuses_when_the_sibling_is_pinned() {
1581 let mut tree = vsplit(0.5, leaf(9), leaf(0));
1582 let swapped = tree.swap_with_sibling(0, &[9]);
1583 assert!(!swapped, "must not swap a pinned sibling out of place");
1584 assert_eq!(tree.leaves(), vec![9, 0], "tree must be untouched");
1585 }
1586
1587 #[test]
1588 fn swap_with_sibling_refuses_when_the_sibling_subtree_holds_a_pin() {
1589 let mut tree = vsplit(0.5, leaf(0), vsplit(0.5, leaf(9), leaf(1)));
1591 let swapped = tree.swap_with_sibling(0, &[9]);
1592 assert!(!swapped, "a pin anywhere in the sibling blocks the swap");
1593 assert_eq!(tree.leaves(), vec![0, 9, 1]);
1594 }
1595
1596 #[test]
1597 fn swap_with_sibling_still_works_beside_an_unrelated_pin() {
1598 let mut tree = vsplit(0.5, leaf(9), vsplit(0.5, leaf(0), leaf(1)));
1600 let swapped = tree.swap_with_sibling(0, &[9]);
1601 assert!(swapped, "the pin is not on either side of this swap");
1602 assert_eq!(tree.leaves(), vec![9, 1, 0]);
1603 }
1604
1605 #[test]
1608 fn equalize_all_leaves_a_pinned_leaf_at_its_size() {
1609 let area = LayoutRect::new(0, 0, 80, 24);
1611 let mut tree = LayoutTree::split_fixed(
1612 SplitDir::Vertical,
1613 0.9,
1614 Fixed::First(30),
1615 leaf(9),
1616 hsplit(0.8, leaf(1), leaf(2)),
1617 );
1618 let dock_before = tree.window_rects(area)[0].1;
1619 tree.equalize_all(&[9]);
1620 let after = tree.window_rects(area);
1621 assert_eq!(after[0].0, 9);
1622 assert_eq!(after[0].1, dock_before, "pinned dock must keep its rect");
1623 assert!((root_ratio(&tree) - 0.9).abs() < 1e-5);
1625 if let LayoutTree::Split { b, .. } = &tree {
1626 assert!(
1627 (root_ratio(b) - 0.5).abs() < 1e-5,
1628 "ordinary splits under a pin still equalize"
1629 );
1630 } else {
1631 panic!("root should still be a split");
1632 }
1633 }
1634
1635 #[test]
1636 fn equalize_all_protects_a_ratio_split_next_to_a_pinned_leaf() {
1637 let mut tree = vsplit(0.2, leaf(9), leaf(0));
1639 tree.equalize_all(&[9]);
1640 assert!(
1641 (root_ratio(&tree) - 0.2).abs() < 1e-5,
1642 "a split with a pinned child keeps its ratio"
1643 );
1644 }
1645
1646 #[test]
1649 fn only_collapses_to_the_kept_leaf_when_nothing_is_pinned() {
1650 let mut tree = hsplit(0.5, leaf(0), vsplit(0.5, leaf(1), leaf(2)));
1651 let mut removed = tree.only(1, &[]);
1652 removed.sort_unstable();
1653 assert_eq!(removed, vec![0, 2]);
1654 assert_eq!(tree.leaves(), vec![1]);
1655 }
1656
1657 #[test]
1658 fn only_retains_pinned_leaves_and_their_arrangement() {
1659 let mut tree = vsplit(
1661 0.5,
1662 leaf(9),
1663 hsplit(0.5, hsplit(0.5, leaf(1), leaf(2)), leaf(8)),
1664 );
1665 let removed = tree.only(1, &[9, 8]);
1666 assert_eq!(removed, vec![2]);
1667 assert_eq!(
1668 tree.leaves(),
1669 vec![9, 1, 8],
1670 "dock stays left of the kept window, quickfix stays below it"
1671 );
1672 }
1673
1674 #[test]
1675 fn only_on_a_single_leaf_is_a_no_op() {
1676 let mut tree = leaf(0);
1677 assert!(tree.only(0, &[]).is_empty());
1678 assert_eq!(tree.leaves(), vec![0]);
1679 }
1680
1681 #[test]
1682 fn only_with_an_absent_keep_changes_nothing() {
1683 let mut tree = hsplit(0.5, leaf(0), leaf(1));
1684 assert!(tree.only(99, &[]).is_empty());
1685 assert_eq!(tree.leaves(), vec![0, 1]);
1686 }
1687
1688 #[test]
1689 fn only_keeping_a_pinned_leaf_drops_the_rest() {
1690 let mut tree = vsplit(0.5, leaf(9), hsplit(0.5, leaf(0), leaf(1)));
1691 let mut removed = tree.only(9, &[9]);
1692 removed.sort_unstable();
1693 assert_eq!(removed, vec![0, 1]);
1694 assert_eq!(tree.leaves(), vec![9]);
1695 }
1696
1697 #[test]
1698 fn only_preserves_the_geometry_of_the_surviving_split() {
1699 let mut tree = vsplit(0.25, leaf(9), hsplit(0.5, leaf(0), leaf(1)));
1700 tree.only(0, &[9]);
1701 assert!(
1702 (root_ratio(&tree) - 0.25).abs() < 1e-5,
1703 "the split joining the retained leaves keeps its ratio"
1704 );
1705 }
1706
1707 #[test]
1710 fn fixed_first_renders_exact_cells_along_a_vertical_split() {
1711 let tree =
1714 LayoutTree::split_fixed(SplitDir::Vertical, 0.5, Fixed::First(30), leaf(0), leaf(1));
1715 let rects = tree.window_rects(LayoutRect::new(0, 0, 80, 24));
1716 assert_eq!(rects[0].1, LayoutRect::new(0, 0, 30, 24));
1717 assert_eq!(rects[1].1, LayoutRect::new(31, 0, 49, 24));
1718 }
1719
1720 #[test]
1721 fn fixed_second_renders_exact_cells_along_a_vertical_split() {
1722 let tree =
1725 LayoutTree::split_fixed(SplitDir::Vertical, 0.5, Fixed::Second(30), leaf(0), leaf(1));
1726 let rects = tree.window_rects(LayoutRect::new(0, 0, 80, 24));
1727 assert_eq!(
1728 rects[1].1.w, 30,
1729 "Second(30) must render 30, like First(30)"
1730 );
1731 assert_eq!(rects[0].1, LayoutRect::new(0, 0, 49, 24));
1732 assert_eq!(rects[1].1, LayoutRect::new(50, 0, 30, 24));
1733 }
1734
1735 #[test]
1736 fn fixed_first_and_second_render_the_same_size_on_both_axes() {
1737 let area = LayoutRect::new(0, 0, 80, 24);
1738 let along = |dir: SplitDir, r: LayoutRect| match dir.axis() {
1739 Axis::Col => r.w,
1740 Axis::Row => r.h,
1741 };
1742 for dir in [SplitDir::Vertical, SplitDir::Horizontal] {
1743 for n in [1u16, 2, 10, 20] {
1744 let first = LayoutTree::split_fixed(dir, 0.5, Fixed::First(n), leaf(0), leaf(1));
1745 let second = LayoutTree::split_fixed(dir, 0.5, Fixed::Second(n), leaf(0), leaf(1));
1746 assert_eq!(
1747 along(dir, first.window_rects(area)[0].1),
1748 n,
1749 "First({n}) on {dir:?} must render {n}"
1750 );
1751 assert_eq!(
1752 along(dir, second.window_rects(area)[1].1),
1753 n,
1754 "Second({n}) on {dir:?} must render {n}"
1755 );
1756 }
1757 }
1758 }
1759
1760 #[test]
1761 fn fixed_second_renders_exact_cells_along_a_horizontal_split() {
1762 let tree = LayoutTree::split_fixed(
1763 SplitDir::Horizontal,
1764 0.5,
1765 Fixed::Second(10),
1766 leaf(0),
1767 leaf(1),
1768 );
1769 let rects = tree.window_rects(LayoutRect::new(0, 0, 80, 24));
1770 assert_eq!(rects[0].1, LayoutRect::new(0, 0, 80, 13));
1771 assert_eq!(rects[1].1, LayoutRect::new(0, 14, 80, 10));
1772 }
1773
1774 #[test]
1775 fn fixed_wins_over_ratio() {
1776 let ratio_only = LayoutTree::split(SplitDir::Vertical, 0.5, leaf(0), leaf(1));
1777 let fixed =
1778 LayoutTree::split_fixed(SplitDir::Vertical, 0.5, Fixed::First(20), leaf(0), leaf(1));
1779 let area = LayoutRect::new(0, 0, 80, 24);
1780 assert_eq!(ratio_only.window_rects(area)[0].1.w, 39);
1781 assert_eq!(fixed.window_rects(area)[0].1.w, 20);
1782 }
1783
1784 #[test]
1785 fn fixed_is_independent_of_the_parent_size() {
1786 let tree =
1787 LayoutTree::split_fixed(SplitDir::Vertical, 0.5, Fixed::First(30), leaf(0), leaf(1));
1788 for total in [60u16, 80, 120, 200] {
1789 let rects = tree.window_rects(LayoutRect::new(0, 0, total, 24));
1790 assert_eq!(rects[0].1.w, 30, "dock width must not track the parent");
1791 assert_eq!(rects[1].1.w, total - 31);
1792 }
1793 }
1794
1795 #[test]
1796 fn fixed_renders_the_requested_size_when_no_separator_is_carved() {
1797 let area = LayoutRect::new(0, 0, 2, 2);
1801 for dir in [SplitDir::Vertical, SplitDir::Horizontal] {
1802 let first = LayoutTree::split_fixed(dir, 0.5, Fixed::First(1), leaf(0), leaf(1));
1803 let second = LayoutTree::split_fixed(dir, 0.5, Fixed::Second(1), leaf(0), leaf(1));
1804 for tree in [first, second] {
1805 let rects = tree.window_rects(area);
1806 let (a, b) = (rects[0].1, rects[1].1);
1807 let (a_len, b_len) = match dir.axis() {
1808 Axis::Col => (a.w, b.w),
1809 Axis::Row => (a.h, b.h),
1810 };
1811 assert_eq!((a_len, b_len), (1, 1), "{dir:?}: both children render 1");
1812 }
1813 }
1814
1815 let area = LayoutRect::new(0, 0, 3, 24);
1818 let first =
1819 LayoutTree::split_fixed(SplitDir::Vertical, 0.5, Fixed::First(1), leaf(0), leaf(1));
1820 let rects = first.window_rects(area);
1821 assert_eq!(rects[0].1, LayoutRect::new(0, 0, 1, 24));
1822 assert_eq!(rects[1].1, LayoutRect::new(2, 0, 1, 24));
1823 let second =
1824 LayoutTree::split_fixed(SplitDir::Vertical, 0.5, Fixed::Second(1), leaf(0), leaf(1));
1825 let rects = second.window_rects(area);
1826 assert_eq!(rects[0].1, LayoutRect::new(0, 0, 1, 24));
1827 assert_eq!(rects[1].1, LayoutRect::new(2, 0, 1, 24));
1828 }
1829
1830 #[test]
1831 fn oversized_fixed_clamps_to_leave_the_sibling_one_cell() {
1832 let area = LayoutRect::new(0, 0, 80, 24);
1833
1834 let first =
1837 LayoutTree::split_fixed(SplitDir::Vertical, 0.5, Fixed::First(200), leaf(0), leaf(1));
1838 let rects = first.window_rects(area);
1839 assert_eq!(rects[0].1.w, 78);
1840 assert_eq!(rects[1].1.w, 1);
1841
1842 let second = LayoutTree::split_fixed(
1845 SplitDir::Vertical,
1846 0.5,
1847 Fixed::Second(200),
1848 leaf(0),
1849 leaf(1),
1850 );
1851 let rects = second.window_rects(area);
1852 assert_eq!(rects[0].1.w, 1);
1853 assert_eq!(rects[1].1.w, 79);
1854 assert_eq!(
1855 rects[0].1.w + rects[1].1.w,
1856 80,
1857 "no cells may be lost or invented"
1858 );
1859 }
1860
1861 #[test]
1862 fn fixed_on_a_degenerate_area_does_not_underflow() {
1863 for dir in [SplitDir::Vertical, SplitDir::Horizontal] {
1866 for fixed in [Fixed::First(u16::MAX), Fixed::Second(u16::MAX)] {
1867 for area in [
1868 LayoutRect::new(0, 0, 0, 0),
1869 LayoutRect::new(0, 0, 1, 1),
1870 LayoutRect::new(0, 0, 2, 2),
1871 ] {
1872 let tree = LayoutTree::split_fixed(dir, 0.5, fixed, leaf(0), leaf(1));
1873 let rects = tree.window_rects(area);
1874 let (a, b) = (rects[0].1, rects[1].1);
1875 assert!(a.w <= area.w && b.w <= area.w, "child wider than parent");
1876 assert!(a.h <= area.h && b.h <= area.h, "child taller than parent");
1877 }
1878 }
1879 }
1880 }
1881
1882 #[test]
1883 fn fixed_nested_under_an_ordinary_split() {
1884 let tree = hsplit(
1886 0.5,
1887 LayoutTree::split_fixed(SplitDir::Vertical, 0.5, Fixed::Second(20), leaf(0), leaf(9)),
1888 leaf(1),
1889 );
1890 let rects = tree.window_rects(LayoutRect::new(0, 0, 80, 24));
1891 assert_eq!(rects.len(), 3);
1892 let dock = rects.iter().find(|(id, _)| *id == 9).unwrap().1;
1893 assert_eq!(dock.w, 20, "nested fixed child keeps its exact width");
1894 }
1895
1896 #[test]
1899 fn mixed_layout_navigation() {
1900 let tree = hsplit(0.5, vsplit(0.5, leaf(0), leaf(1)), leaf(2));
1907
1908 assert_eq!(tree.neighbor_right(0), Some(1));
1909 assert_eq!(tree.neighbor_left(1), Some(0));
1910 assert_eq!(tree.neighbor_right(1), None);
1911 assert_eq!(tree.neighbor_left(0), None);
1912
1913 assert_eq!(tree.neighbor_below(0), Some(2));
1914 assert_eq!(tree.neighbor_below(1), Some(2));
1915 assert_eq!(tree.neighbor_below(2), None);
1916 assert_eq!(tree.neighbor_above(2), Some(1));
1917 assert_eq!(tree.neighbor_above(0), None);
1918 assert_eq!(tree.neighbor_above(1), None);
1919
1920 assert_eq!(tree.next_leaf(0), Some(1));
1921 assert_eq!(tree.next_leaf(1), Some(2));
1922 assert_eq!(tree.next_leaf(2), Some(0));
1923 assert_eq!(tree.prev_leaf(0), Some(2));
1924 assert_eq!(tree.prev_leaf(2), Some(1));
1925 }
1926
1927 #[test]
1930 fn window_rects_single_leaf_gets_full_area() {
1931 let tree = leaf(0);
1932 let area = LayoutRect::new(0, 0, 80, 23);
1933 let rects = tree.window_rects(area);
1934 assert_eq!(rects, vec![(0, area)]);
1935 }
1936
1937 #[test]
1938 fn window_rects_vsplit_two_side_by_side() {
1939 let tree = vsplit(0.5, leaf(0), leaf(1));
1945 let area = LayoutRect::new(0, 0, 80, 23);
1946 let rects = tree.window_rects(area);
1947 assert_eq!(rects.len(), 2);
1948 let (id_a, ra) = rects[0];
1949 let (id_b, rb) = rects[1];
1950 assert_eq!(id_a, 0);
1951 assert_eq!(id_b, 1);
1952 assert_eq!(
1954 ra.w + 1 + rb.w,
1955 80,
1956 "widths + separator must sum to parent width"
1957 );
1958 assert_eq!(ra.h, 23);
1959 assert_eq!(rb.h, 23);
1960 assert_eq!(rb.x, ra.x + ra.w + 1);
1962 }
1963
1964 #[test]
1965 fn window_rects_hsplit_stacked() {
1966 let tree = hsplit(0.5, leaf(0), leaf(1));
1972 let area = LayoutRect::new(0, 0, 80, 23);
1973 let rects = tree.window_rects(area);
1974 assert_eq!(rects.len(), 2);
1975 let (_, ra) = rects[0];
1976 let (_, rb) = rects[1];
1977 assert_eq!(
1979 ra.h + 1 + rb.h,
1980 23,
1981 "heights + separator must sum to parent height"
1982 );
1983 assert_eq!(ra.w, 80);
1984 assert_eq!(rb.w, 80);
1985 assert_eq!(rb.y, ra.y + ra.h + 1);
1987 }
1988
1989 #[test]
1990 fn window_rects_nested_vsplit_inside_vsplit() {
1991 let tree = vsplit(0.5, leaf(0), vsplit(0.5, leaf(1), leaf(2)));
1995 let area = LayoutRect::new(0, 0, 80, 23);
1996 let rects = tree.window_rects(area);
1997 assert_eq!(rects.len(), 3);
1998 let total_w: u16 = rects.iter().map(|(_, r)| r.w).sum::<u16>() + 2;
2000 assert_eq!(total_w, 80, "all window widths + 2 separators == 80");
2001 }
2002
2003 #[test]
2006 fn remove_leaf_error_contains_e444() {
2007 let mut tree = leaf(0);
2008 let err = tree.remove_leaf(0).unwrap_err();
2009 assert!(err.contains("E444"), "error must mention E444, got: {err}");
2010 }
2011
2012 #[test]
2015 fn enclosing_split_mut_ratio_update_persists() {
2016 let mut tree = hsplit(0.5, leaf(0), leaf(1));
2017 {
2018 let (ratio, _, _) = tree.enclosing_split_mut(0, SplitDir::Horizontal).unwrap();
2019 *ratio = 0.75;
2020 }
2021 if let LayoutTree::Split { ratio, .. } = &tree {
2023 assert!((*ratio - 0.75).abs() < 1e-5, "ratio should now be 0.75");
2024 } else {
2025 panic!("tree should still be a Split");
2026 }
2027 }
2028}
2029
2030#[cfg(test)]
2031mod fixed_sizing_sweep {
2032 use super::*;
2033
2034 #[test]
2051 fn fixed_renders_requested_size_and_never_exceeds_parent() {
2052 let mut asym = Vec::new();
2053 for len in 0u16..40 {
2054 for n in 0u16..45 {
2055 for dir in [SplitDir::Vertical, SplitDir::Horizontal] {
2056 let area = match dir.axis() {
2057 Axis::Col => LayoutRect::new(0, 0, len, 10),
2058 Axis::Row => LayoutRect::new(0, 0, 10, len),
2059 };
2060 let ext = |r: LayoutRect| match dir.axis() {
2061 Axis::Col => r.w,
2062 Axis::Row => r.h,
2063 };
2064 let fa = split_geometry(area, dir, 0.5, Some(Fixed::First(n))).a;
2065 let sb = split_geometry(area, dir, 0.5, Some(Fixed::Second(n))).b;
2066 let (first, second) = (ext(fa), ext(sb));
2067 let meaningful = len >= 3 && n >= 1 && n < len - 1;
2070 if meaningful && first != second {
2071 asym.push((len, n, format!("{dir:?}"), first, second));
2072 }
2073 if meaningful {
2074 assert_eq!(first, n, "First({n}) on len {len} rendered {first}");
2075 assert_eq!(second, n, "Second({n}) on len {len} rendered {second}");
2076 }
2077 assert!(first <= len && second <= len, "child exceeds parent");
2078 }
2079 }
2080 }
2081 assert!(
2082 asym.is_empty(),
2083 "First/Second render differently in {} cases, e.g. {:?}",
2084 asym.len(),
2085 &asym[..asym.len().min(6)]
2086 );
2087 }
2088
2089 #[test]
2096 fn split_geometry_children_and_separator_tile_the_parent() {
2097 let area = LayoutRect::new(3, 5, 40, 20);
2098 let fixings = [
2099 None,
2100 Some(Fixed::First(10)),
2101 Some(Fixed::Second(10)),
2102 Some(Fixed::First(1)),
2103 Some(Fixed::Second(1)),
2104 ];
2105 for dir in [SplitDir::Vertical, SplitDir::Horizontal] {
2106 for fixed in fixings {
2107 let geo = split_geometry(area, dir, 0.5, fixed);
2108 let sep = geo
2109 .separator
2110 .unwrap_or_else(|| panic!("{dir:?}/{fixed:?} must fit a separator"));
2111 match dir.axis() {
2112 Axis::Col => {
2113 assert_eq!(geo.a.x, area.x, "a starts at the parent's left edge");
2114 assert_eq!(sep.x, geo.a.x + geo.a.w, "separator abuts a's right edge");
2115 assert_eq!(sep.w, 1, "separator is one column");
2116 assert_eq!(geo.b.x, sep.x + 1, "b starts right after the separator");
2117 assert_eq!(
2118 geo.b.x + geo.b.w,
2119 area.x + area.w,
2120 "b reaches the right edge"
2121 );
2122 assert_eq!((sep.y, sep.h), (area.y, area.h), "separator spans the rows");
2123 }
2124 Axis::Row => {
2125 assert_eq!(geo.a.y, area.y, "a starts at the parent's top edge");
2126 assert_eq!(sep.y, geo.a.y + geo.a.h, "separator abuts a's bottom edge");
2127 assert_eq!(sep.h, 1, "separator is one row");
2128 assert_eq!(geo.b.y, sep.y + 1, "b starts right after the separator");
2129 assert_eq!(
2130 geo.b.y + geo.b.h,
2131 area.y + area.h,
2132 "b reaches the bottom edge"
2133 );
2134 assert_eq!(
2135 (sep.x, sep.w),
2136 (area.x, area.w),
2137 "separator spans the columns"
2138 );
2139 }
2140 }
2141 }
2142 }
2143 }
2144
2145 #[test]
2148 fn split_geometry_reports_no_separator_when_none_is_drawn() {
2149 let geo = split_geometry(LayoutRect::new(0, 0, 1, 10), SplitDir::Vertical, 0.5, None);
2151 assert_eq!(geo.separator, None);
2152 let geo = split_geometry(
2154 LayoutRect::new(0, 0, 10, 1),
2155 SplitDir::Horizontal,
2156 0.5,
2157 None,
2158 );
2159 assert_eq!(geo.separator, None);
2160 }
2161
2162 #[test]
2167 fn enclosing_split_mut_skips_fixed_splits() {
2168 let inner = LayoutTree::Split {
2170 dir: SplitDir::Vertical,
2171 ratio: 0.5,
2172 fixed: Some(Fixed::First(20)),
2173 a: Box::new(LayoutTree::Leaf(0)),
2174 b: Box::new(LayoutTree::Leaf(1)),
2175 last_rect: Some(LayoutRect::new(20, 0, 60, 24)),
2176 };
2177 let mut tree = LayoutTree::Split {
2178 dir: SplitDir::Vertical,
2179 ratio: 0.25,
2180 fixed: None,
2181 a: Box::new(LayoutTree::Leaf(9)),
2182 b: Box::new(inner),
2183 last_rect: Some(LayoutRect::new(0, 0, 80, 24)),
2184 };
2185
2186 let (ratio, rect, in_a) = tree
2187 .enclosing_split_mut(0, SplitDir::Vertical)
2188 .expect("the outer ratio split is still resizable");
2189 assert!(
2190 (*ratio - 0.25).abs() < 1e-5,
2191 "must be the OUTER split's ratio"
2192 );
2193 assert_eq!(rect, Some(LayoutRect::new(0, 0, 80, 24)));
2194 assert!(!in_a, "leaf 0 lives in the outer split's `b` branch");
2195 }
2196
2197 #[test]
2200 fn enclosing_split_mut_returns_none_for_a_lone_fixed_split() {
2201 let mut tree = LayoutTree::Split {
2202 dir: SplitDir::Vertical,
2203 ratio: 0.5,
2204 fixed: Some(Fixed::First(20)),
2205 a: Box::new(LayoutTree::Leaf(0)),
2206 b: Box::new(LayoutTree::Leaf(1)),
2207 last_rect: Some(LayoutRect::new(0, 0, 80, 24)),
2208 };
2209 assert!(tree.enclosing_split_mut(0, SplitDir::Vertical).is_none());
2210 assert!(tree.enclosing_split_mut(1, SplitDir::Vertical).is_none());
2211 }
2212
2213 #[test]
2216 fn for_each_ancestor_skips_fixed_splits() {
2217 let inner = LayoutTree::Split {
2218 dir: SplitDir::Vertical,
2219 ratio: 0.7,
2220 fixed: Some(Fixed::First(20)),
2221 a: Box::new(LayoutTree::Leaf(1)),
2222 b: Box::new(LayoutTree::Leaf(2)),
2223 last_rect: Some(LayoutRect::new(0, 12, 80, 12)),
2224 };
2225 let mut tree = LayoutTree::Split {
2226 dir: SplitDir::Horizontal,
2227 ratio: 0.3,
2228 fixed: None,
2229 a: Box::new(LayoutTree::Leaf(0)),
2230 b: Box::new(inner),
2231 last_rect: Some(LayoutRect::new(0, 0, 80, 24)),
2232 };
2233
2234 let mut seen = Vec::new();
2235 tree.for_each_ancestor(1, &mut |dir, ratio, _in_a, _rect| {
2236 seen.push((dir, *ratio));
2237 *ratio = 0.9;
2238 });
2239 assert_eq!(seen.len(), 1, "only the non-fixed ancestor is visited");
2240 assert_eq!(seen[0].0, SplitDir::Horizontal);
2241 let LayoutTree::Split { b, .. } = &tree else {
2243 panic!("expected a split at the root");
2244 };
2245 let LayoutTree::Split { ratio, .. } = b.as_ref() else {
2246 panic!("expected a split at b");
2247 };
2248 assert!((*ratio - 0.7).abs() < 1e-5, "fixed split's ratio untouched");
2249 }
2250}