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