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    pub fn replace_void_by_id(&mut self, void_id: usize, new_leaf: LayoutNode<Id>) -> bool {
624        match self {
625            LayoutNode::Void(id) if *id == void_id => {
626                *self = new_leaf;
627                true
628            }
629            LayoutNode::Split { children, .. } => {
630                for child in children.iter_mut() {
631                    if child.replace_void_by_id(void_id, new_leaf.clone()) {
632                        return true;
633                    }
634                }
635                false
636            }
637            _ => false,
638        }
639    }
640
641    /// Remove a Void node by its ID from the tree. Returns true if the void
642    /// was found and removed, false otherwise. Cleans up empty parent splits.
643    pub fn remove_void_by_id(&mut self, void_id: usize) -> bool {
644        match self {
645            LayoutNode::Void(id) if *id == void_id => {
646                *self = LayoutNode::Void(VOID_ID_COUNTER.fetch_add(1, Ordering::Relaxed));
647                true
648            }
649            LayoutNode::Split {
650                children, weights, ..
651            } => {
652                let mut i = 0;
653                while i < children.len() {
654                    if children[i].remove_void_by_id(void_id) {
655                        // If the child was replaced with a Void (the one we
656                        // just replaced above), remove it from the split.
657                        if matches!(children[i], LayoutNode::Void(_)) {
658                            children.remove(i);
659                            if i < weights.len() {
660                                weights.remove(i);
661                            }
662                        }
663                        return true;
664                    }
665                    i += 1;
666                }
667                false
668            }
669            _ => false,
670        }
671    }
672
673    pub fn is_empty(&self) -> bool {
674        match self {
675            LayoutNode::Void(_) => true,
676            LayoutNode::Leaf(_) => false,
677            LayoutNode::Split { children, .. } => children.iter().all(|c| c.is_empty()),
678        }
679    }
680
681    fn layout_recursive(
682        &self,
683        area: LayoutRect,
684        regions: &mut Vec<(Id, LayoutRect)>,
685        gaps: &mut Vec<SplitGap>,
686        path: &mut Vec<usize>,
687    ) {
688        match self {
689            LayoutNode::Leaf(id) => {
690                regions.push((*id, area));
691            }
692            LayoutNode::Void(_) => {}
693            LayoutNode::Split {
694                direction,
695                children,
696                weights,
697                resizable,
698            } => {
699                let orientation = Orientation::from(*direction);
700                let total_dim = match direction {
701                    Direction::Horizontal => area.width,
702                    Direction::Vertical => area.height,
703                };
704                let gap = split::gap_size(orientation, total_dim, children.len(), *resizable);
705                let (rects, split_gaps) = split::split_rects_with_gaps(
706                    area,
707                    orientation,
708                    weights.as_slice(),
709                    children.len(),
710                    gap,
711                );
712                for (idx, (child, rect)) in children.iter().zip(rects.iter().copied()).enumerate() {
713                    path.push(idx);
714                    child.layout_recursive(rect, regions, gaps, path);
715                    path.pop();
716                }
717                if *resizable && children.len() > 1 {
718                    for (index, gap_rect) in split_gaps.into_iter().enumerate() {
719                        gaps.push(SplitGap {
720                            rect: gap_rect,
721                            path: path.clone(),
722                            index,
723                            direction: *direction,
724                        });
725                    }
726                }
727            }
728        }
729    }
730
731    /// Build a BSP tree from rectangles using straddle-tolerant cut selection.
732    /// Straddled windows are assigned to the side containing more of their area.
733    /// Weights are count-based to ensure each window gets equal space.
734    /// Fallback uses aspect-aware direction for the sort axis.
735    pub fn from_rects(rects: &[(Id, LayoutRect)]) -> Self {
736        if rects.is_empty() {
737            return Self::Void(0);
738        }
739        if rects.len() == 1 {
740            return Self::Leaf(rects[0].0);
741        }
742
743        let min_x = rects.iter().map(|(_, r)| r.x).min().unwrap_or(0);
744        let min_y = rects.iter().map(|(_, r)| r.y).min().unwrap_or(0);
745        let max_x = rects
746            .iter()
747            .map(|(_, r)| r.x.saturating_add(r.width as i32))
748            .max()
749            .unwrap_or(0);
750        let max_y = rects
751            .iter()
752            .map(|(_, r)| r.y.saturating_add(r.height as i32))
753            .max()
754            .unwrap_or(0);
755
756        struct CutCandidate<Id: Copy + Eq + Ord> {
757            direction: Direction,
758            straddles: usize,
759            balance_delta: usize,
760            part_a: Vec<(Id, LayoutRect)>,
761            part_b: Vec<(Id, LayoutRect)>,
762            weight_a: u16,
763            weight_b: u16,
764        }
765
766        let mut candidates: Vec<CutCandidate<Id>> = Vec::new();
767
768        let mut y_candidates: Vec<i32> = rects
769            .iter()
770            .flat_map(|(_, r)| [r.y, r.y.saturating_add(r.height as i32)])
771            .collect();
772        y_candidates.sort_unstable();
773        y_candidates.dedup();
774
775        for &y in &y_candidates {
776            if y <= min_y || y >= max_y {
777                continue;
778            }
779            let mut top = Vec::new();
780            let mut bottom = Vec::new();
781            let mut straddles = 0;
782
783            for &(k, r) in rects {
784                let r_bottom = r.y.saturating_add(r.height as i32);
785                if r_bottom <= y {
786                    top.push((k, r));
787                } else if r.y >= y {
788                    bottom.push((k, r));
789                } else {
790                    straddles += 1;
791                    let mid = r.y + (r.height as i32 / 2);
792                    if mid < y {
793                        top.push((k, r));
794                    } else {
795                        bottom.push((k, r));
796                    }
797                }
798            }
799
800            if !top.is_empty() && !bottom.is_empty() {
801                let balance_delta = (top.len() as isize - bottom.len() as isize).unsigned_abs();
802                let top_span = {
803                    let min = top.iter().map(|(_, r)| r.y).min().unwrap_or(min_y);
804                    let max = top
805                        .iter()
806                        .map(|(_, r)| r.y.saturating_add(r.height as i32))
807                        .max()
808                        .unwrap_or(y);
809                    max.saturating_sub(min).clamp(1, i32::from(u16::MAX)) as u16
810                };
811                let bot_span = {
812                    let min = bottom.iter().map(|(_, r)| r.y).min().unwrap_or(y);
813                    let max = bottom
814                        .iter()
815                        .map(|(_, r)| r.y.saturating_add(r.height as i32))
816                        .max()
817                        .unwrap_or(max_y);
818                    max.saturating_sub(min).clamp(1, i32::from(u16::MAX)) as u16
819                };
820                candidates.push(CutCandidate {
821                    direction: Direction::Vertical,
822                    straddles,
823                    balance_delta,
824                    weight_a: top_span,
825                    weight_b: bot_span,
826                    part_a: top,
827                    part_b: bottom,
828                });
829            }
830        }
831
832        let mut x_candidates: Vec<i32> = rects
833            .iter()
834            .flat_map(|(_, r)| [r.x, r.x.saturating_add(r.width as i32)])
835            .collect();
836        x_candidates.sort_unstable();
837        x_candidates.dedup();
838
839        for &x in &x_candidates {
840            if x <= min_x || x >= max_x {
841                continue;
842            }
843            let mut left = Vec::new();
844            let mut right = Vec::new();
845            let mut straddles = 0;
846
847            for &(k, r) in rects {
848                let r_right = r.x.saturating_add(r.width as i32);
849                if r_right <= x {
850                    left.push((k, r));
851                } else if r.x >= x {
852                    right.push((k, r));
853                } else {
854                    straddles += 1;
855                    let mid = r.x + (r.width as i32 / 2);
856                    if mid < x {
857                        left.push((k, r));
858                    } else {
859                        right.push((k, r));
860                    }
861                }
862            }
863
864            if !left.is_empty() && !right.is_empty() {
865                let balance_delta = (left.len() as isize - right.len() as isize).unsigned_abs();
866                let left_span = {
867                    let min = left.iter().map(|(_, r)| r.x).min().unwrap_or(min_x);
868                    let max = left
869                        .iter()
870                        .map(|(_, r)| r.x.saturating_add(r.width as i32))
871                        .max()
872                        .unwrap_or(x);
873                    max.saturating_sub(min).clamp(1, i32::from(u16::MAX)) as u16
874                };
875                let right_span = {
876                    let min = right.iter().map(|(_, r)| r.x).min().unwrap_or(x);
877                    let max = right
878                        .iter()
879                        .map(|(_, r)| r.x.saturating_add(r.width as i32))
880                        .max()
881                        .unwrap_or(max_x);
882                    max.saturating_sub(min).clamp(1, i32::from(u16::MAX)) as u16
883                };
884                candidates.push(CutCandidate {
885                    direction: Direction::Horizontal,
886                    straddles,
887                    balance_delta,
888                    weight_a: left_span,
889                    weight_b: right_span,
890                    part_a: left,
891                    part_b: right,
892                });
893            }
894        }
895
896        if let Some(best) = candidates
897            .into_iter()
898            .min_by_key(|c| (c.straddles, c.balance_delta))
899        {
900            return Self::Split {
901                direction: best.direction,
902                children: vec![
903                    Self::from_rects(&best.part_a),
904                    Self::from_rects(&best.part_b),
905                ],
906                weights: vec![best.weight_a, best.weight_b],
907                resizable: true,
908            };
909        }
910
911        let total_w = max_x - min_x;
912        let total_h = (max_y - min_y) * 2;
913
914        let mut sorted = rects.to_vec();
915        let direction = if total_w >= total_h {
916            sorted.sort_unstable_by_key(|(_, r)| (r.x, r.y));
917            Direction::Horizontal
918        } else {
919            sorted.sort_unstable_by_key(|(_, r)| (r.y, r.x));
920            Direction::Vertical
921        };
922
923        let mid = sorted.len() / 2;
924        let left_slice = &sorted[..mid];
925        let right_slice = &sorted[mid..];
926        let (weight_a, weight_b) = if direction == Direction::Horizontal {
927            let left_span = {
928                let min = left_slice.iter().map(|(_, r)| r.x).min().unwrap_or(min_x);
929                let max = left_slice
930                    .iter()
931                    .map(|(_, r)| r.x.saturating_add(r.width as i32))
932                    .max()
933                    .unwrap_or(max_x);
934                max.saturating_sub(min).clamp(1, i32::from(u16::MAX)) as u16
935            };
936            let right_span = {
937                let min = right_slice.iter().map(|(_, r)| r.x).min().unwrap_or(min_x);
938                let max = right_slice
939                    .iter()
940                    .map(|(_, r)| r.x.saturating_add(r.width as i32))
941                    .max()
942                    .unwrap_or(max_x);
943                max.saturating_sub(min).clamp(1, i32::from(u16::MAX)) as u16
944            };
945            (left_span, right_span)
946        } else {
947            let top_span = {
948                let min = left_slice.iter().map(|(_, r)| r.y).min().unwrap_or(min_y);
949                let max = left_slice
950                    .iter()
951                    .map(|(_, r)| r.y.saturating_add(r.height as i32))
952                    .max()
953                    .unwrap_or(max_y);
954                max.saturating_sub(min).clamp(1, i32::from(u16::MAX)) as u16
955            };
956            let bot_span = {
957                let min = right_slice.iter().map(|(_, r)| r.y).min().unwrap_or(min_y);
958                let max = right_slice
959                    .iter()
960                    .map(|(_, r)| r.y.saturating_add(r.height as i32))
961                    .max()
962                    .unwrap_or(max_y);
963                max.saturating_sub(min).clamp(1, i32::from(u16::MAX)) as u16
964            };
965            (top_span, bot_span)
966        };
967        Self::Split {
968            direction,
969            children: vec![Self::from_rects(left_slice), Self::from_rects(right_slice)],
970            weights: vec![weight_a, weight_b],
971            resizable: true,
972        }
973    }
974
975    pub fn split_root(&mut self, insert: Id, position: InsertPosition) {
976        let existing_void_id = match self {
977            LayoutNode::Void(id) => Some(*id),
978            _ => None,
979        };
980        if let Some(existing_void_id) = existing_void_id {
981            *self = match position {
982                InsertPosition::Left | InsertPosition::TopLeft | InsertPosition::BottomLeft => {
983                    LayoutNode::Split {
984                        direction: Direction::Horizontal,
985                        children: vec![
986                            LayoutNode::leaf(insert),
987                            LayoutNode::Void(existing_void_id),
988                        ],
989                        weights: vec![1u16, 1u16],
990                        resizable: true,
991                    }
992                }
993                InsertPosition::Right | InsertPosition::TopRight | InsertPosition::BottomRight => {
994                    LayoutNode::Split {
995                        direction: Direction::Horizontal,
996                        children: vec![
997                            LayoutNode::Void(existing_void_id),
998                            LayoutNode::leaf(insert),
999                        ],
1000                        weights: vec![1u16, 1u16],
1001                        resizable: true,
1002                    }
1003                }
1004                InsertPosition::Top => LayoutNode::Split {
1005                    direction: Direction::Vertical,
1006                    children: vec![LayoutNode::leaf(insert), LayoutNode::Void(existing_void_id)],
1007                    weights: vec![1u16, 1u16],
1008                    resizable: true,
1009                },
1010                InsertPosition::Bottom => LayoutNode::Split {
1011                    direction: Direction::Vertical,
1012                    children: vec![LayoutNode::Void(existing_void_id), LayoutNode::leaf(insert)],
1013                    weights: vec![1u16, 1u16],
1014                    resizable: true,
1015                },
1016            };
1017            return;
1018        }
1019        match position {
1020            InsertPosition::Left => {
1021                *self = LayoutNode::Split {
1022                    direction: Direction::Horizontal,
1023                    children: vec![LayoutNode::leaf(insert), self.clone()],
1024                    weights: vec![1u16, 1u16],
1025                    resizable: true,
1026                };
1027            }
1028            InsertPosition::Right => {
1029                *self = LayoutNode::Split {
1030                    direction: Direction::Horizontal,
1031                    children: vec![self.clone(), LayoutNode::leaf(insert)],
1032                    weights: vec![1u16, 1u16],
1033                    resizable: true,
1034                };
1035            }
1036            InsertPosition::Top => {
1037                *self = LayoutNode::Split {
1038                    direction: Direction::Vertical,
1039                    children: vec![LayoutNode::leaf(insert), self.clone()],
1040                    weights: vec![1u16, 1u16],
1041                    resizable: true,
1042                };
1043            }
1044            InsertPosition::Bottom => {
1045                *self = LayoutNode::Split {
1046                    direction: Direction::Vertical,
1047                    children: vec![self.clone(), LayoutNode::leaf(insert)],
1048                    weights: vec![1u16, 1u16],
1049                    resizable: true,
1050                };
1051            }
1052            InsertPosition::TopLeft => {
1053                let mut ids = self.collect_leaves();
1054                ids.retain(|id| *id != insert);
1055                if ids.is_empty() {
1056                    *self = LayoutNode::leaf(insert);
1057                    return;
1058                }
1059                let first = ids.remove(0);
1060                if ids.is_empty() {
1061                    let void_id = VOID_ID_COUNTER.fetch_add(1, Ordering::Relaxed);
1062                    *self = LayoutNode::Split {
1063                        direction: Direction::Horizontal,
1064                        children: vec![
1065                            LayoutNode::Split {
1066                                direction: Direction::Vertical,
1067                                children: vec![LayoutNode::leaf(insert), LayoutNode::Void(void_id)],
1068                                weights: vec![1u16, 1u16],
1069                                resizable: true,
1070                            },
1071                            LayoutNode::leaf(first),
1072                        ],
1073                        weights: vec![1u16, 1u16],
1074                        resizable: true,
1075                    };
1076                    return;
1077                }
1078                let bottom = LayoutNode::build_flat(Direction::Horizontal, ids);
1079                *self = LayoutNode::Split {
1080                    direction: Direction::Vertical,
1081                    children: vec![
1082                        LayoutNode::Split {
1083                            direction: Direction::Horizontal,
1084                            children: vec![LayoutNode::leaf(insert), LayoutNode::leaf(first)],
1085                            weights: vec![1u16, 1u16],
1086                            resizable: true,
1087                        },
1088                        bottom,
1089                    ],
1090                    weights: vec![1u16, 1u16],
1091                    resizable: true,
1092                };
1093            }
1094            InsertPosition::TopRight => {
1095                let mut ids = self.collect_leaves();
1096                ids.retain(|id| *id != insert);
1097                if ids.is_empty() {
1098                    *self = LayoutNode::leaf(insert);
1099                    return;
1100                }
1101                let first = ids.remove(0);
1102                if ids.is_empty() {
1103                    let void_id = VOID_ID_COUNTER.fetch_add(1, Ordering::Relaxed);
1104                    *self = LayoutNode::Split {
1105                        direction: Direction::Horizontal,
1106                        children: vec![
1107                            LayoutNode::leaf(first),
1108                            LayoutNode::Split {
1109                                direction: Direction::Vertical,
1110                                children: vec![LayoutNode::leaf(insert), LayoutNode::Void(void_id)],
1111                                weights: vec![1u16, 1u16],
1112                                resizable: true,
1113                            },
1114                        ],
1115                        weights: vec![1u16, 1u16],
1116                        resizable: true,
1117                    };
1118                    return;
1119                }
1120                let bottom = LayoutNode::build_flat(Direction::Horizontal, ids);
1121                *self = LayoutNode::Split {
1122                    direction: Direction::Vertical,
1123                    children: vec![
1124                        LayoutNode::Split {
1125                            direction: Direction::Horizontal,
1126                            children: vec![LayoutNode::leaf(first), LayoutNode::leaf(insert)],
1127                            weights: vec![1u16, 1u16],
1128                            resizable: true,
1129                        },
1130                        bottom,
1131                    ],
1132                    weights: vec![1u16, 1u16],
1133                    resizable: true,
1134                };
1135            }
1136            InsertPosition::BottomLeft => {
1137                let mut ids = self.collect_leaves();
1138                ids.retain(|id| *id != insert);
1139                if ids.is_empty() {
1140                    *self = LayoutNode::leaf(insert);
1141                    return;
1142                }
1143                let first = ids.remove(0);
1144                if ids.is_empty() {
1145                    let void_id = VOID_ID_COUNTER.fetch_add(1, Ordering::Relaxed);
1146                    *self = LayoutNode::Split {
1147                        direction: Direction::Horizontal,
1148                        children: vec![
1149                            LayoutNode::Split {
1150                                direction: Direction::Vertical,
1151                                children: vec![LayoutNode::Void(void_id), LayoutNode::leaf(insert)],
1152                                weights: vec![1u16, 1u16],
1153                                resizable: true,
1154                            },
1155                            LayoutNode::leaf(first),
1156                        ],
1157                        weights: vec![1u16, 1u16],
1158                        resizable: true,
1159                    };
1160                    return;
1161                }
1162                let top = LayoutNode::build_flat(Direction::Horizontal, ids);
1163                *self = LayoutNode::Split {
1164                    direction: Direction::Vertical,
1165                    children: vec![
1166                        top,
1167                        LayoutNode::Split {
1168                            direction: Direction::Horizontal,
1169                            children: vec![LayoutNode::leaf(insert), LayoutNode::leaf(first)],
1170                            weights: vec![1u16, 1u16],
1171                            resizable: true,
1172                        },
1173                    ],
1174                    weights: vec![1u16, 1u16],
1175                    resizable: true,
1176                };
1177            }
1178            InsertPosition::BottomRight => {
1179                let mut ids = self.collect_leaves();
1180                ids.retain(|id| *id != insert);
1181                if ids.is_empty() {
1182                    *self = LayoutNode::leaf(insert);
1183                    return;
1184                }
1185                let first = ids.remove(0);
1186                if ids.is_empty() {
1187                    let void_id = VOID_ID_COUNTER.fetch_add(1, Ordering::Relaxed);
1188                    *self = LayoutNode::Split {
1189                        direction: Direction::Horizontal,
1190                        children: vec![
1191                            LayoutNode::leaf(first),
1192                            LayoutNode::Split {
1193                                direction: Direction::Vertical,
1194                                children: vec![LayoutNode::Void(void_id), LayoutNode::leaf(insert)],
1195                                weights: vec![1u16, 1u16],
1196                                resizable: true,
1197                            },
1198                        ],
1199                        weights: vec![1u16, 1u16],
1200                        resizable: true,
1201                    };
1202                    return;
1203                }
1204                let top = LayoutNode::build_flat(Direction::Horizontal, ids);
1205                *self = LayoutNode::Split {
1206                    direction: Direction::Vertical,
1207                    children: vec![
1208                        top,
1209                        LayoutNode::Split {
1210                            direction: Direction::Horizontal,
1211                            children: vec![LayoutNode::leaf(first), LayoutNode::leaf(insert)],
1212                            weights: vec![1u16, 1u16],
1213                            resizable: true,
1214                        },
1215                    ],
1216                    weights: vec![1u16, 1u16],
1217                    resizable: true,
1218                };
1219            }
1220        };
1221    }
1222
1223    pub fn project_insert(
1224        &self,
1225        target: Option<Id>,
1226        insert: Id,
1227        position: InsertPosition,
1228        area: LayoutRect,
1229    ) -> Option<LayoutRect> {
1230        let mut root = self.clone();
1231        let removed = root.remove_leaf(insert);
1232        if !removed && matches!(&root, LayoutNode::Leaf(id) if *id == insert) {
1233            root = LayoutNode::Void(VOID_ID_COUNTER.fetch_add(1, Ordering::Relaxed));
1234        }
1235        let success = match target {
1236            Some(t) => root.insert_leaf(t, insert, position),
1237            None => false,
1238        };
1239        if !success {
1240            root.split_root(insert, position);
1241        }
1242        root.layout_rects(area)
1243            .into_iter()
1244            .find(|(id, _)| *id == insert)
1245            .map(|(_, r)| r)
1246    }
1247
1248    pub fn project_insert_void(
1249        &self,
1250        insert: Id,
1251        void_id: usize,
1252        area: LayoutRect,
1253    ) -> Option<LayoutRect> {
1254        let mut root = self.clone();
1255        root.remove_leaf(insert);
1256        if root.replace_void_by_id(void_id, LayoutNode::leaf(insert)) {
1257            root.layout_rects(area)
1258                .into_iter()
1259                .find(|(id, _)| *id == insert)
1260                .map(|(_, r)| r)
1261        } else {
1262            None
1263        }
1264    }
1265}
1266
1267pub fn split_area_for_path<Id: Copy + Eq + Ord>(
1268    node: &LayoutNode<Id>,
1269    area: LayoutRect,
1270    path: &[usize],
1271) -> Option<LayoutRect> {
1272    let mut area = area;
1273    let mut current = node;
1274    for &idx in path {
1275        let LayoutNode::Split {
1276            direction,
1277            children,
1278            weights,
1279            resizable,
1280            ..
1281        } = current
1282        else {
1283            return None;
1284        };
1285        let orientation = Orientation::from(*direction);
1286        let total_dim = match direction {
1287            Direction::Horizontal => area.width,
1288            Direction::Vertical => area.height,
1289        };
1290        let gap = split::gap_size(orientation, total_dim, children.len(), *resizable);
1291        let (rects, _) = split::split_rects_with_gaps(
1292            area,
1293            orientation,
1294            weights.as_slice(),
1295            children.len(),
1296            gap,
1297        );
1298        area = *rects.get(idx)?;
1299        current = children.get(idx)?;
1300    }
1301    Some(area)
1302}
1303
1304pub fn split_at_path_mut<'a, Id: Copy + Eq + Ord>(
1305    node: &'a mut LayoutNode<Id>,
1306    path: &[usize],
1307) -> Option<&'a mut LayoutNode<Id>> {
1308    let mut current = node;
1309    for &idx in path {
1310        let LayoutNode::Split { children, .. } = current else {
1311            return None;
1312        };
1313        current = children.get_mut(idx)?;
1314    }
1315    Some(current)
1316}
1317
1318#[cfg(test)]
1319mod tests {
1320    use super::*;
1321
1322    #[test]
1323    fn void_id_counter_increments() {
1324        let a = VOID_ID_COUNTER.fetch_add(1, Ordering::Relaxed);
1325        let b = VOID_ID_COUNTER.fetch_add(1, Ordering::Relaxed);
1326        assert!(b > a);
1327    }
1328
1329    #[test]
1330    fn from_rects_empty_returns_void() {
1331        let result = LayoutNode::<i32>::from_rects(&[]);
1332        assert!(matches!(result, LayoutNode::Void(_)));
1333    }
1334
1335    #[test]
1336    fn from_rects_single_leaf() {
1337        let rect = LayoutRect {
1338            x: 0,
1339            y: 0,
1340            width: 40,
1341            height: 24,
1342        };
1343        let result = LayoutNode::from_rects(&[(1, rect)]);
1344        assert!(matches!(result, LayoutNode::Leaf(1)));
1345    }
1346
1347    #[test]
1348    fn from_rects_gapped_windows_equal_columns() {
1349        let a = LayoutRect {
1350            x: 0,
1351            y: 0,
1352            width: 40,
1353            height: 24,
1354        };
1355        let b = LayoutRect {
1356            x: 100,
1357            y: 0,
1358            width: 40,
1359            height: 24,
1360        };
1361        let result = LayoutNode::from_rects(&[(1, a), (2, b)]);
1362        match result {
1363            LayoutNode::Split {
1364                direction: Direction::Horizontal,
1365                children,
1366                weights,
1367                ..
1368            } => {
1369                assert_eq!(children.len(), 2);
1370                assert_eq!(
1371                    weights,
1372                    vec![40, 40],
1373                    "gapped windows should get equal bounding spans"
1374                );
1375            }
1376            other => panic!("Expected Split, got {:?}", other),
1377        }
1378    }
1379
1380    #[test]
1381    fn from_rects_unequal_widths_preserves_proportion() {
1382        let a = LayoutRect {
1383            x: 0,
1384            y: 0,
1385            width: 80,
1386            height: 24,
1387        };
1388        let b = LayoutRect {
1389            x: 80,
1390            y: 0,
1391            width: 20,
1392            height: 24,
1393        };
1394        let result = LayoutNode::from_rects(&[(1, a), (2, b)]);
1395        match result {
1396            LayoutNode::Split {
1397                direction: Direction::Horizontal,
1398                children,
1399                weights,
1400                ..
1401            } => {
1402                assert_eq!(children.len(), 2);
1403                assert_eq!(
1404                    weights,
1405                    vec![80, 20],
1406                    "bounding span weights match window widths"
1407                );
1408            }
1409            other => panic!("Expected Horizontal Split, got {:?}", other),
1410        }
1411    }
1412
1413    #[test]
1414    fn from_rects_3_windows_top_bottom() {
1415        let a = LayoutRect {
1416            x: 0,
1417            y: 0,
1418            width: 100,
1419            height: 25,
1420        };
1421        let b = LayoutRect {
1422            x: 0,
1423            y: 25,
1424            width: 50,
1425            height: 25,
1426        };
1427        let c = LayoutRect {
1428            x: 50,
1429            y: 25,
1430            width: 50,
1431            height: 25,
1432        };
1433        let result = LayoutNode::from_rects(&[(1, a), (2, b), (3, c)]);
1434        match result {
1435            LayoutNode::Split {
1436                direction: Direction::Vertical,
1437                children,
1438                weights,
1439                ..
1440            } => {
1441                assert_eq!(children.len(), 2);
1442                assert_eq!(weights, vec![25, 25], "both sides have 25px Y-extent");
1443            }
1444            other => panic!("Expected Vertical Split, got {:?}", other),
1445        }
1446    }
1447
1448    #[test]
1449    fn from_rects_1v3_stacked_equal_width() {
1450        let a = LayoutRect {
1451            x: 0,
1452            y: 0,
1453            width: 40,
1454            height: 48,
1455        };
1456        let b = LayoutRect {
1457            x: 40,
1458            y: 0,
1459            width: 40,
1460            height: 16,
1461        };
1462        let c = LayoutRect {
1463            x: 40,
1464            y: 16,
1465            width: 40,
1466            height: 16,
1467        };
1468        let d = LayoutRect {
1469            x: 40,
1470            y: 32,
1471            width: 40,
1472            height: 16,
1473        };
1474        let result = LayoutNode::from_rects(&[(1, a), (2, b), (3, c), (4, d)]);
1475        match result {
1476            LayoutNode::Split {
1477                direction: Direction::Horizontal,
1478                children,
1479                weights,
1480                ..
1481            } => {
1482                assert_eq!(
1483                    children.len(),
1484                    2,
1485                    "should split into left=[A], right=[B,C,D]"
1486                );
1487                assert_eq!(
1488                    weights,
1489                    vec![40, 40],
1490                    "1-vs-3 stacked with same width = equal X-span"
1491                );
1492            }
1493            other => panic!("Expected Horizontal Split, got {:?}", other),
1494        }
1495    }
1496
1497    #[test]
1498    fn from_rects_overlapping_fallback() {
1499        let a = LayoutRect {
1500            x: 0,
1501            y: 0,
1502            width: 50,
1503            height: 50,
1504        };
1505        let b = LayoutRect {
1506            x: 10,
1507            y: 10,
1508            width: 50,
1509            height: 50,
1510        };
1511        let c = LayoutRect {
1512            x: 20,
1513            y: 20,
1514            width: 50,
1515            height: 50,
1516        };
1517        let result = LayoutNode::from_rects(&[(1, a), (2, b), (3, c)]);
1518        assert!(matches!(result, LayoutNode::Split { .. }));
1519    }
1520
1521    #[test]
1522    fn from_rects_with_layout_consistency() {
1523        let rects = [
1524            (
1525                1,
1526                LayoutRect {
1527                    x: 0,
1528                    y: 0,
1529                    width: 40,
1530                    height: 24,
1531                },
1532            ),
1533            (
1534                2,
1535                LayoutRect {
1536                    x: 60,
1537                    y: 0,
1538                    width: 40,
1539                    height: 24,
1540                },
1541            ),
1542        ];
1543        let node = LayoutNode::from_rects(&rects);
1544        let area = LayoutRect {
1545            x: 0,
1546            y: 0,
1547            width: 100,
1548            height: 24,
1549        };
1550        let (regions, _) = node.layout_with_gaps(area);
1551        assert_eq!(regions.len(), 2);
1552        let sum_w: u16 = regions.iter().map(|(_, r)| r.width).sum();
1553        assert!(
1554            sum_w == 100 || sum_w == 99,
1555            "regions should fill the full width (got {})",
1556            sum_w
1557        );
1558    }
1559
1560    #[test]
1561    fn split_rects_nary_even() {
1562        let area = LayoutRect {
1563            x: 0,
1564            y: 0,
1565            width: 11,
1566            height: 1,
1567        };
1568        let weights = [1u16, 1u16];
1569        let rects = crate::split_rects_weighted(area, crate::Orientation::Horizontal, &weights, 2);
1570        assert_eq!(rects.len(), 2);
1571        assert_eq!(rects[0].width, 5);
1572        assert_eq!(rects[1].width, 6);
1573    }
1574
1575    #[test]
1576    fn insert_and_remove_leaf_and_split_area_for_path() {
1577        let mut node = LayoutNode::<usize>::leaf(1);
1578        assert!(node.insert_leaf(1, 2, InsertPosition::Right));
1579        if let LayoutNode::Split { children, .. } = &node {
1580            assert_eq!(children.len(), 2);
1581            assert_eq!(children[0].unwrap_leaf(), Some(1));
1582            assert_eq!(children[1].unwrap_leaf(), Some(2));
1583        } else {
1584            panic!("expected split after insert");
1585        }
1586        let area = LayoutRect {
1587            x: 0,
1588            y: 0,
1589            width: 10,
1590            height: 4,
1591        };
1592        let sub = split_area_for_path(&node, area, &[1]).expect("should get area for path");
1593        assert!(sub.x > 0);
1594        assert!(node.remove_leaf(2));
1595        assert_eq!(node.unwrap_leaf(), Some(1));
1596    }
1597
1598    #[test]
1599    fn hit_test_handle_finds_gap() {
1600        let area = LayoutRect {
1601            x: 0,
1602            y: 0,
1603            width: 80,
1604            height: 24,
1605        };
1606        let node = LayoutNode::Split {
1607            direction: Direction::Horizontal,
1608            children: vec![LayoutNode::Leaf(1), LayoutNode::Leaf(2)],
1609            weights: vec![1u16, 1u16],
1610            resizable: true,
1611        };
1612        let (_, gaps) = node.layout_with_gaps(area);
1613        assert_eq!(gaps.len(), 1, "2-window split must produce 1 gap");
1614        let gap = &gaps[0];
1615        assert_eq!(gap.direction, Direction::Horizontal);
1616        assert_eq!(gap.index, 0);
1617        assert!(gap.rect.width > 0);
1618        assert_eq!(gap.rect.height, 24);
1619        let center_col = (gap.rect.x + i32::from(gap.rect.width) / 2) as u16;
1620        let center_row = (gap.rect.y + i32::from(gap.rect.height) / 2) as u16;
1621        let found = node.hit_test_gap(area, center_col, center_row);
1622        assert!(found.is_some(), "hit_test_gap must find the gap");
1623        assert_eq!(found.unwrap().direction, Direction::Horizontal);
1624    }
1625
1626    #[test]
1627    fn normalize_weights_resets_to_equal() {
1628        let mut node = LayoutNode::Split {
1629            direction: Direction::Horizontal,
1630            children: vec![LayoutNode::leaf(1), LayoutNode::leaf(2)],
1631            weights: vec![3u16, 1u16],
1632            resizable: true,
1633        };
1634        node.normalize_weights();
1635        if let LayoutNode::Split { weights, .. } = &node {
1636            assert!(weights.iter().all(|w| *w == 1u16));
1637        } else {
1638            panic!("expected split");
1639        }
1640    }
1641
1642    #[test]
1643    fn build_flat_empty_returns_void() {
1644        let node = LayoutNode::build_flat(Direction::Horizontal, Vec::<usize>::new());
1645        assert!(node.unwrap_leaf().is_none());
1646    }
1647
1648    #[test]
1649    fn build_flat_single_returns_leaf() {
1650        let node = LayoutNode::build_flat(Direction::Horizontal, vec![42]);
1651        assert_eq!(node.unwrap_leaf(), Some(42));
1652    }
1653
1654    #[test]
1655    fn build_flat_multiple_returns_split() {
1656        let node = LayoutNode::build_flat(Direction::Vertical, vec![1, 2, 3]);
1657        if let LayoutNode::Split {
1658            children,
1659            weights,
1660            direction,
1661            ..
1662        } = &node
1663        {
1664            assert_eq!(children.len(), 3);
1665            assert_eq!(*direction, Direction::Vertical);
1666            assert!(weights.iter().all(|w| *w == 1u16));
1667        } else {
1668            panic!("expected split");
1669        }
1670    }
1671
1672    #[test]
1673    fn void_regions_returns_voids() {
1674        let node = LayoutNode::Split {
1675            direction: Direction::Horizontal,
1676            children: vec![LayoutNode::leaf(1), LayoutNode::Void(99)],
1677            weights: vec![1u16, 1u16],
1678            resizable: true,
1679        };
1680        let area = LayoutRect {
1681            x: 0,
1682            y: 0,
1683            width: 80,
1684            height: 24,
1685        };
1686        let voids = node.void_regions(area);
1687        assert_eq!(voids.len(), 1);
1688        assert_eq!(voids[0].0, 99);
1689    }
1690
1691    #[test]
1692    fn swap_leaves_exchanges_positions() {
1693        let mut node = LayoutNode::Split {
1694            direction: Direction::Horizontal,
1695            children: vec![
1696                LayoutNode::leaf(1),
1697                LayoutNode::leaf(2),
1698                LayoutNode::leaf(3),
1699            ],
1700            weights: vec![1u16, 1u16, 1u16],
1701            resizable: true,
1702        };
1703        assert!(node.swap_leaves(&1, &3));
1704        let leaves = node.collect_leaves();
1705        assert_eq!(leaves, vec![3, 2, 1]);
1706    }
1707
1708    #[test]
1709    fn swap_leaves_same_id_returns_false() {
1710        let mut node = LayoutNode::leaf(1);
1711        assert!(!node.swap_leaves(&1, &1));
1712    }
1713
1714    #[test]
1715    fn swap_leaves_nonexistent_returns_false() {
1716        let mut node = LayoutNode::leaf(1);
1717        assert!(!node.swap_leaves(&1, &2));
1718    }
1719
1720    #[test]
1721    fn cleanup_after_removes_void_children() {
1722        let mut node = LayoutNode::Split {
1723            direction: Direction::Horizontal,
1724            children: vec![LayoutNode::leaf(1), LayoutNode::Void(99)],
1725            weights: vec![1u16, 1u16],
1726            resizable: true,
1727        };
1728        node.cleanup_after_removal();
1729        assert_eq!(node.unwrap_leaf(), Some(1));
1730    }
1731
1732    #[test]
1733    fn cleanup_all_voids_becomes_void() {
1734        let mut node: LayoutNode<usize> = LayoutNode::Split {
1735            direction: Direction::Horizontal,
1736            children: vec![LayoutNode::Void(1), LayoutNode::Void(2)],
1737            weights: vec![1u16, 1u16],
1738            resizable: true,
1739        };
1740        node.cleanup_after_removal();
1741        assert!(matches!(node, LayoutNode::Void(_)));
1742    }
1743
1744    #[test]
1745    fn cleanup_empty_split_becomes_void() {
1746        let mut node: LayoutNode<usize> = LayoutNode::Split {
1747            direction: Direction::Horizontal,
1748            children: Vec::new(),
1749            weights: Vec::new(),
1750            resizable: true,
1751        };
1752        node.cleanup_after_removal();
1753        assert!(matches!(node, LayoutNode::Void(_)));
1754    }
1755
1756    #[test]
1757    fn clear_leaf_replaces_with_void() {
1758        let mut node = LayoutNode::leaf(42);
1759        assert!(node.clear_leaf(42));
1760        assert!(matches!(node, LayoutNode::Void(_)));
1761    }
1762
1763    #[test]
1764    fn clear_leaf_wrong_id_returns_false() {
1765        let mut node = LayoutNode::leaf(42);
1766        assert!(!node.clear_leaf(99));
1767    }
1768
1769    #[test]
1770    fn subtree_any_finds_matching_leaf() {
1771        let node = LayoutNode::Split {
1772            direction: Direction::Horizontal,
1773            children: vec![LayoutNode::leaf(1), LayoutNode::leaf(2)],
1774            weights: vec![1u16, 1u16],
1775            resizable: true,
1776        };
1777        assert!(node.subtree_any(|id| id == 2));
1778        assert!(!node.subtree_any(|id| id == 99));
1779    }
1780
1781    #[test]
1782    fn node_at_path_returns_none_for_invalid_path() {
1783        let node = LayoutNode::leaf(1);
1784        assert!(node.node_at_path(&[0]).is_none());
1785    }
1786
1787    #[test]
1788    fn collect_leaves_from_nested() {
1789        let node = LayoutNode::Split {
1790            direction: Direction::Vertical,
1791            children: vec![
1792                LayoutNode::leaf(1),
1793                LayoutNode::Split {
1794                    direction: Direction::Horizontal,
1795                    children: vec![LayoutNode::leaf(2), LayoutNode::leaf(3)],
1796                    weights: vec![1u16, 1u16],
1797                    resizable: true,
1798                },
1799            ],
1800            weights: vec![1u16, 1u16],
1801            resizable: true,
1802        };
1803        assert_eq!(node.collect_leaves(), vec![1, 2, 3]);
1804    }
1805
1806    #[test]
1807    fn insert_leaf_left_on_single() {
1808        let mut node = LayoutNode::leaf(1);
1809        assert!(node.insert_leaf(1, 2, InsertPosition::Left));
1810        let leaves = node.collect_leaves();
1811        assert_eq!(leaves, vec![2, 1]);
1812    }
1813
1814    #[test]
1815    fn insert_leaf_top_on_single() {
1816        let mut node = LayoutNode::leaf(1);
1817        assert!(node.insert_leaf(1, 2, InsertPosition::Top));
1818        let leaves = node.collect_leaves();
1819        assert_eq!(leaves, vec![2, 1]);
1820    }
1821
1822    #[test]
1823    fn insert_leaf_bottom_on_single() {
1824        let mut node = LayoutNode::leaf(1);
1825        assert!(node.insert_leaf(1, 2, InsertPosition::Bottom));
1826        let leaves = node.collect_leaves();
1827        assert_eq!(leaves, vec![1, 2]);
1828    }
1829
1830    #[test]
1831    fn insert_leaf_nonexistent_target_returns_false() {
1832        let mut node = LayoutNode::leaf(1);
1833        assert!(!node.insert_leaf(99, 2, InsertPosition::Right));
1834    }
1835
1836    #[test]
1837    fn insert_leaf_in_nested_split() {
1838        let mut node = LayoutNode::Split {
1839            direction: Direction::Horizontal,
1840            children: vec![LayoutNode::leaf(1), LayoutNode::leaf(2)],
1841            weights: vec![1u16, 1u16],
1842            resizable: true,
1843        };
1844        assert!(node.insert_leaf(2, 3, InsertPosition::Right));
1845        let leaves = node.collect_leaves();
1846        assert_eq!(leaves, vec![1, 2, 3]);
1847    }
1848
1849    const TEST_AREA: crate::rect::LayoutRect = crate::rect::LayoutRect {
1850        x: 0,
1851        y: 0,
1852        width: 80,
1853        height: 24,
1854    };
1855
1856    // ── replace_leaf_with_void ────────────────────────────────────────
1857
1858    #[test]
1859    fn replace_leaf_with_void_single_leaf() {
1860        let mut node = LayoutNode::leaf(42);
1861        let vid = node.replace_leaf_with_void(42);
1862        assert!(vid.is_some(), "should return a void_id");
1863        assert!(matches!(node, LayoutNode::Void(_)), "leaf became void");
1864        // layout_rects should yield nothing
1865        let rects = node.layout_rects(TEST_AREA);
1866        assert!(rects.is_empty(), "void produces no regions");
1867    }
1868
1869    #[test]
1870    fn replace_leaf_with_void_nested_split() {
1871        let mut node = LayoutNode::Split {
1872            direction: Direction::Horizontal,
1873            children: vec![
1874                LayoutNode::leaf(1),
1875                LayoutNode::leaf(2),
1876                LayoutNode::leaf(3),
1877            ],
1878            weights: vec![1u16, 1u16, 1u16],
1879            resizable: true,
1880        };
1881        let vid = node.replace_leaf_with_void(2);
1882        assert!(vid.is_some(), "should return a void_id");
1883        // Structure: Split [Leaf(1), Void(vid), Leaf(3)]
1884        let leaves = node.collect_leaves();
1885        assert_eq!(leaves, vec![1, 3], "leaf 2 removed from tree");
1886        // Weights should be preserved (3 weights still, the void is a child)
1887        if let LayoutNode::Split {
1888            children, weights, ..
1889        } = &node
1890        {
1891            assert_eq!(children.len(), 3, "void placeholder preserved");
1892            assert_eq!(weights.len(), 3, "weights preserved");
1893        } else {
1894            panic!("expected Split");
1895        }
1896    }
1897
1898    #[test]
1899    fn replace_leaf_with_void_nonexistent() {
1900        let mut node = LayoutNode::leaf(42);
1901        let vid = node.replace_leaf_with_void(99);
1902        assert!(vid.is_none(), "nonexistent id returns None");
1903        assert_eq!(node.unwrap_leaf(), Some(42), "tree unchanged");
1904    }
1905
1906    // ── remove_void_by_id ─────────────────────────────────────────────
1907
1908    #[test]
1909    fn remove_void_by_id_existing_root() {
1910        let mut node: LayoutNode<usize> = LayoutNode::Void(99);
1911        assert!(node.remove_void_by_id(99));
1912        // Becomes a new void (fresh id), but it was removed
1913        assert!(matches!(node, LayoutNode::Void(_)));
1914    }
1915
1916    #[test]
1917    fn remove_void_by_id_nested() {
1918        let mut node = LayoutNode::Split {
1919            direction: Direction::Horizontal,
1920            children: vec![
1921                LayoutNode::leaf(1),
1922                LayoutNode::Void(99),
1923                LayoutNode::leaf(3),
1924            ],
1925            weights: vec![1u16, 1u16, 1u16],
1926            resizable: true,
1927        };
1928        assert!(node.remove_void_by_id(99), "void removed");
1929        // After removal the split should have 2 children
1930        let leaves = node.collect_leaves();
1931        assert_eq!(leaves, vec![1, 3], "remaining leaves preserved");
1932    }
1933
1934    #[test]
1935    fn remove_void_by_id_nonexistent() {
1936        let mut node = LayoutNode::leaf(42);
1937        assert!(!node.remove_void_by_id(99), "nonexistent returns false");
1938    }
1939
1940    #[test]
1941    fn remove_void_by_id_deeply_nested() {
1942        // Split [Leaf(1), Split [Void(99), Leaf(2)]]
1943        let mut node = LayoutNode::Split {
1944            direction: Direction::Horizontal,
1945            children: vec![
1946                LayoutNode::leaf(1),
1947                LayoutNode::Split {
1948                    direction: Direction::Vertical,
1949                    children: vec![LayoutNode::Void(99), LayoutNode::leaf(2)],
1950                    weights: vec![1u16, 1u16],
1951                    resizable: true,
1952                },
1953            ],
1954            weights: vec![1u16, 1u16],
1955            resizable: true,
1956        };
1957        assert!(node.remove_void_by_id(99), "deeply nested void removed");
1958        let leaves = node.collect_leaves();
1959        assert_eq!(
1960            leaves,
1961            vec![1, 2],
1962            "remaining leaves preserved after deep removal"
1963        );
1964    }
1965
1966    // ── Void placeholder preserves layout ─────────────────────────────
1967
1968    #[test]
1969    fn void_produces_no_regions_in_split() {
1970        let mut node = LayoutNode::Split {
1971            direction: Direction::Horizontal,
1972            children: vec![LayoutNode::leaf(1), LayoutNode::leaf(2)],
1973            weights: vec![1u16, 1u16],
1974            resizable: true,
1975        };
1976        // Replace leaf 2 with void
1977        node.replace_leaf_with_void(2);
1978        let rects = node.layout_rects(TEST_AREA);
1979        // Only leaf 1 produces a region; leaf 2's slot is preserved but empty
1980        assert_eq!(rects.len(), 1, "only one region");
1981        assert_eq!(rects[0].0, 1, "remaining leaf id");
1982        assert!(rects[0].1.width > 0, "leaf region has positive width");
1983    }
1984
1985    #[test]
1986    fn void_placeholder_preserves_weights() {
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![2u16, 3u16, 5u16],
1995            resizable: true,
1996        };
1997        let _ = node.replace_leaf_with_void(2);
1998        if let LayoutNode::Split { weights, .. } = &node {
1999            assert_eq!(weights.as_slice(), &[2u16, 3u16, 5u16], "weights unchanged");
2000        } else {
2001            panic!("expected Split");
2002        }
2003    }
2004}