use super::{Layout, LayoutContext};
use crate::core::{ObjectId, Rect, Size};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum FlexDirection {
#[default]
Row,
RowReverse,
Column,
ColumnReverse,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum FlexWrap {
#[default]
NoWrap,
Wrap,
WrapReverse,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum JustifyContent {
#[default]
FlexStart,
FlexEnd,
Center,
SpaceBetween,
SpaceAround,
SpaceEvenly,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum AlignItems {
#[default]
Stretch,
FlexStart,
FlexEnd,
Center,
Baseline,
}
#[derive(Debug, Clone)]
pub struct FlexItem {
pub widget_id: Option<ObjectId>,
pub flex_grow: f32,
pub flex_shrink: f32,
pub align_self: Option<AlignItems>,
pub min_size: Size,
pub max_size: Size,
}
impl Default for FlexItem {
fn default() -> Self {
Self {
widget_id: None,
flex_grow: 0.0,
flex_shrink: 1.0,
align_self: None,
min_size: Size::new(0, 0),
max_size: Size::new(0, 0),
}
}
}
#[derive(Debug)]
pub struct FlexLayout {
pub direction: FlexDirection,
pub wrap: FlexWrap,
pub justify_content: JustifyContent,
pub align_items: AlignItems,
pub gap: i32,
pub padding: i32,
items: Vec<FlexItem>,
child_sizes: Vec<Size>,
}
impl FlexLayout {
pub fn new() -> Self {
Self {
direction: FlexDirection::default(),
wrap: FlexWrap::default(),
justify_content: JustifyContent::default(),
align_items: AlignItems::default(),
gap: 0,
padding: 0,
items: Vec::new(),
child_sizes: Vec::new(),
}
}
#[allow(clippy::too_many_arguments)]
pub fn with_params(
direction: FlexDirection,
wrap: FlexWrap,
justify_content: JustifyContent,
align_items: AlignItems,
gap: i32,
padding: i32,
) -> Self {
Self {
direction,
wrap,
justify_content,
align_items,
gap,
padding,
items: Vec::new(),
child_sizes: Vec::new(),
}
}
pub fn items(&self) -> &[FlexItem] {
&self.items
}
pub fn items_mut(&mut self) -> &mut Vec<FlexItem> {
&mut self.items
}
pub fn set_child_sizes(&mut self, sizes: Vec<Size>) {
self.child_sizes = sizes;
}
pub fn item_count(&self) -> usize {
self.items.len()
}
fn is_row(&self) -> bool {
matches!(self.direction, FlexDirection::Row | FlexDirection::RowReverse)
}
fn is_reverse(&self) -> bool {
matches!(self.direction, FlexDirection::RowReverse | FlexDirection::ColumnReverse)
}
fn compute_main_sizes(&self, available_main: i32) -> (Vec<i32>, f32, i32) {
let count = self.items.len();
if count == 0 {
return (Vec::new(), 0.0, 0);
}
let mut intrinsic_main: Vec<i32> = Vec::with_capacity(count);
let mut total_flex_grow: f32 = 0.0;
let mut total_intrinsic: i32 = 0;
for (i, item) in self.items.iter().enumerate() {
let sz = self.child_sizes.get(i).copied().unwrap_or(Size::new(0, 0));
let main = if self.is_row() { sz.width as i32 } else { sz.height as i32 };
let main = main.max(if self.is_row() {
item.min_size.width as i32
} else {
item.min_size.height as i32
});
intrinsic_main.push(main);
total_flex_grow += item.flex_grow;
total_intrinsic += main;
}
let gaps = (count.saturating_sub(1)) as i32 * self.gap;
let remaining = available_main - total_intrinsic - gaps;
let mut main_sizes: Vec<i32> = Vec::with_capacity(count);
if remaining > 0 && total_flex_grow > 0.0 {
let mut distributed = 0i32;
for (i, item) in self.items.iter().enumerate() {
let extra = if total_flex_grow > 0.0 {
((remaining as f32) * (item.flex_grow / total_flex_grow)).round() as i32
} else {
0
};
let size = intrinsic_main[i] + extra;
let max_main = if self.is_row() {
if item.max_size.width > 0 {
item.max_size.width as i32
} else {
i32::MAX
}
} else {
if item.max_size.height > 0 {
item.max_size.height as i32
} else {
i32::MAX
}
};
let size = size.min(max_main);
main_sizes.push(size);
distributed += size - intrinsic_main[i];
}
let leftover = remaining - distributed;
if leftover > 0 && !main_sizes.is_empty() {
main_sizes[count - 1] += leftover;
}
} else if remaining < 0 {
let deficit = -remaining;
let total_flex_shrink: f32 = self.items.iter().map(|i| i.flex_shrink).sum();
for (i, item) in self.items.iter().enumerate() {
let shrink = if total_flex_shrink > 0.0 {
((deficit as f32) * (item.flex_shrink / total_flex_shrink)).round() as i32
} else {
deficit / count as i32
};
let min_main = if self.is_row() {
item.min_size.width as i32
} else {
item.min_size.height as i32
};
let size = (intrinsic_main[i] - shrink).max(min_main);
main_sizes.push(size);
}
let actual_shrunk = total_intrinsic - main_sizes.iter().sum::<i32>() - gaps;
if actual_shrunk < deficit {
let remaining_deficit = deficit - actual_shrunk;
for s in main_sizes.iter_mut().rev() {
if remaining_deficit <= 0 {
break;
}
let possible = *s;
let cut = possible.min(remaining_deficit);
*s -= cut;
}
}
} else {
main_sizes = intrinsic_main;
}
let total_main: i32 = main_sizes.iter().sum::<i32>() + gaps;
(main_sizes, total_flex_grow, total_main)
}
fn justify_positions(
&self,
main_sizes: &[i32],
total_used: i32,
available_main: i32,
start_main: i32,
) -> Vec<i32> {
let count = main_sizes.len();
if count == 0 {
return Vec::new();
}
let leftover = available_main - total_used;
let mut positions = Vec::with_capacity(count);
let (first_gap, inter_gap) = match self.justify_content {
JustifyContent::FlexStart | JustifyContent::FlexEnd | JustifyContent::Center => {
let offset = match self.justify_content {
JustifyContent::FlexStart => 0,
JustifyContent::FlexEnd => leftover,
JustifyContent::Center => leftover / 2,
_ => 0,
};
(offset, self.gap)
}
JustifyContent::SpaceBetween => {
let gap = if count > 1 { leftover / (count as i32 - 1) } else { 0 };
(0, gap)
}
JustifyContent::SpaceAround => {
let gap = if count > 0 { leftover / (count as i32) } else { 0 };
(gap / 2, gap)
}
JustifyContent::SpaceEvenly => {
let gap = if count > 0 { leftover / (count as i32 + 1) } else { 0 };
(gap, gap)
}
};
let end = start_main + available_main;
if self.is_reverse() {
let mut cursor = end - first_gap;
for i in 0..count {
let pos = cursor - main_sizes[i];
positions.push(pos);
cursor = pos - inter_gap;
}
} else {
let mut cursor = start_main + first_gap;
for i in 0..count {
positions.push(cursor);
cursor += main_sizes[i] + inter_gap;
}
}
positions
}
fn compute_cross_positions(&self, main_sizes: &[i32], cross_size: i32) -> Vec<(i32, i32)> {
let count = main_sizes.len();
if count == 0 {
return Vec::new();
}
let mut result = Vec::with_capacity(count);
for (i, _item) in self.items.iter().enumerate() {
let sz = self.child_sizes.get(i).copied().unwrap_or(Size::new(0, 0));
let child_cross = if self.is_row() { sz.height as i32 } else { sz.width as i32 };
let align = self.items[i].align_self.unwrap_or(self.align_items);
let (cross_start, cross_len) = match align {
AlignItems::Stretch => (0, cross_size),
AlignItems::FlexStart => (0, child_cross),
AlignItems::FlexEnd => (cross_size - child_cross, child_cross),
AlignItems::Center => ((cross_size - child_cross) / 2, child_cross),
AlignItems::Baseline => (0, child_cross),
};
result.push((cross_start, cross_len));
}
result
}
fn compute_rects(&self, content_rect: Rect) -> Vec<(Option<ObjectId>, Rect)> {
if self.items.is_empty() {
return Vec::new();
}
let (available_main, start_main, available_cross, cross_origin) = if self.is_row() {
(content_rect.width as i32, content_rect.x, content_rect.height as i32, content_rect.y)
} else {
(content_rect.height as i32, content_rect.y, content_rect.width as i32, content_rect.x)
};
if available_main <= 0 || available_cross <= 0 {
return Vec::new();
}
let (main_sizes, _total_flex_grow, total_used) = self.compute_main_sizes(available_main);
let main_positions =
self.justify_positions(&main_sizes, total_used, available_main, start_main);
let cross_positions = self.compute_cross_positions(&main_sizes, available_cross);
let mut results = Vec::with_capacity(self.items.len());
for (i, item) in self.items.iter().enumerate() {
let main_pos = *main_positions.get(i).unwrap_or(&0);
let (cross_pos, cross_len) =
cross_positions.get(i).copied().unwrap_or((0, available_cross));
let main_len = *main_sizes.get(i).unwrap_or(&0);
let rect = if self.is_row() {
Rect::new(main_pos, cross_origin + cross_pos, main_len as u32, cross_len as u32)
} else {
Rect::new(cross_origin + cross_pos, main_pos, cross_len as u32, main_len as u32)
};
results.push((item.widget_id, rect));
}
results
}
}
impl Default for FlexLayout {
fn default() -> Self {
Self::new()
}
}
impl Layout for FlexLayout {
fn as_any(&self) -> &dyn std::any::Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
self
}
fn add_widget(&mut self, widget_id: ObjectId, stretch: u32) {
self.items.push(FlexItem {
widget_id: Some(widget_id),
flex_grow: stretch as f32,
..FlexItem::default()
});
}
fn remove_widget(&mut self, widget_id: ObjectId) {
self.items.retain(|item| item.widget_id != Some(widget_id));
}
fn child_ids(&self) -> Vec<ObjectId> {
self.items.iter().filter_map(|item| item.widget_id).collect()
}
fn has_child(&self, id: ObjectId) -> bool {
self.items.iter().any(|item| item.widget_id == Some(id))
}
fn clear(&mut self) {
self.items.clear();
self.child_sizes.clear();
}
fn update(&self, rect: Rect, widgets: &mut dyn FnMut(ObjectId, Rect)) {
let content_rect = Rect::new(
rect.x + self.padding,
rect.y + self.padding,
rect.width.saturating_sub(2 * self.padding as u32),
rect.height.saturating_sub(2 * self.padding as u32),
);
let results = self.compute_rects(content_rect);
for (widget_id, child_rect) in results {
if let Some(wid) = widget_id {
widgets(wid, child_rect);
}
}
}
fn update_with_context(
&self,
rect: Rect,
context: &LayoutContext,
widgets: &mut dyn FnMut(ObjectId, Rect),
) {
let scale = context.layout_scale;
let scaled_padding = (self.padding as f32 * scale) as i32;
let content_rect = Rect::new(
rect.x + scaled_padding,
rect.y + scaled_padding,
rect.width.saturating_sub(2 * scaled_padding as u32),
rect.height.saturating_sub(2 * scaled_padding as u32),
);
let results = self.compute_rects(content_rect);
for (widget_id, child_rect) in results {
if let Some(wid) = widget_id {
widgets(wid, child_rect);
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn flex_layout_default_creates_empty() {
let layout = FlexLayout::new();
assert_eq!(layout.item_count(), 0);
}
#[test]
fn flex_layout_add_and_remove_widget() {
let mut layout = FlexLayout::new();
layout.add_widget(1, 1);
layout.add_widget(2, 2);
assert_eq!(layout.item_count(), 2);
assert!(layout.has_child(1));
assert!(layout.has_child(2));
layout.remove_widget(1);
assert_eq!(layout.item_count(), 1);
assert!(!layout.has_child(1));
assert!(layout.has_child(2));
}
#[test]
fn flex_layout_child_ids() {
let mut layout = FlexLayout::new();
layout.add_widget(10, 0);
layout.add_widget(20, 0);
let ids = layout.child_ids();
assert_eq!(ids.len(), 2);
assert!(ids.contains(&10));
assert!(ids.contains(&20));
}
#[test]
fn flex_layout_clear() {
let mut layout = FlexLayout::new();
layout.add_widget(1, 1);
layout.add_widget(2, 1);
assert_eq!(layout.item_count(), 2);
layout.clear();
assert_eq!(layout.item_count(), 0);
}
#[test]
fn flex_layout_distributes_evenly_with_equal_grow() {
let mut layout = FlexLayout::with_params(
FlexDirection::Row,
FlexWrap::NoWrap,
JustifyContent::FlexStart,
AlignItems::Stretch,
0,
0,
);
layout.add_widget(1, 1);
layout.add_widget(2, 1);
let mut rects = std::collections::HashMap::new();
layout.set_child_sizes(vec![Size::new(0, 0), Size::new(0, 0)]);
layout.update(Rect::new(0, 0, 200, 50), &mut |id, rect| {
rects.insert(id, rect);
});
assert_eq!(rects.get(&1).map(|r| r.width), Some(100));
assert_eq!(rects.get(&2).map(|r| r.width), Some(100));
assert_eq!(rects.get(&1).map(|r| r.height), Some(50));
assert_eq!(rects.get(&2).map(|r| r.height), Some(50));
}
#[test]
fn flex_layout_uneven_grow() {
let mut layout = FlexLayout::with_params(
FlexDirection::Row,
FlexWrap::NoWrap,
JustifyContent::FlexStart,
AlignItems::Stretch,
0,
0,
);
layout.add_widget(1, 1);
layout.add_widget(2, 3);
let mut rects = std::collections::HashMap::new();
layout.set_child_sizes(vec![Size::new(0, 0), Size::new(0, 0)]);
layout.update(Rect::new(0, 0, 200, 50), &mut |id, rect| {
rects.insert(id, rect);
});
assert_eq!(rects.get(&1).map(|r| r.width), Some(50));
assert_eq!(rects.get(&2).map(|r| r.width), Some(150));
}
#[test]
fn flex_layout_column_direction() {
let mut layout = FlexLayout::with_params(
FlexDirection::Column,
FlexWrap::NoWrap,
JustifyContent::FlexStart,
AlignItems::Stretch,
0,
0,
);
layout.add_widget(1, 1);
layout.add_widget(2, 1);
let mut rects = std::collections::HashMap::new();
layout.set_child_sizes(vec![Size::new(0, 0), Size::new(0, 0)]);
layout.update(Rect::new(0, 0, 100, 200), &mut |id, rect| {
rects.insert(id, rect);
});
assert_eq!(rects.get(&1).map(|r| r.height), Some(100));
assert_eq!(rects.get(&2).map(|r| r.height), Some(100));
assert_eq!(rects.get(&1).map(|r| r.width), Some(100));
assert_eq!(rects.get(&2).map(|r| r.width), Some(100));
}
#[test]
fn flex_layout_justify_center() {
let mut layout = FlexLayout::with_params(
FlexDirection::Row,
FlexWrap::NoWrap,
JustifyContent::Center,
AlignItems::Stretch,
0,
0,
);
layout.add_widget(1, 0);
layout.add_widget(2, 0);
let mut rects = std::collections::HashMap::new();
layout.set_child_sizes(vec![Size::new(30, 20), Size::new(30, 20)]);
layout.update(Rect::new(0, 0, 100, 50), &mut |id, rect| {
rects.insert(id, rect);
});
let r1 = rects.get(&1).unwrap();
let r2 = rects.get(&2).unwrap();
assert_eq!(r1.x, 20);
assert_eq!(r2.x, 50);
}
#[test]
fn flex_layout_justify_space_between() {
let mut layout = FlexLayout::with_params(
FlexDirection::Row,
FlexWrap::NoWrap,
JustifyContent::SpaceBetween,
AlignItems::Stretch,
0,
0,
);
layout.add_widget(1, 0);
layout.add_widget(2, 0);
let mut rects = std::collections::HashMap::new();
layout.set_child_sizes(vec![Size::new(20, 10), Size::new(20, 10)]);
layout.update(Rect::new(0, 0, 100, 50), &mut |id, rect| {
rects.insert(id, rect);
});
assert_eq!(rects.get(&1).map(|r| r.x), Some(0));
assert_eq!(rects.get(&2).map(|r| r.x), Some(80));
}
#[test]
fn flex_layout_padding_applied() {
let mut layout = FlexLayout::with_params(
FlexDirection::Row,
FlexWrap::NoWrap,
JustifyContent::FlexStart,
AlignItems::Stretch,
0,
10,
);
layout.add_widget(1, 1);
let mut rects = std::collections::HashMap::new();
layout.set_child_sizes(vec![Size::new(0, 0)]);
layout.update(Rect::new(0, 0, 200, 60), &mut |id, rect| {
rects.insert(id, rect);
});
assert_eq!(rects.get(&1).map(|r| r.x), Some(10));
assert_eq!(rects.get(&1).map(|r| r.y), Some(10));
assert_eq!(rects.get(&1).map(|r| r.width), Some(180));
assert_eq!(rects.get(&1).map(|r| r.height), Some(40));
}
#[test]
fn flex_layout_gap_between_items() {
let mut layout = FlexLayout::with_params(
FlexDirection::Row,
FlexWrap::NoWrap,
JustifyContent::FlexStart,
AlignItems::Stretch,
10,
0,
);
layout.add_widget(1, 0);
layout.add_widget(2, 0);
let mut rects = std::collections::HashMap::new();
layout.set_child_sizes(vec![Size::new(30, 20), Size::new(30, 20)]);
layout.update(Rect::new(0, 0, 100, 50), &mut |id, rect| {
rects.insert(id, rect);
});
assert_eq!(rects.get(&1).map(|r| r.x), Some(0));
assert_eq!(rects.get(&2).map(|r| r.x), Some(40));
}
#[test]
fn flex_layout_min_size_constraint() {
let mut layout = FlexLayout::with_params(
FlexDirection::Row,
FlexWrap::NoWrap,
JustifyContent::FlexStart,
AlignItems::Stretch,
0,
0,
);
layout.add_widget(1, 0);
if let Some(item) = layout.items_mut().last_mut() {
item.min_size = Size::new(50, 0);
}
let mut rects = std::collections::HashMap::new();
layout.set_child_sizes(vec![Size::new(10, 20)]);
layout.update(Rect::new(0, 0, 100, 50), &mut |id, rect| {
rects.insert(id, rect);
});
assert_eq!(rects.get(&1).map(|r| r.width), Some(50));
}
#[test]
fn flex_layout_align_items_flex_end() {
let mut layout = FlexLayout::with_params(
FlexDirection::Row,
FlexWrap::NoWrap,
JustifyContent::FlexStart,
AlignItems::FlexEnd,
0,
0,
);
layout.add_widget(1, 0);
let mut rects = std::collections::HashMap::new();
layout.set_child_sizes(vec![Size::new(30, 20)]);
layout.update(Rect::new(0, 0, 100, 100), &mut |id, rect| {
rects.insert(id, rect);
});
assert_eq!(rects.get(&1).map(|r| r.y), Some(80));
}
#[test]
fn flex_layout_align_items_center() {
let mut layout = FlexLayout::with_params(
FlexDirection::Row,
FlexWrap::NoWrap,
JustifyContent::FlexStart,
AlignItems::Center,
0,
0,
);
layout.add_widget(1, 0);
let mut rects = std::collections::HashMap::new();
layout.set_child_sizes(vec![Size::new(30, 20)]);
layout.update(Rect::new(0, 0, 100, 100), &mut |id, rect| {
rects.insert(id, rect);
});
assert_eq!(rects.get(&1).map(|r| r.y), Some(40));
}
#[test]
fn flex_layout_align_self_overrides_align_items() {
let mut layout = FlexLayout::with_params(
FlexDirection::Row,
FlexWrap::NoWrap,
JustifyContent::FlexStart,
AlignItems::FlexStart,
0,
0,
);
layout.add_widget(1, 0);
layout.add_widget(2, 0);
if let Some(item) = layout.items_mut().get_mut(1) {
item.align_self = Some(AlignItems::Center);
}
let mut rects = std::collections::HashMap::new();
layout.set_child_sizes(vec![Size::new(30, 20), Size::new(30, 20)]);
layout.update(Rect::new(0, 0, 100, 100), &mut |id, rect| {
rects.insert(id, rect);
});
assert_eq!(rects.get(&1).map(|r| r.y), Some(0));
assert_eq!(rects.get(&2).map(|r| r.y), Some(40));
}
#[test]
fn flex_layout_update_with_context_scales_gap_and_padding() {
let mut layout = FlexLayout::with_params(
FlexDirection::Row,
FlexWrap::NoWrap,
JustifyContent::FlexStart,
AlignItems::Stretch,
10,
10,
);
layout.add_widget(1, 1);
let context = LayoutContext { layout_scale: 2.0, ..LayoutContext::default() };
let mut rects = std::collections::HashMap::new();
layout.set_child_sizes(vec![Size::new(0, 0)]);
layout.update_with_context(Rect::new(0, 0, 200, 60), &context, &mut |id, rect| {
rects.insert(id, rect);
});
assert_eq!(rects.get(&1).map(|r| r.x), Some(20));
assert_eq!(rects.get(&1).map(|r| r.y), Some(20));
assert_eq!(rects.get(&1).map(|r| r.width), Some(160));
assert_eq!(rects.get(&1).map(|r| r.height), Some(20));
}
#[test]
fn flex_layout_row_reverse() {
let mut layout = FlexLayout::with_params(
FlexDirection::RowReverse,
FlexWrap::NoWrap,
JustifyContent::FlexStart,
AlignItems::Stretch,
0,
0,
);
layout.add_widget(1, 1);
layout.add_widget(2, 1);
let mut rects = std::collections::HashMap::new();
layout.set_child_sizes(vec![Size::new(0, 0), Size::new(0, 0)]);
layout.update(Rect::new(0, 0, 200, 50), &mut |id, rect| {
rects.insert(id, rect);
});
assert_eq!(rects.get(&1).map(|r| r.x), Some(100));
assert_eq!(rects.get(&2).map(|r| r.x), Some(0));
}
}