1use crate::rect::{LayoutRect, Orientation, Ratio};
2
3pub 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
179pub fn handle_thickness(orientation: Orientation, _total_dim: u16) -> u16 {
181 match orientation {
182 Orientation::Horizontal => 1,
183 Orientation::Vertical => 1,
184 }
185}
186
187pub 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
209pub 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
243pub 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
304pub 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
337pub 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 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); assert_eq!(rects[1].width, 60); }
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); 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}