1use crate::geometry::{Rect, Size};
16use rayon::prelude::*;
17
18#[derive(Clone, Copy, Debug, PartialEq, Eq)]
20pub enum FlexDirection {
21 Row,
23 Column,
25}
26
27#[derive(Clone, Copy, Debug, PartialEq, Eq)]
29pub enum JustifyContent {
30 Start,
32 Center,
34 End,
36 SpaceBetween,
38 SpaceAround,
40 SpaceEvenly,
42}
43
44#[derive(Clone, Copy, Debug, PartialEq, Eq)]
46pub enum AlignItems {
47 Start,
49 Center,
51 End,
53 Stretch,
55}
56
57#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
59pub enum FlexWrap {
60 #[default]
62 NoWrap,
63 Wrap,
65 WrapReverse,
67}
68
69#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
73pub enum AlignContent {
74 #[default]
76 Start,
77 Center,
79 End,
81 SpaceBetween,
83 SpaceAround,
85 SpaceEvenly,
87 Stretch,
89}
90
91#[derive(Clone, Copy, Debug)]
93pub struct FlexItem {
94 pub basis: Size,
96 pub grow: f32,
98}
99
100impl FlexItem {
101 pub fn fixed(basis: Size) -> Self {
103 Self { basis, grow: 0.0 }
104 }
105
106 pub fn flexible(basis: Size) -> Self {
108 Self { basis, grow: 1.0 }
109 }
110}
111
112#[derive(Clone, Copy, Debug)]
114pub struct FlexLayout {
115 pub direction: FlexDirection,
117 pub justify: JustifyContent,
119 pub align: AlignItems,
121 pub gap: f32,
123 pub wrap: FlexWrap,
125 pub align_content: AlignContent,
128}
129
130impl Default for FlexLayout {
131 fn default() -> Self {
132 Self {
133 direction: FlexDirection::Row,
134 justify: JustifyContent::Start,
135 align: AlignItems::Stretch,
136 gap: 0.0,
137 wrap: FlexWrap::NoWrap,
138 align_content: AlignContent::Start,
139 }
140 }
141}
142
143impl FlexLayout {
144 pub fn row() -> Self {
146 Self {
147 direction: FlexDirection::Row,
148 ..Self::default()
149 }
150 }
151
152 pub fn column() -> Self {
154 Self {
155 direction: FlexDirection::Column,
156 ..Self::default()
157 }
158 }
159
160 pub fn with_justify(mut self, justify: JustifyContent) -> Self {
162 self.justify = justify;
163 self
164 }
165
166 pub fn with_align(mut self, align: AlignItems) -> Self {
168 self.align = align;
169 self
170 }
171
172 pub fn with_gap(mut self, gap: f32) -> Self {
174 self.gap = gap;
175 self
176 }
177
178 pub fn with_wrap(mut self, wrap: FlexWrap) -> Self {
180 self.wrap = wrap;
181 self
182 }
183
184 pub fn with_align_content(mut self, ac: AlignContent) -> Self {
186 self.align_content = ac;
187 self
188 }
189
190 pub fn layout(&self, container: Rect, items: &[FlexItem]) -> Vec<Rect> {
193 if items.is_empty() {
194 return Vec::new();
195 }
196 match self.wrap {
197 FlexWrap::NoWrap => self.layout_single_line(container, items),
198 FlexWrap::Wrap | FlexWrap::WrapReverse => self.layout_wrapped(container, items),
199 }
200 }
201
202 fn layout_single_line(&self, container: Rect, items: &[FlexItem]) -> Vec<Rect> {
205 let is_row = self.direction == FlexDirection::Row;
206 let main_extent = if is_row {
207 container.width()
208 } else {
209 container.height()
210 };
211 let cross_extent = if is_row {
212 container.height()
213 } else {
214 container.width()
215 };
216
217 let main_of = |it: &FlexItem| {
218 if is_row {
219 it.basis.width
220 } else {
221 it.basis.height
222 }
223 };
224 let total_basis: f32 = items.iter().map(main_of).sum();
225 let total_gap = self.gap * (items.len().saturating_sub(1)) as f32;
226 let total_grow: f32 = items.iter().map(|it| it.grow.max(0.0)).sum();
227
228 let free = (main_extent - total_basis - total_gap).max(0.0);
229
230 let mut main_sizes: Vec<f32> = items
231 .iter()
232 .map(|it| {
233 let extra = if total_grow > 0.0 {
234 free * (it.grow.max(0.0) / total_grow)
235 } else {
236 0.0
237 };
238 main_of(it) + extra
239 })
240 .collect();
241
242 let used_main: f32 = main_sizes.iter().sum::<f32>() + total_gap;
243 let leftover = (main_extent - used_main).max(0.0);
244
245 let n = items.len() as f32;
246 let (lead, between) = if total_grow > 0.0 {
247 (0.0, self.gap)
248 } else {
249 match self.justify {
250 JustifyContent::Start => (0.0, self.gap),
251 JustifyContent::Center => (leftover * 0.5, self.gap),
252 JustifyContent::End => (leftover, self.gap),
253 JustifyContent::SpaceBetween => {
254 if items.len() == 1 {
255 (0.0, self.gap)
256 } else {
257 (0.0, self.gap + leftover / (n - 1.0))
258 }
259 }
260 JustifyContent::SpaceAround => {
261 let unit = leftover / n;
262 (unit * 0.5, self.gap + unit)
263 }
264 JustifyContent::SpaceEvenly => {
265 let unit = leftover / (n + 1.0);
266 (unit, self.gap + unit)
267 }
268 }
269 };
270
271 for s in &mut main_sizes {
272 if *s < 0.0 {
273 *s = 0.0;
274 }
275 }
276
277 let mut rects = Vec::with_capacity(items.len());
278 let mut main_cursor = lead;
279 for (i, it) in items.iter().enumerate() {
280 let main_size = main_sizes[i];
281 let item_cross = if is_row {
282 it.basis.height
283 } else {
284 it.basis.width
285 };
286 let (cross_size, cross_pos) = match self.align {
287 AlignItems::Stretch => (cross_extent, 0.0),
288 AlignItems::Start => (item_cross, 0.0),
289 AlignItems::Center => (item_cross, (cross_extent - item_cross) * 0.5),
290 AlignItems::End => (item_cross, cross_extent - item_cross),
291 };
292
293 let rect = if is_row {
294 Rect::new(
295 container.left() + main_cursor,
296 container.top() + cross_pos,
297 main_size,
298 cross_size,
299 )
300 } else {
301 Rect::new(
302 container.left() + cross_pos,
303 container.top() + main_cursor,
304 cross_size,
305 main_size,
306 )
307 };
308 rects.push(rect);
309
310 main_cursor += main_size;
311 if i + 1 < items.len() {
312 main_cursor += between;
313 }
314 }
315 rects
316 }
317
318 fn layout_wrapped(&self, container: Rect, items: &[FlexItem]) -> Vec<Rect> {
321 let is_row = self.direction == FlexDirection::Row;
322 let main_extent = if is_row {
323 container.width()
324 } else {
325 container.height()
326 };
327 let cross_extent = if is_row {
328 container.height()
329 } else {
330 container.width()
331 };
332
333 let main_of = |it: &FlexItem| {
334 if is_row {
335 it.basis.width
336 } else {
337 it.basis.height
338 }
339 };
340 let cross_of = |it: &FlexItem| {
341 if is_row {
342 it.basis.height
343 } else {
344 it.basis.width
345 }
346 };
347
348 let mut lines: Vec<Vec<usize>> = Vec::new(); let mut current_line: Vec<usize> = Vec::new();
353 let mut current_main: f32 = 0.0;
354
355 for (i, it) in items.iter().enumerate() {
356 let item_main = main_of(it).max(0.0);
357 let needed = if current_line.is_empty() {
358 item_main
359 } else {
360 current_main + self.gap + item_main
361 };
362
363 if !current_line.is_empty() && needed > main_extent + 1e-4 {
364 lines.push(current_line);
365 current_line = Vec::new();
366 current_main = item_main;
367 } else {
368 current_main = needed;
369 }
370 current_line.push(i);
371 }
372 if !current_line.is_empty() {
373 lines.push(current_line);
374 }
375
376 let line_cross_sizes: Vec<f32> = lines
380 .iter()
381 .map(|line| {
382 line.iter()
383 .map(|&i| cross_of(&items[i]).max(0.0))
384 .fold(0.0_f32, f32::max)
385 })
386 .collect();
387
388 let line_order: Vec<usize> = if self.wrap == FlexWrap::WrapReverse {
392 (0..lines.len()).rev().collect()
393 } else {
394 (0..lines.len()).collect()
395 };
396
397 let n_lines = lines.len() as f32;
403 let display_cross_sizes: Vec<f32> = if matches!(self.align_content, AlignContent::Stretch) {
404 vec![cross_extent / n_lines; lines.len()]
405 } else {
406 line_order.iter().map(|&li| line_cross_sizes[li]).collect()
407 };
408 let total_display_cross: f32 = display_cross_sizes.iter().sum();
409 let leftover_cross = (cross_extent - total_display_cross).max(0.0);
410
411 let (line_cross_starts, resolved_cross_sizes): (Vec<f32>, Vec<f32>) =
413 match self.align_content {
414 AlignContent::Start | AlignContent::Stretch => {
415 let mut pos = 0.0;
416 let starts = display_cross_sizes
417 .iter()
418 .map(|&sz| {
419 let s = pos;
420 pos += sz;
421 s
422 })
423 .collect();
424 (starts, display_cross_sizes.clone())
425 }
426 AlignContent::End => {
427 let mut pos = leftover_cross;
428 let starts = display_cross_sizes
429 .iter()
430 .map(|&sz| {
431 let s = pos;
432 pos += sz;
433 s
434 })
435 .collect();
436 (starts, display_cross_sizes.clone())
437 }
438 AlignContent::Center => {
439 let mut pos = leftover_cross * 0.5;
440 let starts = display_cross_sizes
441 .iter()
442 .map(|&sz| {
443 let s = pos;
444 pos += sz;
445 s
446 })
447 .collect();
448 (starts, display_cross_sizes.clone())
449 }
450 AlignContent::SpaceBetween => {
451 let gap = if lines.len() <= 1 {
452 0.0
453 } else {
454 leftover_cross / (n_lines - 1.0)
455 };
456 let mut pos = 0.0;
457 let starts = display_cross_sizes
458 .iter()
459 .map(|&sz| {
460 let s = pos;
461 pos += sz + gap;
462 s
463 })
464 .collect();
465 (starts, display_cross_sizes.clone())
466 }
467 AlignContent::SpaceAround => {
468 let unit = leftover_cross / n_lines;
469 let mut pos = unit * 0.5;
470 let starts = display_cross_sizes
471 .iter()
472 .map(|&sz| {
473 let s = pos;
474 pos += sz + unit;
475 s
476 })
477 .collect();
478 (starts, display_cross_sizes.clone())
479 }
480 AlignContent::SpaceEvenly => {
481 let unit = leftover_cross / (n_lines + 1.0);
482 let mut pos = unit;
483 let starts = display_cross_sizes
484 .iter()
485 .map(|&sz| {
486 let s = pos;
487 pos += sz + unit;
488 s
489 })
490 .collect();
491 (starts, display_cross_sizes.clone())
492 }
493 };
494
495 let mut rects_by_index: Vec<Rect> = vec![Rect::new(0.0, 0.0, 0.0, 0.0); items.len()];
497
498 for (display_order, &line_idx) in line_order.iter().enumerate() {
499 let line = &lines[line_idx];
500 let cross_start = line_cross_starts[display_order];
502 let line_cross = resolved_cross_sizes[display_order];
503
504 let line_items: Vec<FlexItem> = line.iter().map(|&i| items[i]).collect();
506 let line_main_sizes = self.resolve_main_sizes(&line_items, main_extent);
507 let (main_lead, main_between) = self.justify_offsets(&line_main_sizes, main_extent);
508
509 let mut main_cursor = main_lead;
510 for (j, &orig_idx) in line.iter().enumerate() {
511 let it = &items[orig_idx];
512 let main_size = line_main_sizes[j];
513 let item_cross = cross_of(it).max(0.0);
514
515 let (cross_size, cross_off) = match self.align {
516 AlignItems::Stretch => (line_cross, 0.0),
517 AlignItems::Start => (item_cross, 0.0),
518 AlignItems::Center => (item_cross, (line_cross - item_cross) * 0.5),
519 AlignItems::End => (item_cross, line_cross - item_cross),
520 };
521
522 let rect = if is_row {
523 Rect::new(
524 container.left() + main_cursor,
525 container.top() + cross_start + cross_off,
526 main_size,
527 cross_size,
528 )
529 } else {
530 Rect::new(
531 container.left() + cross_start + cross_off,
532 container.top() + main_cursor,
533 cross_size,
534 main_size,
535 )
536 };
537 rects_by_index[orig_idx] = rect;
538
539 main_cursor += main_size;
540 if j + 1 < line.len() {
541 main_cursor += main_between;
542 }
543 }
544 }
545
546 rects_by_index
547 }
548
549 fn resolve_main_sizes(&self, line_items: &[FlexItem], main_extent: f32) -> Vec<f32> {
553 let is_row = self.direction == FlexDirection::Row;
554 let main_of = |it: &FlexItem| {
555 if is_row {
556 it.basis.width
557 } else {
558 it.basis.height
559 }
560 };
561
562 let total_basis: f32 = line_items.iter().map(main_of).sum();
563 let total_gap = self.gap * (line_items.len().saturating_sub(1)) as f32;
564 let total_grow: f32 = line_items.iter().map(|it| it.grow.max(0.0)).sum();
565 let free = (main_extent - total_basis - total_gap).max(0.0);
566
567 line_items
568 .iter()
569 .map(|it| {
570 let extra = if total_grow > 0.0 {
571 free * (it.grow.max(0.0) / total_grow)
572 } else {
573 0.0
574 };
575 (main_of(it) + extra).max(0.0)
576 })
577 .collect()
578 }
579
580 fn justify_offsets(&self, main_sizes: &[f32], main_extent: f32) -> (f32, f32) {
582 let total_gap = self.gap * (main_sizes.len().saturating_sub(1)) as f32;
583 let used: f32 = main_sizes.iter().sum::<f32>() + total_gap;
584 let leftover = (main_extent - used).max(0.0);
585 let n = main_sizes.len() as f32;
586
587 if leftover < 1e-4 {
590 return (0.0, self.gap);
591 }
592
593 match self.justify {
594 JustifyContent::Start => (0.0, self.gap),
595 JustifyContent::Center => (leftover * 0.5, self.gap),
596 JustifyContent::End => (leftover, self.gap),
597 JustifyContent::SpaceBetween => {
598 if main_sizes.len() == 1 {
599 (0.0, self.gap)
600 } else {
601 (0.0, self.gap + leftover / (n - 1.0))
602 }
603 }
604 JustifyContent::SpaceAround => {
605 let unit = leftover / n;
606 (unit * 0.5, self.gap + unit)
607 }
608 JustifyContent::SpaceEvenly => {
609 let unit = leftover / (n + 1.0);
610 (unit, self.gap + unit)
611 }
612 }
613 }
614}
615
616pub struct LayoutTask {
622 pub layout: FlexLayout,
624 pub container: Rect,
626 pub items: Vec<FlexItem>,
628}
629
630pub fn layout_subtrees_parallel(tasks: &[LayoutTask]) -> Vec<Vec<Rect>> {
667 tasks
668 .par_iter()
669 .map(|task| task.layout.layout(task.container, &task.items))
670 .collect()
671}
672
673#[cfg(test)]
674mod tests {
675 use super::*;
676 use crate::geometry::{Rect, Size};
677
678 fn approx(a: f32, b: f32) -> bool {
679 (a - b).abs() < 0.5
680 }
681
682 fn close(a: f32, b: f32) -> bool {
683 (a - b).abs() < 0.01
684 }
685
686 #[test]
687 fn row_start_no_grow() {
688 let l = FlexLayout::row();
689 let items = [
690 FlexItem::fixed(Size::new(20.0, 10.0)),
691 FlexItem::fixed(Size::new(30.0, 10.0)),
692 ];
693 let rects = l.layout(Rect::new(0.0, 0.0, 100.0, 40.0), &items);
694 assert_eq!(rects.len(), 2);
695 assert!(approx(rects[0].left(), 0.0));
696 assert!(approx(rects[0].width(), 20.0));
697 assert!(approx(rects[1].left(), 20.0));
698 assert!(approx(rects[1].width(), 30.0));
699 }
700
701 #[test]
702 fn row_grow_fills_container() {
703 let l = FlexLayout::row();
704 let items = [
705 FlexItem::flexible(Size::new(0.0, 10.0)),
706 FlexItem::flexible(Size::new(0.0, 10.0)),
707 ];
708 let rects = l.layout(Rect::new(0.0, 0.0, 100.0, 10.0), &items);
709 assert!(approx(rects[0].width(), 50.0));
711 assert!(approx(rects[1].width(), 50.0));
712 assert!(approx(rects[1].left(), 50.0));
713 }
714
715 #[test]
716 fn row_grow_with_gap() {
717 let l = FlexLayout::row().with_gap(10.0);
718 let items = [
719 FlexItem::flexible(Size::new(0.0, 10.0)),
720 FlexItem::flexible(Size::new(0.0, 10.0)),
721 ];
722 let rects = l.layout(Rect::new(0.0, 0.0, 100.0, 10.0), &items);
723 assert!(approx(rects[0].width(), 45.0));
725 assert!(approx(rects[1].left(), 55.0));
726 assert!(approx(rects[1].width(), 45.0));
727 }
728
729 #[test]
730 fn justify_center() {
731 let l = FlexLayout::row().with_justify(JustifyContent::Center);
732 let items = [FlexItem::fixed(Size::new(40.0, 10.0))];
733 let rects = l.layout(Rect::new(0.0, 0.0, 100.0, 10.0), &items);
734 assert!(approx(rects[0].left(), 30.0));
736 }
737
738 #[test]
739 fn justify_space_between() {
740 let l = FlexLayout::row().with_justify(JustifyContent::SpaceBetween);
741 let items = [
742 FlexItem::fixed(Size::new(20.0, 10.0)),
743 FlexItem::fixed(Size::new(20.0, 10.0)),
744 FlexItem::fixed(Size::new(20.0, 10.0)),
745 ];
746 let rects = l.layout(Rect::new(0.0, 0.0, 120.0, 10.0), &items);
747 assert!(approx(rects[0].left(), 0.0));
749 assert!(approx(rects[1].left(), 50.0));
750 assert!(approx(rects[2].left(), 100.0));
751 }
752
753 #[test]
754 fn justify_space_evenly() {
755 let l = FlexLayout::row().with_justify(JustifyContent::SpaceEvenly);
756 let items = [
757 FlexItem::fixed(Size::new(20.0, 10.0)),
758 FlexItem::fixed(Size::new(20.0, 10.0)),
759 ];
760 let rects = l.layout(Rect::new(0.0, 0.0, 100.0, 10.0), &items);
761 assert!(approx(rects[0].left(), 20.0));
763 assert!(approx(rects[1].left(), 60.0));
764 }
765
766 #[test]
767 fn align_items_cross_axis() {
768 let l = FlexLayout::column().with_align(AlignItems::Center);
770 let items = [FlexItem::fixed(Size::new(40.0, 20.0))];
771 let rects = l.layout(Rect::new(0.0, 0.0, 100.0, 200.0), &items);
772 assert!(approx(rects[0].left(), 30.0));
774 assert!(approx(rects[0].width(), 40.0));
775
776 let stretch = FlexLayout::column().with_align(AlignItems::Stretch);
777 let r2 = stretch.layout(Rect::new(0.0, 0.0, 100.0, 200.0), &items);
778 assert!(approx(r2[0].width(), 100.0));
779 }
780
781 #[test]
782 fn empty_items_returns_empty() {
783 let l = FlexLayout::row();
784 assert!(l.layout(Rect::new(0.0, 0.0, 10.0, 10.0), &[]).is_empty());
785 }
786
787 #[test]
791 fn wrap_single_row_fits() {
792 let l = FlexLayout::row().with_wrap(FlexWrap::Wrap);
793 let items = [
794 FlexItem::fixed(Size::new(30.0, 10.0)),
795 FlexItem::fixed(Size::new(30.0, 10.0)),
796 ];
797 let rects = l.layout(Rect::new(0.0, 0.0, 100.0, 40.0), &items);
798 assert_eq!(rects.len(), 2);
799 assert!(close(rects[0].top(), 0.0));
801 assert!(close(rects[1].top(), 0.0));
802 assert!(close(rects[0].left(), 0.0));
803 assert!(close(rects[1].left(), 30.0));
804 }
805
806 #[test]
808 fn wrap_three_items_two_lines() {
809 let l = FlexLayout::row().with_wrap(FlexWrap::Wrap);
810 let items = [
814 FlexItem::fixed(Size::new(40.0, 10.0)),
815 FlexItem::fixed(Size::new(40.0, 10.0)),
816 FlexItem::fixed(Size::new(40.0, 10.0)),
817 ];
818 let rects = l.layout(Rect::new(0.0, 0.0, 90.0, 40.0), &items);
819 assert_eq!(rects.len(), 3);
820 assert!(close(rects[0].top(), 0.0), "item0 top={}", rects[0].top());
822 assert!(close(rects[1].top(), 0.0), "item1 top={}", rects[1].top());
823 assert!(approx(rects[2].top(), 10.0), "item2 top={}", rects[2].top());
825 }
826
827 #[test]
829 fn wrap_reverse_line_order() {
830 let l = FlexLayout::row().with_wrap(FlexWrap::WrapReverse);
831 let items = [
832 FlexItem::fixed(Size::new(60.0, 10.0)),
833 FlexItem::fixed(Size::new(60.0, 10.0)), ];
835 let rects = l.layout(Rect::new(0.0, 0.0, 80.0, 40.0), &items);
836 assert!(
840 rects[0].top() > rects[1].top(),
841 "item0.top={} item1.top={} — WrapReverse should put item1 above item0",
842 rects[0].top(),
843 rects[1].top()
844 );
845 }
846
847 #[test]
849 fn align_content_center_two_lines() {
850 let l = FlexLayout::row()
851 .with_wrap(FlexWrap::Wrap)
852 .with_align_content(AlignContent::Center);
853 let items = [
854 FlexItem::fixed(Size::new(60.0, 10.0)),
855 FlexItem::fixed(Size::new(60.0, 10.0)),
856 ];
857 let rects = l.layout(Rect::new(0.0, 0.0, 80.0, 60.0), &items);
860 assert!(
861 rects[0].top() > 5.0,
862 "line1 should be offset from top: top={}",
863 rects[0].top()
864 );
865 assert!(rects[1].top() > rects[0].top(), "line2 below line1");
866 }
867
868 #[test]
870 fn align_content_space_between() {
871 let l = FlexLayout::row()
872 .with_wrap(FlexWrap::Wrap)
873 .with_align_content(AlignContent::SpaceBetween);
874 let items = [
875 FlexItem::fixed(Size::new(60.0, 10.0)),
876 FlexItem::fixed(Size::new(60.0, 10.0)),
877 ];
878 let rects = l.layout(Rect::new(0.0, 0.0, 80.0, 60.0), &items);
880 assert!(close(rects[0].top(), 0.0), "line1 top={}", rects[0].top());
881 assert!(approx(rects[1].top(), 50.0), "line2 top={}", rects[1].top());
882 }
883
884 #[test]
886 fn align_content_space_around() {
887 let l = FlexLayout::row()
888 .with_wrap(FlexWrap::Wrap)
889 .with_align_content(AlignContent::SpaceAround);
890 let items = [
891 FlexItem::fixed(Size::new(60.0, 10.0)),
892 FlexItem::fixed(Size::new(60.0, 10.0)),
893 ];
894 let rects = l.layout(Rect::new(0.0, 0.0, 80.0, 60.0), &items);
897 assert!(approx(rects[0].top(), 10.0), "line1 top={}", rects[0].top());
898 assert!(approx(rects[1].top(), 40.0), "line2 top={}", rects[1].top());
899 }
900
901 #[test]
903 fn align_content_space_evenly() {
904 let l = FlexLayout::row()
905 .with_wrap(FlexWrap::Wrap)
906 .with_align_content(AlignContent::SpaceEvenly);
907 let items = [
908 FlexItem::fixed(Size::new(60.0, 10.0)),
909 FlexItem::fixed(Size::new(60.0, 10.0)),
910 ];
911 let rects = l.layout(Rect::new(0.0, 0.0, 80.0, 60.0), &items);
914 let unit = 40.0 / 3.0;
915 assert!(
916 approx(rects[0].top(), unit),
917 "line1 top={} unit={unit}",
918 rects[0].top()
919 );
920 assert!(
921 approx(rects[1].top(), unit + 10.0 + unit),
922 "line2 top={}",
923 rects[1].top()
924 );
925 }
926
927 #[test]
929 fn align_content_stretch() {
930 let l = FlexLayout::row()
931 .with_wrap(FlexWrap::Wrap)
932 .with_align_content(AlignContent::Stretch)
933 .with_align(AlignItems::Stretch);
934 let items = [
935 FlexItem::fixed(Size::new(60.0, 10.0)),
936 FlexItem::fixed(Size::new(60.0, 10.0)),
937 ];
938 let rects = l.layout(Rect::new(0.0, 0.0, 80.0, 60.0), &items);
940 assert!(close(rects[0].top(), 0.0));
941 assert!(approx(rects[0].height(), 30.0), "h={}", rects[0].height());
942 assert!(approx(rects[1].top(), 30.0), "top={}", rects[1].top());
943 assert!(approx(rects[1].height(), 30.0), "h={}", rects[1].height());
944 }
945
946 #[test]
948 fn wrap_oversized_item_own_line() {
949 let l = FlexLayout::row().with_wrap(FlexWrap::Wrap);
950 let items = [
951 FlexItem::fixed(Size::new(200.0, 10.0)), FlexItem::fixed(Size::new(30.0, 10.0)),
953 ];
954 let rects = l.layout(Rect::new(0.0, 0.0, 80.0, 40.0), &items);
955 assert_eq!(rects.len(), 2);
956 assert!(
958 rects[1].top() > rects[0].top(),
959 "item1 should be below oversized item0"
960 );
961 }
962
963 #[test]
965 fn wrap_zero_gap() {
966 let l = FlexLayout::row().with_wrap(FlexWrap::Wrap).with_gap(0.0);
967 let items = [
968 FlexItem::fixed(Size::new(50.0, 10.0)),
969 FlexItem::fixed(Size::new(50.0, 10.0)),
970 FlexItem::fixed(Size::new(50.0, 10.0)),
971 ];
972 let rects = l.layout(Rect::new(0.0, 0.0, 80.0, 40.0), &items);
975 assert!(rects[1].top() > rects[0].top(), "item1 below item0");
978 }
979
980 #[test]
982 fn wrap_column_direction() {
983 let l = FlexLayout::column().with_wrap(FlexWrap::Wrap);
984 let items = [
985 FlexItem::fixed(Size::new(10.0, 60.0)),
986 FlexItem::fixed(Size::new(10.0, 60.0)), ];
988 let rects = l.layout(Rect::new(0.0, 0.0, 40.0, 80.0), &items);
990 assert!(
992 rects[1].left() > rects[0].left(),
993 "column wrap: item1 should be in next column; item0.left={} item1.left={}",
994 rects[0].left(),
995 rects[1].left()
996 );
997 }
998
999 #[test]
1001 fn wrap_with_justify_space_between_per_line() {
1002 let l = FlexLayout::row()
1003 .with_wrap(FlexWrap::Wrap)
1004 .with_justify(JustifyContent::SpaceBetween);
1005 let items = [
1006 FlexItem::fixed(Size::new(20.0, 10.0)),
1007 FlexItem::fixed(Size::new(20.0, 10.0)),
1008 FlexItem::fixed(Size::new(20.0, 10.0)),
1009 FlexItem::fixed(Size::new(20.0, 10.0)),
1010 ];
1011 let rects = l.layout(Rect::new(0.0, 0.0, 100.0, 40.0), &items);
1014 assert_eq!(rects.len(), 4);
1015 assert!(close(rects[0].left(), 0.0));
1016 assert!(approx(rects[3].left() + rects[3].width(), 100.0));
1017 }
1018
1019 #[test]
1021 fn wrap_exact_boundary() {
1022 let l = FlexLayout::row().with_wrap(FlexWrap::Wrap);
1023 let items = [
1025 FlexItem::fixed(Size::new(30.0, 10.0)),
1026 FlexItem::fixed(Size::new(30.0, 10.0)),
1027 FlexItem::fixed(Size::new(30.0, 10.0)),
1028 ];
1029 let rects = l.layout(Rect::new(0.0, 0.0, 90.0, 20.0), &items);
1030 assert!(close(rects[0].top(), rects[1].top()));
1032 assert!(close(rects[1].top(), rects[2].top()));
1033 }
1034
1035 #[test]
1037 fn wrap_with_flex_grow() {
1038 let l = FlexLayout::row().with_wrap(FlexWrap::Wrap);
1039 let items = [
1040 FlexItem::flexible(Size::new(20.0, 10.0)), FlexItem::fixed(Size::new(80.0, 10.0)), ];
1043 let rects = l.layout(Rect::new(0.0, 0.0, 100.0, 20.0), &items);
1046 assert_eq!(rects.len(), 2);
1047 assert!(close(rects[0].top(), rects[1].top()));
1049 }
1050
1051 #[test]
1053 fn wrap_empty_items() {
1054 let l = FlexLayout::row().with_wrap(FlexWrap::Wrap);
1055 let rects = l.layout(Rect::new(0.0, 0.0, 100.0, 100.0), &[]);
1056 assert!(rects.is_empty());
1057 }
1058
1059 #[test]
1061 fn wrap_single_item() {
1062 let l = FlexLayout::row().with_wrap(FlexWrap::Wrap);
1063 let items = [FlexItem::fixed(Size::new(40.0, 20.0))];
1064 let rects = l.layout(Rect::new(0.0, 0.0, 100.0, 40.0), &items);
1065 assert_eq!(rects.len(), 1);
1066 assert!(close(rects[0].left(), 0.0));
1067 assert!(close(rects[0].top(), 0.0));
1068 assert!(close(rects[0].width(), 40.0));
1069 }
1070
1071 #[test]
1073 fn wrap_large_gap() {
1074 let l = FlexLayout::row().with_wrap(FlexWrap::Wrap).with_gap(30.0);
1075 let items = [
1076 FlexItem::fixed(Size::new(30.0, 10.0)),
1077 FlexItem::fixed(Size::new(30.0, 10.0)),
1078 ];
1079 let rects = l.layout(Rect::new(0.0, 0.0, 80.0, 40.0), &items);
1081 assert!(
1082 rects[1].top() > rects[0].top(),
1083 "item1 should be on second line"
1084 );
1085 }
1086
1087 #[test]
1089 fn wrap_reverse_align_content_end() {
1090 let l = FlexLayout::row()
1091 .with_wrap(FlexWrap::WrapReverse)
1092 .with_align_content(AlignContent::End);
1093 let items = [
1094 FlexItem::fixed(Size::new(60.0, 10.0)),
1095 FlexItem::fixed(Size::new(60.0, 10.0)),
1096 ];
1097 let rects = l.layout(Rect::new(0.0, 0.0, 80.0, 60.0), &items);
1099 let max_top = rects.iter().map(|r| r.top()).fold(0.0_f32, f32::max);
1101 assert!(
1102 max_top > 30.0,
1103 "lines should be packed toward the end, max_top={max_top}"
1104 );
1105 }
1106
1107 #[test]
1109 fn wrap_align_items_center_per_line() {
1110 let l = FlexLayout::row()
1111 .with_wrap(FlexWrap::Wrap)
1112 .with_align(AlignItems::Center);
1113 let items = [
1114 FlexItem::fixed(Size::new(60.0, 5.0)), FlexItem::fixed(Size::new(60.0, 15.0)), ];
1117 let rects = l.layout(Rect::new(0.0, 0.0, 80.0, 40.0), &items);
1120 assert!(
1122 close(rects[0].height(), 5.0),
1123 "item0 h={}",
1124 rects[0].height()
1125 );
1126 assert!(
1128 close(rects[1].height(), 15.0),
1129 "item1 h={}",
1130 rects[1].height()
1131 );
1132 }
1133
1134 #[test]
1136 fn wrap_output_preserves_original_order() {
1137 let l = FlexLayout::row().with_wrap(FlexWrap::Wrap);
1138 let items = [
1139 FlexItem::fixed(Size::new(70.0, 10.0)), FlexItem::fixed(Size::new(70.0, 10.0)), FlexItem::fixed(Size::new(70.0, 10.0)), ];
1143 let rects = l.layout(Rect::new(0.0, 0.0, 80.0, 60.0), &items);
1144 assert_eq!(rects.len(), 3);
1145 assert!(rects[0].top() < rects[1].top(), "idx0 above idx1");
1147 assert!(rects[1].top() < rects[2].top(), "idx1 above idx2");
1148 }
1149
1150 #[test]
1157 fn wrap_reverse_unequal_cross_sizes() {
1158 let l = FlexLayout::row()
1159 .with_wrap(FlexWrap::WrapReverse)
1160 .with_align(AlignItems::Start); let items = [
1162 FlexItem::fixed(Size::new(60.0, 10.0)), FlexItem::fixed(Size::new(60.0, 30.0)), ];
1165 let rects = l.layout(Rect::new(0.0, 0.0, 80.0, 60.0), &items);
1167 assert_eq!(rects.len(), 2);
1168
1169 let top1 = rects[1].top(); let top0 = rects[0].top(); assert!(top1 < top0,
1176 "WrapReverse: item1 (30px cross, display-first) top={top1} should be < item0 top={top0}");
1177
1178 let bottom1 = top1 + rects[1].height();
1180 assert!(
1181 top0 >= bottom1 - 1e-3,
1182 "no overlap: item0.top={top0} must be >= item1.bottom={bottom1}"
1183 );
1184
1185 assert!(
1187 close(rects[1].height(), 30.0),
1188 "item1 height={}",
1189 rects[1].height()
1190 );
1191 assert!(
1193 close(rects[0].height(), 10.0),
1194 "item0 height={}",
1195 rects[0].height()
1196 );
1197 }
1198
1199 #[test]
1204 fn parallel_layout_matches_sequential() {
1205 let tasks: Vec<LayoutTask> = (0..8_u32)
1206 .map(|i| LayoutTask {
1207 layout: FlexLayout::row(),
1208 container: Rect::new(0.0, 0.0, 400.0, 40.0),
1209 items: vec![
1210 FlexItem::fixed(Size::new(100.0, 40.0)),
1211 FlexItem::flexible(Size::new(50.0 + i as f32, 40.0)),
1212 ],
1213 })
1214 .collect();
1215
1216 let parallel_results = layout_subtrees_parallel(&tasks);
1217 assert_eq!(parallel_results.len(), 8);
1218
1219 for (task, par_rects) in tasks.iter().zip(parallel_results.iter()) {
1220 let seq_rects = task.layout.layout(task.container, &task.items);
1221 assert_eq!(seq_rects.len(), par_rects.len());
1222 for (sr, pr) in seq_rects.iter().zip(par_rects.iter()) {
1223 assert!(
1224 close(sr.left(), pr.left()) && close(sr.width(), pr.width()),
1225 "parallel and sequential results diverge"
1226 );
1227 }
1228 }
1229 }
1230
1231 #[test]
1233 fn parallel_layout_empty_tasks() {
1234 let results = layout_subtrees_parallel(&[]);
1235 assert!(results.is_empty());
1236 }
1237
1238 #[test]
1241 fn parallel_layout_single_empty_items() {
1242 let tasks = [LayoutTask {
1243 layout: FlexLayout::column(),
1244 container: Rect::new(0.0, 0.0, 200.0, 200.0),
1245 items: vec![],
1246 }];
1247 let results = layout_subtrees_parallel(&tasks);
1248 assert_eq!(results.len(), 1);
1249 assert!(results[0].is_empty());
1250 }
1251
1252 #[test]
1254 fn parallel_layout_large_batch() {
1255 let tasks: Vec<LayoutTask> = (0..64)
1256 .map(|_| LayoutTask {
1257 layout: FlexLayout::column(),
1258 container: Rect::new(0.0, 0.0, 100.0, 150.0),
1259 items: vec![
1260 FlexItem::fixed(Size::new(100.0, 30.0)),
1261 FlexItem::flexible(Size::new(100.0, 20.0)),
1262 FlexItem::fixed(Size::new(100.0, 30.0)),
1263 ],
1264 })
1265 .collect();
1266 let results = layout_subtrees_parallel(&tasks);
1267 assert_eq!(results.len(), 64);
1268 for rects in &results {
1269 assert_eq!(rects.len(), 3);
1270 assert!(rects[0].top() <= rects[1].top());
1272 assert!(rects[1].top() <= rects[2].top());
1273 }
1274 }
1275}