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 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 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 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 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 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 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 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 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 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 #[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 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 let leaves = node.collect_leaves();
2001 assert_eq!(leaves, vec![1, 3], "leaf 2 removed from tree");
2002 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 #[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 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 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 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 #[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 node.replace_leaf_with_void(2);
2094 let rects = node.layout_rects(TEST_AREA);
2095 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}