1use serde::{Deserialize, Serialize};
14
15use crate::{
16 direction::{Direction, SplitOrientation},
17 geometry::Rect,
18 id::PaneId,
19};
20
21pub const MIN_RATIO: f32 = 0.05;
25
26#[derive(Copy, Clone, Debug, PartialEq, Serialize)]
54#[serde(transparent)]
55pub struct SplitRatio(f32);
56
57impl SplitRatio {
58 pub const BALANCED: Self = Self(0.5);
60
61 #[must_use]
68 pub fn new(v: f32) -> Self {
69 if v.is_finite() {
70 Self(v.clamp(MIN_RATIO, 1.0 - MIN_RATIO))
71 } else {
72 Self::BALANCED
73 }
74 }
75
76 #[must_use]
79 pub const fn get(self) -> f32 {
80 self.0
81 }
82}
83
84impl Default for SplitRatio {
85 fn default() -> Self {
86 Self::BALANCED
87 }
88}
89
90impl From<f32> for SplitRatio {
91 fn from(v: f32) -> Self {
92 Self::new(v)
93 }
94}
95
96impl<'de> Deserialize<'de> for SplitRatio {
97 fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
100 Ok(Self::new(f32::deserialize(d)?))
101 }
102}
103
104#[derive(Copy, Clone, Debug, PartialEq, Eq)]
109pub enum LeafRemoval {
110 Removed,
113 WasRoot,
116 NotFound,
118}
119
120#[derive(Clone, Debug, PartialEq)]
127pub enum LayoutError {
128 NullLeaf,
131 DuplicatePane(PaneId),
134 BadRatio(f32),
137}
138
139#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
141#[serde(tag = "kind", rename_all = "lowercase")]
142pub enum LayoutNode {
143 Leaf {
145 pane: PaneId,
146 },
147 Split {
152 orientation: SplitOrientation,
153 ratio: SplitRatio,
154 a: Box<LayoutNode>,
155 b: Box<LayoutNode>,
156 },
157}
158
159impl LayoutNode {
160 #[must_use]
162 pub fn leaf(pane: PaneId) -> Self {
163 Self::Leaf { pane }
164 }
165
166 #[must_use]
168 pub fn split(orientation: SplitOrientation, a: LayoutNode, b: LayoutNode) -> Self {
169 Self::Split {
170 orientation,
171 ratio: SplitRatio::BALANCED,
172 a: Box::new(a),
173 b: Box::new(b),
174 }
175 }
176
177 #[must_use]
182 pub fn panes(&self) -> Vec<PaneId> {
183 let mut out = Vec::new();
184 self.collect(&mut out);
185 out
186 }
187
188 fn collect(&self, out: &mut Vec<PaneId>) {
189 match self {
190 Self::Leaf { pane } => out.push(*pane),
191 Self::Split { a, b, .. } => {
192 a.collect(out);
193 b.collect(out);
194 }
195 }
196 }
197
198 #[must_use]
200 pub fn pane_count(&self) -> usize {
201 match self {
202 Self::Leaf { .. } => 1,
203 Self::Split { a, b, .. } => a.pane_count() + b.pane_count(),
204 }
205 }
206
207 #[must_use]
209 fn is_leaf_of(&self, pane: PaneId) -> bool {
210 matches!(self, Self::Leaf { pane: p } if *p == pane)
211 }
212
213 #[must_use]
215 pub fn contains_pane(&self, pane: PaneId) -> bool {
216 match self {
217 Self::Leaf { pane: p } => *p == pane,
218 Self::Split { a, b, .. } => a.contains_pane(pane) || b.contains_pane(pane),
219 }
220 }
221
222 #[must_use]
234 pub fn from_kind(kind: LayoutKind, panes: &[PaneId]) -> Option<Self> {
235 match panes {
236 [] => None,
237 [only] => Some(Self::leaf(*only)),
238 [main, rest @ ..] => match kind {
239 LayoutKind::EvenHorizontal => {
241 even_chain(SplitOrientation::Vertical, &leaves(panes))
242 }
243 LayoutKind::EvenVertical => {
245 even_chain(SplitOrientation::Horizontal, &leaves(panes))
246 }
247 LayoutKind::MainHorizontal => {
249 let bottom = even_chain(SplitOrientation::Vertical, &leaves(rest))?;
250 Some(Self::Split {
251 orientation: SplitOrientation::Horizontal,
252 ratio: SplitRatio::BALANCED,
253 a: Box::new(Self::leaf(*main)),
254 b: Box::new(bottom),
255 })
256 }
257 LayoutKind::MainVertical => {
259 let right = even_chain(SplitOrientation::Horizontal, &leaves(rest))?;
260 Some(Self::Split {
261 orientation: SplitOrientation::Vertical,
262 ratio: SplitRatio::BALANCED,
263 a: Box::new(Self::leaf(*main)),
264 b: Box::new(right),
265 })
266 }
267 LayoutKind::Tiled => tiled(panes),
268 LayoutKind::Custom => None,
270 },
271 }
272 }
273
274 pub fn split_leaf(
286 &mut self,
287 target: PaneId,
288 new_pane: PaneId,
289 direction: Direction,
290 origin_ratio: f32,
291 ) -> bool {
292 match self {
293 Self::Leaf { pane } if *pane == target => {
294 let origin = Self::leaf(*pane);
295 let fresh = Self::leaf(new_pane);
296 let keep = SplitRatio::new(origin_ratio).get();
300 let orientation = direction.orientation();
301 let (a, b, ratio) = match direction {
306 Direction::Right | Direction::Below => (origin, fresh, keep),
307 Direction::Left | Direction::Above => (fresh, origin, 1.0 - keep),
308 };
309 *self = Self::Split {
310 orientation,
311 ratio: SplitRatio::new(ratio),
312 a: Box::new(a),
313 b: Box::new(b),
314 };
315 true
316 }
317 Self::Leaf { .. } => false,
318 Self::Split { a, b, .. } => {
319 a.split_leaf(target, new_pane, direction, origin_ratio)
320 || b.split_leaf(target, new_pane, direction, origin_ratio)
321 }
322 }
323 }
324
325 pub fn remove_leaf(&mut self, target: PaneId) -> LeafRemoval {
329 match self {
330 Self::Leaf { pane } if *pane == target => LeafRemoval::WasRoot,
331 Self::Leaf { .. } => LeafRemoval::NotFound,
332 Self::Split { a, b, .. } => {
333 if a.is_leaf_of(target) {
336 *self = std::mem::replace(b.as_mut(), Self::leaf(PaneId::NULL));
337 return LeafRemoval::Removed;
338 }
339 if b.is_leaf_of(target) {
340 *self = std::mem::replace(a.as_mut(), Self::leaf(PaneId::NULL));
341 return LeafRemoval::Removed;
342 }
343 match a.remove_leaf(target) {
345 LeafRemoval::NotFound => b.remove_leaf(target),
346 other => other,
347 }
348 }
349 }
350 }
351
352 pub fn resize_leaf(&mut self, target: PaneId, direction: Direction, delta_frac: f32) -> bool {
366 let want = direction.orientation();
367 let toward_b = matches!(direction, Direction::Right | Direction::Below);
375 let Some(path) = self.governing_split_path(target, want, toward_b) else {
378 return false;
379 };
380 let Some(Self::Split { ratio, .. }) = self.split_at_path(&path) else {
381 return false;
382 };
383 let sign = if toward_b { 1.0 } else { -1.0 };
389 let next = ratio.get() + sign * delta_frac;
390 if next.is_finite() {
391 *ratio = SplitRatio::new(next);
392 }
393 true
394 }
395
396 fn governing_split_path(
402 &self,
403 target: PaneId,
404 want: SplitOrientation,
405 need_side_a: bool,
406 ) -> Option<Vec<bool>> {
407 let mut best: Option<Vec<bool>> = None;
408 let mut path: Vec<bool> = Vec::new();
409 self.walk_governing(target, want, need_side_a, &mut path, &mut best);
410 best
411 }
412
413 fn walk_governing(
414 &self,
415 target: PaneId,
416 want: SplitOrientation,
417 need_side_a: bool,
418 path: &mut Vec<bool>,
419 best: &mut Option<Vec<bool>>,
420 ) {
421 if let Self::Split {
422 orientation, a, b, ..
423 } = self
424 {
425 let in_a = a.contains_pane(target);
426 let in_b = b.contains_pane(target);
427 if *orientation == want && ((in_a && need_side_a) || (in_b && !need_side_a)) {
431 *best = Some(path.clone());
432 }
433 if in_a {
434 path.push(true);
435 a.walk_governing(target, want, need_side_a, path, best);
436 path.pop();
437 } else if in_b {
438 path.push(false);
439 b.walk_governing(target, want, need_side_a, path, best);
440 path.pop();
441 }
442 }
443 }
444
445 fn split_at_path(&mut self, path: &[bool]) -> Option<&mut Self> {
446 let mut node = self;
447 for &into_a in path {
448 match node {
449 Self::Split { a, b, .. } => {
450 node = if into_a { a.as_mut() } else { b.as_mut() };
451 }
452 Self::Leaf { .. } => return None,
453 }
454 }
455 Some(node)
456 }
457
458 #[must_use]
463 pub fn compute_rects(&self, bounds: Rect) -> Vec<(PaneId, Rect)> {
464 let mut out = Vec::with_capacity(self.pane_count());
465 self.lay_out(bounds, &mut out);
466 out
467 }
468
469 fn lay_out(&self, bounds: Rect, out: &mut Vec<(PaneId, Rect)>) {
470 match self {
471 Self::Leaf { pane } => out.push((*pane, bounds)),
472 Self::Split {
473 orientation,
474 ratio,
475 a,
476 b,
477 } => {
478 let (ra, rb) = split_rect(bounds, *orientation, ratio.get());
479 a.lay_out(ra, out);
480 b.lay_out(rb, out);
481 }
482 }
483 }
484
485 #[must_use]
493 pub fn neighbor(&self, target: PaneId, direction: Direction, bounds: Rect) -> Option<PaneId> {
494 let rects = self.compute_rects(bounds);
495 let me = rects.iter().find(|(p, _)| *p == target).map(|(_, r)| *r)?;
496 let mut best: Option<(PaneId, u32, u32)> = None;
500 for (pane, r) in &rects {
501 if *pane == target {
502 continue;
503 }
504 let (on_side, gap, overlap) = match direction {
509 Direction::Left => (
510 r.right() <= u32::from(me.x),
511 u32::from(me.x).saturating_sub(r.right()),
512 Rect::span_overlap(u32::from(r.y), r.bottom(), u32::from(me.y), me.bottom()),
513 ),
514 Direction::Right => (
515 u32::from(r.x) >= me.right(),
516 u32::from(r.x).saturating_sub(me.right()),
517 Rect::span_overlap(u32::from(r.y), r.bottom(), u32::from(me.y), me.bottom()),
518 ),
519 Direction::Above => (
520 r.bottom() <= u32::from(me.y),
521 u32::from(me.y).saturating_sub(r.bottom()),
522 Rect::span_overlap(u32::from(r.x), r.right(), u32::from(me.x), me.right()),
523 ),
524 Direction::Below => (
525 u32::from(r.y) >= me.bottom(),
526 u32::from(r.y).saturating_sub(me.bottom()),
527 Rect::span_overlap(u32::from(r.x), r.right(), u32::from(me.x), me.right()),
528 ),
529 };
530 if on_side && overlap > 0 {
531 let better = match best {
532 None => true,
533 Some((_, best_gap, best_overlap)) => {
534 gap < best_gap || (gap == best_gap && overlap > best_overlap)
535 }
536 };
537 if better {
538 best = Some((*pane, gap, overlap));
539 }
540 }
541 }
542 best.map(|(p, _, _)| p)
543 }
544
545 pub fn validate(&self) -> Result<(), LayoutError> {
549 let mut seen = Vec::new();
550 self.validate_into(&mut seen)
551 }
552
553 fn validate_into(&self, seen: &mut Vec<PaneId>) -> Result<(), LayoutError> {
554 match self {
555 Self::Leaf { pane } => {
556 if *pane == PaneId::NULL {
557 return Err(LayoutError::NullLeaf);
558 }
559 if seen.contains(pane) {
560 return Err(LayoutError::DuplicatePane(*pane));
561 }
562 seen.push(*pane);
563 Ok(())
564 }
565 Self::Split { ratio, a, b, .. } => {
566 let r = ratio.get();
573 if !(r > 0.0 && r < 1.0) {
574 return Err(LayoutError::BadRatio(r));
575 }
576 a.validate_into(seen)?;
577 b.validate_into(seen)
578 }
579 }
580 }
581}
582
583fn split_rect(bounds: Rect, orientation: SplitOrientation, ratio: f32) -> (Rect, Rect) {
589 match orientation {
590 SplitOrientation::Horizontal => {
591 let a_h = split_extent(bounds.h, ratio);
592 let b_h = bounds.h - a_h;
593 (
594 Rect::new(bounds.x, bounds.y, bounds.w, a_h),
595 Rect::new(bounds.x, bounds.y + a_h, bounds.w, b_h),
596 )
597 }
598 SplitOrientation::Vertical => {
599 let a_w = split_extent(bounds.w, ratio);
600 let b_w = bounds.w - a_w;
601 (
602 Rect::new(bounds.x, bounds.y, a_w, bounds.h),
603 Rect::new(bounds.x + a_w, bounds.y, b_w, bounds.h),
604 )
605 }
606 }
607}
608
609fn split_extent(total: u16, ratio: f32) -> u16 {
613 if total <= 1 {
614 return total;
615 }
616 let raw = (f32::from(total) * ratio).round();
617 let a = raw.clamp(1.0, f32::from(total) - 1.0);
620 a as u16
621}
622
623fn leaves(panes: &[PaneId]) -> Vec<LayoutNode> {
625 panes.iter().map(|p| LayoutNode::leaf(*p)).collect()
626}
627
628fn even_chain(orientation: SplitOrientation, nodes: &[LayoutNode]) -> Option<LayoutNode> {
633 match nodes {
634 [] => None,
635 [single] => Some(single.clone()),
636 [first, rest @ ..] => {
637 let n = nodes.len() as f32;
638 let rest_tree = even_chain(orientation, rest)?;
639 Some(LayoutNode::Split {
640 orientation,
641 ratio: SplitRatio::new(1.0 / n),
642 a: Box::new(first.clone()),
643 b: Box::new(rest_tree),
644 })
645 }
646 }
647}
648
649fn tiled(panes: &[PaneId]) -> Option<LayoutNode> {
652 let n = panes.len();
653 if n == 0 {
654 return None;
655 }
656 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
657 let rows = (n as f64).sqrt().ceil() as usize;
658 let per = n / rows;
659 let extra = n % rows;
660 let mut row_trees = Vec::with_capacity(rows);
661 let mut i = 0;
662 for r in 0..rows {
663 let cnt = per + usize::from(r < extra);
665 let row = even_chain(SplitOrientation::Vertical, &leaves(&panes[i..i + cnt]))?;
666 row_trees.push(row);
667 i += cnt;
668 }
669 even_chain(SplitOrientation::Horizontal, &row_trees)
670}
671
672#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
675#[serde(rename_all = "kebab-case")]
676pub enum LayoutKind {
677 EvenHorizontal,
679 EvenVertical,
681 MainHorizontal,
683 MainVertical,
685 Tiled,
687 Custom,
692}
693
694#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
696#[serde(rename_all = "lowercase")]
697pub enum Size {
698 Cells(u16),
700 Fraction(f32),
702 Auto,
704}
705
706impl Default for Size {
707 fn default() -> Self {
708 Self::Auto
709 }
710}
711
712#[cfg(test)]
713mod tests {
714 use super::*;
715 use crate::direction::SplitOrientation;
716
717 #[test]
718 fn leaf_has_one_pane() {
719 let n = LayoutNode::leaf(PaneId(7));
720 assert_eq!(n.pane_count(), 1);
721 assert_eq!(n.panes(), vec![PaneId(7)]);
722 }
723
724 #[test]
725 fn split_aggregates_panes_left_then_right() {
726 let n = LayoutNode::split(
727 SplitOrientation::Vertical,
728 LayoutNode::leaf(PaneId(1)),
729 LayoutNode::leaf(PaneId(2)),
730 );
731 assert_eq!(n.pane_count(), 2);
732 assert_eq!(n.panes(), vec![PaneId(1), PaneId(2)]);
733 }
734
735 #[test]
736 fn nested_split_traversal_is_predictable() {
737 let n = LayoutNode::split(
738 SplitOrientation::Horizontal,
739 LayoutNode::leaf(PaneId(1)),
740 LayoutNode::split(
741 SplitOrientation::Vertical,
742 LayoutNode::leaf(PaneId(2)),
743 LayoutNode::leaf(PaneId(3)),
744 ),
745 );
746 assert_eq!(n.panes(), vec![PaneId(1), PaneId(2), PaneId(3)]);
747 assert_eq!(n.pane_count(), 3);
748 }
749
750 #[test]
753 fn split_leaf_targets_the_matched_leaf_only() {
754 let mut n = LayoutNode::split(
758 SplitOrientation::Vertical,
759 LayoutNode::leaf(PaneId(1)),
760 LayoutNode::split(
761 SplitOrientation::Horizontal,
762 LayoutNode::leaf(PaneId(2)),
763 LayoutNode::leaf(PaneId(3)),
764 ),
765 );
766 assert!(n.split_leaf(PaneId(2), PaneId(9), Direction::Right, 0.5));
767 assert_eq!(n.panes(), vec![PaneId(1), PaneId(2), PaneId(9), PaneId(3)]);
769 assert_eq!(n.pane_count(), 4);
770 n.validate().unwrap();
771 }
772
773 #[test]
774 fn split_leaf_orders_new_pane_by_direction() {
775 let mut right = LayoutNode::leaf(PaneId(1));
776 right.split_leaf(PaneId(1), PaneId(2), Direction::Right, 0.5);
777 assert_eq!(right.panes(), vec![PaneId(1), PaneId(2)]); let mut left = LayoutNode::leaf(PaneId(1));
780 left.split_leaf(PaneId(1), PaneId(2), Direction::Left, 0.5);
781 assert_eq!(left.panes(), vec![PaneId(2), PaneId(1)]); }
783
784 #[test]
785 fn split_leaf_unknown_target_is_noop() {
786 let mut n = LayoutNode::leaf(PaneId(1));
787 assert!(!n.split_leaf(PaneId(99), PaneId(2), Direction::Right, 0.5));
788 assert_eq!(n.panes(), vec![PaneId(1)]);
789 }
790
791 #[test]
792 fn split_leaf_clamps_extreme_ratio() {
793 let mut n = LayoutNode::leaf(PaneId(1));
794 n.split_leaf(PaneId(1), PaneId(2), Direction::Right, 0.0);
795 n.validate().unwrap();
797 if let LayoutNode::Split { ratio, .. } = n {
798 assert!(ratio.get() >= MIN_RATIO && ratio.get() <= 1.0 - MIN_RATIO);
799 } else {
800 panic!("expected a split");
801 }
802 }
803
804 #[test]
807 fn remove_leaf_collapses_parent_into_sibling() {
808 let mut n = LayoutNode::split(
810 SplitOrientation::Vertical,
811 LayoutNode::leaf(PaneId(1)),
812 LayoutNode::leaf(PaneId(2)),
813 );
814 assert_eq!(n.remove_leaf(PaneId(1)), LeafRemoval::Removed);
815 assert_eq!(n, LayoutNode::leaf(PaneId(2)));
816 n.validate().unwrap();
817 }
818
819 #[test]
820 fn remove_leaf_collapses_deep_node() {
821 let mut n = LayoutNode::split(
823 SplitOrientation::Vertical,
824 LayoutNode::leaf(PaneId(1)),
825 LayoutNode::split(
826 SplitOrientation::Horizontal,
827 LayoutNode::leaf(PaneId(2)),
828 LayoutNode::leaf(PaneId(3)),
829 ),
830 );
831 assert_eq!(n.remove_leaf(PaneId(3)), LeafRemoval::Removed);
832 assert_eq!(n.panes(), vec![PaneId(1), PaneId(2)]);
833 assert!(!n.contains_pane(PaneId(3)));
834 n.validate().unwrap();
836 }
837
838 #[test]
839 fn remove_leaf_root_reports_was_root() {
840 let mut n = LayoutNode::leaf(PaneId(1));
841 assert_eq!(n.remove_leaf(PaneId(1)), LeafRemoval::WasRoot);
842 assert_eq!(n, LayoutNode::leaf(PaneId(1)));
844 }
845
846 #[test]
847 fn remove_leaf_unknown_is_not_found() {
848 let mut n = LayoutNode::split(
849 SplitOrientation::Vertical,
850 LayoutNode::leaf(PaneId(1)),
851 LayoutNode::leaf(PaneId(2)),
852 );
853 assert_eq!(n.remove_leaf(PaneId(99)), LeafRemoval::NotFound);
854 assert_eq!(n.pane_count(), 2);
855 }
856
857 #[test]
860 fn compute_rects_single_pane_fills_bounds() {
861 let n = LayoutNode::leaf(PaneId(1));
862 let r = n.compute_rects(Rect::sized(80, 24));
863 assert_eq!(r, vec![(PaneId(1), Rect::new(0, 0, 80, 24))]);
864 }
865
866 #[test]
867 fn compute_rects_vertical_split_is_side_by_side_gapless() {
868 let n = LayoutNode::split(
870 SplitOrientation::Vertical,
871 LayoutNode::leaf(PaneId(1)),
872 LayoutNode::leaf(PaneId(2)),
873 );
874 let r = n.compute_rects(Rect::sized(80, 24));
875 let a = r.iter().find(|(p, _)| *p == PaneId(1)).unwrap().1;
876 let b = r.iter().find(|(p, _)| *p == PaneId(2)).unwrap().1;
877 assert_eq!(a, Rect::new(0, 0, 40, 24));
878 assert_eq!(b, Rect::new(40, 0, 40, 24));
879 assert_eq!(a.right(), u32::from(b.x)); }
881
882 #[test]
883 fn compute_rects_horizontal_split_stacks_rows() {
884 let n = LayoutNode::split(
885 SplitOrientation::Horizontal,
886 LayoutNode::leaf(PaneId(1)),
887 LayoutNode::leaf(PaneId(2)),
888 );
889 let r = n.compute_rects(Rect::sized(80, 24));
890 let a = r.iter().find(|(p, _)| *p == PaneId(1)).unwrap().1;
891 let b = r.iter().find(|(p, _)| *p == PaneId(2)).unwrap().1;
892 assert_eq!(a, Rect::new(0, 0, 80, 12));
893 assert_eq!(b, Rect::new(0, 12, 80, 12));
894 assert_eq!(a.bottom(), u32::from(b.y));
895 }
896
897 #[test]
898 fn compute_rects_tiles_bounds_exactly() {
899 let n = LayoutNode::split(
903 SplitOrientation::Vertical,
904 LayoutNode::leaf(PaneId(1)),
905 LayoutNode::split(
906 SplitOrientation::Horizontal,
907 LayoutNode::leaf(PaneId(2)),
908 LayoutNode::leaf(PaneId(3)),
909 ),
910 );
911 let bounds = Rect::sized(81, 25);
912 let rects = n.compute_rects(bounds);
913 let total: u32 = rects.iter().map(|(_, r)| r.area()).sum();
914 assert_eq!(total, bounds.area());
915 for y in 0..bounds.h {
917 for x in 0..bounds.w {
918 let owners = rects.iter().filter(|(_, r)| r.contains(x, y)).count();
919 assert_eq!(owners, 1, "cell ({x},{y}) owned by {owners} panes");
920 }
921 }
922 }
923
924 #[test]
925 fn compute_rects_tiny_window_never_panics() {
926 let n = LayoutNode::split(
927 SplitOrientation::Vertical,
928 LayoutNode::leaf(PaneId(1)),
929 LayoutNode::leaf(PaneId(2)),
930 );
931 let r = n.compute_rects(Rect::sized(1, 5));
934 let total: u32 = r.iter().map(|(_, rr)| rr.area()).sum();
935 assert_eq!(total, 5);
936 }
937
938 #[test]
941 fn neighbor_walks_left_and_right() {
942 let n = LayoutNode::split(
944 SplitOrientation::Vertical,
945 LayoutNode::leaf(PaneId(1)),
946 LayoutNode::split(
947 SplitOrientation::Vertical,
948 LayoutNode::leaf(PaneId(2)),
949 LayoutNode::leaf(PaneId(3)),
950 ),
951 );
952 let b = Rect::sized(90, 24);
953 assert_eq!(n.neighbor(PaneId(2), Direction::Left, b), Some(PaneId(1)));
954 assert_eq!(n.neighbor(PaneId(2), Direction::Right, b), Some(PaneId(3)));
955 assert_eq!(n.neighbor(PaneId(1), Direction::Left, b), None); assert_eq!(n.neighbor(PaneId(3), Direction::Right, b), None);
957 }
958
959 #[test]
960 fn neighbor_crosses_split_boundary_by_edge_overlap() {
961 let n = LayoutNode::split(
966 SplitOrientation::Vertical,
967 LayoutNode::leaf(PaneId(1)),
968 LayoutNode::split(
969 SplitOrientation::Horizontal,
970 LayoutNode::leaf(PaneId(2)),
971 LayoutNode::leaf(PaneId(3)),
972 ),
973 );
974 let b = Rect::sized(80, 24);
975 let right = n.neighbor(PaneId(1), Direction::Right, b);
976 assert!(matches!(right, Some(PaneId(2)) | Some(PaneId(3))));
977 assert_eq!(n.neighbor(PaneId(2), Direction::Left, b), Some(PaneId(1)));
979 }
980
981 #[test]
982 fn neighbor_unknown_target_is_none() {
983 let n = LayoutNode::leaf(PaneId(1));
984 assert_eq!(n.neighbor(PaneId(99), Direction::Left, Rect::sized(80, 24)), None);
985 }
986
987 #[test]
988 fn neighbor_prefers_nearest_not_just_max_overlap() {
989 let right_leaning = LayoutNode::split(
993 SplitOrientation::Vertical,
994 LayoutNode::leaf(PaneId(1)),
995 LayoutNode::split(
996 SplitOrientation::Vertical,
997 LayoutNode::leaf(PaneId(2)),
998 LayoutNode::leaf(PaneId(3)),
999 ),
1000 );
1001 let left_leaning = LayoutNode::split(
1002 SplitOrientation::Vertical,
1003 LayoutNode::split(
1004 SplitOrientation::Vertical,
1005 LayoutNode::leaf(PaneId(1)),
1006 LayoutNode::leaf(PaneId(2)),
1007 ),
1008 LayoutNode::leaf(PaneId(3)),
1009 );
1010 let b = Rect::sized(90, 24);
1011 assert_eq!(right_leaning.neighbor(PaneId(1), Direction::Right, b), Some(PaneId(2)));
1012 assert_eq!(left_leaning.neighbor(PaneId(1), Direction::Right, b), Some(PaneId(2)));
1013 }
1014
1015 #[test]
1018 fn from_kind_empty_and_custom_are_none() {
1019 assert_eq!(LayoutNode::from_kind(LayoutKind::Tiled, &[]), None);
1020 assert_eq!(
1021 LayoutNode::from_kind(LayoutKind::Custom, &[PaneId(1), PaneId(2)]),
1022 None
1023 );
1024 }
1025
1026 #[test]
1027 fn from_kind_single_pane_is_a_leaf_for_every_kind() {
1028 for kind in [
1029 LayoutKind::EvenHorizontal,
1030 LayoutKind::EvenVertical,
1031 LayoutKind::MainHorizontal,
1032 LayoutKind::MainVertical,
1033 LayoutKind::Tiled,
1034 ] {
1035 assert_eq!(
1036 LayoutNode::from_kind(kind, &[PaneId(1)]),
1037 Some(LayoutNode::leaf(PaneId(1)))
1038 );
1039 }
1040 }
1041
1042 #[test]
1043 fn from_kind_even_horizontal_gives_equal_thirds() {
1044 let panes = [PaneId(1), PaneId(2), PaneId(3)];
1047 let tree = LayoutNode::from_kind(LayoutKind::EvenHorizontal, &panes).unwrap();
1048 tree.validate().unwrap();
1049 let rects = tree.compute_rects(Rect::sized(90, 24));
1050 let mut widths: Vec<u16> = rects.iter().map(|(_, r)| r.w).collect();
1051 widths.sort_unstable();
1052 assert_eq!(widths, vec![30, 30, 30]);
1054 }
1055
1056 #[test]
1061 fn from_kind_every_preset_validates_and_tiles_exactly() {
1062 let kinds = [
1063 LayoutKind::EvenHorizontal,
1064 LayoutKind::EvenVertical,
1065 LayoutKind::MainHorizontal,
1066 LayoutKind::MainVertical,
1067 LayoutKind::Tiled,
1068 ];
1069 for kind in kinds {
1070 for n in 1..=7usize {
1071 let panes: Vec<PaneId> = (1..=n as u64).map(PaneId).collect();
1072 let tree = LayoutNode::from_kind(kind, &panes)
1073 .unwrap_or_else(|| panic!("{kind:?} n={n} produced None"));
1074 tree.validate()
1076 .unwrap_or_else(|e| panic!("{kind:?} n={n} invalid: {e:?}"));
1077 assert_eq!(tree.pane_count(), n, "{kind:?} n={n} pane count");
1078 assert_eq!(tree.panes().len(), n);
1079 for &(w, h) in &[(80u16, 24u16), (81, 25), (97, 31)] {
1081 let bounds = Rect::sized(w, h);
1082 let rects = tree.compute_rects(bounds);
1083 let total: u32 = rects.iter().map(|(_, r)| r.area()).sum();
1084 assert_eq!(total, bounds.area(), "{kind:?} n={n} at {w}x{h}");
1085 }
1086 }
1087 }
1088 }
1089
1090 #[test]
1091 fn from_kind_main_vertical_keeps_main_on_the_left() {
1092 let panes = [PaneId(1), PaneId(2), PaneId(3)];
1093 let tree = LayoutNode::from_kind(LayoutKind::MainVertical, &panes).unwrap();
1094 match &tree {
1096 LayoutNode::Split { orientation, a, .. } => {
1097 assert_eq!(*orientation, SplitOrientation::Vertical);
1098 assert_eq!(a.as_ref(), &LayoutNode::leaf(PaneId(1)));
1099 }
1100 LayoutNode::Leaf { .. } => panic!("expected a split"),
1101 }
1102 let rects = tree.compute_rects(Rect::sized(80, 24));
1104 let main = rects.iter().find(|(p, _)| *p == PaneId(1)).unwrap().1;
1105 assert_eq!(main.x, 0);
1106 }
1107
1108 #[test]
1114 fn compute_rects_tiles_exactly_across_shapes_and_sizes() {
1115 let shapes = [
1116 LayoutNode::leaf(PaneId(1)),
1117 LayoutNode::split(
1118 SplitOrientation::Vertical,
1119 LayoutNode::leaf(PaneId(1)),
1120 LayoutNode::leaf(PaneId(2)),
1121 ),
1122 LayoutNode::split(
1123 SplitOrientation::Horizontal,
1124 LayoutNode::split(
1125 SplitOrientation::Vertical,
1126 LayoutNode::leaf(PaneId(1)),
1127 LayoutNode::leaf(PaneId(2)),
1128 ),
1129 LayoutNode::split(
1130 SplitOrientation::Vertical,
1131 LayoutNode::leaf(PaneId(3)),
1132 LayoutNode::split(
1133 SplitOrientation::Horizontal,
1134 LayoutNode::leaf(PaneId(4)),
1135 LayoutNode::leaf(PaneId(5)),
1136 ),
1137 ),
1138 ),
1139 ];
1140 for shape in &shapes {
1141 for &(w, h) in &[(80u16, 24u16), (81, 25), (1, 1), (3, 200), (200, 3), (2, 2)] {
1142 let bounds = Rect::sized(w, h);
1143 let rects = shape.compute_rects(bounds);
1144 let total: u32 = rects.iter().map(|(_, r)| r.area()).sum();
1145 assert_eq!(total, bounds.area(), "area mismatch at {w}x{h}");
1146 assert_eq!(rects.len(), shape.pane_count());
1148 if bounds.area() <= 8192 {
1151 for y in 0..h {
1152 for x in 0..w {
1153 let owners = rects.iter().filter(|(_, r)| r.contains(x, y)).count();
1154 assert!(owners <= 1, "cell ({x},{y}) owned by {owners} at {w}x{h}");
1155 }
1156 }
1157 }
1158 }
1159 }
1160 }
1161
1162 #[test]
1165 fn resize_leaf_grows_focused_pane_rightward() {
1166 let mut n = LayoutNode::split(
1168 SplitOrientation::Vertical,
1169 LayoutNode::leaf(PaneId(1)),
1170 LayoutNode::leaf(PaneId(2)),
1171 );
1172 let before = n.compute_rects(Rect::sized(80, 24));
1173 let w1_before = before.iter().find(|(p, _)| *p == PaneId(1)).unwrap().1.w;
1174 assert!(n.resize_leaf(PaneId(1), Direction::Right, 0.2));
1175 let after = n.compute_rects(Rect::sized(80, 24));
1176 let w1_after = after.iter().find(|(p, _)| *p == PaneId(1)).unwrap().1.w;
1177 assert!(w1_after > w1_before, "{w1_after} !> {w1_before}");
1178 }
1179
1180 #[test]
1181 fn resize_leaf_grows_pane_on_side_b_too() {
1182 let mut n = LayoutNode::split(
1184 SplitOrientation::Vertical,
1185 LayoutNode::leaf(PaneId(1)),
1186 LayoutNode::leaf(PaneId(2)),
1187 );
1188 let before = n.compute_rects(Rect::sized(80, 24));
1189 let w2_before = before.iter().find(|(p, _)| *p == PaneId(2)).unwrap().1.w;
1190 assert!(n.resize_leaf(PaneId(2), Direction::Left, 0.2));
1191 let after = n.compute_rects(Rect::sized(80, 24));
1192 let w2_after = after.iter().find(|(p, _)| *p == PaneId(2)).unwrap().1.w;
1193 assert!(w2_after > w2_before, "{w2_after} !> {w2_before}");
1194 }
1195
1196 #[test]
1197 fn resize_leaf_grows_toward_outer_neighbour_across_a_deeper_split() {
1198 let mut n = LayoutNode::split(
1203 SplitOrientation::Vertical,
1204 LayoutNode::leaf(PaneId(1)),
1205 LayoutNode::split(
1206 SplitOrientation::Vertical,
1207 LayoutNode::leaf(PaneId(2)),
1208 LayoutNode::leaf(PaneId(3)),
1209 ),
1210 );
1211 let b = Rect::sized(90, 24);
1212 let w2_before = n.compute_rects(b).iter().find(|(p, _)| *p == PaneId(2)).unwrap().1.w;
1213 assert!(n.resize_leaf(PaneId(2), Direction::Left, 0.2));
1214 let w2_after = n.compute_rects(b).iter().find(|(p, _)| *p == PaneId(2)).unwrap().1.w;
1215 assert!(w2_after > w2_before, "grow-Left should enlarge pane 2: {w2_before} -> {w2_after}");
1216 }
1217
1218 #[test]
1219 fn resize_leaf_side_b_of_deeper_split_grows_right_toward_outer_neighbour() {
1220 let mut n = LayoutNode::split(
1224 SplitOrientation::Vertical,
1225 LayoutNode::split(
1226 SplitOrientation::Vertical,
1227 LayoutNode::leaf(PaneId(1)),
1228 LayoutNode::leaf(PaneId(2)),
1229 ),
1230 LayoutNode::leaf(PaneId(3)),
1231 );
1232 let b = Rect::sized(90, 24);
1233 let w2_before = n.compute_rects(b).iter().find(|(p, _)| *p == PaneId(2)).unwrap().1.w;
1234 assert!(n.resize_leaf(PaneId(2), Direction::Right, 0.2));
1235 let w2_after = n.compute_rects(b).iter().find(|(p, _)| *p == PaneId(2)).unwrap().1.w;
1236 assert!(w2_after > w2_before, "grow-Right should enlarge pane 2: {w2_before} -> {w2_after}");
1237 }
1238
1239 #[test]
1240 fn resize_leaf_no_neighbour_that_way_is_noop() {
1241 let mut n = LayoutNode::split(
1244 SplitOrientation::Vertical,
1245 LayoutNode::leaf(PaneId(1)),
1246 LayoutNode::leaf(PaneId(2)),
1247 );
1248 assert!(!n.resize_leaf(PaneId(1), Direction::Left, 0.2));
1249 }
1250
1251 #[test]
1252 fn resize_leaf_grows_focused_pane_in_all_four_directions() {
1253 let cases: &[(SplitOrientation, Direction)] = &[
1258 (SplitOrientation::Vertical, Direction::Right),
1259 (SplitOrientation::Vertical, Direction::Left),
1260 (SplitOrientation::Horizontal, Direction::Below),
1261 (SplitOrientation::Horizontal, Direction::Above),
1262 ];
1263 for &(orient, dir) in cases {
1264 let (focus, other) = match dir {
1268 Direction::Right | Direction::Below => (PaneId(1), PaneId(2)),
1269 Direction::Left | Direction::Above => (PaneId(2), PaneId(1)),
1270 };
1271 let mut n = LayoutNode::split(
1272 orient,
1273 LayoutNode::leaf(PaneId(1)),
1274 LayoutNode::leaf(PaneId(2)),
1275 );
1276 let b = Rect::sized(80, 24);
1277 let axis = |r: Rect| if orient == SplitOrientation::Vertical { r.w } else { r.h };
1278 let before = axis(n.compute_rects(b).iter().find(|(p, _)| *p == focus).unwrap().1);
1279 assert!(n.resize_leaf(focus, dir, 0.2), "{dir:?} should find a divider");
1280 let after = axis(n.compute_rects(b).iter().find(|(p, _)| *p == focus).unwrap().1);
1281 assert!(after > before, "focus pane should grow {dir:?}: {before} -> {after}");
1282 let _ = other;
1283 }
1284 }
1285
1286 #[test]
1287 fn split_leaf_nan_ratio_coerces_to_valid_split() {
1288 let mut n = LayoutNode::leaf(PaneId(1));
1291 assert!(n.split_leaf(PaneId(1), PaneId(2), Direction::Right, f32::NAN));
1292 n.validate().unwrap();
1293 }
1294
1295 #[test]
1296 fn resize_leaf_nan_delta_leaves_tree_valid() {
1297 let mut n = LayoutNode::split(
1298 SplitOrientation::Vertical,
1299 LayoutNode::leaf(PaneId(1)),
1300 LayoutNode::leaf(PaneId(2)),
1301 );
1302 n.resize_leaf(PaneId(1), Direction::Right, f32::NAN);
1304 n.validate().unwrap();
1305 }
1306
1307 #[test]
1308 fn resize_leaf_ignores_wrong_axis() {
1309 let mut n = LayoutNode::split(
1311 SplitOrientation::Vertical,
1312 LayoutNode::leaf(PaneId(1)),
1313 LayoutNode::leaf(PaneId(2)),
1314 );
1315 assert!(!n.resize_leaf(PaneId(1), Direction::Below, 0.2));
1316 }
1317
1318 #[test]
1319 fn resize_leaf_picks_deepest_governing_split() {
1320 let mut n = LayoutNode::split(
1324 SplitOrientation::Horizontal,
1325 LayoutNode::split(
1326 SplitOrientation::Vertical,
1327 LayoutNode::leaf(PaneId(1)),
1328 LayoutNode::leaf(PaneId(2)),
1329 ),
1330 LayoutNode::leaf(PaneId(3)),
1331 );
1332 let bounds = Rect::sized(80, 24);
1333 let w1_before = n.compute_rects(bounds).iter().find(|(p, _)| *p == PaneId(1)).unwrap().1.w;
1334 let h3_before = n.compute_rects(bounds).iter().find(|(p, _)| *p == PaneId(3)).unwrap().1.h;
1335 assert!(n.resize_leaf(PaneId(1), Direction::Right, 0.2));
1336 let after = n.compute_rects(bounds);
1337 let w1_after = after.iter().find(|(p, _)| *p == PaneId(1)).unwrap().1.w;
1338 let h3_after = after.iter().find(|(p, _)| *p == PaneId(3)).unwrap().1.h;
1339 assert!(w1_after > w1_before); assert_eq!(h3_after, h3_before); }
1342
1343 #[test]
1346 fn validate_rejects_null_leaf() {
1347 let n = LayoutNode::leaf(PaneId::NULL);
1348 assert_eq!(n.validate(), Err(LayoutError::NullLeaf));
1349 }
1350
1351 #[test]
1352 fn validate_rejects_duplicate_pane() {
1353 let n = LayoutNode::split(
1354 SplitOrientation::Vertical,
1355 LayoutNode::leaf(PaneId(5)),
1356 LayoutNode::leaf(PaneId(5)),
1357 );
1358 assert_eq!(n.validate(), Err(LayoutError::DuplicatePane(PaneId(5))));
1359 }
1360
1361 #[test]
1370 fn a_degenerate_ratio_has_no_representation() {
1371 for bad in [0.0, 1.0, -3.0, 42.0] {
1372 let r = SplitRatio::new(bad);
1373 assert!(
1374 r.get() >= MIN_RATIO && r.get() <= 1.0 - MIN_RATIO,
1375 "{bad} must refine into range, got {}",
1376 r.get()
1377 );
1378 }
1379 let n = LayoutNode::Split {
1382 orientation: SplitOrientation::Vertical,
1383 ratio: SplitRatio::new(0.0),
1384 a: Box::new(LayoutNode::leaf(PaneId(1))),
1385 b: Box::new(LayoutNode::leaf(PaneId(2))),
1386 };
1387 n.validate().expect("a refined ratio always validates");
1388 }
1389
1390 #[test]
1395 fn a_nan_ratio_cannot_reach_the_geometry() {
1396 assert_eq!(SplitRatio::new(f32::NAN).get(), SplitRatio::BALANCED.get());
1397 assert_eq!(SplitRatio::new(f32::INFINITY).get(), SplitRatio::BALANCED.get());
1398 assert_eq!(
1399 SplitRatio::new(f32::NEG_INFINITY).get(),
1400 SplitRatio::BALANCED.get()
1401 );
1402
1403 let n = LayoutNode::Split {
1405 orientation: SplitOrientation::Vertical,
1406 ratio: SplitRatio::new(f32::NAN),
1407 a: Box::new(LayoutNode::leaf(PaneId(1))),
1408 b: Box::new(LayoutNode::leaf(PaneId(2))),
1409 };
1410 let rects = n.compute_rects(Rect::sized(80, 24));
1411 assert_eq!(rects.len(), 2);
1412 for (pane, r) in rects {
1413 assert!(r.w > 0 && r.h > 0, "pane {pane:?} vanished: {r:?}");
1414 }
1415 }
1416
1417 #[test]
1420 fn deserialisation_refines_a_hostile_ratio() {
1421 let zero: SplitRatio = serde_json::from_str("0.0").expect("deserialises");
1422 assert!(zero.get() >= MIN_RATIO, "wire value must be refined");
1423
1424 let huge: SplitRatio = serde_json::from_str("42.0").expect("deserialises");
1425 assert!(huge.get() <= 1.0 - MIN_RATIO, "wire value must be refined");
1426
1427 let neg: SplitRatio = serde_json::from_str("-3.0").expect("deserialises");
1428 assert!(neg.get() >= MIN_RATIO, "wire value must be refined");
1429 }
1430
1431 #[test]
1439 fn split_ratio_is_wire_identical_to_the_bare_f32_it_replaced() {
1440 let as_ratio = serde_json::to_string(&SplitRatio::new(0.25)).unwrap();
1441 let as_f32 = serde_json::to_string(&0.25_f32).unwrap();
1442 assert_eq!(
1443 as_ratio, as_f32,
1444 "SplitRatio must serialise exactly like the f32 it replaced, or \
1445 a running daemon cannot talk to a new client"
1446 );
1447
1448 let tree = LayoutNode::Split {
1450 orientation: SplitOrientation::Vertical,
1451 ratio: SplitRatio::new(0.25),
1452 a: Box::new(LayoutNode::leaf(PaneId(1))),
1453 b: Box::new(LayoutNode::leaf(PaneId(2))),
1454 };
1455 let json = serde_json::to_string(&tree).unwrap();
1456 let back: LayoutNode = serde_json::from_str(&json).unwrap();
1457 assert_eq!(back, tree);
1458 }
1459
1460 #[test]
1461 fn validate_accepts_well_formed_tree() {
1462 let n = LayoutNode::split(
1463 SplitOrientation::Vertical,
1464 LayoutNode::leaf(PaneId(1)),
1465 LayoutNode::split(
1466 SplitOrientation::Horizontal,
1467 LayoutNode::leaf(PaneId(2)),
1468 LayoutNode::leaf(PaneId(3)),
1469 ),
1470 );
1471 n.validate().unwrap();
1472 }
1473
1474 #[test]
1477 fn split_then_remove_is_identity() {
1478 let original = LayoutNode::leaf(PaneId(1));
1479 let mut n = original.clone();
1480 n.split_leaf(PaneId(1), PaneId(2), Direction::Right, 0.5);
1481 assert_eq!(n.pane_count(), 2);
1482 assert_eq!(n.remove_leaf(PaneId(2)), LeafRemoval::Removed);
1483 assert_eq!(n, original);
1484 }
1485}