use crate::geometry::{Rect, Size};
use rayon::prelude::*;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum FlexDirection {
Row,
Column,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum JustifyContent {
Start,
Center,
End,
SpaceBetween,
SpaceAround,
SpaceEvenly,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum AlignItems {
Start,
Center,
End,
Stretch,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum FlexWrap {
#[default]
NoWrap,
Wrap,
WrapReverse,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum AlignContent {
#[default]
Start,
Center,
End,
SpaceBetween,
SpaceAround,
SpaceEvenly,
Stretch,
}
#[derive(Clone, Copy, Debug)]
pub struct FlexItem {
pub basis: Size,
pub grow: f32,
}
impl FlexItem {
pub fn fixed(basis: Size) -> Self {
Self { basis, grow: 0.0 }
}
pub fn flexible(basis: Size) -> Self {
Self { basis, grow: 1.0 }
}
}
#[derive(Clone, Copy, Debug)]
pub struct FlexLayout {
pub direction: FlexDirection,
pub justify: JustifyContent,
pub align: AlignItems,
pub gap: f32,
pub wrap: FlexWrap,
pub align_content: AlignContent,
}
impl Default for FlexLayout {
fn default() -> Self {
Self {
direction: FlexDirection::Row,
justify: JustifyContent::Start,
align: AlignItems::Stretch,
gap: 0.0,
wrap: FlexWrap::NoWrap,
align_content: AlignContent::Start,
}
}
}
impl FlexLayout {
pub fn row() -> Self {
Self {
direction: FlexDirection::Row,
..Self::default()
}
}
pub fn column() -> Self {
Self {
direction: FlexDirection::Column,
..Self::default()
}
}
pub fn with_justify(mut self, justify: JustifyContent) -> Self {
self.justify = justify;
self
}
pub fn with_align(mut self, align: AlignItems) -> Self {
self.align = align;
self
}
pub fn with_gap(mut self, gap: f32) -> Self {
self.gap = gap;
self
}
pub fn with_wrap(mut self, wrap: FlexWrap) -> Self {
self.wrap = wrap;
self
}
pub fn with_align_content(mut self, ac: AlignContent) -> Self {
self.align_content = ac;
self
}
pub fn layout(&self, container: Rect, items: &[FlexItem]) -> Vec<Rect> {
if items.is_empty() {
return Vec::new();
}
match self.wrap {
FlexWrap::NoWrap => self.layout_single_line(container, items),
FlexWrap::Wrap | FlexWrap::WrapReverse => self.layout_wrapped(container, items),
}
}
fn layout_single_line(&self, container: Rect, items: &[FlexItem]) -> Vec<Rect> {
let is_row = self.direction == FlexDirection::Row;
let main_extent = if is_row {
container.width()
} else {
container.height()
};
let cross_extent = if is_row {
container.height()
} else {
container.width()
};
let main_of = |it: &FlexItem| {
if is_row {
it.basis.width
} else {
it.basis.height
}
};
let total_basis: f32 = items.iter().map(main_of).sum();
let total_gap = self.gap * (items.len().saturating_sub(1)) as f32;
let total_grow: f32 = items.iter().map(|it| it.grow.max(0.0)).sum();
let free = (main_extent - total_basis - total_gap).max(0.0);
let mut main_sizes: Vec<f32> = items
.iter()
.map(|it| {
let extra = if total_grow > 0.0 {
free * (it.grow.max(0.0) / total_grow)
} else {
0.0
};
main_of(it) + extra
})
.collect();
let used_main: f32 = main_sizes.iter().sum::<f32>() + total_gap;
let leftover = (main_extent - used_main).max(0.0);
let n = items.len() as f32;
let (lead, between) = if total_grow > 0.0 {
(0.0, self.gap)
} else {
match self.justify {
JustifyContent::Start => (0.0, self.gap),
JustifyContent::Center => (leftover * 0.5, self.gap),
JustifyContent::End => (leftover, self.gap),
JustifyContent::SpaceBetween => {
if items.len() == 1 {
(0.0, self.gap)
} else {
(0.0, self.gap + leftover / (n - 1.0))
}
}
JustifyContent::SpaceAround => {
let unit = leftover / n;
(unit * 0.5, self.gap + unit)
}
JustifyContent::SpaceEvenly => {
let unit = leftover / (n + 1.0);
(unit, self.gap + unit)
}
}
};
for s in &mut main_sizes {
if *s < 0.0 {
*s = 0.0;
}
}
let mut rects = Vec::with_capacity(items.len());
let mut main_cursor = lead;
for (i, it) in items.iter().enumerate() {
let main_size = main_sizes[i];
let item_cross = if is_row {
it.basis.height
} else {
it.basis.width
};
let (cross_size, cross_pos) = match self.align {
AlignItems::Stretch => (cross_extent, 0.0),
AlignItems::Start => (item_cross, 0.0),
AlignItems::Center => (item_cross, (cross_extent - item_cross) * 0.5),
AlignItems::End => (item_cross, cross_extent - item_cross),
};
let rect = if is_row {
Rect::new(
container.left() + main_cursor,
container.top() + cross_pos,
main_size,
cross_size,
)
} else {
Rect::new(
container.left() + cross_pos,
container.top() + main_cursor,
cross_size,
main_size,
)
};
rects.push(rect);
main_cursor += main_size;
if i + 1 < items.len() {
main_cursor += between;
}
}
rects
}
fn layout_wrapped(&self, container: Rect, items: &[FlexItem]) -> Vec<Rect> {
let is_row = self.direction == FlexDirection::Row;
let main_extent = if is_row {
container.width()
} else {
container.height()
};
let cross_extent = if is_row {
container.height()
} else {
container.width()
};
let main_of = |it: &FlexItem| {
if is_row {
it.basis.width
} else {
it.basis.height
}
};
let cross_of = |it: &FlexItem| {
if is_row {
it.basis.height
} else {
it.basis.width
}
};
let mut lines: Vec<Vec<usize>> = Vec::new(); let mut current_line: Vec<usize> = Vec::new();
let mut current_main: f32 = 0.0;
for (i, it) in items.iter().enumerate() {
let item_main = main_of(it).max(0.0);
let needed = if current_line.is_empty() {
item_main
} else {
current_main + self.gap + item_main
};
if !current_line.is_empty() && needed > main_extent + 1e-4 {
lines.push(current_line);
current_line = Vec::new();
current_main = item_main;
} else {
current_main = needed;
}
current_line.push(i);
}
if !current_line.is_empty() {
lines.push(current_line);
}
let line_cross_sizes: Vec<f32> = lines
.iter()
.map(|line| {
line.iter()
.map(|&i| cross_of(&items[i]).max(0.0))
.fold(0.0_f32, f32::max)
})
.collect();
let line_order: Vec<usize> = if self.wrap == FlexWrap::WrapReverse {
(0..lines.len()).rev().collect()
} else {
(0..lines.len()).collect()
};
let n_lines = lines.len() as f32;
let display_cross_sizes: Vec<f32> = if matches!(self.align_content, AlignContent::Stretch) {
vec![cross_extent / n_lines; lines.len()]
} else {
line_order.iter().map(|&li| line_cross_sizes[li]).collect()
};
let total_display_cross: f32 = display_cross_sizes.iter().sum();
let leftover_cross = (cross_extent - total_display_cross).max(0.0);
let (line_cross_starts, resolved_cross_sizes): (Vec<f32>, Vec<f32>) =
match self.align_content {
AlignContent::Start | AlignContent::Stretch => {
let mut pos = 0.0;
let starts = display_cross_sizes
.iter()
.map(|&sz| {
let s = pos;
pos += sz;
s
})
.collect();
(starts, display_cross_sizes.clone())
}
AlignContent::End => {
let mut pos = leftover_cross;
let starts = display_cross_sizes
.iter()
.map(|&sz| {
let s = pos;
pos += sz;
s
})
.collect();
(starts, display_cross_sizes.clone())
}
AlignContent::Center => {
let mut pos = leftover_cross * 0.5;
let starts = display_cross_sizes
.iter()
.map(|&sz| {
let s = pos;
pos += sz;
s
})
.collect();
(starts, display_cross_sizes.clone())
}
AlignContent::SpaceBetween => {
let gap = if lines.len() <= 1 {
0.0
} else {
leftover_cross / (n_lines - 1.0)
};
let mut pos = 0.0;
let starts = display_cross_sizes
.iter()
.map(|&sz| {
let s = pos;
pos += sz + gap;
s
})
.collect();
(starts, display_cross_sizes.clone())
}
AlignContent::SpaceAround => {
let unit = leftover_cross / n_lines;
let mut pos = unit * 0.5;
let starts = display_cross_sizes
.iter()
.map(|&sz| {
let s = pos;
pos += sz + unit;
s
})
.collect();
(starts, display_cross_sizes.clone())
}
AlignContent::SpaceEvenly => {
let unit = leftover_cross / (n_lines + 1.0);
let mut pos = unit;
let starts = display_cross_sizes
.iter()
.map(|&sz| {
let s = pos;
pos += sz + unit;
s
})
.collect();
(starts, display_cross_sizes.clone())
}
};
let mut rects_by_index: Vec<Rect> = vec![Rect::new(0.0, 0.0, 0.0, 0.0); items.len()];
for (display_order, &line_idx) in line_order.iter().enumerate() {
let line = &lines[line_idx];
let cross_start = line_cross_starts[display_order];
let line_cross = resolved_cross_sizes[display_order];
let line_items: Vec<FlexItem> = line.iter().map(|&i| items[i]).collect();
let line_main_sizes = self.resolve_main_sizes(&line_items, main_extent);
let (main_lead, main_between) = self.justify_offsets(&line_main_sizes, main_extent);
let mut main_cursor = main_lead;
for (j, &orig_idx) in line.iter().enumerate() {
let it = &items[orig_idx];
let main_size = line_main_sizes[j];
let item_cross = cross_of(it).max(0.0);
let (cross_size, cross_off) = match self.align {
AlignItems::Stretch => (line_cross, 0.0),
AlignItems::Start => (item_cross, 0.0),
AlignItems::Center => (item_cross, (line_cross - item_cross) * 0.5),
AlignItems::End => (item_cross, line_cross - item_cross),
};
let rect = if is_row {
Rect::new(
container.left() + main_cursor,
container.top() + cross_start + cross_off,
main_size,
cross_size,
)
} else {
Rect::new(
container.left() + cross_start + cross_off,
container.top() + main_cursor,
cross_size,
main_size,
)
};
rects_by_index[orig_idx] = rect;
main_cursor += main_size;
if j + 1 < line.len() {
main_cursor += main_between;
}
}
}
rects_by_index
}
fn resolve_main_sizes(&self, line_items: &[FlexItem], main_extent: f32) -> Vec<f32> {
let is_row = self.direction == FlexDirection::Row;
let main_of = |it: &FlexItem| {
if is_row {
it.basis.width
} else {
it.basis.height
}
};
let total_basis: f32 = line_items.iter().map(main_of).sum();
let total_gap = self.gap * (line_items.len().saturating_sub(1)) as f32;
let total_grow: f32 = line_items.iter().map(|it| it.grow.max(0.0)).sum();
let free = (main_extent - total_basis - total_gap).max(0.0);
line_items
.iter()
.map(|it| {
let extra = if total_grow > 0.0 {
free * (it.grow.max(0.0) / total_grow)
} else {
0.0
};
(main_of(it) + extra).max(0.0)
})
.collect()
}
fn justify_offsets(&self, main_sizes: &[f32], main_extent: f32) -> (f32, f32) {
let total_gap = self.gap * (main_sizes.len().saturating_sub(1)) as f32;
let used: f32 = main_sizes.iter().sum::<f32>() + total_gap;
let leftover = (main_extent - used).max(0.0);
let n = main_sizes.len() as f32;
if leftover < 1e-4 {
return (0.0, self.gap);
}
match self.justify {
JustifyContent::Start => (0.0, self.gap),
JustifyContent::Center => (leftover * 0.5, self.gap),
JustifyContent::End => (leftover, self.gap),
JustifyContent::SpaceBetween => {
if main_sizes.len() == 1 {
(0.0, self.gap)
} else {
(0.0, self.gap + leftover / (n - 1.0))
}
}
JustifyContent::SpaceAround => {
let unit = leftover / n;
(unit * 0.5, self.gap + unit)
}
JustifyContent::SpaceEvenly => {
let unit = leftover / (n + 1.0);
(unit, self.gap + unit)
}
}
}
}
pub struct LayoutTask {
pub layout: FlexLayout,
pub container: Rect,
pub items: Vec<FlexItem>,
}
pub fn layout_subtrees_parallel(tasks: &[LayoutTask]) -> Vec<Vec<Rect>> {
tasks
.par_iter()
.map(|task| task.layout.layout(task.container, &task.items))
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::geometry::{Rect, Size};
fn approx(a: f32, b: f32) -> bool {
(a - b).abs() < 0.5
}
fn close(a: f32, b: f32) -> bool {
(a - b).abs() < 0.01
}
#[test]
fn row_start_no_grow() {
let l = FlexLayout::row();
let items = [
FlexItem::fixed(Size::new(20.0, 10.0)),
FlexItem::fixed(Size::new(30.0, 10.0)),
];
let rects = l.layout(Rect::new(0.0, 0.0, 100.0, 40.0), &items);
assert_eq!(rects.len(), 2);
assert!(approx(rects[0].left(), 0.0));
assert!(approx(rects[0].width(), 20.0));
assert!(approx(rects[1].left(), 20.0));
assert!(approx(rects[1].width(), 30.0));
}
#[test]
fn row_grow_fills_container() {
let l = FlexLayout::row();
let items = [
FlexItem::flexible(Size::new(0.0, 10.0)),
FlexItem::flexible(Size::new(0.0, 10.0)),
];
let rects = l.layout(Rect::new(0.0, 0.0, 100.0, 10.0), &items);
assert!(approx(rects[0].width(), 50.0));
assert!(approx(rects[1].width(), 50.0));
assert!(approx(rects[1].left(), 50.0));
}
#[test]
fn row_grow_with_gap() {
let l = FlexLayout::row().with_gap(10.0);
let items = [
FlexItem::flexible(Size::new(0.0, 10.0)),
FlexItem::flexible(Size::new(0.0, 10.0)),
];
let rects = l.layout(Rect::new(0.0, 0.0, 100.0, 10.0), &items);
assert!(approx(rects[0].width(), 45.0));
assert!(approx(rects[1].left(), 55.0));
assert!(approx(rects[1].width(), 45.0));
}
#[test]
fn justify_center() {
let l = FlexLayout::row().with_justify(JustifyContent::Center);
let items = [FlexItem::fixed(Size::new(40.0, 10.0))];
let rects = l.layout(Rect::new(0.0, 0.0, 100.0, 10.0), &items);
assert!(approx(rects[0].left(), 30.0));
}
#[test]
fn justify_space_between() {
let l = FlexLayout::row().with_justify(JustifyContent::SpaceBetween);
let items = [
FlexItem::fixed(Size::new(20.0, 10.0)),
FlexItem::fixed(Size::new(20.0, 10.0)),
FlexItem::fixed(Size::new(20.0, 10.0)),
];
let rects = l.layout(Rect::new(0.0, 0.0, 120.0, 10.0), &items);
assert!(approx(rects[0].left(), 0.0));
assert!(approx(rects[1].left(), 50.0));
assert!(approx(rects[2].left(), 100.0));
}
#[test]
fn justify_space_evenly() {
let l = FlexLayout::row().with_justify(JustifyContent::SpaceEvenly);
let items = [
FlexItem::fixed(Size::new(20.0, 10.0)),
FlexItem::fixed(Size::new(20.0, 10.0)),
];
let rects = l.layout(Rect::new(0.0, 0.0, 100.0, 10.0), &items);
assert!(approx(rects[0].left(), 20.0));
assert!(approx(rects[1].left(), 60.0));
}
#[test]
fn align_items_cross_axis() {
let l = FlexLayout::column().with_align(AlignItems::Center);
let items = [FlexItem::fixed(Size::new(40.0, 20.0))];
let rects = l.layout(Rect::new(0.0, 0.0, 100.0, 200.0), &items);
assert!(approx(rects[0].left(), 30.0));
assert!(approx(rects[0].width(), 40.0));
let stretch = FlexLayout::column().with_align(AlignItems::Stretch);
let r2 = stretch.layout(Rect::new(0.0, 0.0, 100.0, 200.0), &items);
assert!(approx(r2[0].width(), 100.0));
}
#[test]
fn empty_items_returns_empty() {
let l = FlexLayout::row();
assert!(l.layout(Rect::new(0.0, 0.0, 10.0, 10.0), &[]).is_empty());
}
#[test]
fn wrap_single_row_fits() {
let l = FlexLayout::row().with_wrap(FlexWrap::Wrap);
let items = [
FlexItem::fixed(Size::new(30.0, 10.0)),
FlexItem::fixed(Size::new(30.0, 10.0)),
];
let rects = l.layout(Rect::new(0.0, 0.0, 100.0, 40.0), &items);
assert_eq!(rects.len(), 2);
assert!(close(rects[0].top(), 0.0));
assert!(close(rects[1].top(), 0.0));
assert!(close(rects[0].left(), 0.0));
assert!(close(rects[1].left(), 30.0));
}
#[test]
fn wrap_three_items_two_lines() {
let l = FlexLayout::row().with_wrap(FlexWrap::Wrap);
let items = [
FlexItem::fixed(Size::new(40.0, 10.0)),
FlexItem::fixed(Size::new(40.0, 10.0)),
FlexItem::fixed(Size::new(40.0, 10.0)),
];
let rects = l.layout(Rect::new(0.0, 0.0, 90.0, 40.0), &items);
assert_eq!(rects.len(), 3);
assert!(close(rects[0].top(), 0.0), "item0 top={}", rects[0].top());
assert!(close(rects[1].top(), 0.0), "item1 top={}", rects[1].top());
assert!(approx(rects[2].top(), 10.0), "item2 top={}", rects[2].top());
}
#[test]
fn wrap_reverse_line_order() {
let l = FlexLayout::row().with_wrap(FlexWrap::WrapReverse);
let items = [
FlexItem::fixed(Size::new(60.0, 10.0)),
FlexItem::fixed(Size::new(60.0, 10.0)), ];
let rects = l.layout(Rect::new(0.0, 0.0, 80.0, 40.0), &items);
assert!(
rects[0].top() > rects[1].top(),
"item0.top={} item1.top={} — WrapReverse should put item1 above item0",
rects[0].top(),
rects[1].top()
);
}
#[test]
fn align_content_center_two_lines() {
let l = FlexLayout::row()
.with_wrap(FlexWrap::Wrap)
.with_align_content(AlignContent::Center);
let items = [
FlexItem::fixed(Size::new(60.0, 10.0)),
FlexItem::fixed(Size::new(60.0, 10.0)),
];
let rects = l.layout(Rect::new(0.0, 0.0, 80.0, 60.0), &items);
assert!(
rects[0].top() > 5.0,
"line1 should be offset from top: top={}",
rects[0].top()
);
assert!(rects[1].top() > rects[0].top(), "line2 below line1");
}
#[test]
fn align_content_space_between() {
let l = FlexLayout::row()
.with_wrap(FlexWrap::Wrap)
.with_align_content(AlignContent::SpaceBetween);
let items = [
FlexItem::fixed(Size::new(60.0, 10.0)),
FlexItem::fixed(Size::new(60.0, 10.0)),
];
let rects = l.layout(Rect::new(0.0, 0.0, 80.0, 60.0), &items);
assert!(close(rects[0].top(), 0.0), "line1 top={}", rects[0].top());
assert!(approx(rects[1].top(), 50.0), "line2 top={}", rects[1].top());
}
#[test]
fn align_content_space_around() {
let l = FlexLayout::row()
.with_wrap(FlexWrap::Wrap)
.with_align_content(AlignContent::SpaceAround);
let items = [
FlexItem::fixed(Size::new(60.0, 10.0)),
FlexItem::fixed(Size::new(60.0, 10.0)),
];
let rects = l.layout(Rect::new(0.0, 0.0, 80.0, 60.0), &items);
assert!(approx(rects[0].top(), 10.0), "line1 top={}", rects[0].top());
assert!(approx(rects[1].top(), 40.0), "line2 top={}", rects[1].top());
}
#[test]
fn align_content_space_evenly() {
let l = FlexLayout::row()
.with_wrap(FlexWrap::Wrap)
.with_align_content(AlignContent::SpaceEvenly);
let items = [
FlexItem::fixed(Size::new(60.0, 10.0)),
FlexItem::fixed(Size::new(60.0, 10.0)),
];
let rects = l.layout(Rect::new(0.0, 0.0, 80.0, 60.0), &items);
let unit = 40.0 / 3.0;
assert!(
approx(rects[0].top(), unit),
"line1 top={} unit={unit}",
rects[0].top()
);
assert!(
approx(rects[1].top(), unit + 10.0 + unit),
"line2 top={}",
rects[1].top()
);
}
#[test]
fn align_content_stretch() {
let l = FlexLayout::row()
.with_wrap(FlexWrap::Wrap)
.with_align_content(AlignContent::Stretch)
.with_align(AlignItems::Stretch);
let items = [
FlexItem::fixed(Size::new(60.0, 10.0)),
FlexItem::fixed(Size::new(60.0, 10.0)),
];
let rects = l.layout(Rect::new(0.0, 0.0, 80.0, 60.0), &items);
assert!(close(rects[0].top(), 0.0));
assert!(approx(rects[0].height(), 30.0), "h={}", rects[0].height());
assert!(approx(rects[1].top(), 30.0), "top={}", rects[1].top());
assert!(approx(rects[1].height(), 30.0), "h={}", rects[1].height());
}
#[test]
fn wrap_oversized_item_own_line() {
let l = FlexLayout::row().with_wrap(FlexWrap::Wrap);
let items = [
FlexItem::fixed(Size::new(200.0, 10.0)), FlexItem::fixed(Size::new(30.0, 10.0)),
];
let rects = l.layout(Rect::new(0.0, 0.0, 80.0, 40.0), &items);
assert_eq!(rects.len(), 2);
assert!(
rects[1].top() > rects[0].top(),
"item1 should be below oversized item0"
);
}
#[test]
fn wrap_zero_gap() {
let l = FlexLayout::row().with_wrap(FlexWrap::Wrap).with_gap(0.0);
let items = [
FlexItem::fixed(Size::new(50.0, 10.0)),
FlexItem::fixed(Size::new(50.0, 10.0)),
FlexItem::fixed(Size::new(50.0, 10.0)),
];
let rects = l.layout(Rect::new(0.0, 0.0, 80.0, 40.0), &items);
assert!(rects[1].top() > rects[0].top(), "item1 below item0");
}
#[test]
fn wrap_column_direction() {
let l = FlexLayout::column().with_wrap(FlexWrap::Wrap);
let items = [
FlexItem::fixed(Size::new(10.0, 60.0)),
FlexItem::fixed(Size::new(10.0, 60.0)), ];
let rects = l.layout(Rect::new(0.0, 0.0, 40.0, 80.0), &items);
assert!(
rects[1].left() > rects[0].left(),
"column wrap: item1 should be in next column; item0.left={} item1.left={}",
rects[0].left(),
rects[1].left()
);
}
#[test]
fn wrap_with_justify_space_between_per_line() {
let l = FlexLayout::row()
.with_wrap(FlexWrap::Wrap)
.with_justify(JustifyContent::SpaceBetween);
let items = [
FlexItem::fixed(Size::new(20.0, 10.0)),
FlexItem::fixed(Size::new(20.0, 10.0)),
FlexItem::fixed(Size::new(20.0, 10.0)),
FlexItem::fixed(Size::new(20.0, 10.0)),
];
let rects = l.layout(Rect::new(0.0, 0.0, 100.0, 40.0), &items);
assert_eq!(rects.len(), 4);
assert!(close(rects[0].left(), 0.0));
assert!(approx(rects[3].left() + rects[3].width(), 100.0));
}
#[test]
fn wrap_exact_boundary() {
let l = FlexLayout::row().with_wrap(FlexWrap::Wrap);
let items = [
FlexItem::fixed(Size::new(30.0, 10.0)),
FlexItem::fixed(Size::new(30.0, 10.0)),
FlexItem::fixed(Size::new(30.0, 10.0)),
];
let rects = l.layout(Rect::new(0.0, 0.0, 90.0, 20.0), &items);
assert!(close(rects[0].top(), rects[1].top()));
assert!(close(rects[1].top(), rects[2].top()));
}
#[test]
fn wrap_with_flex_grow() {
let l = FlexLayout::row().with_wrap(FlexWrap::Wrap);
let items = [
FlexItem::flexible(Size::new(20.0, 10.0)), FlexItem::fixed(Size::new(80.0, 10.0)), ];
let rects = l.layout(Rect::new(0.0, 0.0, 100.0, 20.0), &items);
assert_eq!(rects.len(), 2);
assert!(close(rects[0].top(), rects[1].top()));
}
#[test]
fn wrap_empty_items() {
let l = FlexLayout::row().with_wrap(FlexWrap::Wrap);
let rects = l.layout(Rect::new(0.0, 0.0, 100.0, 100.0), &[]);
assert!(rects.is_empty());
}
#[test]
fn wrap_single_item() {
let l = FlexLayout::row().with_wrap(FlexWrap::Wrap);
let items = [FlexItem::fixed(Size::new(40.0, 20.0))];
let rects = l.layout(Rect::new(0.0, 0.0, 100.0, 40.0), &items);
assert_eq!(rects.len(), 1);
assert!(close(rects[0].left(), 0.0));
assert!(close(rects[0].top(), 0.0));
assert!(close(rects[0].width(), 40.0));
}
#[test]
fn wrap_large_gap() {
let l = FlexLayout::row().with_wrap(FlexWrap::Wrap).with_gap(30.0);
let items = [
FlexItem::fixed(Size::new(30.0, 10.0)),
FlexItem::fixed(Size::new(30.0, 10.0)),
];
let rects = l.layout(Rect::new(0.0, 0.0, 80.0, 40.0), &items);
assert!(
rects[1].top() > rects[0].top(),
"item1 should be on second line"
);
}
#[test]
fn wrap_reverse_align_content_end() {
let l = FlexLayout::row()
.with_wrap(FlexWrap::WrapReverse)
.with_align_content(AlignContent::End);
let items = [
FlexItem::fixed(Size::new(60.0, 10.0)),
FlexItem::fixed(Size::new(60.0, 10.0)),
];
let rects = l.layout(Rect::new(0.0, 0.0, 80.0, 60.0), &items);
let max_top = rects.iter().map(|r| r.top()).fold(0.0_f32, f32::max);
assert!(
max_top > 30.0,
"lines should be packed toward the end, max_top={max_top}"
);
}
#[test]
fn wrap_align_items_center_per_line() {
let l = FlexLayout::row()
.with_wrap(FlexWrap::Wrap)
.with_align(AlignItems::Center);
let items = [
FlexItem::fixed(Size::new(60.0, 5.0)), FlexItem::fixed(Size::new(60.0, 15.0)), ];
let rects = l.layout(Rect::new(0.0, 0.0, 80.0, 40.0), &items);
assert!(
close(rects[0].height(), 5.0),
"item0 h={}",
rects[0].height()
);
assert!(
close(rects[1].height(), 15.0),
"item1 h={}",
rects[1].height()
);
}
#[test]
fn wrap_output_preserves_original_order() {
let l = FlexLayout::row().with_wrap(FlexWrap::Wrap);
let items = [
FlexItem::fixed(Size::new(70.0, 10.0)), FlexItem::fixed(Size::new(70.0, 10.0)), FlexItem::fixed(Size::new(70.0, 10.0)), ];
let rects = l.layout(Rect::new(0.0, 0.0, 80.0, 60.0), &items);
assert_eq!(rects.len(), 3);
assert!(rects[0].top() < rects[1].top(), "idx0 above idx1");
assert!(rects[1].top() < rects[2].top(), "idx1 above idx2");
}
#[test]
fn wrap_reverse_unequal_cross_sizes() {
let l = FlexLayout::row()
.with_wrap(FlexWrap::WrapReverse)
.with_align(AlignItems::Start); let items = [
FlexItem::fixed(Size::new(60.0, 10.0)), FlexItem::fixed(Size::new(60.0, 30.0)), ];
let rects = l.layout(Rect::new(0.0, 0.0, 80.0, 60.0), &items);
assert_eq!(rects.len(), 2);
let top1 = rects[1].top(); let top0 = rects[0].top();
assert!(top1 < top0,
"WrapReverse: item1 (30px cross, display-first) top={top1} should be < item0 top={top0}");
let bottom1 = top1 + rects[1].height();
assert!(
top0 >= bottom1 - 1e-3,
"no overlap: item0.top={top0} must be >= item1.bottom={bottom1}"
);
assert!(
close(rects[1].height(), 30.0),
"item1 height={}",
rects[1].height()
);
assert!(
close(rects[0].height(), 10.0),
"item0 height={}",
rects[0].height()
);
}
#[test]
fn parallel_layout_matches_sequential() {
let tasks: Vec<LayoutTask> = (0..8_u32)
.map(|i| LayoutTask {
layout: FlexLayout::row(),
container: Rect::new(0.0, 0.0, 400.0, 40.0),
items: vec![
FlexItem::fixed(Size::new(100.0, 40.0)),
FlexItem::flexible(Size::new(50.0 + i as f32, 40.0)),
],
})
.collect();
let parallel_results = layout_subtrees_parallel(&tasks);
assert_eq!(parallel_results.len(), 8);
for (task, par_rects) in tasks.iter().zip(parallel_results.iter()) {
let seq_rects = task.layout.layout(task.container, &task.items);
assert_eq!(seq_rects.len(), par_rects.len());
for (sr, pr) in seq_rects.iter().zip(par_rects.iter()) {
assert!(
close(sr.left(), pr.left()) && close(sr.width(), pr.width()),
"parallel and sequential results diverge"
);
}
}
}
#[test]
fn parallel_layout_empty_tasks() {
let results = layout_subtrees_parallel(&[]);
assert!(results.is_empty());
}
#[test]
fn parallel_layout_single_empty_items() {
let tasks = [LayoutTask {
layout: FlexLayout::column(),
container: Rect::new(0.0, 0.0, 200.0, 200.0),
items: vec![],
}];
let results = layout_subtrees_parallel(&tasks);
assert_eq!(results.len(), 1);
assert!(results[0].is_empty());
}
#[test]
fn parallel_layout_large_batch() {
let tasks: Vec<LayoutTask> = (0..64)
.map(|_| LayoutTask {
layout: FlexLayout::column(),
container: Rect::new(0.0, 0.0, 100.0, 150.0),
items: vec![
FlexItem::fixed(Size::new(100.0, 30.0)),
FlexItem::flexible(Size::new(100.0, 20.0)),
FlexItem::fixed(Size::new(100.0, 30.0)),
],
})
.collect();
let results = layout_subtrees_parallel(&tasks);
assert_eq!(results.len(), 64);
for rects in &results {
assert_eq!(rects.len(), 3);
assert!(rects[0].top() <= rects[1].top());
assert!(rects[1].top() <= rects[2].top());
}
}
}