1use crate::geometry::{Rect, Size};
8
9#[derive(Clone, Copy, Debug, PartialEq, Eq)]
11pub enum FlexDirection {
12 Row,
14 Column,
16}
17
18#[derive(Clone, Copy, Debug, PartialEq, Eq)]
20pub enum JustifyContent {
21 Start,
23 Center,
25 End,
27 SpaceBetween,
29 SpaceAround,
31 SpaceEvenly,
33}
34
35#[derive(Clone, Copy, Debug, PartialEq, Eq)]
37pub enum AlignItems {
38 Start,
40 Center,
42 End,
44 Stretch,
46}
47
48#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
50pub enum FlexWrap {
51 #[default]
53 NoWrap,
54 Wrap,
56 WrapReverse,
58}
59
60#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
64pub enum AlignContent {
65 #[default]
67 Start,
68 Center,
70 End,
72 SpaceBetween,
74 SpaceAround,
76 SpaceEvenly,
78 Stretch,
80}
81
82#[derive(Clone, Copy, Debug)]
84pub struct FlexItem {
85 pub basis: Size,
87 pub grow: f32,
89}
90
91impl FlexItem {
92 pub fn fixed(basis: Size) -> Self {
94 Self { basis, grow: 0.0 }
95 }
96
97 pub fn flexible(basis: Size) -> Self {
99 Self { basis, grow: 1.0 }
100 }
101}
102
103#[derive(Clone, Copy, Debug)]
105pub struct FlexLayout {
106 pub direction: FlexDirection,
108 pub justify: JustifyContent,
110 pub align: AlignItems,
112 pub gap: f32,
114 pub wrap: FlexWrap,
116 pub align_content: AlignContent,
119}
120
121impl Default for FlexLayout {
122 fn default() -> Self {
123 Self {
124 direction: FlexDirection::Row,
125 justify: JustifyContent::Start,
126 align: AlignItems::Stretch,
127 gap: 0.0,
128 wrap: FlexWrap::NoWrap,
129 align_content: AlignContent::Start,
130 }
131 }
132}
133
134impl FlexLayout {
135 pub fn row() -> Self {
137 Self {
138 direction: FlexDirection::Row,
139 ..Self::default()
140 }
141 }
142
143 pub fn column() -> Self {
145 Self {
146 direction: FlexDirection::Column,
147 ..Self::default()
148 }
149 }
150
151 pub fn with_justify(mut self, justify: JustifyContent) -> Self {
153 self.justify = justify;
154 self
155 }
156
157 pub fn with_align(mut self, align: AlignItems) -> Self {
159 self.align = align;
160 self
161 }
162
163 pub fn with_gap(mut self, gap: f32) -> Self {
165 self.gap = gap;
166 self
167 }
168
169 pub fn with_wrap(mut self, wrap: FlexWrap) -> Self {
171 self.wrap = wrap;
172 self
173 }
174
175 pub fn with_align_content(mut self, ac: AlignContent) -> Self {
177 self.align_content = ac;
178 self
179 }
180
181 pub fn layout(&self, container: Rect, items: &[FlexItem]) -> Vec<Rect> {
184 if items.is_empty() {
185 return Vec::new();
186 }
187 match self.wrap {
188 FlexWrap::NoWrap => self.layout_single_line(container, items),
189 FlexWrap::Wrap | FlexWrap::WrapReverse => self.layout_wrapped(container, items),
190 }
191 }
192
193 fn layout_single_line(&self, container: Rect, items: &[FlexItem]) -> Vec<Rect> {
196 let is_row = self.direction == FlexDirection::Row;
197 let main_extent = if is_row {
198 container.width()
199 } else {
200 container.height()
201 };
202 let cross_extent = if is_row {
203 container.height()
204 } else {
205 container.width()
206 };
207
208 let main_of = |it: &FlexItem| {
209 if is_row {
210 it.basis.width
211 } else {
212 it.basis.height
213 }
214 };
215 let total_basis: f32 = items.iter().map(main_of).sum();
216 let total_gap = self.gap * (items.len().saturating_sub(1)) as f32;
217 let total_grow: f32 = items.iter().map(|it| it.grow.max(0.0)).sum();
218
219 let free = (main_extent - total_basis - total_gap).max(0.0);
220
221 let mut main_sizes: Vec<f32> = items
222 .iter()
223 .map(|it| {
224 let extra = if total_grow > 0.0 {
225 free * (it.grow.max(0.0) / total_grow)
226 } else {
227 0.0
228 };
229 main_of(it) + extra
230 })
231 .collect();
232
233 let used_main: f32 = main_sizes.iter().sum::<f32>() + total_gap;
234 let leftover = (main_extent - used_main).max(0.0);
235
236 let n = items.len() as f32;
237 let (lead, between) = if total_grow > 0.0 {
238 (0.0, self.gap)
239 } else {
240 match self.justify {
241 JustifyContent::Start => (0.0, self.gap),
242 JustifyContent::Center => (leftover * 0.5, self.gap),
243 JustifyContent::End => (leftover, self.gap),
244 JustifyContent::SpaceBetween => {
245 if items.len() == 1 {
246 (0.0, self.gap)
247 } else {
248 (0.0, self.gap + leftover / (n - 1.0))
249 }
250 }
251 JustifyContent::SpaceAround => {
252 let unit = leftover / n;
253 (unit * 0.5, self.gap + unit)
254 }
255 JustifyContent::SpaceEvenly => {
256 let unit = leftover / (n + 1.0);
257 (unit, self.gap + unit)
258 }
259 }
260 };
261
262 for s in &mut main_sizes {
263 if *s < 0.0 {
264 *s = 0.0;
265 }
266 }
267
268 let mut rects = Vec::with_capacity(items.len());
269 let mut main_cursor = lead;
270 for (i, it) in items.iter().enumerate() {
271 let main_size = main_sizes[i];
272 let item_cross = if is_row {
273 it.basis.height
274 } else {
275 it.basis.width
276 };
277 let (cross_size, cross_pos) = match self.align {
278 AlignItems::Stretch => (cross_extent, 0.0),
279 AlignItems::Start => (item_cross, 0.0),
280 AlignItems::Center => (item_cross, (cross_extent - item_cross) * 0.5),
281 AlignItems::End => (item_cross, cross_extent - item_cross),
282 };
283
284 let rect = if is_row {
285 Rect::new(
286 container.left() + main_cursor,
287 container.top() + cross_pos,
288 main_size,
289 cross_size,
290 )
291 } else {
292 Rect::new(
293 container.left() + cross_pos,
294 container.top() + main_cursor,
295 cross_size,
296 main_size,
297 )
298 };
299 rects.push(rect);
300
301 main_cursor += main_size;
302 if i + 1 < items.len() {
303 main_cursor += between;
304 }
305 }
306 rects
307 }
308
309 fn layout_wrapped(&self, container: Rect, items: &[FlexItem]) -> Vec<Rect> {
312 let is_row = self.direction == FlexDirection::Row;
313 let main_extent = if is_row {
314 container.width()
315 } else {
316 container.height()
317 };
318 let cross_extent = if is_row {
319 container.height()
320 } else {
321 container.width()
322 };
323
324 let main_of = |it: &FlexItem| {
325 if is_row {
326 it.basis.width
327 } else {
328 it.basis.height
329 }
330 };
331 let cross_of = |it: &FlexItem| {
332 if is_row {
333 it.basis.height
334 } else {
335 it.basis.width
336 }
337 };
338
339 let mut lines: Vec<Vec<usize>> = Vec::new(); let mut current_line: Vec<usize> = Vec::new();
344 let mut current_main: f32 = 0.0;
345
346 for (i, it) in items.iter().enumerate() {
347 let item_main = main_of(it).max(0.0);
348 let needed = if current_line.is_empty() {
349 item_main
350 } else {
351 current_main + self.gap + item_main
352 };
353
354 if !current_line.is_empty() && needed > main_extent + 1e-4 {
355 lines.push(current_line);
356 current_line = Vec::new();
357 current_main = item_main;
358 } else {
359 current_main = needed;
360 }
361 current_line.push(i);
362 }
363 if !current_line.is_empty() {
364 lines.push(current_line);
365 }
366
367 let line_cross_sizes: Vec<f32> = lines
371 .iter()
372 .map(|line| {
373 line.iter()
374 .map(|&i| cross_of(&items[i]).max(0.0))
375 .fold(0.0_f32, f32::max)
376 })
377 .collect();
378
379 let line_order: Vec<usize> = if self.wrap == FlexWrap::WrapReverse {
383 (0..lines.len()).rev().collect()
384 } else {
385 (0..lines.len()).collect()
386 };
387
388 let n_lines = lines.len() as f32;
394 let display_cross_sizes: Vec<f32> = if matches!(self.align_content, AlignContent::Stretch) {
395 vec![cross_extent / n_lines; lines.len()]
396 } else {
397 line_order.iter().map(|&li| line_cross_sizes[li]).collect()
398 };
399 let total_display_cross: f32 = display_cross_sizes.iter().sum();
400 let leftover_cross = (cross_extent - total_display_cross).max(0.0);
401
402 let (line_cross_starts, resolved_cross_sizes): (Vec<f32>, Vec<f32>) =
404 match self.align_content {
405 AlignContent::Start | AlignContent::Stretch => {
406 let mut pos = 0.0;
407 let starts = display_cross_sizes
408 .iter()
409 .map(|&sz| {
410 let s = pos;
411 pos += sz;
412 s
413 })
414 .collect();
415 (starts, display_cross_sizes.clone())
416 }
417 AlignContent::End => {
418 let mut pos = leftover_cross;
419 let starts = display_cross_sizes
420 .iter()
421 .map(|&sz| {
422 let s = pos;
423 pos += sz;
424 s
425 })
426 .collect();
427 (starts, display_cross_sizes.clone())
428 }
429 AlignContent::Center => {
430 let mut pos = leftover_cross * 0.5;
431 let starts = display_cross_sizes
432 .iter()
433 .map(|&sz| {
434 let s = pos;
435 pos += sz;
436 s
437 })
438 .collect();
439 (starts, display_cross_sizes.clone())
440 }
441 AlignContent::SpaceBetween => {
442 let gap = if lines.len() <= 1 {
443 0.0
444 } else {
445 leftover_cross / (n_lines - 1.0)
446 };
447 let mut pos = 0.0;
448 let starts = display_cross_sizes
449 .iter()
450 .map(|&sz| {
451 let s = pos;
452 pos += sz + gap;
453 s
454 })
455 .collect();
456 (starts, display_cross_sizes.clone())
457 }
458 AlignContent::SpaceAround => {
459 let unit = leftover_cross / n_lines;
460 let mut pos = unit * 0.5;
461 let starts = display_cross_sizes
462 .iter()
463 .map(|&sz| {
464 let s = pos;
465 pos += sz + unit;
466 s
467 })
468 .collect();
469 (starts, display_cross_sizes.clone())
470 }
471 AlignContent::SpaceEvenly => {
472 let unit = leftover_cross / (n_lines + 1.0);
473 let mut pos = unit;
474 let starts = display_cross_sizes
475 .iter()
476 .map(|&sz| {
477 let s = pos;
478 pos += sz + unit;
479 s
480 })
481 .collect();
482 (starts, display_cross_sizes.clone())
483 }
484 };
485
486 let mut rects_by_index: Vec<Rect> = vec![Rect::new(0.0, 0.0, 0.0, 0.0); items.len()];
488
489 for (display_order, &line_idx) in line_order.iter().enumerate() {
490 let line = &lines[line_idx];
491 let cross_start = line_cross_starts[display_order];
493 let line_cross = resolved_cross_sizes[display_order];
494
495 let line_items: Vec<FlexItem> = line.iter().map(|&i| items[i]).collect();
497 let line_main_sizes = self.resolve_main_sizes(&line_items, main_extent);
498 let (main_lead, main_between) = self.justify_offsets(&line_main_sizes, main_extent);
499
500 let mut main_cursor = main_lead;
501 for (j, &orig_idx) in line.iter().enumerate() {
502 let it = &items[orig_idx];
503 let main_size = line_main_sizes[j];
504 let item_cross = cross_of(it).max(0.0);
505
506 let (cross_size, cross_off) = match self.align {
507 AlignItems::Stretch => (line_cross, 0.0),
508 AlignItems::Start => (item_cross, 0.0),
509 AlignItems::Center => (item_cross, (line_cross - item_cross) * 0.5),
510 AlignItems::End => (item_cross, line_cross - item_cross),
511 };
512
513 let rect = if is_row {
514 Rect::new(
515 container.left() + main_cursor,
516 container.top() + cross_start + cross_off,
517 main_size,
518 cross_size,
519 )
520 } else {
521 Rect::new(
522 container.left() + cross_start + cross_off,
523 container.top() + main_cursor,
524 cross_size,
525 main_size,
526 )
527 };
528 rects_by_index[orig_idx] = rect;
529
530 main_cursor += main_size;
531 if j + 1 < line.len() {
532 main_cursor += main_between;
533 }
534 }
535 }
536
537 rects_by_index
538 }
539
540 fn resolve_main_sizes(&self, line_items: &[FlexItem], main_extent: f32) -> Vec<f32> {
544 let is_row = self.direction == FlexDirection::Row;
545 let main_of = |it: &FlexItem| {
546 if is_row {
547 it.basis.width
548 } else {
549 it.basis.height
550 }
551 };
552
553 let total_basis: f32 = line_items.iter().map(main_of).sum();
554 let total_gap = self.gap * (line_items.len().saturating_sub(1)) as f32;
555 let total_grow: f32 = line_items.iter().map(|it| it.grow.max(0.0)).sum();
556 let free = (main_extent - total_basis - total_gap).max(0.0);
557
558 line_items
559 .iter()
560 .map(|it| {
561 let extra = if total_grow > 0.0 {
562 free * (it.grow.max(0.0) / total_grow)
563 } else {
564 0.0
565 };
566 (main_of(it) + extra).max(0.0)
567 })
568 .collect()
569 }
570
571 fn justify_offsets(&self, main_sizes: &[f32], main_extent: f32) -> (f32, f32) {
573 let total_gap = self.gap * (main_sizes.len().saturating_sub(1)) as f32;
574 let used: f32 = main_sizes.iter().sum::<f32>() + total_gap;
575 let leftover = (main_extent - used).max(0.0);
576 let n = main_sizes.len() as f32;
577
578 if leftover < 1e-4 {
581 return (0.0, self.gap);
582 }
583
584 match self.justify {
585 JustifyContent::Start => (0.0, self.gap),
586 JustifyContent::Center => (leftover * 0.5, self.gap),
587 JustifyContent::End => (leftover, self.gap),
588 JustifyContent::SpaceBetween => {
589 if main_sizes.len() == 1 {
590 (0.0, self.gap)
591 } else {
592 (0.0, self.gap + leftover / (n - 1.0))
593 }
594 }
595 JustifyContent::SpaceAround => {
596 let unit = leftover / n;
597 (unit * 0.5, self.gap + unit)
598 }
599 JustifyContent::SpaceEvenly => {
600 let unit = leftover / (n + 1.0);
601 (unit, self.gap + unit)
602 }
603 }
604 }
605}
606
607#[cfg(test)]
608mod tests {
609 use super::*;
610 use crate::geometry::{Rect, Size};
611
612 fn approx(a: f32, b: f32) -> bool {
613 (a - b).abs() < 0.5
614 }
615
616 fn close(a: f32, b: f32) -> bool {
617 (a - b).abs() < 0.01
618 }
619
620 #[test]
621 fn row_start_no_grow() {
622 let l = FlexLayout::row();
623 let items = [
624 FlexItem::fixed(Size::new(20.0, 10.0)),
625 FlexItem::fixed(Size::new(30.0, 10.0)),
626 ];
627 let rects = l.layout(Rect::new(0.0, 0.0, 100.0, 40.0), &items);
628 assert_eq!(rects.len(), 2);
629 assert!(approx(rects[0].left(), 0.0));
630 assert!(approx(rects[0].width(), 20.0));
631 assert!(approx(rects[1].left(), 20.0));
632 assert!(approx(rects[1].width(), 30.0));
633 }
634
635 #[test]
636 fn row_grow_fills_container() {
637 let l = FlexLayout::row();
638 let items = [
639 FlexItem::flexible(Size::new(0.0, 10.0)),
640 FlexItem::flexible(Size::new(0.0, 10.0)),
641 ];
642 let rects = l.layout(Rect::new(0.0, 0.0, 100.0, 10.0), &items);
643 assert!(approx(rects[0].width(), 50.0));
645 assert!(approx(rects[1].width(), 50.0));
646 assert!(approx(rects[1].left(), 50.0));
647 }
648
649 #[test]
650 fn row_grow_with_gap() {
651 let l = FlexLayout::row().with_gap(10.0);
652 let items = [
653 FlexItem::flexible(Size::new(0.0, 10.0)),
654 FlexItem::flexible(Size::new(0.0, 10.0)),
655 ];
656 let rects = l.layout(Rect::new(0.0, 0.0, 100.0, 10.0), &items);
657 assert!(approx(rects[0].width(), 45.0));
659 assert!(approx(rects[1].left(), 55.0));
660 assert!(approx(rects[1].width(), 45.0));
661 }
662
663 #[test]
664 fn justify_center() {
665 let l = FlexLayout::row().with_justify(JustifyContent::Center);
666 let items = [FlexItem::fixed(Size::new(40.0, 10.0))];
667 let rects = l.layout(Rect::new(0.0, 0.0, 100.0, 10.0), &items);
668 assert!(approx(rects[0].left(), 30.0));
670 }
671
672 #[test]
673 fn justify_space_between() {
674 let l = FlexLayout::row().with_justify(JustifyContent::SpaceBetween);
675 let items = [
676 FlexItem::fixed(Size::new(20.0, 10.0)),
677 FlexItem::fixed(Size::new(20.0, 10.0)),
678 FlexItem::fixed(Size::new(20.0, 10.0)),
679 ];
680 let rects = l.layout(Rect::new(0.0, 0.0, 120.0, 10.0), &items);
681 assert!(approx(rects[0].left(), 0.0));
683 assert!(approx(rects[1].left(), 50.0));
684 assert!(approx(rects[2].left(), 100.0));
685 }
686
687 #[test]
688 fn justify_space_evenly() {
689 let l = FlexLayout::row().with_justify(JustifyContent::SpaceEvenly);
690 let items = [
691 FlexItem::fixed(Size::new(20.0, 10.0)),
692 FlexItem::fixed(Size::new(20.0, 10.0)),
693 ];
694 let rects = l.layout(Rect::new(0.0, 0.0, 100.0, 10.0), &items);
695 assert!(approx(rects[0].left(), 20.0));
697 assert!(approx(rects[1].left(), 60.0));
698 }
699
700 #[test]
701 fn align_items_cross_axis() {
702 let l = FlexLayout::column().with_align(AlignItems::Center);
704 let items = [FlexItem::fixed(Size::new(40.0, 20.0))];
705 let rects = l.layout(Rect::new(0.0, 0.0, 100.0, 200.0), &items);
706 assert!(approx(rects[0].left(), 30.0));
708 assert!(approx(rects[0].width(), 40.0));
709
710 let stretch = FlexLayout::column().with_align(AlignItems::Stretch);
711 let r2 = stretch.layout(Rect::new(0.0, 0.0, 100.0, 200.0), &items);
712 assert!(approx(r2[0].width(), 100.0));
713 }
714
715 #[test]
716 fn empty_items_returns_empty() {
717 let l = FlexLayout::row();
718 assert!(l.layout(Rect::new(0.0, 0.0, 10.0, 10.0), &[]).is_empty());
719 }
720
721 #[test]
725 fn wrap_single_row_fits() {
726 let l = FlexLayout::row().with_wrap(FlexWrap::Wrap);
727 let items = [
728 FlexItem::fixed(Size::new(30.0, 10.0)),
729 FlexItem::fixed(Size::new(30.0, 10.0)),
730 ];
731 let rects = l.layout(Rect::new(0.0, 0.0, 100.0, 40.0), &items);
732 assert_eq!(rects.len(), 2);
733 assert!(close(rects[0].top(), 0.0));
735 assert!(close(rects[1].top(), 0.0));
736 assert!(close(rects[0].left(), 0.0));
737 assert!(close(rects[1].left(), 30.0));
738 }
739
740 #[test]
742 fn wrap_three_items_two_lines() {
743 let l = FlexLayout::row().with_wrap(FlexWrap::Wrap);
744 let items = [
748 FlexItem::fixed(Size::new(40.0, 10.0)),
749 FlexItem::fixed(Size::new(40.0, 10.0)),
750 FlexItem::fixed(Size::new(40.0, 10.0)),
751 ];
752 let rects = l.layout(Rect::new(0.0, 0.0, 90.0, 40.0), &items);
753 assert_eq!(rects.len(), 3);
754 assert!(close(rects[0].top(), 0.0), "item0 top={}", rects[0].top());
756 assert!(close(rects[1].top(), 0.0), "item1 top={}", rects[1].top());
757 assert!(approx(rects[2].top(), 10.0), "item2 top={}", rects[2].top());
759 }
760
761 #[test]
763 fn wrap_reverse_line_order() {
764 let l = FlexLayout::row().with_wrap(FlexWrap::WrapReverse);
765 let items = [
766 FlexItem::fixed(Size::new(60.0, 10.0)),
767 FlexItem::fixed(Size::new(60.0, 10.0)), ];
769 let rects = l.layout(Rect::new(0.0, 0.0, 80.0, 40.0), &items);
770 assert!(
774 rects[0].top() > rects[1].top(),
775 "item0.top={} item1.top={} — WrapReverse should put item1 above item0",
776 rects[0].top(),
777 rects[1].top()
778 );
779 }
780
781 #[test]
783 fn align_content_center_two_lines() {
784 let l = FlexLayout::row()
785 .with_wrap(FlexWrap::Wrap)
786 .with_align_content(AlignContent::Center);
787 let items = [
788 FlexItem::fixed(Size::new(60.0, 10.0)),
789 FlexItem::fixed(Size::new(60.0, 10.0)),
790 ];
791 let rects = l.layout(Rect::new(0.0, 0.0, 80.0, 60.0), &items);
794 assert!(
795 rects[0].top() > 5.0,
796 "line1 should be offset from top: top={}",
797 rects[0].top()
798 );
799 assert!(rects[1].top() > rects[0].top(), "line2 below line1");
800 }
801
802 #[test]
804 fn align_content_space_between() {
805 let l = FlexLayout::row()
806 .with_wrap(FlexWrap::Wrap)
807 .with_align_content(AlignContent::SpaceBetween);
808 let items = [
809 FlexItem::fixed(Size::new(60.0, 10.0)),
810 FlexItem::fixed(Size::new(60.0, 10.0)),
811 ];
812 let rects = l.layout(Rect::new(0.0, 0.0, 80.0, 60.0), &items);
814 assert!(close(rects[0].top(), 0.0), "line1 top={}", rects[0].top());
815 assert!(approx(rects[1].top(), 50.0), "line2 top={}", rects[1].top());
816 }
817
818 #[test]
820 fn align_content_space_around() {
821 let l = FlexLayout::row()
822 .with_wrap(FlexWrap::Wrap)
823 .with_align_content(AlignContent::SpaceAround);
824 let items = [
825 FlexItem::fixed(Size::new(60.0, 10.0)),
826 FlexItem::fixed(Size::new(60.0, 10.0)),
827 ];
828 let rects = l.layout(Rect::new(0.0, 0.0, 80.0, 60.0), &items);
831 assert!(approx(rects[0].top(), 10.0), "line1 top={}", rects[0].top());
832 assert!(approx(rects[1].top(), 40.0), "line2 top={}", rects[1].top());
833 }
834
835 #[test]
837 fn align_content_space_evenly() {
838 let l = FlexLayout::row()
839 .with_wrap(FlexWrap::Wrap)
840 .with_align_content(AlignContent::SpaceEvenly);
841 let items = [
842 FlexItem::fixed(Size::new(60.0, 10.0)),
843 FlexItem::fixed(Size::new(60.0, 10.0)),
844 ];
845 let rects = l.layout(Rect::new(0.0, 0.0, 80.0, 60.0), &items);
848 let unit = 40.0 / 3.0;
849 assert!(
850 approx(rects[0].top(), unit),
851 "line1 top={} unit={unit}",
852 rects[0].top()
853 );
854 assert!(
855 approx(rects[1].top(), unit + 10.0 + unit),
856 "line2 top={}",
857 rects[1].top()
858 );
859 }
860
861 #[test]
863 fn align_content_stretch() {
864 let l = FlexLayout::row()
865 .with_wrap(FlexWrap::Wrap)
866 .with_align_content(AlignContent::Stretch)
867 .with_align(AlignItems::Stretch);
868 let items = [
869 FlexItem::fixed(Size::new(60.0, 10.0)),
870 FlexItem::fixed(Size::new(60.0, 10.0)),
871 ];
872 let rects = l.layout(Rect::new(0.0, 0.0, 80.0, 60.0), &items);
874 assert!(close(rects[0].top(), 0.0));
875 assert!(approx(rects[0].height(), 30.0), "h={}", rects[0].height());
876 assert!(approx(rects[1].top(), 30.0), "top={}", rects[1].top());
877 assert!(approx(rects[1].height(), 30.0), "h={}", rects[1].height());
878 }
879
880 #[test]
882 fn wrap_oversized_item_own_line() {
883 let l = FlexLayout::row().with_wrap(FlexWrap::Wrap);
884 let items = [
885 FlexItem::fixed(Size::new(200.0, 10.0)), FlexItem::fixed(Size::new(30.0, 10.0)),
887 ];
888 let rects = l.layout(Rect::new(0.0, 0.0, 80.0, 40.0), &items);
889 assert_eq!(rects.len(), 2);
890 assert!(
892 rects[1].top() > rects[0].top(),
893 "item1 should be below oversized item0"
894 );
895 }
896
897 #[test]
899 fn wrap_zero_gap() {
900 let l = FlexLayout::row().with_wrap(FlexWrap::Wrap).with_gap(0.0);
901 let items = [
902 FlexItem::fixed(Size::new(50.0, 10.0)),
903 FlexItem::fixed(Size::new(50.0, 10.0)),
904 FlexItem::fixed(Size::new(50.0, 10.0)),
905 ];
906 let rects = l.layout(Rect::new(0.0, 0.0, 80.0, 40.0), &items);
909 assert!(rects[1].top() > rects[0].top(), "item1 below item0");
912 }
913
914 #[test]
916 fn wrap_column_direction() {
917 let l = FlexLayout::column().with_wrap(FlexWrap::Wrap);
918 let items = [
919 FlexItem::fixed(Size::new(10.0, 60.0)),
920 FlexItem::fixed(Size::new(10.0, 60.0)), ];
922 let rects = l.layout(Rect::new(0.0, 0.0, 40.0, 80.0), &items);
924 assert!(
926 rects[1].left() > rects[0].left(),
927 "column wrap: item1 should be in next column; item0.left={} item1.left={}",
928 rects[0].left(),
929 rects[1].left()
930 );
931 }
932
933 #[test]
935 fn wrap_with_justify_space_between_per_line() {
936 let l = FlexLayout::row()
937 .with_wrap(FlexWrap::Wrap)
938 .with_justify(JustifyContent::SpaceBetween);
939 let items = [
940 FlexItem::fixed(Size::new(20.0, 10.0)),
941 FlexItem::fixed(Size::new(20.0, 10.0)),
942 FlexItem::fixed(Size::new(20.0, 10.0)),
943 FlexItem::fixed(Size::new(20.0, 10.0)),
944 ];
945 let rects = l.layout(Rect::new(0.0, 0.0, 100.0, 40.0), &items);
948 assert_eq!(rects.len(), 4);
949 assert!(close(rects[0].left(), 0.0));
950 assert!(approx(rects[3].left() + rects[3].width(), 100.0));
951 }
952
953 #[test]
955 fn wrap_exact_boundary() {
956 let l = FlexLayout::row().with_wrap(FlexWrap::Wrap);
957 let items = [
959 FlexItem::fixed(Size::new(30.0, 10.0)),
960 FlexItem::fixed(Size::new(30.0, 10.0)),
961 FlexItem::fixed(Size::new(30.0, 10.0)),
962 ];
963 let rects = l.layout(Rect::new(0.0, 0.0, 90.0, 20.0), &items);
964 assert!(close(rects[0].top(), rects[1].top()));
966 assert!(close(rects[1].top(), rects[2].top()));
967 }
968
969 #[test]
971 fn wrap_with_flex_grow() {
972 let l = FlexLayout::row().with_wrap(FlexWrap::Wrap);
973 let items = [
974 FlexItem::flexible(Size::new(20.0, 10.0)), FlexItem::fixed(Size::new(80.0, 10.0)), ];
977 let rects = l.layout(Rect::new(0.0, 0.0, 100.0, 20.0), &items);
980 assert_eq!(rects.len(), 2);
981 assert!(close(rects[0].top(), rects[1].top()));
983 }
984
985 #[test]
987 fn wrap_empty_items() {
988 let l = FlexLayout::row().with_wrap(FlexWrap::Wrap);
989 let rects = l.layout(Rect::new(0.0, 0.0, 100.0, 100.0), &[]);
990 assert!(rects.is_empty());
991 }
992
993 #[test]
995 fn wrap_single_item() {
996 let l = FlexLayout::row().with_wrap(FlexWrap::Wrap);
997 let items = [FlexItem::fixed(Size::new(40.0, 20.0))];
998 let rects = l.layout(Rect::new(0.0, 0.0, 100.0, 40.0), &items);
999 assert_eq!(rects.len(), 1);
1000 assert!(close(rects[0].left(), 0.0));
1001 assert!(close(rects[0].top(), 0.0));
1002 assert!(close(rects[0].width(), 40.0));
1003 }
1004
1005 #[test]
1007 fn wrap_large_gap() {
1008 let l = FlexLayout::row().with_wrap(FlexWrap::Wrap).with_gap(30.0);
1009 let items = [
1010 FlexItem::fixed(Size::new(30.0, 10.0)),
1011 FlexItem::fixed(Size::new(30.0, 10.0)),
1012 ];
1013 let rects = l.layout(Rect::new(0.0, 0.0, 80.0, 40.0), &items);
1015 assert!(
1016 rects[1].top() > rects[0].top(),
1017 "item1 should be on second line"
1018 );
1019 }
1020
1021 #[test]
1023 fn wrap_reverse_align_content_end() {
1024 let l = FlexLayout::row()
1025 .with_wrap(FlexWrap::WrapReverse)
1026 .with_align_content(AlignContent::End);
1027 let items = [
1028 FlexItem::fixed(Size::new(60.0, 10.0)),
1029 FlexItem::fixed(Size::new(60.0, 10.0)),
1030 ];
1031 let rects = l.layout(Rect::new(0.0, 0.0, 80.0, 60.0), &items);
1033 let max_top = rects.iter().map(|r| r.top()).fold(0.0_f32, f32::max);
1035 assert!(
1036 max_top > 30.0,
1037 "lines should be packed toward the end, max_top={max_top}"
1038 );
1039 }
1040
1041 #[test]
1043 fn wrap_align_items_center_per_line() {
1044 let l = FlexLayout::row()
1045 .with_wrap(FlexWrap::Wrap)
1046 .with_align(AlignItems::Center);
1047 let items = [
1048 FlexItem::fixed(Size::new(60.0, 5.0)), FlexItem::fixed(Size::new(60.0, 15.0)), ];
1051 let rects = l.layout(Rect::new(0.0, 0.0, 80.0, 40.0), &items);
1054 assert!(
1056 close(rects[0].height(), 5.0),
1057 "item0 h={}",
1058 rects[0].height()
1059 );
1060 assert!(
1062 close(rects[1].height(), 15.0),
1063 "item1 h={}",
1064 rects[1].height()
1065 );
1066 }
1067
1068 #[test]
1070 fn wrap_output_preserves_original_order() {
1071 let l = FlexLayout::row().with_wrap(FlexWrap::Wrap);
1072 let items = [
1073 FlexItem::fixed(Size::new(70.0, 10.0)), FlexItem::fixed(Size::new(70.0, 10.0)), FlexItem::fixed(Size::new(70.0, 10.0)), ];
1077 let rects = l.layout(Rect::new(0.0, 0.0, 80.0, 60.0), &items);
1078 assert_eq!(rects.len(), 3);
1079 assert!(rects[0].top() < rects[1].top(), "idx0 above idx1");
1081 assert!(rects[1].top() < rects[2].top(), "idx1 above idx2");
1082 }
1083
1084 #[test]
1091 fn wrap_reverse_unequal_cross_sizes() {
1092 let l = FlexLayout::row()
1093 .with_wrap(FlexWrap::WrapReverse)
1094 .with_align(AlignItems::Start); let items = [
1096 FlexItem::fixed(Size::new(60.0, 10.0)), FlexItem::fixed(Size::new(60.0, 30.0)), ];
1099 let rects = l.layout(Rect::new(0.0, 0.0, 80.0, 60.0), &items);
1101 assert_eq!(rects.len(), 2);
1102
1103 let top1 = rects[1].top(); let top0 = rects[0].top(); assert!(top1 < top0,
1110 "WrapReverse: item1 (30px cross, display-first) top={top1} should be < item0 top={top0}");
1111
1112 let bottom1 = top1 + rects[1].height();
1114 assert!(
1115 top0 >= bottom1 - 1e-3,
1116 "no overlap: item0.top={top0} must be >= item1.bottom={bottom1}"
1117 );
1118
1119 assert!(
1121 close(rects[1].height(), 30.0),
1122 "item1 height={}",
1123 rects[1].height()
1124 );
1125 assert!(
1127 close(rects[0].height(), 10.0),
1128 "item0 height={}",
1129 rects[0].height()
1130 );
1131 }
1132}