Skip to main content

term_wm_layout_engine/
tiling.rs

1use core::sync::atomic::{AtomicUsize, Ordering};
2
3use crate::BspNode;
4use crate::rect::{LayoutRect, Orientation, rect_contains as engine_rect_contains};
5use crate::snap::InsertPosition;
6use crate::split;
7
8static VOID_ID_COUNTER: AtomicUsize = AtomicUsize::new(1);
9
10#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
11pub enum Direction {
12    #[default]
13    Horizontal,
14    Vertical,
15}
16
17impl From<Direction> for Orientation {
18    fn from(d: Direction) -> Self {
19        match d {
20            Direction::Horizontal => Orientation::Horizontal,
21            Direction::Vertical => Orientation::Vertical,
22        }
23    }
24}
25
26impl From<Orientation> for Direction {
27    fn from(o: Orientation) -> Self {
28        match o {
29            Orientation::Horizontal => Direction::Horizontal,
30            Orientation::Vertical => Direction::Vertical,
31        }
32    }
33}
34
35#[derive(Debug, Clone)]
36pub struct SplitGap {
37    pub rect: LayoutRect,
38    pub path: Vec<usize>,
39    pub index: usize,
40    pub direction: Direction,
41}
42
43#[derive(Debug, Clone)]
44pub enum LayoutNode<Id: Copy + Eq + Ord> {
45    Leaf(Id),
46    Void(usize),
47    Split {
48        direction: Direction,
49        children: Vec<LayoutNode<Id>>,
50        weights: Vec<u16>,
51        resizable: bool,
52    },
53}
54
55impl<Id: Copy + Eq + Ord> From<BspNode<Id>> for LayoutNode<Id> {
56    fn from(bsp: BspNode<Id>) -> Self {
57        match bsp {
58            BspNode::Leaf(id) => LayoutNode::leaf(id),
59            BspNode::Split {
60                orientation,
61                left,
62                right,
63                ratio,
64            } => {
65                let direction = Direction::from(orientation);
66                let left_node: LayoutNode<Id> = LayoutNode::from(*left);
67                let right_node: LayoutNode<Id> = LayoutNode::from(*right);
68                let weights = if ratio.total() == 0 {
69                    vec![1u16, 1u16]
70                } else {
71                    vec![ratio.left_part(), ratio.right_part()]
72                };
73                LayoutNode::Split {
74                    direction,
75                    children: vec![left_node, right_node],
76                    weights,
77                    resizable: true,
78                }
79            }
80        }
81    }
82}
83
84impl<Id: Copy + Eq + Ord> LayoutNode<Id> {
85    pub fn leaf(id: Id) -> Self {
86        Self::Leaf(id)
87    }
88
89    pub fn void() -> Self {
90        Self::Void(VOID_ID_COUNTER.fetch_add(1, Ordering::Relaxed))
91    }
92
93    pub fn split(direction: Direction, children: Vec<LayoutNode<Id>>) -> Self {
94        Self::Split {
95            direction,
96            children,
97            weights: Vec::new(),
98            resizable: true,
99        }
100    }
101
102    pub fn split_resizable(
103        direction: Direction,
104        children: Vec<LayoutNode<Id>>,
105        resizable: bool,
106    ) -> Self {
107        Self::Split {
108            direction,
109            children,
110            weights: Vec::new(),
111            resizable,
112        }
113    }
114
115    pub fn unwrap_leaf(&self) -> Option<Id> {
116        match self {
117            LayoutNode::Leaf(id) => Some(*id),
118            _ => None,
119        }
120    }
121
122    pub fn layout_rects(&self, area: LayoutRect) -> Vec<(Id, LayoutRect)> {
123        self.layout_with_gaps(area).0
124    }
125
126    pub fn layout_with_gaps(&self, area: LayoutRect) -> (Vec<(Id, LayoutRect)>, Vec<SplitGap>) {
127        let mut regions = Vec::new();
128        let mut gaps = Vec::new();
129        self.layout_recursive(area, &mut regions, &mut gaps, &mut Vec::new());
130        (regions, gaps)
131    }
132
133    pub fn node_at_path(&self, path: &[usize]) -> Option<&LayoutNode<Id>> {
134        let mut current = self;
135        for &idx in path {
136            let LayoutNode::Split { children, .. } = current else {
137                return None;
138            };
139            current = children.get(idx)?;
140        }
141        Some(current)
142    }
143
144    pub fn collect_leaves(&self) -> Vec<Id> {
145        let mut ids = Vec::new();
146        self.collect_leaves_recursive(&mut ids);
147        ids
148    }
149
150    fn collect_leaves_recursive(&self, out: &mut Vec<Id>) {
151        match self {
152            LayoutNode::Leaf(id) => out.push(*id),
153            LayoutNode::Split { children, .. } => {
154                for child in children {
155                    child.collect_leaves_recursive(out);
156                }
157            }
158            _ => {}
159        }
160    }
161
162    pub fn swap_leaves(&mut self, source: &Id, target: &Id) -> bool {
163        let mut source_path = Vec::new();
164        let mut target_path = Vec::new();
165        if !self.find_leaf_path(source, &mut source_path, &mut Vec::new()) {
166            return false;
167        }
168        if !self.find_leaf_path(target, &mut target_path, &mut Vec::new()) {
169            return false;
170        }
171        let source_id = {
172            let source_node = self.node_at_path_mut(&source_path);
173            match source_node {
174                Some(LayoutNode::Leaf(id)) => *id,
175                _ => return false,
176            }
177        };
178        {
179            let source_node = self.node_at_path_mut(&source_path);
180            if let Some(node) = source_node {
181                *node = LayoutNode::Void(VOID_ID_COUNTER.fetch_add(1, Ordering::Relaxed));
182            }
183        }
184        {
185            let target_node = self.node_at_path_mut(&target_path);
186            match target_node {
187                Some(LayoutNode::Leaf(target_id)) => {
188                    let target_id_copy = *target_id;
189                    if let Some(node) = self.node_at_path_mut(&target_path) {
190                        *node = LayoutNode::Leaf(source_id);
191                    }
192                    if let Some(node) = self.node_at_path_mut(&source_path) {
193                        *node = LayoutNode::Leaf(target_id_copy);
194                    }
195                    true
196                }
197                _ => false,
198            }
199        }
200    }
201
202    fn find_leaf_path(&self, target: &Id, path: &mut Vec<usize>, current: &mut Vec<usize>) -> bool {
203        match self {
204            LayoutNode::Leaf(id) if id == target => {
205                path.extend_from_slice(current);
206                true
207            }
208            LayoutNode::Split { children, .. } => {
209                for (idx, child) in children.iter().enumerate() {
210                    current.push(idx);
211                    if child.find_leaf_path(target, path, current) {
212                        return true;
213                    }
214                    current.pop();
215                }
216                false
217            }
218            _ => false,
219        }
220    }
221
222    fn node_at_path_mut(&mut self, path: &[usize]) -> Option<&mut LayoutNode<Id>> {
223        let mut current = self;
224        for &idx in path {
225            let LayoutNode::Split { children, .. } = current else {
226                return None;
227            };
228            current = children.get_mut(idx)?;
229        }
230        Some(current)
231    }
232
233    pub fn build_flat(direction: Direction, ids: Vec<Id>) -> Self {
234        if ids.is_empty() {
235            return LayoutNode::Void(VOID_ID_COUNTER.fetch_add(1, Ordering::Relaxed));
236        }
237        if ids.len() == 1 {
238            return LayoutNode::leaf(ids[0]);
239        }
240        let n = ids.len();
241        LayoutNode::Split {
242            direction,
243            children: ids.into_iter().map(LayoutNode::leaf).collect(),
244            weights: vec![1u16; n],
245            resizable: true,
246        }
247    }
248
249    pub fn subtree_any<F>(&self, mut predicate: F) -> bool
250    where
251        F: FnMut(Id) -> bool,
252    {
253        fn walk<Id: Copy + Eq + Ord, F: FnMut(Id) -> bool>(
254            node: &LayoutNode<Id>,
255            predicate: &mut F,
256        ) -> bool {
257            match node {
258                LayoutNode::Leaf(id) => predicate(*id),
259                LayoutNode::Void(_) => false,
260                LayoutNode::Split { children, .. } => {
261                    children.iter().any(|child| walk(child, predicate))
262                }
263            }
264        }
265        walk(self, &mut predicate)
266    }
267
268    pub fn hit_test_gap(&self, area: LayoutRect, column: u16, row: u16) -> Option<SplitGap> {
269        let (_, gaps) = self.layout_with_gaps(area);
270        gaps.into_iter()
271            .find(|gap| engine_rect_contains(&gap.rect, column, row))
272    }
273
274    pub fn apply_drag(
275        &mut self,
276        area: LayoutRect,
277        path: &[usize],
278        index: usize,
279        direction: Direction,
280        delta: i16,
281        min_size: i16,
282    ) -> bool {
283        let Some(split_area) = split_area_for_path(self, area, path) else {
284            return false;
285        };
286        let Some(split) = split_at_path_mut(self, path) else {
287            return false;
288        };
289        let LayoutNode::Split {
290            weights,
291            children,
292            resizable,
293            ..
294        } = split
295        else {
296            return false;
297        };
298        if !*resizable || children.len() < 2 || index + 1 >= children.len() {
299            return false;
300        }
301        let orientation = Orientation::from(direction);
302        let total_dim = match direction {
303            Direction::Horizontal => split_area.width,
304            Direction::Vertical => split_area.height,
305        };
306        let gap = split::gap_size(orientation, total_dim, children.len(), *resizable);
307        let sizes = split::split_sizes(
308            split_area,
309            orientation,
310            weights.as_slice(),
311            children.len(),
312            gap,
313        );
314        if sizes.is_empty() {
315            return false;
316        }
317        let mut sizes = sizes.into_iter().map(|v| v as i16).collect::<Vec<_>>();
318        let total_pair = sizes[index] + sizes[index + 1];
319        let mut left = sizes[index] + delta;
320        let min_left = min_size;
321        let max_left = (total_pair - min_size).max(min_size);
322        left = left.clamp(min_left, max_left);
323        let right = total_pair - left;
324        sizes[index] = left;
325        sizes[index + 1] = right;
326        *weights = sizes.iter().map(|v| (*v).max(1) as u16).collect();
327        true
328    }
329
330    pub fn remove_leaf(&mut self, id: Id) -> bool {
331        match self {
332            LayoutNode::Leaf(_) => false,
333            LayoutNode::Void(_) => false,
334            LayoutNode::Split {
335                children, weights, ..
336            } => {
337                let mut removed = false;
338                let mut index = 0;
339                while index < children.len() {
340                    let is_target = match &children[index] {
341                        LayoutNode::Leaf(i) => *i == id,
342                        _ => false,
343                    };
344                    if is_target {
345                        children.remove(index);
346                        if index < weights.len() {
347                            weights.remove(index);
348                        }
349                        removed = true;
350                        break;
351                    }
352                    if children[index].remove_leaf(id) {
353                        removed = true;
354                        let is_empty_split = match &children[index] {
355                            LayoutNode::Split { children: s, .. } => s.is_empty(),
356                            _ => false,
357                        };
358                        if is_empty_split {
359                            children.remove(index);
360                            if index < weights.len() {
361                                weights.remove(index);
362                            }
363                        }
364                        break;
365                    }
366                    index += 1;
367                }
368                if removed {
369                    if children.len() == 1 {
370                        let only = children.remove(0);
371                        *self = only;
372                    } else if children.iter().all(|c| matches!(c, LayoutNode::Void(_))) {
373                        *self = LayoutNode::Void(VOID_ID_COUNTER.fetch_add(1, Ordering::Relaxed));
374                        return true;
375                    }
376                }
377                removed
378            }
379        }
380    }
381
382    pub fn clear_leaf(&mut self, id: Id) -> bool {
383        if matches!(self, LayoutNode::Leaf(current) if *current == id) {
384            *self = LayoutNode::Void(VOID_ID_COUNTER.fetch_add(1, Ordering::Relaxed));
385            true
386        } else {
387            false
388        }
389    }
390
391    /// Replace a leaf identified by `id` with a Void placeholder, preserving
392    /// the tree structure and split weights. Returns the fresh void_id, or
393    /// None if the leaf was not found.
394    pub fn replace_leaf_with_void(&mut self, id: Id) -> Option<usize> {
395        let mut path = Vec::new();
396        if !self.find_leaf_path(&id, &mut path, &mut Vec::new()) {
397            return None;
398        }
399        let void_id = VOID_ID_COUNTER.fetch_add(1, Ordering::Relaxed);
400        if let Some(node) = self.node_at_path_mut(&path) {
401            *node = LayoutNode::Void(void_id);
402            Some(void_id)
403        } else {
404            None
405        }
406    }
407
408    pub fn insert_leaf(&mut self, target: Id, insert: Id, position: InsertPosition) -> bool {
409        match self {
410            LayoutNode::Leaf(current) => {
411                if *current != target {
412                    return false;
413                }
414                match position {
415                    InsertPosition::Left => {
416                        *self = LayoutNode::Split {
417                            direction: Direction::Horizontal,
418                            children: vec![LayoutNode::leaf(insert), LayoutNode::leaf(*current)],
419                            weights: vec![1u16, 1u16],
420                            resizable: true,
421                        };
422                    }
423                    InsertPosition::Right => {
424                        *self = LayoutNode::Split {
425                            direction: Direction::Horizontal,
426                            children: vec![LayoutNode::leaf(*current), LayoutNode::leaf(insert)],
427                            weights: vec![1u16, 1u16],
428                            resizable: true,
429                        };
430                    }
431                    InsertPosition::Top => {
432                        *self = LayoutNode::Split {
433                            direction: Direction::Vertical,
434                            children: vec![LayoutNode::leaf(insert), LayoutNode::leaf(*current)],
435                            weights: vec![1u16, 1u16],
436                            resizable: true,
437                        };
438                    }
439                    InsertPosition::Bottom => {
440                        *self = LayoutNode::Split {
441                            direction: Direction::Vertical,
442                            children: vec![LayoutNode::leaf(*current), LayoutNode::leaf(insert)],
443                            weights: vec![1u16, 1u16],
444                            resizable: true,
445                        };
446                    }
447                    InsertPosition::TopLeft => {
448                        let inner = LayoutNode::Split {
449                            direction: Direction::Vertical,
450                            children: vec![
451                                LayoutNode::leaf(insert),
452                                LayoutNode::Void(VOID_ID_COUNTER.fetch_add(1, Ordering::Relaxed)),
453                            ],
454                            weights: vec![1u16, 1u16],
455                            resizable: true,
456                        };
457                        *self = LayoutNode::Split {
458                            direction: Direction::Horizontal,
459                            children: vec![inner, LayoutNode::leaf(*current)],
460                            weights: vec![1u16, 1u16],
461                            resizable: true,
462                        };
463                    }
464                    InsertPosition::TopRight => {
465                        let inner = LayoutNode::Split {
466                            direction: Direction::Vertical,
467                            children: vec![
468                                LayoutNode::leaf(insert),
469                                LayoutNode::Void(VOID_ID_COUNTER.fetch_add(1, Ordering::Relaxed)),
470                            ],
471                            weights: vec![1u16, 1u16],
472                            resizable: true,
473                        };
474                        *self = LayoutNode::Split {
475                            direction: Direction::Horizontal,
476                            children: vec![LayoutNode::leaf(*current), inner],
477                            weights: vec![1u16, 1u16],
478                            resizable: true,
479                        };
480                    }
481                    InsertPosition::BottomLeft => {
482                        let inner = LayoutNode::Split {
483                            direction: Direction::Vertical,
484                            children: vec![
485                                LayoutNode::Void(VOID_ID_COUNTER.fetch_add(1, Ordering::Relaxed)),
486                                LayoutNode::leaf(insert),
487                            ],
488                            weights: vec![1u16, 1u16],
489                            resizable: true,
490                        };
491                        *self = LayoutNode::Split {
492                            direction: Direction::Horizontal,
493                            children: vec![inner, LayoutNode::leaf(*current)],
494                            weights: vec![1u16, 1u16],
495                            resizable: true,
496                        };
497                    }
498                    InsertPosition::BottomRight => {
499                        let inner = LayoutNode::Split {
500                            direction: Direction::Vertical,
501                            children: vec![
502                                LayoutNode::Void(VOID_ID_COUNTER.fetch_add(1, Ordering::Relaxed)),
503                                LayoutNode::leaf(insert),
504                            ],
505                            weights: vec![1u16, 1u16],
506                            resizable: true,
507                        };
508                        *self = LayoutNode::Split {
509                            direction: Direction::Horizontal,
510                            children: vec![LayoutNode::leaf(*current), inner],
511                            weights: vec![1u16, 1u16],
512                            resizable: true,
513                        };
514                    }
515                }
516                true
517            }
518            LayoutNode::Void(_) => false,
519            LayoutNode::Split { children, .. } => {
520                for child in children.iter_mut() {
521                    if child.insert_leaf(target, insert, position) {
522                        return true;
523                    }
524                }
525                false
526            }
527        }
528    }
529
530    pub fn void_regions(&self, area: LayoutRect) -> Vec<(usize, LayoutRect)> {
531        let mut rects = Vec::new();
532        self.void_regions_recursive(area, &mut rects);
533        rects
534    }
535
536    fn void_regions_recursive(&self, area: LayoutRect, out: &mut Vec<(usize, LayoutRect)>) {
537        match self {
538            LayoutNode::Void(id) => out.push((*id, area)),
539            LayoutNode::Split {
540                direction,
541                children,
542                weights,
543                resizable,
544            } => {
545                let orientation = Orientation::from(*direction);
546                let total_dim = match direction {
547                    Direction::Horizontal => area.width,
548                    Direction::Vertical => area.height,
549                };
550                let gap = split::gap_size(orientation, total_dim, children.len(), *resizable);
551                let (rects, _) = split::split_rects_with_gaps(
552                    area,
553                    orientation,
554                    weights.as_slice(),
555                    children.len(),
556                    gap,
557                );
558                for (child, sub) in children.iter().zip(rects) {
559                    child.void_regions_recursive(sub, out);
560                }
561            }
562            _ => {}
563        }
564    }
565
566    #[allow(clippy::single_match)]
567    pub fn cleanup_after_removal(&mut self) {
568        match self {
569            LayoutNode::Split {
570                children, weights, ..
571            } => {
572                for child in children.iter_mut() {
573                    child.cleanup_after_removal();
574                }
575                let mut i = 0;
576                while i < children.len() {
577                    if matches!(children[i], LayoutNode::Void(_)) {
578                        children.remove(i);
579                        if i < weights.len() {
580                            weights.remove(i);
581                        }
582                    } else {
583                        i += 1;
584                    }
585                }
586                match children.len() {
587                    0 => {
588                        *self = LayoutNode::Void(VOID_ID_COUNTER.fetch_add(1, Ordering::Relaxed));
589                    }
590                    1 => {
591                        let only = children.remove(0);
592                        *self = only;
593                    }
594                    _ => {
595                        if children.iter().all(|c| matches!(c, LayoutNode::Void(_))) {
596                            *self =
597                                LayoutNode::Void(VOID_ID_COUNTER.fetch_add(1, Ordering::Relaxed));
598                        }
599                    }
600                }
601            }
602            _ => {}
603        }
604    }
605
606    #[allow(clippy::single_match)]
607    pub fn normalize_weights(&mut self) {
608        match self {
609            LayoutNode::Split {
610                weights, children, ..
611            } => {
612                for w in weights.iter_mut() {
613                    *w = 1;
614                }
615                for child in children.iter_mut() {
616                    child.normalize_weights();
617                }
618            }
619            _ => {}
620        }
621    }
622
623    /// Reweight every `Split` to its children's descendant leaf counts, so all
624    /// leaves occupy an equal share of the parent area regardless of tree depth
625    /// (a leaf in a shallow branch gets the same area as a leaf in a deep one).
626    /// Returns this subtree's leaf count.
627    pub fn reweight_by_leaf_count(&mut self) -> u16 {
628        match self {
629            LayoutNode::Leaf(_) | LayoutNode::Void(_) => 1,
630            LayoutNode::Split {
631                children, weights, ..
632            } => {
633                let counts: Vec<u16> = children
634                    .iter_mut()
635                    .map(|c| c.reweight_by_leaf_count())
636                    .collect();
637                let total = counts.iter().fold(0u16, |acc, &x| acc.saturating_add(x));
638                *weights = counts;
639                total
640            }
641        }
642    }
643
644    pub fn replace_void_by_id(&mut self, void_id: usize, new_leaf: LayoutNode<Id>) -> bool {
645        match self {
646            LayoutNode::Void(id) if *id == void_id => {
647                *self = new_leaf;
648                true
649            }
650            LayoutNode::Split { children, .. } => {
651                for child in children.iter_mut() {
652                    if child.replace_void_by_id(void_id, new_leaf.clone()) {
653                        return true;
654                    }
655                }
656                false
657            }
658            _ => false,
659        }
660    }
661
662    /// Remove a Void node by its ID from the tree. Returns true if the void
663    /// was found and removed, false otherwise. Cleans up empty parent splits.
664    pub fn remove_void_by_id(&mut self, void_id: usize) -> bool {
665        match self {
666            LayoutNode::Void(id) if *id == void_id => {
667                *self = LayoutNode::Void(VOID_ID_COUNTER.fetch_add(1, Ordering::Relaxed));
668                true
669            }
670            LayoutNode::Split {
671                children, weights, ..
672            } => {
673                let mut i = 0;
674                while i < children.len() {
675                    if children[i].remove_void_by_id(void_id) {
676                        // If the child was replaced with a Void (the one we
677                        // just replaced above), remove it from the split.
678                        if matches!(children[i], LayoutNode::Void(_)) {
679                            children.remove(i);
680                            if i < weights.len() {
681                                weights.remove(i);
682                            }
683                        }
684                        return true;
685                    }
686                    i += 1;
687                }
688                false
689            }
690            _ => false,
691        }
692    }
693
694    pub fn is_empty(&self) -> bool {
695        match self {
696            LayoutNode::Void(_) => true,
697            LayoutNode::Leaf(_) => false,
698            LayoutNode::Split { children, .. } => children.iter().all(|c| c.is_empty()),
699        }
700    }
701
702    fn layout_recursive(
703        &self,
704        area: LayoutRect,
705        regions: &mut Vec<(Id, LayoutRect)>,
706        gaps: &mut Vec<SplitGap>,
707        path: &mut Vec<usize>,
708    ) {
709        match self {
710            LayoutNode::Leaf(id) => {
711                regions.push((*id, area));
712            }
713            LayoutNode::Void(_) => {}
714            LayoutNode::Split {
715                direction,
716                children,
717                weights,
718                resizable,
719            } => {
720                let orientation = Orientation::from(*direction);
721                let total_dim = match direction {
722                    Direction::Horizontal => area.width,
723                    Direction::Vertical => area.height,
724                };
725                let gap = split::gap_size(orientation, total_dim, children.len(), *resizable);
726                let (rects, split_gaps) = split::split_rects_with_gaps(
727                    area,
728                    orientation,
729                    weights.as_slice(),
730                    children.len(),
731                    gap,
732                );
733                for (idx, (child, rect)) in children.iter().zip(rects.iter().copied()).enumerate() {
734                    path.push(idx);
735                    child.layout_recursive(rect, regions, gaps, path);
736                    path.pop();
737                }
738                if *resizable && children.len() > 1 {
739                    for (index, gap_rect) in split_gaps.into_iter().enumerate() {
740                        gaps.push(SplitGap {
741                            rect: gap_rect,
742                            path: path.clone(),
743                            index,
744                            direction: *direction,
745                        });
746                    }
747                }
748            }
749        }
750    }
751
752    /// Build a BSP tree from rectangles using straddle-tolerant cut selection.
753    /// Straddled windows are assigned to the side containing more of their area.
754    /// Weights are count-based to ensure each window gets equal space.
755    /// Fallback uses aspect-aware direction for the sort axis.
756    pub fn from_rects(rects: &[(Id, LayoutRect)]) -> Self {
757        if rects.is_empty() {
758            return Self::Void(0);
759        }
760        if rects.len() == 1 {
761            return Self::Leaf(rects[0].0);
762        }
763
764        let min_x = rects.iter().map(|(_, r)| r.x).min().unwrap_or(0);
765        let min_y = rects.iter().map(|(_, r)| r.y).min().unwrap_or(0);
766        let max_x = rects
767            .iter()
768            .map(|(_, r)| r.x.saturating_add(r.width as i32))
769            .max()
770            .unwrap_or(0);
771        let max_y = rects
772            .iter()
773            .map(|(_, r)| r.y.saturating_add(r.height as i32))
774            .max()
775            .unwrap_or(0);
776
777        struct CutCandidate<Id: Copy + Eq + Ord> {
778            direction: Direction,
779            straddles: usize,
780            balance_delta: usize,
781            part_a: Vec<(Id, LayoutRect)>,
782            part_b: Vec<(Id, LayoutRect)>,
783            weight_a: u16,
784            weight_b: u16,
785        }
786
787        let mut candidates: Vec<CutCandidate<Id>> = Vec::new();
788
789        let mut y_candidates: Vec<i32> = rects
790            .iter()
791            .flat_map(|(_, r)| [r.y, r.y.saturating_add(r.height as i32)])
792            .collect();
793        y_candidates.sort_unstable();
794        y_candidates.dedup();
795
796        for &y in &y_candidates {
797            if y <= min_y || y >= max_y {
798                continue;
799            }
800            let mut top = Vec::new();
801            let mut bottom = Vec::new();
802            let mut straddles = 0;
803
804            for &(k, r) in rects {
805                let r_bottom = r.y.saturating_add(r.height as i32);
806                if r_bottom <= y {
807                    top.push((k, r));
808                } else if r.y >= y {
809                    bottom.push((k, r));
810                } else {
811                    straddles += 1;
812                    let mid = r.y + (r.height as i32 / 2);
813                    if mid < y {
814                        top.push((k, r));
815                    } else {
816                        bottom.push((k, r));
817                    }
818                }
819            }
820
821            if !top.is_empty() && !bottom.is_empty() {
822                let balance_delta = (top.len() as isize - bottom.len() as isize).unsigned_abs();
823                let top_span = {
824                    let min = top.iter().map(|(_, r)| r.y).min().unwrap_or(min_y);
825                    let max = top
826                        .iter()
827                        .map(|(_, r)| r.y.saturating_add(r.height as i32))
828                        .max()
829                        .unwrap_or(y);
830                    max.saturating_sub(min).clamp(1, i32::from(u16::MAX)) as u16
831                };
832                let bot_span = {
833                    let min = bottom.iter().map(|(_, r)| r.y).min().unwrap_or(y);
834                    let max = bottom
835                        .iter()
836                        .map(|(_, r)| r.y.saturating_add(r.height as i32))
837                        .max()
838                        .unwrap_or(max_y);
839                    max.saturating_sub(min).clamp(1, i32::from(u16::MAX)) as u16
840                };
841                candidates.push(CutCandidate {
842                    direction: Direction::Vertical,
843                    straddles,
844                    balance_delta,
845                    weight_a: top_span,
846                    weight_b: bot_span,
847                    part_a: top,
848                    part_b: bottom,
849                });
850            }
851        }
852
853        let mut x_candidates: Vec<i32> = rects
854            .iter()
855            .flat_map(|(_, r)| [r.x, r.x.saturating_add(r.width as i32)])
856            .collect();
857        x_candidates.sort_unstable();
858        x_candidates.dedup();
859
860        for &x in &x_candidates {
861            if x <= min_x || x >= max_x {
862                continue;
863            }
864            let mut left = Vec::new();
865            let mut right = Vec::new();
866            let mut straddles = 0;
867
868            for &(k, r) in rects {
869                let r_right = r.x.saturating_add(r.width as i32);
870                if r_right <= x {
871                    left.push((k, r));
872                } else if r.x >= x {
873                    right.push((k, r));
874                } else {
875                    straddles += 1;
876                    let mid = r.x + (r.width as i32 / 2);
877                    if mid < x {
878                        left.push((k, r));
879                    } else {
880                        right.push((k, r));
881                    }
882                }
883            }
884
885            if !left.is_empty() && !right.is_empty() {
886                let balance_delta = (left.len() as isize - right.len() as isize).unsigned_abs();
887                let left_span = {
888                    let min = left.iter().map(|(_, r)| r.x).min().unwrap_or(min_x);
889                    let max = left
890                        .iter()
891                        .map(|(_, r)| r.x.saturating_add(r.width as i32))
892                        .max()
893                        .unwrap_or(x);
894                    max.saturating_sub(min).clamp(1, i32::from(u16::MAX)) as u16
895                };
896                let right_span = {
897                    let min = right.iter().map(|(_, r)| r.x).min().unwrap_or(x);
898                    let max = right
899                        .iter()
900                        .map(|(_, r)| r.x.saturating_add(r.width as i32))
901                        .max()
902                        .unwrap_or(max_x);
903                    max.saturating_sub(min).clamp(1, i32::from(u16::MAX)) as u16
904                };
905                candidates.push(CutCandidate {
906                    direction: Direction::Horizontal,
907                    straddles,
908                    balance_delta,
909                    weight_a: left_span,
910                    weight_b: right_span,
911                    part_a: left,
912                    part_b: right,
913                });
914            }
915        }
916
917        if let Some(best) = candidates
918            .into_iter()
919            .min_by_key(|c| (c.straddles, c.balance_delta))
920        {
921            return Self::Split {
922                direction: best.direction,
923                children: vec![
924                    Self::from_rects(&best.part_a),
925                    Self::from_rects(&best.part_b),
926                ],
927                weights: vec![best.weight_a, best.weight_b],
928                resizable: true,
929            };
930        }
931
932        let total_w = max_x - min_x;
933        let total_h = (max_y - min_y) * 2;
934
935        let mut sorted = rects.to_vec();
936        let direction = if total_w >= total_h {
937            sorted.sort_unstable_by_key(|(_, r)| (r.x, r.y));
938            Direction::Horizontal
939        } else {
940            sorted.sort_unstable_by_key(|(_, r)| (r.y, r.x));
941            Direction::Vertical
942        };
943
944        let mid = sorted.len() / 2;
945        let left_slice = &sorted[..mid];
946        let right_slice = &sorted[mid..];
947        let (weight_a, weight_b) = if direction == Direction::Horizontal {
948            let left_span = {
949                let min = left_slice.iter().map(|(_, r)| r.x).min().unwrap_or(min_x);
950                let max = left_slice
951                    .iter()
952                    .map(|(_, r)| r.x.saturating_add(r.width as i32))
953                    .max()
954                    .unwrap_or(max_x);
955                max.saturating_sub(min).clamp(1, i32::from(u16::MAX)) as u16
956            };
957            let right_span = {
958                let min = right_slice.iter().map(|(_, r)| r.x).min().unwrap_or(min_x);
959                let max = right_slice
960                    .iter()
961                    .map(|(_, r)| r.x.saturating_add(r.width as i32))
962                    .max()
963                    .unwrap_or(max_x);
964                max.saturating_sub(min).clamp(1, i32::from(u16::MAX)) as u16
965            };
966            (left_span, right_span)
967        } else {
968            let top_span = {
969                let min = left_slice.iter().map(|(_, r)| r.y).min().unwrap_or(min_y);
970                let max = left_slice
971                    .iter()
972                    .map(|(_, r)| r.y.saturating_add(r.height as i32))
973                    .max()
974                    .unwrap_or(max_y);
975                max.saturating_sub(min).clamp(1, i32::from(u16::MAX)) as u16
976            };
977            let bot_span = {
978                let min = right_slice.iter().map(|(_, r)| r.y).min().unwrap_or(min_y);
979                let max = right_slice
980                    .iter()
981                    .map(|(_, r)| r.y.saturating_add(r.height as i32))
982                    .max()
983                    .unwrap_or(max_y);
984                max.saturating_sub(min).clamp(1, i32::from(u16::MAX)) as u16
985            };
986            (top_span, bot_span)
987        };
988        Self::Split {
989            direction,
990            children: vec![Self::from_rects(left_slice), Self::from_rects(right_slice)],
991            weights: vec![weight_a, weight_b],
992            resizable: true,
993        }
994    }
995
996    pub fn split_root(&mut self, insert: Id, position: InsertPosition) {
997        let existing_void_id = match self {
998            LayoutNode::Void(id) => Some(*id),
999            _ => None,
1000        };
1001        if let Some(existing_void_id) = existing_void_id {
1002            *self = match position {
1003                InsertPosition::Left | InsertPosition::TopLeft | InsertPosition::BottomLeft => {
1004                    LayoutNode::Split {
1005                        direction: Direction::Horizontal,
1006                        children: vec![
1007                            LayoutNode::leaf(insert),
1008                            LayoutNode::Void(existing_void_id),
1009                        ],
1010                        weights: vec![1u16, 1u16],
1011                        resizable: true,
1012                    }
1013                }
1014                InsertPosition::Right | InsertPosition::TopRight | InsertPosition::BottomRight => {
1015                    LayoutNode::Split {
1016                        direction: Direction::Horizontal,
1017                        children: vec![
1018                            LayoutNode::Void(existing_void_id),
1019                            LayoutNode::leaf(insert),
1020                        ],
1021                        weights: vec![1u16, 1u16],
1022                        resizable: true,
1023                    }
1024                }
1025                InsertPosition::Top => LayoutNode::Split {
1026                    direction: Direction::Vertical,
1027                    children: vec![LayoutNode::leaf(insert), LayoutNode::Void(existing_void_id)],
1028                    weights: vec![1u16, 1u16],
1029                    resizable: true,
1030                },
1031                InsertPosition::Bottom => LayoutNode::Split {
1032                    direction: Direction::Vertical,
1033                    children: vec![LayoutNode::Void(existing_void_id), LayoutNode::leaf(insert)],
1034                    weights: vec![1u16, 1u16],
1035                    resizable: true,
1036                },
1037            };
1038            return;
1039        }
1040        match position {
1041            InsertPosition::Left => {
1042                *self = LayoutNode::Split {
1043                    direction: Direction::Horizontal,
1044                    children: vec![LayoutNode::leaf(insert), self.clone()],
1045                    weights: vec![1u16, 1u16],
1046                    resizable: true,
1047                };
1048            }
1049            InsertPosition::Right => {
1050                *self = LayoutNode::Split {
1051                    direction: Direction::Horizontal,
1052                    children: vec![self.clone(), LayoutNode::leaf(insert)],
1053                    weights: vec![1u16, 1u16],
1054                    resizable: true,
1055                };
1056            }
1057            InsertPosition::Top => {
1058                *self = LayoutNode::Split {
1059                    direction: Direction::Vertical,
1060                    children: vec![LayoutNode::leaf(insert), self.clone()],
1061                    weights: vec![1u16, 1u16],
1062                    resizable: true,
1063                };
1064            }
1065            InsertPosition::Bottom => {
1066                *self = LayoutNode::Split {
1067                    direction: Direction::Vertical,
1068                    children: vec![self.clone(), LayoutNode::leaf(insert)],
1069                    weights: vec![1u16, 1u16],
1070                    resizable: true,
1071                };
1072            }
1073            InsertPosition::TopLeft => {
1074                let mut ids = self.collect_leaves();
1075                ids.retain(|id| *id != insert);
1076                if ids.is_empty() {
1077                    *self = LayoutNode::leaf(insert);
1078                    return;
1079                }
1080                let first = ids.remove(0);
1081                if ids.is_empty() {
1082                    let void_id = VOID_ID_COUNTER.fetch_add(1, Ordering::Relaxed);
1083                    *self = LayoutNode::Split {
1084                        direction: Direction::Horizontal,
1085                        children: vec![
1086                            LayoutNode::Split {
1087                                direction: Direction::Vertical,
1088                                children: vec![LayoutNode::leaf(insert), LayoutNode::Void(void_id)],
1089                                weights: vec![1u16, 1u16],
1090                                resizable: true,
1091                            },
1092                            LayoutNode::leaf(first),
1093                        ],
1094                        weights: vec![1u16, 1u16],
1095                        resizable: true,
1096                    };
1097                    return;
1098                }
1099                let bottom = LayoutNode::build_flat(Direction::Horizontal, ids);
1100                *self = LayoutNode::Split {
1101                    direction: Direction::Vertical,
1102                    children: vec![
1103                        LayoutNode::Split {
1104                            direction: Direction::Horizontal,
1105                            children: vec![LayoutNode::leaf(insert), LayoutNode::leaf(first)],
1106                            weights: vec![1u16, 1u16],
1107                            resizable: true,
1108                        },
1109                        bottom,
1110                    ],
1111                    weights: vec![1u16, 1u16],
1112                    resizable: true,
1113                };
1114            }
1115            InsertPosition::TopRight => {
1116                let mut ids = self.collect_leaves();
1117                ids.retain(|id| *id != insert);
1118                if ids.is_empty() {
1119                    *self = LayoutNode::leaf(insert);
1120                    return;
1121                }
1122                let first = ids.remove(0);
1123                if ids.is_empty() {
1124                    let void_id = VOID_ID_COUNTER.fetch_add(1, Ordering::Relaxed);
1125                    *self = LayoutNode::Split {
1126                        direction: Direction::Horizontal,
1127                        children: vec![
1128                            LayoutNode::leaf(first),
1129                            LayoutNode::Split {
1130                                direction: Direction::Vertical,
1131                                children: vec![LayoutNode::leaf(insert), LayoutNode::Void(void_id)],
1132                                weights: vec![1u16, 1u16],
1133                                resizable: true,
1134                            },
1135                        ],
1136                        weights: vec![1u16, 1u16],
1137                        resizable: true,
1138                    };
1139                    return;
1140                }
1141                let bottom = LayoutNode::build_flat(Direction::Horizontal, ids);
1142                *self = LayoutNode::Split {
1143                    direction: Direction::Vertical,
1144                    children: vec![
1145                        LayoutNode::Split {
1146                            direction: Direction::Horizontal,
1147                            children: vec![LayoutNode::leaf(first), LayoutNode::leaf(insert)],
1148                            weights: vec![1u16, 1u16],
1149                            resizable: true,
1150                        },
1151                        bottom,
1152                    ],
1153                    weights: vec![1u16, 1u16],
1154                    resizable: true,
1155                };
1156            }
1157            InsertPosition::BottomLeft => {
1158                let mut ids = self.collect_leaves();
1159                ids.retain(|id| *id != insert);
1160                if ids.is_empty() {
1161                    *self = LayoutNode::leaf(insert);
1162                    return;
1163                }
1164                let first = ids.remove(0);
1165                if ids.is_empty() {
1166                    let void_id = VOID_ID_COUNTER.fetch_add(1, Ordering::Relaxed);
1167                    *self = LayoutNode::Split {
1168                        direction: Direction::Horizontal,
1169                        children: vec![
1170                            LayoutNode::Split {
1171                                direction: Direction::Vertical,
1172                                children: vec![LayoutNode::Void(void_id), LayoutNode::leaf(insert)],
1173                                weights: vec![1u16, 1u16],
1174                                resizable: true,
1175                            },
1176                            LayoutNode::leaf(first),
1177                        ],
1178                        weights: vec![1u16, 1u16],
1179                        resizable: true,
1180                    };
1181                    return;
1182                }
1183                let top = LayoutNode::build_flat(Direction::Horizontal, ids);
1184                *self = LayoutNode::Split {
1185                    direction: Direction::Vertical,
1186                    children: vec![
1187                        top,
1188                        LayoutNode::Split {
1189                            direction: Direction::Horizontal,
1190                            children: vec![LayoutNode::leaf(insert), LayoutNode::leaf(first)],
1191                            weights: vec![1u16, 1u16],
1192                            resizable: true,
1193                        },
1194                    ],
1195                    weights: vec![1u16, 1u16],
1196                    resizable: true,
1197                };
1198            }
1199            InsertPosition::BottomRight => {
1200                let mut ids = self.collect_leaves();
1201                ids.retain(|id| *id != insert);
1202                if ids.is_empty() {
1203                    *self = LayoutNode::leaf(insert);
1204                    return;
1205                }
1206                let first = ids.remove(0);
1207                if ids.is_empty() {
1208                    let void_id = VOID_ID_COUNTER.fetch_add(1, Ordering::Relaxed);
1209                    *self = LayoutNode::Split {
1210                        direction: Direction::Horizontal,
1211                        children: vec![
1212                            LayoutNode::leaf(first),
1213                            LayoutNode::Split {
1214                                direction: Direction::Vertical,
1215                                children: vec![LayoutNode::Void(void_id), LayoutNode::leaf(insert)],
1216                                weights: vec![1u16, 1u16],
1217                                resizable: true,
1218                            },
1219                        ],
1220                        weights: vec![1u16, 1u16],
1221                        resizable: true,
1222                    };
1223                    return;
1224                }
1225                let top = LayoutNode::build_flat(Direction::Horizontal, ids);
1226                *self = LayoutNode::Split {
1227                    direction: Direction::Vertical,
1228                    children: vec![
1229                        top,
1230                        LayoutNode::Split {
1231                            direction: Direction::Horizontal,
1232                            children: vec![LayoutNode::leaf(first), LayoutNode::leaf(insert)],
1233                            weights: vec![1u16, 1u16],
1234                            resizable: true,
1235                        },
1236                    ],
1237                    weights: vec![1u16, 1u16],
1238                    resizable: true,
1239                };
1240            }
1241        };
1242    }
1243
1244    pub fn project_insert(
1245        &self,
1246        target: Option<Id>,
1247        insert: Id,
1248        position: InsertPosition,
1249        area: LayoutRect,
1250    ) -> Option<LayoutRect> {
1251        let mut root = self.clone();
1252        let removed = root.remove_leaf(insert);
1253        if !removed && matches!(&root, LayoutNode::Leaf(id) if *id == insert) {
1254            root = LayoutNode::Void(VOID_ID_COUNTER.fetch_add(1, Ordering::Relaxed));
1255        }
1256        let success = match target {
1257            Some(t) => root.insert_leaf(t, insert, position),
1258            None => false,
1259        };
1260        if !success {
1261            root.split_root(insert, position);
1262        }
1263        root.layout_rects(area)
1264            .into_iter()
1265            .find(|(id, _)| *id == insert)
1266            .map(|(_, r)| r)
1267    }
1268
1269    pub fn project_insert_void(
1270        &self,
1271        insert: Id,
1272        void_id: usize,
1273        area: LayoutRect,
1274    ) -> Option<LayoutRect> {
1275        let mut root = self.clone();
1276        root.remove_leaf(insert);
1277        if root.replace_void_by_id(void_id, LayoutNode::leaf(insert)) {
1278            root.layout_rects(area)
1279                .into_iter()
1280                .find(|(id, _)| *id == insert)
1281                .map(|(_, r)| r)
1282        } else {
1283            None
1284        }
1285    }
1286}
1287
1288pub fn split_area_for_path<Id: Copy + Eq + Ord>(
1289    node: &LayoutNode<Id>,
1290    area: LayoutRect,
1291    path: &[usize],
1292) -> Option<LayoutRect> {
1293    let mut area = area;
1294    let mut current = node;
1295    for &idx in path {
1296        let LayoutNode::Split {
1297            direction,
1298            children,
1299            weights,
1300            resizable,
1301            ..
1302        } = current
1303        else {
1304            return None;
1305        };
1306        let orientation = Orientation::from(*direction);
1307        let total_dim = match direction {
1308            Direction::Horizontal => area.width,
1309            Direction::Vertical => area.height,
1310        };
1311        let gap = split::gap_size(orientation, total_dim, children.len(), *resizable);
1312        let (rects, _) = split::split_rects_with_gaps(
1313            area,
1314            orientation,
1315            weights.as_slice(),
1316            children.len(),
1317            gap,
1318        );
1319        area = *rects.get(idx)?;
1320        current = children.get(idx)?;
1321    }
1322    Some(area)
1323}
1324
1325pub fn split_at_path_mut<'a, Id: Copy + Eq + Ord>(
1326    node: &'a mut LayoutNode<Id>,
1327    path: &[usize],
1328) -> Option<&'a mut LayoutNode<Id>> {
1329    let mut current = node;
1330    for &idx in path {
1331        let LayoutNode::Split { children, .. } = current else {
1332            return None;
1333        };
1334        current = children.get_mut(idx)?;
1335    }
1336    Some(current)
1337}
1338
1339#[cfg(test)]
1340mod tests {
1341    use super::*;
1342
1343    #[test]
1344    fn void_id_counter_increments() {
1345        let a = VOID_ID_COUNTER.fetch_add(1, Ordering::Relaxed);
1346        let b = VOID_ID_COUNTER.fetch_add(1, Ordering::Relaxed);
1347        assert!(b > a);
1348    }
1349
1350    #[test]
1351    fn from_rects_empty_returns_void() {
1352        let result = LayoutNode::<i32>::from_rects(&[]);
1353        assert!(matches!(result, LayoutNode::Void(_)));
1354    }
1355
1356    #[test]
1357    fn from_rects_single_leaf() {
1358        let rect = LayoutRect {
1359            x: 0,
1360            y: 0,
1361            width: 40,
1362            height: 24,
1363        };
1364        let result = LayoutNode::from_rects(&[(1, rect)]);
1365        assert!(matches!(result, LayoutNode::Leaf(1)));
1366    }
1367
1368    #[test]
1369    fn from_rects_gapped_windows_equal_columns() {
1370        let a = LayoutRect {
1371            x: 0,
1372            y: 0,
1373            width: 40,
1374            height: 24,
1375        };
1376        let b = LayoutRect {
1377            x: 100,
1378            y: 0,
1379            width: 40,
1380            height: 24,
1381        };
1382        let result = LayoutNode::from_rects(&[(1, a), (2, b)]);
1383        match result {
1384            LayoutNode::Split {
1385                direction: Direction::Horizontal,
1386                children,
1387                weights,
1388                ..
1389            } => {
1390                assert_eq!(children.len(), 2);
1391                assert_eq!(
1392                    weights,
1393                    vec![40, 40],
1394                    "gapped windows should get equal bounding spans"
1395                );
1396            }
1397            other => panic!("Expected Split, got {:?}", other),
1398        }
1399    }
1400
1401    #[test]
1402    fn from_rects_unequal_widths_preserves_proportion() {
1403        let a = LayoutRect {
1404            x: 0,
1405            y: 0,
1406            width: 80,
1407            height: 24,
1408        };
1409        let b = LayoutRect {
1410            x: 80,
1411            y: 0,
1412            width: 20,
1413            height: 24,
1414        };
1415        let result = LayoutNode::from_rects(&[(1, a), (2, b)]);
1416        match result {
1417            LayoutNode::Split {
1418                direction: Direction::Horizontal,
1419                children,
1420                weights,
1421                ..
1422            } => {
1423                assert_eq!(children.len(), 2);
1424                assert_eq!(
1425                    weights,
1426                    vec![80, 20],
1427                    "bounding span weights match window widths"
1428                );
1429            }
1430            other => panic!("Expected Horizontal Split, got {:?}", other),
1431        }
1432    }
1433
1434    #[test]
1435    fn from_rects_3_windows_top_bottom() {
1436        let a = LayoutRect {
1437            x: 0,
1438            y: 0,
1439            width: 100,
1440            height: 25,
1441        };
1442        let b = LayoutRect {
1443            x: 0,
1444            y: 25,
1445            width: 50,
1446            height: 25,
1447        };
1448        let c = LayoutRect {
1449            x: 50,
1450            y: 25,
1451            width: 50,
1452            height: 25,
1453        };
1454        let result = LayoutNode::from_rects(&[(1, a), (2, b), (3, c)]);
1455        match result {
1456            LayoutNode::Split {
1457                direction: Direction::Vertical,
1458                children,
1459                weights,
1460                ..
1461            } => {
1462                assert_eq!(children.len(), 2);
1463                assert_eq!(weights, vec![25, 25], "both sides have 25px Y-extent");
1464            }
1465            other => panic!("Expected Vertical Split, got {:?}", other),
1466        }
1467    }
1468
1469    #[test]
1470    fn from_rects_1v3_stacked_equal_width() {
1471        let a = LayoutRect {
1472            x: 0,
1473            y: 0,
1474            width: 40,
1475            height: 48,
1476        };
1477        let b = LayoutRect {
1478            x: 40,
1479            y: 0,
1480            width: 40,
1481            height: 16,
1482        };
1483        let c = LayoutRect {
1484            x: 40,
1485            y: 16,
1486            width: 40,
1487            height: 16,
1488        };
1489        let d = LayoutRect {
1490            x: 40,
1491            y: 32,
1492            width: 40,
1493            height: 16,
1494        };
1495        let result = LayoutNode::from_rects(&[(1, a), (2, b), (3, c), (4, d)]);
1496        match result {
1497            LayoutNode::Split {
1498                direction: Direction::Horizontal,
1499                children,
1500                weights,
1501                ..
1502            } => {
1503                assert_eq!(
1504                    children.len(),
1505                    2,
1506                    "should split into left=[A], right=[B,C,D]"
1507                );
1508                assert_eq!(
1509                    weights,
1510                    vec![40, 40],
1511                    "1-vs-3 stacked with same width = equal X-span"
1512                );
1513            }
1514            other => panic!("Expected Horizontal Split, got {:?}", other),
1515        }
1516    }
1517
1518    #[test]
1519    fn from_rects_overlapping_fallback() {
1520        let a = LayoutRect {
1521            x: 0,
1522            y: 0,
1523            width: 50,
1524            height: 50,
1525        };
1526        let b = LayoutRect {
1527            x: 10,
1528            y: 10,
1529            width: 50,
1530            height: 50,
1531        };
1532        let c = LayoutRect {
1533            x: 20,
1534            y: 20,
1535            width: 50,
1536            height: 50,
1537        };
1538        let result = LayoutNode::from_rects(&[(1, a), (2, b), (3, c)]);
1539        assert!(matches!(result, LayoutNode::Split { .. }));
1540    }
1541
1542    #[test]
1543    fn from_rects_with_layout_consistency() {
1544        let rects = [
1545            (
1546                1,
1547                LayoutRect {
1548                    x: 0,
1549                    y: 0,
1550                    width: 40,
1551                    height: 24,
1552                },
1553            ),
1554            (
1555                2,
1556                LayoutRect {
1557                    x: 60,
1558                    y: 0,
1559                    width: 40,
1560                    height: 24,
1561                },
1562            ),
1563        ];
1564        let node = LayoutNode::from_rects(&rects);
1565        let area = LayoutRect {
1566            x: 0,
1567            y: 0,
1568            width: 100,
1569            height: 24,
1570        };
1571        let (regions, _) = node.layout_with_gaps(area);
1572        assert_eq!(regions.len(), 2);
1573        let sum_w: u16 = regions.iter().map(|(_, r)| r.width).sum();
1574        assert!(
1575            sum_w == 100 || sum_w == 99,
1576            "regions should fill the full width (got {})",
1577            sum_w
1578        );
1579    }
1580
1581    #[test]
1582    fn split_rects_nary_even() {
1583        let area = LayoutRect {
1584            x: 0,
1585            y: 0,
1586            width: 11,
1587            height: 1,
1588        };
1589        let weights = [1u16, 1u16];
1590        let rects = crate::split_rects_weighted(area, crate::Orientation::Horizontal, &weights, 2);
1591        assert_eq!(rects.len(), 2);
1592        assert_eq!(rects[0].width, 5);
1593        assert_eq!(rects[1].width, 6);
1594    }
1595
1596    #[test]
1597    fn insert_and_remove_leaf_and_split_area_for_path() {
1598        let mut node = LayoutNode::<usize>::leaf(1);
1599        assert!(node.insert_leaf(1, 2, InsertPosition::Right));
1600        if let LayoutNode::Split { children, .. } = &node {
1601            assert_eq!(children.len(), 2);
1602            assert_eq!(children[0].unwrap_leaf(), Some(1));
1603            assert_eq!(children[1].unwrap_leaf(), Some(2));
1604        } else {
1605            panic!("expected split after insert");
1606        }
1607        let area = LayoutRect {
1608            x: 0,
1609            y: 0,
1610            width: 10,
1611            height: 4,
1612        };
1613        let sub = split_area_for_path(&node, area, &[1]).expect("should get area for path");
1614        assert!(sub.x > 0);
1615        assert!(node.remove_leaf(2));
1616        assert_eq!(node.unwrap_leaf(), Some(1));
1617    }
1618
1619    #[test]
1620    fn hit_test_handle_finds_gap() {
1621        let area = LayoutRect {
1622            x: 0,
1623            y: 0,
1624            width: 80,
1625            height: 24,
1626        };
1627        let node = LayoutNode::Split {
1628            direction: Direction::Horizontal,
1629            children: vec![LayoutNode::Leaf(1), LayoutNode::Leaf(2)],
1630            weights: vec![1u16, 1u16],
1631            resizable: true,
1632        };
1633        let (_, gaps) = node.layout_with_gaps(area);
1634        assert_eq!(gaps.len(), 1, "2-window split must produce 1 gap");
1635        let gap = &gaps[0];
1636        assert_eq!(gap.direction, Direction::Horizontal);
1637        assert_eq!(gap.index, 0);
1638        assert!(gap.rect.width > 0);
1639        assert_eq!(gap.rect.height, 24);
1640        let center_col = (gap.rect.x + i32::from(gap.rect.width) / 2) as u16;
1641        let center_row = (gap.rect.y + i32::from(gap.rect.height) / 2) as u16;
1642        let found = node.hit_test_gap(area, center_col, center_row);
1643        assert!(found.is_some(), "hit_test_gap must find the gap");
1644        assert_eq!(found.unwrap().direction, Direction::Horizontal);
1645    }
1646
1647    #[test]
1648    fn normalize_weights_resets_to_equal() {
1649        let mut node = LayoutNode::Split {
1650            direction: Direction::Horizontal,
1651            children: vec![LayoutNode::leaf(1), LayoutNode::leaf(2)],
1652            weights: vec![3u16, 1u16],
1653            resizable: true,
1654        };
1655        node.normalize_weights();
1656        if let LayoutNode::Split { weights, .. } = &node {
1657            assert!(weights.iter().all(|w| *w == 1u16));
1658        } else {
1659            panic!("expected split");
1660        }
1661    }
1662
1663    /// Assert every leaf region in a tree occupies near-equal area (within a
1664    /// few percent; integer rounding and split gaps cause small deviations, but
1665    /// a real imbalance such as ½ vs ¼ must fail).
1666    fn assert_equal_leaf_areas(node: &LayoutNode<usize>, area: LayoutRect) {
1667        let rects = node.layout_rects(area);
1668        assert!(!rects.is_empty(), "expected at least one leaf");
1669        let areas: Vec<u32> = rects
1670            .iter()
1671            .map(|(_, r)| (r.width as u32) * (r.height as u32))
1672            .collect();
1673        let min_a = *areas.iter().min().unwrap();
1674        let max_a = *areas.iter().max().unwrap();
1675        assert!(
1676            (max_a as u64) * 100 <= (min_a as u64) * 115,
1677            "leaves must be near equal-area, got {areas:?}"
1678        );
1679    }
1680
1681    #[test]
1682    fn reweight_by_leaf_count_equalizes_depth_weighted_tree() {
1683        // ½ + ¼ + ¼ tree: [leaf(1) | Split[leaf(2), leaf(3)]] all weights [1,1].
1684        let mut node = LayoutNode::Split {
1685            direction: Direction::Horizontal,
1686            children: vec![
1687                LayoutNode::leaf(1),
1688                LayoutNode::Split {
1689                    direction: Direction::Horizontal,
1690                    children: vec![LayoutNode::leaf(2), LayoutNode::leaf(3)],
1691                    weights: vec![1u16, 1u16],
1692                    resizable: true,
1693                },
1694            ],
1695            weights: vec![1u16, 1u16],
1696            resizable: true,
1697        };
1698        node.reweight_by_leaf_count();
1699        let area = LayoutRect {
1700            x: 0,
1701            y: 0,
1702            width: 90,
1703            height: 30,
1704        };
1705        assert_equal_leaf_areas(&node, area);
1706    }
1707
1708    #[test]
1709    fn reweight_by_leaf_count_four_leaves_all_equal() {
1710        // ½ + ¼ + ⅛ + ⅛ tree; after reweight every leaf must be ¼.
1711        let mut node = LayoutNode::Split {
1712            direction: Direction::Horizontal,
1713            children: vec![
1714                LayoutNode::leaf(1),
1715                LayoutNode::Split {
1716                    direction: Direction::Horizontal,
1717                    children: vec![
1718                        LayoutNode::leaf(2),
1719                        LayoutNode::Split {
1720                            direction: Direction::Horizontal,
1721                            children: vec![LayoutNode::leaf(3), LayoutNode::leaf(4)],
1722                            weights: vec![1u16, 1u16],
1723                            resizable: true,
1724                        },
1725                    ],
1726                    weights: vec![1u16, 1u16],
1727                    resizable: true,
1728                },
1729            ],
1730            weights: vec![1u16, 1u16],
1731            resizable: true,
1732        };
1733        node.reweight_by_leaf_count();
1734        let area = LayoutRect {
1735            x: 0,
1736            y: 0,
1737            width: 80,
1738            height: 40,
1739        };
1740        assert_equal_leaf_areas(&node, area);
1741    }
1742
1743    #[test]
1744    fn reweight_by_leaf_count_populates_empty_weights() {
1745        // `LayoutNode::split` initializes weights empty; reweight must fill them.
1746        let mut node = LayoutNode::split(
1747            Direction::Horizontal,
1748            vec![LayoutNode::leaf(1), LayoutNode::leaf(2)],
1749        );
1750        node.reweight_by_leaf_count();
1751        if let LayoutNode::Split { weights, .. } = &node {
1752            assert_eq!(weights, &vec![1u16, 1u16]);
1753        } else {
1754            panic!("expected split");
1755        }
1756    }
1757
1758    #[test]
1759    fn build_flat_empty_returns_void() {
1760        let node = LayoutNode::build_flat(Direction::Horizontal, Vec::<usize>::new());
1761        assert!(node.unwrap_leaf().is_none());
1762    }
1763
1764    #[test]
1765    fn build_flat_single_returns_leaf() {
1766        let node = LayoutNode::build_flat(Direction::Horizontal, vec![42]);
1767        assert_eq!(node.unwrap_leaf(), Some(42));
1768    }
1769
1770    #[test]
1771    fn build_flat_multiple_returns_split() {
1772        let node = LayoutNode::build_flat(Direction::Vertical, vec![1, 2, 3]);
1773        if let LayoutNode::Split {
1774            children,
1775            weights,
1776            direction,
1777            ..
1778        } = &node
1779        {
1780            assert_eq!(children.len(), 3);
1781            assert_eq!(*direction, Direction::Vertical);
1782            assert!(weights.iter().all(|w| *w == 1u16));
1783        } else {
1784            panic!("expected split");
1785        }
1786    }
1787
1788    #[test]
1789    fn void_regions_returns_voids() {
1790        let node = LayoutNode::Split {
1791            direction: Direction::Horizontal,
1792            children: vec![LayoutNode::leaf(1), LayoutNode::Void(99)],
1793            weights: vec![1u16, 1u16],
1794            resizable: true,
1795        };
1796        let area = LayoutRect {
1797            x: 0,
1798            y: 0,
1799            width: 80,
1800            height: 24,
1801        };
1802        let voids = node.void_regions(area);
1803        assert_eq!(voids.len(), 1);
1804        assert_eq!(voids[0].0, 99);
1805    }
1806
1807    #[test]
1808    fn swap_leaves_exchanges_positions() {
1809        let mut node = LayoutNode::Split {
1810            direction: Direction::Horizontal,
1811            children: vec![
1812                LayoutNode::leaf(1),
1813                LayoutNode::leaf(2),
1814                LayoutNode::leaf(3),
1815            ],
1816            weights: vec![1u16, 1u16, 1u16],
1817            resizable: true,
1818        };
1819        assert!(node.swap_leaves(&1, &3));
1820        let leaves = node.collect_leaves();
1821        assert_eq!(leaves, vec![3, 2, 1]);
1822    }
1823
1824    #[test]
1825    fn swap_leaves_same_id_returns_false() {
1826        let mut node = LayoutNode::leaf(1);
1827        assert!(!node.swap_leaves(&1, &1));
1828    }
1829
1830    #[test]
1831    fn swap_leaves_nonexistent_returns_false() {
1832        let mut node = LayoutNode::leaf(1);
1833        assert!(!node.swap_leaves(&1, &2));
1834    }
1835
1836    #[test]
1837    fn cleanup_after_removes_void_children() {
1838        let mut node = LayoutNode::Split {
1839            direction: Direction::Horizontal,
1840            children: vec![LayoutNode::leaf(1), LayoutNode::Void(99)],
1841            weights: vec![1u16, 1u16],
1842            resizable: true,
1843        };
1844        node.cleanup_after_removal();
1845        assert_eq!(node.unwrap_leaf(), Some(1));
1846    }
1847
1848    #[test]
1849    fn cleanup_all_voids_becomes_void() {
1850        let mut node: LayoutNode<usize> = LayoutNode::Split {
1851            direction: Direction::Horizontal,
1852            children: vec![LayoutNode::Void(1), LayoutNode::Void(2)],
1853            weights: vec![1u16, 1u16],
1854            resizable: true,
1855        };
1856        node.cleanup_after_removal();
1857        assert!(matches!(node, LayoutNode::Void(_)));
1858    }
1859
1860    #[test]
1861    fn cleanup_empty_split_becomes_void() {
1862        let mut node: LayoutNode<usize> = LayoutNode::Split {
1863            direction: Direction::Horizontal,
1864            children: Vec::new(),
1865            weights: Vec::new(),
1866            resizable: true,
1867        };
1868        node.cleanup_after_removal();
1869        assert!(matches!(node, LayoutNode::Void(_)));
1870    }
1871
1872    #[test]
1873    fn clear_leaf_replaces_with_void() {
1874        let mut node = LayoutNode::leaf(42);
1875        assert!(node.clear_leaf(42));
1876        assert!(matches!(node, LayoutNode::Void(_)));
1877    }
1878
1879    #[test]
1880    fn clear_leaf_wrong_id_returns_false() {
1881        let mut node = LayoutNode::leaf(42);
1882        assert!(!node.clear_leaf(99));
1883    }
1884
1885    #[test]
1886    fn subtree_any_finds_matching_leaf() {
1887        let node = LayoutNode::Split {
1888            direction: Direction::Horizontal,
1889            children: vec![LayoutNode::leaf(1), LayoutNode::leaf(2)],
1890            weights: vec![1u16, 1u16],
1891            resizable: true,
1892        };
1893        assert!(node.subtree_any(|id| id == 2));
1894        assert!(!node.subtree_any(|id| id == 99));
1895    }
1896
1897    #[test]
1898    fn node_at_path_returns_none_for_invalid_path() {
1899        let node = LayoutNode::leaf(1);
1900        assert!(node.node_at_path(&[0]).is_none());
1901    }
1902
1903    #[test]
1904    fn collect_leaves_from_nested() {
1905        let node = LayoutNode::Split {
1906            direction: Direction::Vertical,
1907            children: vec![
1908                LayoutNode::leaf(1),
1909                LayoutNode::Split {
1910                    direction: Direction::Horizontal,
1911                    children: vec![LayoutNode::leaf(2), LayoutNode::leaf(3)],
1912                    weights: vec![1u16, 1u16],
1913                    resizable: true,
1914                },
1915            ],
1916            weights: vec![1u16, 1u16],
1917            resizable: true,
1918        };
1919        assert_eq!(node.collect_leaves(), vec![1, 2, 3]);
1920    }
1921
1922    #[test]
1923    fn insert_leaf_left_on_single() {
1924        let mut node = LayoutNode::leaf(1);
1925        assert!(node.insert_leaf(1, 2, InsertPosition::Left));
1926        let leaves = node.collect_leaves();
1927        assert_eq!(leaves, vec![2, 1]);
1928    }
1929
1930    #[test]
1931    fn insert_leaf_top_on_single() {
1932        let mut node = LayoutNode::leaf(1);
1933        assert!(node.insert_leaf(1, 2, InsertPosition::Top));
1934        let leaves = node.collect_leaves();
1935        assert_eq!(leaves, vec![2, 1]);
1936    }
1937
1938    #[test]
1939    fn insert_leaf_bottom_on_single() {
1940        let mut node = LayoutNode::leaf(1);
1941        assert!(node.insert_leaf(1, 2, InsertPosition::Bottom));
1942        let leaves = node.collect_leaves();
1943        assert_eq!(leaves, vec![1, 2]);
1944    }
1945
1946    #[test]
1947    fn insert_leaf_nonexistent_target_returns_false() {
1948        let mut node = LayoutNode::leaf(1);
1949        assert!(!node.insert_leaf(99, 2, InsertPosition::Right));
1950    }
1951
1952    #[test]
1953    fn insert_leaf_in_nested_split() {
1954        let mut node = LayoutNode::Split {
1955            direction: Direction::Horizontal,
1956            children: vec![LayoutNode::leaf(1), LayoutNode::leaf(2)],
1957            weights: vec![1u16, 1u16],
1958            resizable: true,
1959        };
1960        assert!(node.insert_leaf(2, 3, InsertPosition::Right));
1961        let leaves = node.collect_leaves();
1962        assert_eq!(leaves, vec![1, 2, 3]);
1963    }
1964
1965    const TEST_AREA: crate::rect::LayoutRect = crate::rect::LayoutRect {
1966        x: 0,
1967        y: 0,
1968        width: 80,
1969        height: 24,
1970    };
1971
1972    // ── replace_leaf_with_void ────────────────────────────────────────
1973
1974    #[test]
1975    fn replace_leaf_with_void_single_leaf() {
1976        let mut node = LayoutNode::leaf(42);
1977        let vid = node.replace_leaf_with_void(42);
1978        assert!(vid.is_some(), "should return a void_id");
1979        assert!(matches!(node, LayoutNode::Void(_)), "leaf became void");
1980        // layout_rects should yield nothing
1981        let rects = node.layout_rects(TEST_AREA);
1982        assert!(rects.is_empty(), "void produces no regions");
1983    }
1984
1985    #[test]
1986    fn replace_leaf_with_void_nested_split() {
1987        let mut node = LayoutNode::Split {
1988            direction: Direction::Horizontal,
1989            children: vec![
1990                LayoutNode::leaf(1),
1991                LayoutNode::leaf(2),
1992                LayoutNode::leaf(3),
1993            ],
1994            weights: vec![1u16, 1u16, 1u16],
1995            resizable: true,
1996        };
1997        let vid = node.replace_leaf_with_void(2);
1998        assert!(vid.is_some(), "should return a void_id");
1999        // Structure: Split [Leaf(1), Void(vid), Leaf(3)]
2000        let leaves = node.collect_leaves();
2001        assert_eq!(leaves, vec![1, 3], "leaf 2 removed from tree");
2002        // Weights should be preserved (3 weights still, the void is a child)
2003        if let LayoutNode::Split {
2004            children, weights, ..
2005        } = &node
2006        {
2007            assert_eq!(children.len(), 3, "void placeholder preserved");
2008            assert_eq!(weights.len(), 3, "weights preserved");
2009        } else {
2010            panic!("expected Split");
2011        }
2012    }
2013
2014    #[test]
2015    fn replace_leaf_with_void_nonexistent() {
2016        let mut node = LayoutNode::leaf(42);
2017        let vid = node.replace_leaf_with_void(99);
2018        assert!(vid.is_none(), "nonexistent id returns None");
2019        assert_eq!(node.unwrap_leaf(), Some(42), "tree unchanged");
2020    }
2021
2022    // ── remove_void_by_id ─────────────────────────────────────────────
2023
2024    #[test]
2025    fn remove_void_by_id_existing_root() {
2026        let mut node: LayoutNode<usize> = LayoutNode::Void(99);
2027        assert!(node.remove_void_by_id(99));
2028        // Becomes a new void (fresh id), but it was removed
2029        assert!(matches!(node, LayoutNode::Void(_)));
2030    }
2031
2032    #[test]
2033    fn remove_void_by_id_nested() {
2034        let mut node = LayoutNode::Split {
2035            direction: Direction::Horizontal,
2036            children: vec![
2037                LayoutNode::leaf(1),
2038                LayoutNode::Void(99),
2039                LayoutNode::leaf(3),
2040            ],
2041            weights: vec![1u16, 1u16, 1u16],
2042            resizable: true,
2043        };
2044        assert!(node.remove_void_by_id(99), "void removed");
2045        // After removal the split should have 2 children
2046        let leaves = node.collect_leaves();
2047        assert_eq!(leaves, vec![1, 3], "remaining leaves preserved");
2048    }
2049
2050    #[test]
2051    fn remove_void_by_id_nonexistent() {
2052        let mut node = LayoutNode::leaf(42);
2053        assert!(!node.remove_void_by_id(99), "nonexistent returns false");
2054    }
2055
2056    #[test]
2057    fn remove_void_by_id_deeply_nested() {
2058        // Split [Leaf(1), Split [Void(99), Leaf(2)]]
2059        let mut node = LayoutNode::Split {
2060            direction: Direction::Horizontal,
2061            children: vec![
2062                LayoutNode::leaf(1),
2063                LayoutNode::Split {
2064                    direction: Direction::Vertical,
2065                    children: vec![LayoutNode::Void(99), LayoutNode::leaf(2)],
2066                    weights: vec![1u16, 1u16],
2067                    resizable: true,
2068                },
2069            ],
2070            weights: vec![1u16, 1u16],
2071            resizable: true,
2072        };
2073        assert!(node.remove_void_by_id(99), "deeply nested void removed");
2074        let leaves = node.collect_leaves();
2075        assert_eq!(
2076            leaves,
2077            vec![1, 2],
2078            "remaining leaves preserved after deep removal"
2079        );
2080    }
2081
2082    // ── Void placeholder preserves layout ─────────────────────────────
2083
2084    #[test]
2085    fn void_produces_no_regions_in_split() {
2086        let mut node = LayoutNode::Split {
2087            direction: Direction::Horizontal,
2088            children: vec![LayoutNode::leaf(1), LayoutNode::leaf(2)],
2089            weights: vec![1u16, 1u16],
2090            resizable: true,
2091        };
2092        // Replace leaf 2 with void
2093        node.replace_leaf_with_void(2);
2094        let rects = node.layout_rects(TEST_AREA);
2095        // Only leaf 1 produces a region; leaf 2's slot is preserved but empty
2096        assert_eq!(rects.len(), 1, "only one region");
2097        assert_eq!(rects[0].0, 1, "remaining leaf id");
2098        assert!(rects[0].1.width > 0, "leaf region has positive width");
2099    }
2100
2101    #[test]
2102    fn void_placeholder_preserves_weights() {
2103        let mut node = LayoutNode::Split {
2104            direction: Direction::Horizontal,
2105            children: vec![
2106                LayoutNode::leaf(1),
2107                LayoutNode::leaf(2),
2108                LayoutNode::leaf(3),
2109            ],
2110            weights: vec![2u16, 3u16, 5u16],
2111            resizable: true,
2112        };
2113        let _ = node.replace_leaf_with_void(2);
2114        if let LayoutNode::Split { weights, .. } = &node {
2115            assert_eq!(weights.as_slice(), &[2u16, 3u16, 5u16], "weights unchanged");
2116        } else {
2117            panic!("expected Split");
2118        }
2119    }
2120}