Skip to main content

oxiui_core/
layout.rs

1//! Flexbox layout engine — single-line and multi-line (wrapping).
2//!
3//! Computes child rectangles along a main axis with `flex-grow` distribution,
4//! `justify-content` main-axis alignment, `align-items` cross-axis alignment,
5//! and optional multi-line wrapping with `align-content` cross-axis distribution.
6
7use crate::geometry::{Rect, Size};
8
9/// The direction children are laid out along the main axis.
10#[derive(Clone, Copy, Debug, PartialEq, Eq)]
11pub enum FlexDirection {
12    /// Left-to-right (main axis = horizontal).
13    Row,
14    /// Top-to-bottom (main axis = vertical).
15    Column,
16}
17
18/// Main-axis distribution of free space.
19#[derive(Clone, Copy, Debug, PartialEq, Eq)]
20pub enum JustifyContent {
21    /// Pack items at the start.
22    Start,
23    /// Centre items as a group.
24    Center,
25    /// Pack items at the end.
26    End,
27    /// First item at start, last at end, equal gaps between.
28    SpaceBetween,
29    /// Equal space around each item (half-size gaps at the edges).
30    SpaceAround,
31    /// Equal space between and around every item.
32    SpaceEvenly,
33}
34
35/// Cross-axis alignment of items within the container.
36#[derive(Clone, Copy, Debug, PartialEq, Eq)]
37pub enum AlignItems {
38    /// Align to the cross-axis start.
39    Start,
40    /// Centre on the cross axis.
41    Center,
42    /// Align to the cross-axis end.
43    End,
44    /// Stretch to fill the cross axis.
45    Stretch,
46}
47
48/// Whether and how the flex container wraps its items.
49#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
50pub enum FlexWrap {
51    /// All items fit in a single line (CSS `flex-wrap: nowrap`).
52    #[default]
53    NoWrap,
54    /// Items wrap into additional lines in the forward direction.
55    Wrap,
56    /// Items wrap into additional lines in the reverse direction (lines are reversed).
57    WrapReverse,
58}
59
60/// Distribution of multiple lines along the cross axis (analogous to
61/// `justify-content` but for lines, not items).  Only applies when
62/// `wrap != NoWrap` and there is more than one line.
63#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
64pub enum AlignContent {
65    /// Lines packed at the cross-axis start.
66    #[default]
67    Start,
68    /// Lines centred on the cross axis.
69    Center,
70    /// Lines packed at the cross-axis end.
71    End,
72    /// First line at start, last at end, equal gaps between.
73    SpaceBetween,
74    /// Equal space around each line (half-size gaps at the edges).
75    SpaceAround,
76    /// Equal space between and around every line.
77    SpaceEvenly,
78    /// Lines stretched to fill the cross axis equally.
79    Stretch,
80}
81
82/// A flex item: its base (preferred) size plus grow factor.
83#[derive(Clone, Copy, Debug)]
84pub struct FlexItem {
85    /// Preferred size before any growth/shrink is applied.
86    pub basis: Size,
87    /// Proportional share of leftover main-axis space (`flex-grow`).
88    pub grow: f32,
89}
90
91impl FlexItem {
92    /// A non-growing item with the given base size.
93    pub fn fixed(basis: Size) -> Self {
94        Self { basis, grow: 0.0 }
95    }
96
97    /// A growing item (`grow = 1.0`) with the given base size.
98    pub fn flexible(basis: Size) -> Self {
99        Self { basis, grow: 1.0 }
100    }
101}
102
103/// A flexbox container (single-line or multi-line).
104#[derive(Clone, Copy, Debug)]
105pub struct FlexLayout {
106    /// Main-axis direction.
107    pub direction: FlexDirection,
108    /// Main-axis distribution.
109    pub justify: JustifyContent,
110    /// Cross-axis alignment of items within each line.
111    pub align: AlignItems,
112    /// Gap between adjacent items in logical pixels.
113    pub gap: f32,
114    /// Whether and how items wrap into multiple lines.
115    pub wrap: FlexWrap,
116    /// Distribution of lines along the cross axis (only relevant when
117    /// `wrap != NoWrap` and there are multiple lines).
118    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    /// A row layout (children left-to-right).
136    pub fn row() -> Self {
137        Self {
138            direction: FlexDirection::Row,
139            ..Self::default()
140        }
141    }
142
143    /// A column layout (children top-to-bottom).
144    pub fn column() -> Self {
145        Self {
146            direction: FlexDirection::Column,
147            ..Self::default()
148        }
149    }
150
151    /// Builder: set `justify-content`.
152    pub fn with_justify(mut self, justify: JustifyContent) -> Self {
153        self.justify = justify;
154        self
155    }
156
157    /// Builder: set `align-items`.
158    pub fn with_align(mut self, align: AlignItems) -> Self {
159        self.align = align;
160        self
161    }
162
163    /// Builder: set the inter-item gap.
164    pub fn with_gap(mut self, gap: f32) -> Self {
165        self.gap = gap;
166        self
167    }
168
169    /// Builder: set line-wrapping behaviour.
170    pub fn with_wrap(mut self, wrap: FlexWrap) -> Self {
171        self.wrap = wrap;
172        self
173    }
174
175    /// Builder: set cross-axis line distribution (only applies when wrapping).
176    pub fn with_align_content(mut self, ac: AlignContent) -> Self {
177        self.align_content = ac;
178        self
179    }
180
181    /// Lay out `items` inside `container`, returning one [`Rect`] per item in
182    /// the same order. Rectangles are in `container`'s coordinate space.
183    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    // ── Single-line layout (original algorithm, unchanged) ──────────────
194
195    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    // ── Multi-line (wrapping) layout ─────────────────────────────────────
310
311    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        // ── Step 1: partition items into lines ──────────────────────────
340        // A new line starts when adding the next item (plus gap) would exceed
341        // main_extent.  Each line gets at least one item.
342        let mut lines: Vec<Vec<usize>> = Vec::new(); // indices into `items`
343        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        // ── Step 2: compute each line's cross-axis size ─────────────────
368        // The cross size of a line is the maximum cross size of its items
369        // (or cross_extent / num_lines for Stretch, resolved below).
370        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        // ── Step 3: determine display order for lines ───────────────────
380        // WrapReverse reverses the cross-axis order: the last logical line
381        // is displayed first (at the cross-axis start).
382        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        // ── Step 4: compute cross-axis sizes in display order ───────────
389        // `display_cross_sizes[d]` is the cross size of the line shown at
390        // display position `d`.  For Stretch the per-line size ignores actual
391        // item sizes; for all other modes we use the max item cross for each
392        // display slot.
393        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        // Compute the cross-start for each display slot.
403        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        // ── Step 5: lay out each line and build the output rects ────────
487        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            // `cross_start` and `line_cross` are indexed by display position.
492            let cross_start = line_cross_starts[display_order];
493            let line_cross = resolved_cross_sizes[display_order];
494
495            // Lay out main axis for this line using the existing single-line logic.
496            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    // ── Shared helpers ───────────────────────────────────────────────────
541
542    /// Resolve main-axis sizes with grow distribution for a line.
543    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    /// Compute leading offset and between-item spacing from justify-content.
572    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 any item had grow > 0 in the original items, the free space is
579        // already consumed; approximate by checking whether leftover ≈ 0.
580        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        // Two equal-grow items split 100 evenly.
644        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        // 100 - 10 gap = 90 split => 45 each.
658        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        // 60 leftover, centred => offset 30.
669        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        // 60 used by items, 60 leftover split into 2 gaps = 30 each.
682        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        // 60 leftover / 3 gaps = 20 each: lead 20, then 20+20 gap.
696        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        // Column layout: cross axis is horizontal.
703        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        // Item width 40, container width 100 => centred at x=30.
707        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    // ── CSS Flexbox wrapping conformance tests (20 scenarios) ────────────
722
723    /// 1. Single row, all items fit — same as NoWrap behavior.
724    #[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        // All in one row.
734        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    /// 2. Wrap: 3 items, container too small for all → 2 lines.
741    #[test]
742    fn wrap_three_items_two_lines() {
743        let l = FlexLayout::row().with_wrap(FlexWrap::Wrap);
744        // Container width 70, each item width 40 → items 0 and 1 can't fit (need 80).
745        // Line 1: item 0 (40px); Line 2: items 1, 2 (40+40=80 > 70... wait that's also too big).
746        // Let's use width 90: item 0+1 (40+40=80 ≤ 90), item 2 overflows → Line 1: [0,1], Line 2: [2].
747        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        // Items 0 and 1 on line 1 (top=0).
755        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        // Item 2 on line 2 (top=10).
758        assert!(approx(rects[2].top(), 10.0), "item2 top={}", rects[2].top());
759    }
760
761    /// 3. WrapReverse: verify line order reversed.
762    #[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)), // wraps to line 2
768        ];
769        let rects = l.layout(Rect::new(0.0, 0.0, 80.0, 40.0), &items);
770        // With WrapReverse, the SECOND logical line (item 1) appears at the TOP.
771        // item 0 → line 1 (logical), displayed at cross=10 (second display position)
772        // item 1 → line 2 (logical), displayed at cross=0 (first display position)
773        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    /// 4. AlignContent::Center: 2 lines → centered in cross-axis.
782    #[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        // 2 lines × 10px = 20px total, container height=60, so 20 leftover.
792        // Center: offset = 10.
793        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    /// 5. AlignContent::SpaceBetween: 2 lines → endpoints.
803    #[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        // Container height=60: first line at top=0, second at top=50 (60-10).
813        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    /// 6. AlignContent::SpaceAround.
819    #[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        // Container height=60, total cross=20, leftover=40. 2 lines → unit=20.
829        // Line 1: offset = 10 (unit/2). Line 2: 10 + 10 + 20 = 40.
830        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    /// 7. AlignContent::SpaceEvenly.
836    #[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        // Container height=60, total cross=20, leftover=40. 2 lines → unit=40/3≈13.3.
846        // Line 1: 13.3. Line 2: 13.3 + 10 + 13.3 = 36.6.
847        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    /// 8. AlignContent::Stretch: lines stretch to fill cross axis.
862    #[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        // Container height=60, 2 lines → each line gets 30px.
873        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    /// 9. Single-item line (oversized item) — gets its own line, no panic.
881    #[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)), // wider than container
886            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        // Each item on its own line.
891        assert!(
892            rects[1].top() > rects[0].top(),
893            "item1 should be below oversized item0"
894        );
895    }
896
897    /// 10. Zero-gap wrapping.
898    #[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        // Container width=80: items 0 (50≤80), items 0+1 (100>80) → wrap after 0.
907        // Line 1: [0], Line 2: [1], Line 3: [2].
908        let rects = l.layout(Rect::new(0.0, 0.0, 80.0, 40.0), &items);
909        // All items on separate lines OR items 1+2 share a line? width 80 ≥ 50+50=100? No.
910        // 50 ≤ 80, 50+50=100 > 80 → item 1 wraps. 50 ≤ 80 → item 2 alone. 3 lines.
911        assert!(rects[1].top() > rects[0].top(), "item1 below item0");
912    }
913
914    /// 11. Wrap + FlexDirection::Column.
915    #[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)), // wraps to second column
921        ];
922        // Container height=80: first item (60≤80), second item (60+60=120>80) → wraps.
923        let rects = l.layout(Rect::new(0.0, 0.0, 40.0, 80.0), &items);
924        // Item 1 should be in a new column (different left).
925        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    /// 12. Wrap + JustifyContent::SpaceBetween within each line.
934    #[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        // Container width=100: items 0-1 (40≤100), items 0-2 (60≤100), items 0-3 (80≤100) — all fit!
946        // So single line, SpaceBetween: leftover=20/3 gaps.
947        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    /// 13. All items same size, wraps exactly at boundary.
954    #[test]
955    fn wrap_exact_boundary() {
956        let l = FlexLayout::row().with_wrap(FlexWrap::Wrap);
957        // 3 items of 30px in a 90px container — all fit on one line.
958        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        // All on same row.
965        assert!(close(rects[0].top(), rects[1].top()));
966        assert!(close(rects[1].top(), rects[2].top()));
967    }
968
969    /// 14. Items with grow > 0 in wrapped lines.
970    #[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)), // grows
975            FlexItem::fixed(Size::new(80.0, 10.0)),    // won't fit with item0 growing
976        ];
977        // Container 100px. Item 0 basis=20, item 1 basis=80. 20+80=100 fits.
978        // But with grow, item 0 would consume free space. No wrapping needed.
979        let rects = l.layout(Rect::new(0.0, 0.0, 100.0, 20.0), &items);
980        assert_eq!(rects.len(), 2);
981        // Both on same line; item0 grows to fill (100-80=20).
982        assert!(close(rects[0].top(), rects[1].top()));
983    }
984
985    /// 15. Empty items list with wrap.
986    #[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    /// 16. Single item fits in one line.
994    #[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    /// 17. Large gap causes more wrapping.
1006    #[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        // Container width=80: first item 30, then +gap30+30=90 > 80 → wraps.
1014        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    /// 18. WrapReverse + AlignContent::End.
1022    #[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        // 2 lines. AlignContent::End: both lines packed at bottom.
1032        let rects = l.layout(Rect::new(0.0, 0.0, 80.0, 60.0), &items);
1033        // Both items should be in the lower portion of the container.
1034        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    /// 19. Cross-axis AlignItems::Center within each line.
1042    #[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)),  // line 1
1049            FlexItem::fixed(Size::new(60.0, 15.0)), // line 2
1050        ];
1051        // Container height=40. Line 1 height=5, line 2 height=15.
1052        // AlignItems::Center: item 0 centered within its line's cross size.
1053        let rects = l.layout(Rect::new(0.0, 0.0, 80.0, 40.0), &items);
1054        // Item 0's height should remain 5 (not stretched).
1055        assert!(
1056            close(rects[0].height(), 5.0),
1057            "item0 h={}",
1058            rects[0].height()
1059        );
1060        // Item 1's height should remain 15.
1061        assert!(
1062            close(rects[1].height(), 15.0),
1063            "item1 h={}",
1064            rects[1].height()
1065        );
1066    }
1067
1068    /// 20. Verify original indices are preserved after wrapping.
1069    #[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)), // idx 0
1074            FlexItem::fixed(Size::new(70.0, 10.0)), // idx 1 — wraps
1075            FlexItem::fixed(Size::new(70.0, 10.0)), // idx 2 — wraps again
1076        ];
1077        let rects = l.layout(Rect::new(0.0, 0.0, 80.0, 60.0), &items);
1078        assert_eq!(rects.len(), 3);
1079        // Each item on its own line; positions increase monotonically.
1080        assert!(rects[0].top() < rects[1].top(), "idx0 above idx1");
1081        assert!(rects[1].top() < rects[2].top(), "idx1 above idx2");
1082    }
1083
1084    /// 21. WrapReverse with unequal cross sizes: each line gets its OWN cross slot.
1085    ///
1086    /// Line 0 (logical): item0, cross=10px.  Line 1 (logical): item1, cross=30px.
1087    /// WrapReverse: display order is [1, 0], so item1 (30px) is displayed at
1088    /// the top (display slot 0) and item0 (10px) at the bottom (display slot 1).
1089    /// The two slots must not overlap.
1090    #[test]
1091    fn wrap_reverse_unequal_cross_sizes() {
1092        let l = FlexLayout::row()
1093            .with_wrap(FlexWrap::WrapReverse)
1094            .with_align(AlignItems::Start); // don't stretch items
1095        let items = [
1096            FlexItem::fixed(Size::new(60.0, 10.0)), // line 0 (logical), cross=10
1097            FlexItem::fixed(Size::new(60.0, 30.0)), // line 1 (logical), cross=30
1098        ];
1099        // Container: 80×60. Each item wraps (80<60+60).
1100        let rects = l.layout(Rect::new(0.0, 0.0, 80.0, 60.0), &items);
1101        assert_eq!(rects.len(), 2);
1102
1103        // WrapReverse: item1 (30px) is at display position 0 (top).
1104        //              item0 (10px) is at display position 1 (below item1).
1105        let top1 = rects[1].top(); // item1 (logical line 1, displayed first)
1106        let top0 = rects[0].top(); // item0 (logical line 0, displayed second)
1107
1108        // item1 should be above item0.
1109        assert!(top1 < top0,
1110            "WrapReverse: item1 (30px cross, display-first) top={top1} should be < item0 top={top0}");
1111
1112        // The two rects must not overlap (item0 starts at or after item1's bottom).
1113        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        // item1 height is 30 (not stretched to 10px).
1120        assert!(
1121            close(rects[1].height(), 30.0),
1122            "item1 height={}",
1123            rects[1].height()
1124        );
1125        // item0 height is 10 (not stretched to 30px).
1126        assert!(
1127            close(rects[0].height(), 10.0),
1128            "item0 height={}",
1129            rects[0].height()
1130        );
1131    }
1132}