Skip to main content

term_wm_layout_engine/
split.rs

1use crate::rect::{LayoutRect, Orientation, Ratio};
2
3/// Split a rectangle into two along the given orientation using a ratio.
4pub fn split_rect_bsp(
5    area: LayoutRect,
6    orientation: Orientation,
7    ratio: Ratio,
8) -> (LayoutRect, LayoutRect) {
9    match orientation {
10        Orientation::Horizontal => split_horizontal(area, ratio),
11        Orientation::Vertical => split_vertical(area, ratio),
12    }
13}
14
15fn split_horizontal(area: LayoutRect, ratio: Ratio) -> (LayoutRect, LayoutRect) {
16    let total = u32::from(area.width);
17    let left_w = if ratio.total() == 0 {
18        total / 2
19    } else {
20        total * u32::from(ratio.left_part()) / u32::from(ratio.total())
21    };
22    let left_w = left_w as u16;
23    let right_w = area.width.saturating_sub(left_w);
24
25    let left = LayoutRect {
26        x: area.x,
27        y: area.y,
28        width: left_w,
29        height: area.height,
30    };
31    let right = LayoutRect {
32        x: area.x.saturating_add(i32::from(left_w)),
33        y: area.y,
34        width: right_w,
35        height: area.height,
36    };
37    (left, right)
38}
39
40fn split_vertical(area: LayoutRect, ratio: Ratio) -> (LayoutRect, LayoutRect) {
41    let total = u32::from(area.height);
42    let top_h = if ratio.total() == 0 {
43        total / 2
44    } else {
45        total * u32::from(ratio.left_part()) / u32::from(ratio.total())
46    };
47    let top_h = top_h as u16;
48    let bottom_h = area.height.saturating_sub(top_h);
49
50    let top = LayoutRect {
51        x: area.x,
52        y: area.y,
53        width: area.width,
54        height: top_h,
55    };
56    let bottom = LayoutRect {
57        x: area.x,
58        y: area.y.saturating_add(i32::from(top_h)),
59        width: area.width,
60        height: bottom_h,
61    };
62    (top, bottom)
63}
64
65pub fn split_rects_nary(
66    area: LayoutRect,
67    orientation: Orientation,
68    weights: &[u16],
69    child_count: usize,
70) -> Vec<LayoutRect> {
71    if child_count == 0 {
72        return Vec::new();
73    }
74    if child_count == 1 {
75        return vec![area];
76    }
77    let total_weight: u32 = weights.iter().map(|w| u32::from(*w)).sum();
78    if total_weight == 0 {
79        return split_evenly(area, orientation, child_count);
80    }
81
82    let (total_dim, fixed_start) = match orientation {
83        Orientation::Horizontal => (u32::from(area.width), area.x),
84        Orientation::Vertical => (u32::from(area.height), area.y),
85    };
86
87    let mut rects = Vec::with_capacity(child_count);
88    let mut offset = fixed_start;
89    let mut allocated: u32 = 0;
90
91    for (i, &w) in weights.iter().enumerate() {
92        let is_last = i == child_count - 1;
93        let size = if is_last {
94            (total_dim.saturating_sub(allocated)) as u16
95        } else {
96            let s = (total_dim * u32::from(w) / total_weight) as u16;
97            allocated = allocated.saturating_add(u32::from(s));
98            s
99        };
100
101        let rect = match orientation {
102            Orientation::Horizontal => LayoutRect {
103                x: offset,
104                y: area.y,
105                width: size,
106                height: area.height,
107            },
108            Orientation::Vertical => LayoutRect {
109                x: area.x,
110                y: offset,
111                width: area.width,
112                height: size,
113            },
114        };
115        rects.push(rect);
116
117        match orientation {
118            Orientation::Horizontal => offset = offset.saturating_add(i32::from(size)),
119            Orientation::Vertical => offset = offset.saturating_add(i32::from(size)),
120        }
121    }
122
123    rects
124}
125
126fn split_evenly(area: LayoutRect, orientation: Orientation, count: usize) -> Vec<LayoutRect> {
127    if count == 0 {
128        return Vec::new();
129    }
130    if count == 1 {
131        return vec![area];
132    }
133
134    let (total_dim, fixed_start) = match orientation {
135        Orientation::Horizontal => (u32::from(area.width), area.x),
136        Orientation::Vertical => (u32::from(area.height), area.y),
137    };
138
139    let per_child = total_dim / count as u32;
140    let mut remainder = (total_dim % count as u32) as u16;
141
142    let mut rects = Vec::with_capacity(count);
143    let mut offset = fixed_start;
144
145    for _ in 0..count {
146        let extra = if remainder > 0 {
147            remainder -= 1;
148            1
149        } else {
150            0
151        };
152        let size = (per_child as u16).saturating_add(extra);
153
154        let rect = match orientation {
155            Orientation::Horizontal => LayoutRect {
156                x: offset,
157                y: area.y,
158                width: size,
159                height: area.height,
160            },
161            Orientation::Vertical => LayoutRect {
162                x: area.x,
163                y: offset,
164                width: area.width,
165                height: size,
166            },
167        };
168        rects.push(rect);
169
170        match orientation {
171            Orientation::Horizontal => offset = offset.saturating_add(i32::from(size)),
172            Orientation::Vertical => offset = offset.saturating_add(i32::from(size)),
173        }
174    }
175
176    rects
177}
178
179/// Compute the visual thickness of a split handle gap.
180pub fn handle_thickness(orientation: Orientation, _total_dim: u16) -> u16 {
181    match orientation {
182        Orientation::Horizontal => 1,
183        Orientation::Vertical => 1,
184    }
185}
186
187/// Compute the per-gap size between children in a split.
188pub fn gap_size(
189    orientation: Orientation,
190    total_dim: u16,
191    child_count: usize,
192    resizable: bool,
193) -> u16 {
194    if !resizable || child_count < 2 {
195        return 0;
196    }
197    if total_dim == 0 {
198        return 0;
199    }
200    let min_content = child_count as u16;
201    if total_dim <= min_content {
202        return 0;
203    }
204    let max_gap = total_dim.saturating_sub(min_content);
205    let per_gap = max_gap / (child_count as u16).saturating_sub(1);
206    handle_thickness(orientation, total_dim).min(per_gap)
207}
208
209/// Weighted split of a rect into `child_count` rects using integer weights.
210pub fn split_rects_weighted(
211    area: LayoutRect,
212    orientation: Orientation,
213    weights: &[u16],
214    child_count: usize,
215) -> Vec<LayoutRect> {
216    let count = child_count.max(1);
217    let weights = if weights.len() == count {
218        weights.to_vec()
219    } else {
220        vec![1u16; count]
221    };
222    let total_weight: u32 = weights.iter().map(|w| u32::from(*w)).sum::<u32>().max(1);
223    let total = match orientation {
224        Orientation::Horizontal => u32::from(area.width),
225        Orientation::Vertical => u32::from(area.height),
226    };
227
228    let mut sizes = Vec::with_capacity(count);
229    let mut allocated: u32 = 0;
230    for (idx, &w) in weights.iter().enumerate() {
231        let size = if idx + 1 == count {
232            total.saturating_sub(allocated) as u16
233        } else {
234            let s = (total * u32::from(w) / total_weight) as u16;
235            allocated = allocated.saturating_add(u32::from(s));
236            s
237        };
238        sizes.push(size);
239    }
240    build_rects_from_sizes(area, orientation, &sizes)
241}
242
243/// Split a rect into `child_count` rects separated by `gap`-width gaps.
244pub fn split_rects_with_gaps(
245    area: LayoutRect,
246    orientation: Orientation,
247    weights: &[u16],
248    child_count: usize,
249    gap: u16,
250) -> (Vec<LayoutRect>, Vec<LayoutRect>) {
251    if gap == 0 || child_count < 2 {
252        return (
253            split_rects_weighted(area, orientation, weights, child_count),
254            Vec::new(),
255        );
256    }
257    let gap_total = gap.saturating_mul((child_count.saturating_sub(1)) as u16);
258    let mut shrunk = area;
259    match orientation {
260        Orientation::Horizontal => {
261            shrunk.width = area.width.saturating_sub(gap_total);
262        }
263        Orientation::Vertical => {
264            shrunk.height = area.height.saturating_sub(gap_total);
265        }
266    }
267    let raw = split_rects_weighted(shrunk, orientation, weights, child_count);
268    let mut rects = Vec::with_capacity(raw.len());
269    for (idx, rect) in raw.into_iter().enumerate() {
270        let offset = gap.saturating_mul(idx as u16);
271        let shifted = match orientation {
272            Orientation::Horizontal => LayoutRect {
273                x: rect.x.saturating_add(i32::from(offset)),
274                ..rect
275            },
276            Orientation::Vertical => LayoutRect {
277                y: rect.y.saturating_add(i32::from(offset)),
278                ..rect
279            },
280        };
281        rects.push(shifted);
282    }
283    let mut gaps = Vec::with_capacity(child_count.saturating_sub(1));
284    for rect in rects.iter().take(child_count.saturating_sub(1)) {
285        let gap_rect = match orientation {
286            Orientation::Horizontal => LayoutRect {
287                x: rect.x.saturating_add(i32::from(rect.width)),
288                y: area.y,
289                width: gap,
290                height: area.height,
291            },
292            Orientation::Vertical => LayoutRect {
293                x: area.x,
294                y: rect.y.saturating_add(i32::from(rect.height)),
295                width: area.width,
296                height: gap,
297            },
298        };
299        gaps.push(gap_rect);
300    }
301    (rects, gaps)
302}
303
304/// Build rects from a list of per-child sizes along an orientation.
305pub fn build_rects_from_sizes(
306    area: LayoutRect,
307    orientation: Orientation,
308    sizes: &[u16],
309) -> Vec<LayoutRect> {
310    let mut rects = Vec::with_capacity(sizes.len());
311    let mut cursor_x = area.x;
312    let mut cursor_y = area.y;
313    for &size in sizes {
314        let rect = match orientation {
315            Orientation::Horizontal => LayoutRect {
316                x: cursor_x,
317                y: area.y,
318                width: size,
319                height: area.height,
320            },
321            Orientation::Vertical => LayoutRect {
322                x: area.x,
323                y: cursor_y,
324                width: area.width,
325                height: size,
326            },
327        };
328        rects.push(rect);
329        match orientation {
330            Orientation::Horizontal => cursor_x = cursor_x.saturating_add(i32::from(size)),
331            Orientation::Vertical => cursor_y = cursor_y.saturating_add(i32::from(size)),
332        }
333    }
334    rects
335}
336
337/// Extract the per-child dimension sizes from a split result.
338pub fn split_sizes(
339    area: LayoutRect,
340    orientation: Orientation,
341    weights: &[u16],
342    child_count: usize,
343    gap: u16,
344) -> Vec<u16> {
345    let (rects, _) = split_rects_with_gaps(area, orientation, weights, child_count, gap);
346    rects
347        .iter()
348        .map(|r| match orientation {
349            Orientation::Horizontal => r.width,
350            Orientation::Vertical => r.height,
351        })
352        .collect()
353}
354
355#[cfg(test)]
356mod tests {
357    use super::*;
358
359    fn area(w: u16, h: u16) -> LayoutRect {
360        LayoutRect {
361            x: 0,
362            y: 0,
363            width: w,
364            height: h,
365        }
366    }
367
368    #[test]
369    fn split_horizontal_evenly() {
370        let (l, r) = split_rect_bsp(area(80, 24), Orientation::Horizontal, Ratio(1, 1));
371        assert_eq!(l.width, 40);
372        assert_eq!(r.width, 40);
373        assert_eq!(l.x, 0);
374        assert_eq!(r.x, 40);
375    }
376
377    #[test]
378    fn split_horizontal_uneven() {
379        let (l, r) = split_rect_bsp(area(81, 24), Orientation::Horizontal, Ratio(1, 1));
380        assert_eq!(l.width, 40);
381        assert_eq!(r.width, 41);
382        assert_eq!(l.x, 0);
383        assert_eq!(r.x, 40);
384    }
385
386    #[test]
387    fn split_horizontal_third() {
388        let (l, r) = split_rect_bsp(area(90, 24), Orientation::Horizontal, Ratio(1, 2));
389        assert_eq!(l.width, 30);
390        assert_eq!(r.width, 60);
391    }
392
393    #[test]
394    fn split_vertical_evenly() {
395        let (t, b) = split_rect_bsp(area(80, 24), Orientation::Vertical, Ratio(1, 1));
396        assert_eq!(t.height, 12);
397        assert_eq!(b.height, 12);
398    }
399
400    #[test]
401    fn split_vertical_uneven() {
402        let (t, b) = split_rect_bsp(area(80, 25), Orientation::Vertical, Ratio(1, 1));
403        assert_eq!(t.height, 12);
404        assert_eq!(b.height, 13);
405    }
406
407    #[test]
408    fn split_rects_nary_two_equal() {
409        let rects = split_rects_nary(area(80, 24), Orientation::Horizontal, &[1, 1], 2);
410        assert_eq!(rects.len(), 2);
411        assert_eq!(rects[0].width, 40);
412        assert_eq!(rects[1].width, 40);
413        assert_eq!(rects[0].x, 0);
414        assert_eq!(rects[1].x, 40);
415    }
416
417    #[test]
418    fn split_rects_nary_three_equal() {
419        let rects = split_rects_nary(area(80, 24), Orientation::Vertical, &[1, 1, 1], 3);
420        assert_eq!(rects.len(), 3);
421        // 24/3 = 8 each
422        assert_eq!(rects[0].height, 8);
423        assert_eq!(rects[1].height, 8);
424        assert_eq!(rects[2].height, 8);
425    }
426
427    #[test]
428    fn split_rects_nary_weighted() {
429        let rects = split_rects_nary(area(80, 24), Orientation::Horizontal, &[1, 3], 2);
430        assert_eq!(rects.len(), 2);
431        assert_eq!(rects[0].width, 20); // 1/4 of 80
432        assert_eq!(rects[1].width, 60); // 3/4 of 80
433    }
434
435    #[test]
436    fn split_evenly_remainder_distribution() {
437        let rects = split_evenly(area(10, 24), Orientation::Horizontal, 3);
438        assert_eq!(rects[0].width, 4); // 10/3 = 3, remainder 1 → first gets +1
439        assert_eq!(rects[1].width, 3);
440        assert_eq!(rects[2].width, 3);
441    }
442
443    #[test]
444    fn split_rects_no_remainder_dead_zones() {
445        let rects = split_rects_nary(area(81, 24), Orientation::Horizontal, &[1, 1, 1], 3);
446        let total_w: u16 = rects.iter().map(|r| r.width).sum();
447        assert_eq!(total_w, 81);
448    }
449
450    #[test]
451    fn bsp_split_no_dead_zones() {
452        let (l, r) = split_rect_bsp(area(81, 24), Orientation::Horizontal, Ratio(1, 1));
453        assert_eq!(l.width + r.width, 81);
454        assert_eq!(l.x, 0);
455        assert_eq!(r.x, i32::from(l.width));
456    }
457
458    #[test]
459    fn handle_thickness_is_one() {
460        assert_eq!(handle_thickness(Orientation::Horizontal, 100), 1);
461        assert_eq!(handle_thickness(Orientation::Vertical, 100), 1);
462    }
463
464    #[test]
465    fn gap_size_no_gap_when_not_resizable() {
466        assert_eq!(gap_size(Orientation::Horizontal, 80, 2, false), 0);
467    }
468
469    #[test]
470    fn gap_size_zero_when_too_small() {
471        assert_eq!(gap_size(Orientation::Horizontal, 2, 3, true), 0);
472        assert_eq!(gap_size(Orientation::Horizontal, 0, 2, true), 0);
473    }
474
475    #[test]
476    fn gap_size_returns_gap() {
477        let g = gap_size(Orientation::Horizontal, 80, 4, true);
478        assert!(g >= 1);
479    }
480
481    #[test]
482    fn split_rects_weighted_two_equal() {
483        let rects = split_rects_weighted(area(80, 24), Orientation::Horizontal, &[1, 1], 2);
484        assert_eq!(rects.len(), 2);
485        assert_eq!(rects[0].width, 40);
486        assert_eq!(rects[1].width, 40);
487    }
488
489    #[test]
490    fn split_rects_weighted_uneven() {
491        let rects = split_rects_weighted(area(80, 24), Orientation::Horizontal, &[1, 3], 2);
492        assert_eq!(rects[0].width, 20);
493        assert_eq!(rects[1].width, 60);
494    }
495
496    #[test]
497    fn split_rects_weighted_vertical() {
498        let rects = split_rects_weighted(area(80, 24), Orientation::Vertical, &[1, 1], 2);
499        assert_eq!(rects[0].height, 12);
500        assert_eq!(rects[1].height, 12);
501    }
502
503    #[test]
504    fn split_rects_weighted_no_remainder() {
505        let rects = split_rects_weighted(area(81, 24), Orientation::Horizontal, &[1, 1, 1], 3);
506        let total: u16 = rects.iter().map(|r| r.width).sum();
507        assert_eq!(total, 81);
508    }
509
510    #[test]
511    fn split_rects_with_gaps_horizontal() {
512        let (rects, gaps) =
513            split_rects_with_gaps(area(80, 24), Orientation::Horizontal, &[1, 1], 2, 2);
514        assert_eq!(rects.len(), 2);
515        assert_eq!(gaps.len(), 1);
516        assert_eq!(rects[0].width + rects[1].width + gaps[0].width, 80);
517        assert_eq!(gaps[0].width, 2);
518    }
519
520    #[test]
521    fn split_rects_with_gaps_vertical() {
522        let (rects, gaps) =
523            split_rects_with_gaps(area(80, 24), Orientation::Vertical, &[1, 1], 2, 2);
524        assert_eq!(rects.len(), 2);
525        assert_eq!(gaps.len(), 1);
526        assert_eq!(rects[0].height + rects[1].height + gaps[0].height, 24);
527        assert_eq!(gaps[0].height, 2);
528    }
529
530    #[test]
531    fn split_rects_with_gaps_zero_gap() {
532        let (rects, gaps) =
533            split_rects_with_gaps(area(80, 24), Orientation::Horizontal, &[1, 1], 2, 0);
534        assert_eq!(rects.len(), 2);
535        assert!(gaps.is_empty());
536    }
537
538    #[test]
539    fn split_rects_with_gaps_single_child() {
540        let (rects, gaps) =
541            split_rects_with_gaps(area(80, 24), Orientation::Horizontal, &[1], 1, 2);
542        assert_eq!(rects.len(), 1);
543        assert!(gaps.is_empty());
544    }
545
546    #[test]
547    fn build_rects_from_sizes_horizontal() {
548        let rects = build_rects_from_sizes(area(80, 24), Orientation::Horizontal, &[10, 20, 30]);
549        assert_eq!(rects.len(), 3);
550        assert_eq!(rects[0].x, 0);
551        assert_eq!(rects[0].width, 10);
552        assert_eq!(rects[1].x, 10);
553        assert_eq!(rects[1].width, 20);
554        assert_eq!(rects[2].x, 30);
555        assert_eq!(rects[2].width, 30);
556    }
557
558    #[test]
559    fn build_rects_from_sizes_vertical() {
560        let rects = build_rects_from_sizes(area(80, 24), Orientation::Vertical, &[5, 10, 9]);
561        assert_eq!(rects.len(), 3);
562        assert_eq!(rects[0].y, 0);
563        assert_eq!(rects[0].height, 5);
564        assert_eq!(rects[1].y, 5);
565        assert_eq!(rects[1].height, 10);
566        assert_eq!(rects[2].y, 15);
567        assert_eq!(rects[2].height, 9);
568    }
569
570    #[test]
571    fn split_sizes_horizontal() {
572        let sizes = split_sizes(area(80, 24), Orientation::Horizontal, &[1, 1], 2, 0);
573        assert_eq!(sizes, vec![40, 40]);
574    }
575
576    #[test]
577    fn split_sizes_with_gap() {
578        let sizes = split_sizes(area(80, 24), Orientation::Horizontal, &[1, 1], 2, 2);
579        assert_eq!(sizes, vec![39, 39]);
580    }
581
582    #[cfg(feature = "std")]
583    mod proptests {
584        use super::*;
585        use proptest::prelude::*;
586
587        proptest! {
588            #[test]
589            fn split_rects_weighted_sum_equals_area(
590                total in 2u16..200u16,
591                count in 2usize..8usize,
592            ) {
593                let weights: Vec<u16> = (0..count).map(|_| 1).collect();
594                let area = LayoutRect { x: 0, y: 0, width: total, height: 24 };
595                let rects = split_rects_weighted(area, Orientation::Horizontal, &weights, count);
596                let sum: u16 = rects.iter().map(|r| r.width).sum();
597                prop_assert_eq!(sum, total, "sum of child widths must equal parent width");
598            }
599
600            #[test]
601            fn split_rects_weighted_no_overlap(
602                total in 2u16..200u16,
603                count in 2usize..8usize,
604            ) {
605                let weights: Vec<u16> = (0..count).map(|_| 1).collect();
606                let area = LayoutRect { x: 0, y: 0, width: total, height: 24 };
607                let rects = split_rects_weighted(area, Orientation::Horizontal, &weights, count);
608                for i in 1..rects.len() {
609                    let prev_end = rects[i-1].x + rects[i-1].width as i32;
610                    prop_assert_eq!(rects[i].x, prev_end, "child {} must start where child {} ends", i, i-1);
611                }
612            }
613
614            #[test]
615            fn split_rects_weighted_with_gaps_sum_equals_area(
616                total in 6u16..200u16,
617                count in 2usize..5usize,
618                gap in 0u16..4u16,
619            ) {
620                prop_assume!(gap * (count as u16 - 1) < total,
621                    "gaps alone must not exceed total width");
622                let weights: Vec<u16> = (0..count).map(|_| 1).collect();
623                let area = LayoutRect { x: 0, y: 0, width: total, height: 24 };
624                let (rects, gaps) = split_rects_with_gaps(area, Orientation::Horizontal, &weights, count, gap);
625                let rect_sum: u16 = rects.iter().map(|r| r.width).sum();
626                let gap_sum: u16 = gaps.iter().map(|r| r.width).sum();
627                prop_assert_eq!(rect_sum + gap_sum, total,
628                    "rects + gaps must fill parent exactly");
629            }
630        }
631    }
632}