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//!
7//! ## Parallel subtree layout
8//!
9//! [`layout_subtrees_parallel`] dispatches a batch of independent container
10//! layouts onto Rayon's work-stealing thread pool. It is safe to call from any
11//! thread and never allocates on the common path beyond the input/output
12//! `Vec`s. Use it when you have many sibling containers whose layouts do not
13//! depend on each other.
14
15use crate::geometry::{Rect, Size};
16use rayon::prelude::*;
17
18/// The direction children are laid out along the main axis.
19#[derive(Clone, Copy, Debug, PartialEq, Eq)]
20pub enum FlexDirection {
21    /// Left-to-right (main axis = horizontal).
22    Row,
23    /// Top-to-bottom (main axis = vertical).
24    Column,
25}
26
27/// Main-axis distribution of free space.
28#[derive(Clone, Copy, Debug, PartialEq, Eq)]
29pub enum JustifyContent {
30    /// Pack items at the start.
31    Start,
32    /// Centre items as a group.
33    Center,
34    /// Pack items at the end.
35    End,
36    /// First item at start, last at end, equal gaps between.
37    SpaceBetween,
38    /// Equal space around each item (half-size gaps at the edges).
39    SpaceAround,
40    /// Equal space between and around every item.
41    SpaceEvenly,
42}
43
44/// Cross-axis alignment of items within the container.
45#[derive(Clone, Copy, Debug, PartialEq, Eq)]
46pub enum AlignItems {
47    /// Align to the cross-axis start.
48    Start,
49    /// Centre on the cross axis.
50    Center,
51    /// Align to the cross-axis end.
52    End,
53    /// Stretch to fill the cross axis.
54    Stretch,
55}
56
57/// Whether and how the flex container wraps its items.
58#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
59pub enum FlexWrap {
60    /// All items fit in a single line (CSS `flex-wrap: nowrap`).
61    #[default]
62    NoWrap,
63    /// Items wrap into additional lines in the forward direction.
64    Wrap,
65    /// Items wrap into additional lines in the reverse direction (lines are reversed).
66    WrapReverse,
67}
68
69/// Distribution of multiple lines along the cross axis (analogous to
70/// `justify-content` but for lines, not items).  Only applies when
71/// `wrap != NoWrap` and there is more than one line.
72#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
73pub enum AlignContent {
74    /// Lines packed at the cross-axis start.
75    #[default]
76    Start,
77    /// Lines centred on the cross axis.
78    Center,
79    /// Lines packed at the cross-axis end.
80    End,
81    /// First line at start, last at end, equal gaps between.
82    SpaceBetween,
83    /// Equal space around each line (half-size gaps at the edges).
84    SpaceAround,
85    /// Equal space between and around every line.
86    SpaceEvenly,
87    /// Lines stretched to fill the cross axis equally.
88    Stretch,
89}
90
91/// A flex item: its base (preferred) size plus grow factor.
92#[derive(Clone, Copy, Debug)]
93pub struct FlexItem {
94    /// Preferred size before any growth/shrink is applied.
95    pub basis: Size,
96    /// Proportional share of leftover main-axis space (`flex-grow`).
97    pub grow: f32,
98}
99
100impl FlexItem {
101    /// A non-growing item with the given base size.
102    pub fn fixed(basis: Size) -> Self {
103        Self { basis, grow: 0.0 }
104    }
105
106    /// A growing item (`grow = 1.0`) with the given base size.
107    pub fn flexible(basis: Size) -> Self {
108        Self { basis, grow: 1.0 }
109    }
110}
111
112/// A flexbox container (single-line or multi-line).
113#[derive(Clone, Copy, Debug)]
114pub struct FlexLayout {
115    /// Main-axis direction.
116    pub direction: FlexDirection,
117    /// Main-axis distribution.
118    pub justify: JustifyContent,
119    /// Cross-axis alignment of items within each line.
120    pub align: AlignItems,
121    /// Gap between adjacent items in logical pixels.
122    pub gap: f32,
123    /// Whether and how items wrap into multiple lines.
124    pub wrap: FlexWrap,
125    /// Distribution of lines along the cross axis (only relevant when
126    /// `wrap != NoWrap` and there are multiple lines).
127    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    /// A row layout (children left-to-right).
145    pub fn row() -> Self {
146        Self {
147            direction: FlexDirection::Row,
148            ..Self::default()
149        }
150    }
151
152    /// A column layout (children top-to-bottom).
153    pub fn column() -> Self {
154        Self {
155            direction: FlexDirection::Column,
156            ..Self::default()
157        }
158    }
159
160    /// Builder: set `justify-content`.
161    pub fn with_justify(mut self, justify: JustifyContent) -> Self {
162        self.justify = justify;
163        self
164    }
165
166    /// Builder: set `align-items`.
167    pub fn with_align(mut self, align: AlignItems) -> Self {
168        self.align = align;
169        self
170    }
171
172    /// Builder: set the inter-item gap.
173    pub fn with_gap(mut self, gap: f32) -> Self {
174        self.gap = gap;
175        self
176    }
177
178    /// Builder: set line-wrapping behaviour.
179    pub fn with_wrap(mut self, wrap: FlexWrap) -> Self {
180        self.wrap = wrap;
181        self
182    }
183
184    /// Builder: set cross-axis line distribution (only applies when wrapping).
185    pub fn with_align_content(mut self, ac: AlignContent) -> Self {
186        self.align_content = ac;
187        self
188    }
189
190    /// Lay out `items` inside `container`, returning one [`Rect`] per item in
191    /// the same order. Rectangles are in `container`'s coordinate space.
192    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    // ── Single-line layout (original algorithm, unchanged) ──────────────
203
204    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    // ── Multi-line (wrapping) layout ─────────────────────────────────────
319
320    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        // ── Step 1: partition items into lines ──────────────────────────
349        // A new line starts when adding the next item (plus gap) would exceed
350        // main_extent.  Each line gets at least one item.
351        let mut lines: Vec<Vec<usize>> = Vec::new(); // indices into `items`
352        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        // ── Step 2: compute each line's cross-axis size ─────────────────
377        // The cross size of a line is the maximum cross size of its items
378        // (or cross_extent / num_lines for Stretch, resolved below).
379        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        // ── Step 3: determine display order for lines ───────────────────
389        // WrapReverse reverses the cross-axis order: the last logical line
390        // is displayed first (at the cross-axis start).
391        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        // ── Step 4: compute cross-axis sizes in display order ───────────
398        // `display_cross_sizes[d]` is the cross size of the line shown at
399        // display position `d`.  For Stretch the per-line size ignores actual
400        // item sizes; for all other modes we use the max item cross for each
401        // display slot.
402        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        // Compute the cross-start for each display slot.
412        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        // ── Step 5: lay out each line and build the output rects ────────
496        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            // `cross_start` and `line_cross` are indexed by display position.
501            let cross_start = line_cross_starts[display_order];
502            let line_cross = resolved_cross_sizes[display_order];
503
504            // Lay out main axis for this line using the existing single-line logic.
505            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    // ── Shared helpers ───────────────────────────────────────────────────
550
551    /// Resolve main-axis sizes with grow distribution for a line.
552    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    /// Compute leading offset and between-item spacing from justify-content.
581    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 any item had grow > 0 in the original items, the free space is
588        // already consumed; approximate by checking whether leftover ≈ 0.
589        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
616/// A single layout task for [`layout_subtrees_parallel`].
617///
618/// Encapsulates one independent subtree layout request: the flex spec, the
619/// container rectangle, and the slice of items to lay out. The result is a
620/// `Vec<Rect>` in the same order as `items`.
621pub struct LayoutTask {
622    /// The flexbox configuration to use.
623    pub layout: FlexLayout,
624    /// The container rectangle to lay out into.
625    pub container: Rect,
626    /// The items to lay out inside the container.
627    pub items: Vec<FlexItem>,
628}
629
630/// Lay out multiple **independent** subtrees in parallel on Rayon's thread pool.
631///
632/// Each [`LayoutTask`] in `tasks` is a self-contained layout (its own container
633/// rect and item list). Because no task depends on any other, Rayon can compute
634/// them concurrently across available CPU cores.
635///
636/// Returns one `Vec<Rect>` per task, in the same order as `tasks`.
637///
638/// # When to use
639///
640/// Prefer this over sequential `FlexLayout::layout` calls when:
641/// - You have ≥ 4 independent containers to lay out in one frame.
642/// - Each container has at least a handful of items (overhead dominates below ~8
643///   items on modern hardware).
644///
645/// For tiny trees, sequential layout is faster due to lower overhead.
646///
647/// # Example
648///
649/// ```rust
650/// # use oxiui_core::layout::{FlexLayout, FlexItem, LayoutTask, layout_subtrees_parallel};
651/// # use oxiui_core::geometry::{Rect, Size};
652/// let tasks: Vec<LayoutTask> = (0..8)
653///     .map(|_| LayoutTask {
654///         layout: FlexLayout::row(),
655///         container: Rect::new(0.0, 0.0, 400.0, 40.0),
656///         items: vec![
657///             FlexItem::fixed(Size::new(100.0, 40.0)),
658///             FlexItem::flexible(Size::new(50.0, 40.0)),
659///         ],
660///     })
661///     .collect();
662/// let results = layout_subtrees_parallel(&tasks);
663/// assert_eq!(results.len(), 8);
664/// assert_eq!(results[0].len(), 2);
665/// ```
666pub 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        // Two equal-grow items split 100 evenly.
710        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        // 100 - 10 gap = 90 split => 45 each.
724        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        // 60 leftover, centred => offset 30.
735        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        // 60 used by items, 60 leftover split into 2 gaps = 30 each.
748        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        // 60 leftover / 3 gaps = 20 each: lead 20, then 20+20 gap.
762        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        // Column layout: cross axis is horizontal.
769        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        // Item width 40, container width 100 => centred at x=30.
773        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    // ── CSS Flexbox wrapping conformance tests (20 scenarios) ────────────
788
789    /// 1. Single row, all items fit — same as NoWrap behavior.
790    #[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        // All in one row.
800        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    /// 2. Wrap: 3 items, container too small for all → 2 lines.
807    #[test]
808    fn wrap_three_items_two_lines() {
809        let l = FlexLayout::row().with_wrap(FlexWrap::Wrap);
810        // Container width 70, each item width 40 → items 0 and 1 can't fit (need 80).
811        // Line 1: item 0 (40px); Line 2: items 1, 2 (40+40=80 > 70... wait that's also too big).
812        // Let's use width 90: item 0+1 (40+40=80 ≤ 90), item 2 overflows → Line 1: [0,1], Line 2: [2].
813        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        // Items 0 and 1 on line 1 (top=0).
821        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        // Item 2 on line 2 (top=10).
824        assert!(approx(rects[2].top(), 10.0), "item2 top={}", rects[2].top());
825    }
826
827    /// 3. WrapReverse: verify line order reversed.
828    #[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)), // wraps to line 2
834        ];
835        let rects = l.layout(Rect::new(0.0, 0.0, 80.0, 40.0), &items);
836        // With WrapReverse, the SECOND logical line (item 1) appears at the TOP.
837        // item 0 → line 1 (logical), displayed at cross=10 (second display position)
838        // item 1 → line 2 (logical), displayed at cross=0 (first display position)
839        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    /// 4. AlignContent::Center: 2 lines → centered in cross-axis.
848    #[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        // 2 lines × 10px = 20px total, container height=60, so 20 leftover.
858        // Center: offset = 10.
859        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    /// 5. AlignContent::SpaceBetween: 2 lines → endpoints.
869    #[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        // Container height=60: first line at top=0, second at top=50 (60-10).
879        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    /// 6. AlignContent::SpaceAround.
885    #[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        // Container height=60, total cross=20, leftover=40. 2 lines → unit=20.
895        // Line 1: offset = 10 (unit/2). Line 2: 10 + 10 + 20 = 40.
896        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    /// 7. AlignContent::SpaceEvenly.
902    #[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        // Container height=60, total cross=20, leftover=40. 2 lines → unit=40/3≈13.3.
912        // Line 1: 13.3. Line 2: 13.3 + 10 + 13.3 = 36.6.
913        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    /// 8. AlignContent::Stretch: lines stretch to fill cross axis.
928    #[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        // Container height=60, 2 lines → each line gets 30px.
939        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    /// 9. Single-item line (oversized item) — gets its own line, no panic.
947    #[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)), // wider than container
952            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        // Each item on its own line.
957        assert!(
958            rects[1].top() > rects[0].top(),
959            "item1 should be below oversized item0"
960        );
961    }
962
963    /// 10. Zero-gap wrapping.
964    #[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        // Container width=80: items 0 (50≤80), items 0+1 (100>80) → wrap after 0.
973        // Line 1: [0], Line 2: [1], Line 3: [2].
974        let rects = l.layout(Rect::new(0.0, 0.0, 80.0, 40.0), &items);
975        // All items on separate lines OR items 1+2 share a line? width 80 ≥ 50+50=100? No.
976        // 50 ≤ 80, 50+50=100 > 80 → item 1 wraps. 50 ≤ 80 → item 2 alone. 3 lines.
977        assert!(rects[1].top() > rects[0].top(), "item1 below item0");
978    }
979
980    /// 11. Wrap + FlexDirection::Column.
981    #[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)), // wraps to second column
987        ];
988        // Container height=80: first item (60≤80), second item (60+60=120>80) → wraps.
989        let rects = l.layout(Rect::new(0.0, 0.0, 40.0, 80.0), &items);
990        // Item 1 should be in a new column (different left).
991        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    /// 12. Wrap + JustifyContent::SpaceBetween within each line.
1000    #[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        // Container width=100: items 0-1 (40≤100), items 0-2 (60≤100), items 0-3 (80≤100) — all fit!
1012        // So single line, SpaceBetween: leftover=20/3 gaps.
1013        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    /// 13. All items same size, wraps exactly at boundary.
1020    #[test]
1021    fn wrap_exact_boundary() {
1022        let l = FlexLayout::row().with_wrap(FlexWrap::Wrap);
1023        // 3 items of 30px in a 90px container — all fit on one line.
1024        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        // All on same row.
1031        assert!(close(rects[0].top(), rects[1].top()));
1032        assert!(close(rects[1].top(), rects[2].top()));
1033    }
1034
1035    /// 14. Items with grow > 0 in wrapped lines.
1036    #[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)), // grows
1041            FlexItem::fixed(Size::new(80.0, 10.0)),    // won't fit with item0 growing
1042        ];
1043        // Container 100px. Item 0 basis=20, item 1 basis=80. 20+80=100 fits.
1044        // But with grow, item 0 would consume free space. No wrapping needed.
1045        let rects = l.layout(Rect::new(0.0, 0.0, 100.0, 20.0), &items);
1046        assert_eq!(rects.len(), 2);
1047        // Both on same line; item0 grows to fill (100-80=20).
1048        assert!(close(rects[0].top(), rects[1].top()));
1049    }
1050
1051    /// 15. Empty items list with wrap.
1052    #[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    /// 16. Single item fits in one line.
1060    #[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    /// 17. Large gap causes more wrapping.
1072    #[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        // Container width=80: first item 30, then +gap30+30=90 > 80 → wraps.
1080        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    /// 18. WrapReverse + AlignContent::End.
1088    #[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        // 2 lines. AlignContent::End: both lines packed at bottom.
1098        let rects = l.layout(Rect::new(0.0, 0.0, 80.0, 60.0), &items);
1099        // Both items should be in the lower portion of the container.
1100        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    /// 19. Cross-axis AlignItems::Center within each line.
1108    #[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)),  // line 1
1115            FlexItem::fixed(Size::new(60.0, 15.0)), // line 2
1116        ];
1117        // Container height=40. Line 1 height=5, line 2 height=15.
1118        // AlignItems::Center: item 0 centered within its line's cross size.
1119        let rects = l.layout(Rect::new(0.0, 0.0, 80.0, 40.0), &items);
1120        // Item 0's height should remain 5 (not stretched).
1121        assert!(
1122            close(rects[0].height(), 5.0),
1123            "item0 h={}",
1124            rects[0].height()
1125        );
1126        // Item 1's height should remain 15.
1127        assert!(
1128            close(rects[1].height(), 15.0),
1129            "item1 h={}",
1130            rects[1].height()
1131        );
1132    }
1133
1134    /// 20. Verify original indices are preserved after wrapping.
1135    #[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)), // idx 0
1140            FlexItem::fixed(Size::new(70.0, 10.0)), // idx 1 — wraps
1141            FlexItem::fixed(Size::new(70.0, 10.0)), // idx 2 — wraps again
1142        ];
1143        let rects = l.layout(Rect::new(0.0, 0.0, 80.0, 60.0), &items);
1144        assert_eq!(rects.len(), 3);
1145        // Each item on its own line; positions increase monotonically.
1146        assert!(rects[0].top() < rects[1].top(), "idx0 above idx1");
1147        assert!(rects[1].top() < rects[2].top(), "idx1 above idx2");
1148    }
1149
1150    /// 21. WrapReverse with unequal cross sizes: each line gets its OWN cross slot.
1151    ///
1152    /// Line 0 (logical): item0, cross=10px.  Line 1 (logical): item1, cross=30px.
1153    /// WrapReverse: display order is [1, 0], so item1 (30px) is displayed at
1154    /// the top (display slot 0) and item0 (10px) at the bottom (display slot 1).
1155    /// The two slots must not overlap.
1156    #[test]
1157    fn wrap_reverse_unequal_cross_sizes() {
1158        let l = FlexLayout::row()
1159            .with_wrap(FlexWrap::WrapReverse)
1160            .with_align(AlignItems::Start); // don't stretch items
1161        let items = [
1162            FlexItem::fixed(Size::new(60.0, 10.0)), // line 0 (logical), cross=10
1163            FlexItem::fixed(Size::new(60.0, 30.0)), // line 1 (logical), cross=30
1164        ];
1165        // Container: 80×60. Each item wraps (80<60+60).
1166        let rects = l.layout(Rect::new(0.0, 0.0, 80.0, 60.0), &items);
1167        assert_eq!(rects.len(), 2);
1168
1169        // WrapReverse: item1 (30px) is at display position 0 (top).
1170        //              item0 (10px) is at display position 1 (below item1).
1171        let top1 = rects[1].top(); // item1 (logical line 1, displayed first)
1172        let top0 = rects[0].top(); // item0 (logical line 0, displayed second)
1173
1174        // item1 should be above item0.
1175        assert!(top1 < top0,
1176            "WrapReverse: item1 (30px cross, display-first) top={top1} should be < item0 top={top0}");
1177
1178        // The two rects must not overlap (item0 starts at or after item1's bottom).
1179        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        // item1 height is 30 (not stretched to 10px).
1186        assert!(
1187            close(rects[1].height(), 30.0),
1188            "item1 height={}",
1189            rects[1].height()
1190        );
1191        // item0 height is 10 (not stretched to 30px).
1192        assert!(
1193            close(rects[0].height(), 10.0),
1194            "item0 height={}",
1195            rects[0].height()
1196        );
1197    }
1198
1199    // ── Parallel layout tests ──────────────────────────────────────────────
1200
1201    /// Parallel layout of 8 independent row containers produces identical
1202    /// results to sequential layout.
1203    #[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    /// Parallel layout of an empty task list returns an empty result.
1232    #[test]
1233    fn parallel_layout_empty_tasks() {
1234        let results = layout_subtrees_parallel(&[]);
1235        assert!(results.is_empty());
1236    }
1237
1238    /// Parallel layout of a single task with an empty item list returns an
1239    /// empty rect vec (mirrors the sequential behaviour).
1240    #[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    /// Parallel layout scales correctly: 64 column containers, each with 3 items.
1253    #[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            // Items must be stacked vertically (non-decreasing top).
1271            assert!(rects[0].top() <= rects[1].top());
1272            assert!(rects[1].top() <= rects[2].top());
1273        }
1274    }
1275}