use super::{Axis, Constraints, CrossAlignment, Layout, MainAlignment, Size};
use crate::{
reactive::{IntoSignal, Prop, Signal},
tree::{LayoutCtx, Tree, WidgetId},
};
const MIN_VISIBLE_SIZE: f32 = 0.5;
pub struct Flex {
direction: Signal<Axis>,
spacing: Prop<f32>,
main_alignment: Prop<MainAlignment>,
cross_alignment: Prop<CrossAlignment>,
child_sizes: Vec<Size>,
fill_indices: Vec<usize>,
}
impl Flex {
pub fn new<M>(direction: impl IntoSignal<Axis, M>) -> Self {
Self {
direction: direction.into_signal(),
spacing: Prop::Unset,
main_alignment: Prop::Unset,
cross_alignment: Prop::Unset,
child_sizes: Vec::with_capacity(8),
fill_indices: Vec::new(),
}
}
pub fn row() -> Self {
Self::new(Axis::Horizontal)
}
pub fn column() -> Self {
Self::new(Axis::Vertical)
}
pub fn spacing<M>(mut self, spacing: impl IntoSignal<f32, M>) -> Self {
self.spacing = spacing.into_prop();
self
}
pub fn main_alignment<M>(mut self, alignment: impl IntoSignal<MainAlignment, M>) -> Self {
self.main_alignment = alignment.into_prop();
self
}
pub fn cross_alignment<M>(mut self, alignment: impl IntoSignal<CrossAlignment, M>) -> Self {
self.cross_alignment = alignment.into_prop();
self
}
pub fn center(self) -> Self {
self.main_alignment(MainAlignment::Center)
.cross_alignment(CrossAlignment::Center)
}
fn calc_main_axis_spacing(
&self,
main_align: MainAlignment,
spacing: f32,
free_space: f32,
child_count: usize,
) -> (f32, f32) {
match main_align {
MainAlignment::Start => (0.0, spacing),
MainAlignment::Center => (free_space / 2.0, spacing),
MainAlignment::End => (free_space, spacing),
MainAlignment::SpaceBetween => {
if child_count > 1 {
(0.0, free_space / (child_count - 1) as f32 + spacing)
} else {
(0.0, spacing)
}
}
MainAlignment::SpaceAround => {
let space = free_space / child_count as f32;
(space / 2.0, space + spacing)
}
MainAlignment::SpaceEvenly => {
let space = free_space / (child_count + 1) as f32;
(space, space + spacing)
}
}
}
fn layout_axis(
&mut self,
ctx: &mut LayoutCtx,
children: &[WidgetId],
constraints: Constraints,
origin: (f32, f32),
axis: Axis,
) -> Size {
let spacing = self.spacing.get_or(0.0);
let main_align = self.main_alignment.get_or(MainAlignment::Start);
let cross_align = self.cross_alignment.get_or(CrossAlignment::Stretch);
let (main_max, cross_min, cross_max) = match axis {
Axis::Horizontal => (
constraints.max_width,
constraints.min_height,
constraints.max_height,
),
Axis::Vertical => (
constraints.max_height,
constraints.min_width,
constraints.max_width,
),
};
let stretch_cross = if cross_align == CrossAlignment::Stretch && cross_min > 0.0 {
Some(cross_min)
} else {
None
};
let child_constraints = match axis {
Axis::Horizontal => Constraints {
min_width: 0.0,
min_height: stretch_cross.unwrap_or(0.0),
max_width: main_max,
max_height: stretch_cross.unwrap_or(cross_max),
},
Axis::Vertical => Constraints {
min_width: stretch_cross.unwrap_or(0.0),
min_height: 0.0,
max_width: stretch_cross.unwrap_or(cross_max),
max_height: main_max,
},
};
self.child_sizes.clear();
self.child_sizes.resize(children.len(), Size::zero());
let mut non_fill_main = 0.0f32;
let mut max_cross = 0.0f32;
self.fill_indices.clear();
for (i, &child_id) in children.iter().enumerate() {
let is_fill = ctx
.tree_ref()
.with_widget(child_id, |w| match axis {
Axis::Horizontal => w.layout_hints().fill_width,
Axis::Vertical => w.layout_hints().fill_height,
})
.unwrap_or(false);
if is_fill {
self.fill_indices.push(i);
} else if let Some(size) = ctx.layout_child(child_id, child_constraints) {
non_fill_main += size.main_axis(axis);
max_cross = max_cross.max(size.cross_axis(axis));
self.child_sizes[i] = size;
}
}
let total_spacing = if children.len() > 1 {
spacing * (children.len() - 1) as f32
} else {
0.0
};
let per_fill = if !self.fill_indices.is_empty() {
let remaining = (main_max - non_fill_main - total_spacing).max(0.0);
remaining / self.fill_indices.len() as f32
} else {
0.0
};
if !self.fill_indices.is_empty() {
let fill_constraints = match axis {
Axis::Horizontal => Constraints {
min_width: per_fill,
max_width: per_fill,
..child_constraints
},
Axis::Vertical => Constraints {
min_height: per_fill,
max_height: per_fill,
..child_constraints
},
};
for &i in &self.fill_indices {
let child_id = children[i];
if let Some(size) = ctx.layout_child(child_id, fill_constraints) {
max_cross = max_cross.max(size.cross_axis(axis));
self.child_sizes[i] = size;
}
}
}
let mut children_main: f32 = self.child_sizes.iter().map(|s| s.main_axis(axis)).sum();
let (main_min, cross_constraint_min) = match axis {
Axis::Horizontal => (constraints.min_width, constraints.min_height),
Axis::Vertical => (constraints.min_height, constraints.min_width),
};
let cross_size = max_cross.max(cross_constraint_min).min(cross_max);
if cross_align == CrossAlignment::Stretch && stretch_cross.is_none() && cross_size > 0.0 {
children_main = 0.0;
let mut fill_cursor = 0;
for (i, &child_id) in children.iter().enumerate() {
if (self.child_sizes[i].cross_axis(axis) - cross_size).abs() < 0.5 {
children_main += self.child_sizes[i].main_axis(axis);
if fill_cursor < self.fill_indices.len() && self.fill_indices[fill_cursor] == i
{
fill_cursor += 1;
}
continue;
}
let child_is_fill =
fill_cursor < self.fill_indices.len() && self.fill_indices[fill_cursor] == i;
if child_is_fill {
fill_cursor += 1;
}
let main_constraint = if child_is_fill { per_fill } else { main_max };
let stretch_constraints = match axis {
Axis::Horizontal => Constraints {
min_width: if child_is_fill { per_fill } else { 0.0 },
min_height: cross_size,
max_width: main_constraint,
max_height: cross_size,
},
Axis::Vertical => Constraints {
min_width: cross_size,
min_height: if child_is_fill { per_fill } else { 0.0 },
max_width: cross_size,
max_height: main_constraint,
},
};
if let Some(size) = ctx.layout_child(child_id, stretch_constraints) {
children_main += size.main_axis(axis);
self.child_sizes[i] = size;
}
}
}
let visible_count = self
.child_sizes
.iter()
.filter(|s| s.main_axis(axis) > MIN_VISIBLE_SIZE)
.count();
let visible_spacing = if visible_count > 1 {
spacing * (visible_count - 1) as f32
} else {
0.0
};
let visible_total_main = children_main + visible_spacing;
let main_size = match main_align {
MainAlignment::SpaceBetween
| MainAlignment::SpaceAround
| MainAlignment::SpaceEvenly => main_max,
MainAlignment::Start | MainAlignment::Center | MainAlignment::End => {
visible_total_main.max(main_min).min(main_max)
}
};
let (width, height) = match axis {
Axis::Horizontal => (main_size, cross_size),
Axis::Vertical => (cross_size, main_size),
};
let size = Size::new(width, height);
let free_space = (main_size - children_main - visible_spacing).max(0.0);
let (initial_offset, between_spacing) =
self.calc_main_axis_spacing(main_align, spacing, free_space, visible_count);
let mut main_pos = match axis {
Axis::Horizontal => origin.0,
Axis::Vertical => origin.1,
} + initial_offset;
let max_baseline = if cross_align == CrossAlignment::Baseline && axis == Axis::Horizontal {
children
.iter()
.enumerate()
.map(|(i, &id)| {
baseline_of(ctx.tree_ref(), id, self.child_sizes[i].cross_axis(axis))
})
.fold(0.0f32, f32::max)
} else {
0.0
};
let mut prev_nonzero = false;
for (i, &child_id) in children.iter().enumerate() {
let child_size = self.child_sizes[i];
let child_main = child_size.main_axis(axis);
let child_cross = child_size.cross_axis(axis);
if prev_nonzero && child_main > MIN_VISIBLE_SIZE {
main_pos += between_spacing;
}
let cross_pos = match cross_align {
CrossAlignment::Start => match axis {
Axis::Horizontal => origin.1,
Axis::Vertical => origin.0,
},
CrossAlignment::Center => match axis {
Axis::Horizontal => origin.1 + (cross_size - child_cross) / 2.0,
Axis::Vertical => origin.0 + (cross_size - child_cross) / 2.0,
},
CrossAlignment::End => match axis {
Axis::Horizontal => origin.1 + cross_size - child_cross,
Axis::Vertical => origin.0 + cross_size - child_cross,
},
CrossAlignment::Stretch => match axis {
Axis::Horizontal => origin.1,
Axis::Vertical => origin.0,
},
CrossAlignment::Baseline => match axis {
Axis::Horizontal => {
origin.1 + max_baseline - baseline_of(ctx.tree_ref(), child_id, child_cross)
}
Axis::Vertical => origin.0,
},
};
let (x, y) = match axis {
Axis::Horizontal => (main_pos, cross_pos),
Axis::Vertical => (cross_pos, main_pos),
};
ctx.tree().set_origin(child_id, x, y);
main_pos += child_main;
if child_main > MIN_VISIBLE_SIZE {
prev_nonzero = true;
}
}
size
}
}
impl Layout for Flex {
fn layout(
&mut self,
ctx: &mut LayoutCtx,
children: &[WidgetId],
constraints: Constraints,
origin: (f32, f32),
) -> Size {
let direction = self.direction.get();
self.layout_axis(ctx, children, constraints, origin, direction)
}
}
fn baseline_of(tree: &Tree, id: WidgetId, cross_size: f32) -> f32 {
tree.baseline(id).unwrap_or(cross_size)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::jobs;
use crate::reactive::create_signal;
use crate::widgets::container;
fn frame(tree: &mut Tree, root: WidgetId) {
jobs::pump_and_layout(tree, root, Constraints::new(0.0, 0.0, 400.0, 400.0));
}
#[test]
fn a_row_becomes_a_column_when_its_direction_says_so() {
let horizontal = create_signal(true);
let axis = move || {
if horizontal.get() {
Axis::Horizontal
} else {
Axis::Vertical
}
};
let mut tree = Tree::new();
let root = tree.register(Box::new(container().layout(Flex::new(axis)).children(
vec![
container().width(20.0).height(20.0),
container().width(20.0).height(20.0),
],
)));
tree.with_widget_mut(root, |w, id, t| w.register_children(t, id));
frame(&mut tree, root);
let kids = tree.get_children(root).to_vec();
assert_eq!(kids.len(), 2, "two children, laid out");
let second = tree.get_origin(kids[1]).expect("laid out");
assert!(
second.0 > 0.0 && second.1 == 0.0,
"a row puts the second child to the right, got {second:?}"
);
horizontal.set(false);
frame(&mut tree, root);
let same_kids = tree.get_children(root).to_vec();
assert_eq!(same_kids, kids, "the children were rebuilt, not relaid out");
let second = tree.get_origin(kids[1]).expect("laid out");
assert!(
second.1 > 0.0 && second.0 == 0.0,
"a column puts the second child below, got {second:?}"
);
}
}