use std::collections::HashMap;
use frus_core::{Rect, Size};
use taffy::{AvailableSpace, TaffyTree, TraversePartialTree};
use crate::style::Style;
pub type NodeId = taffy::NodeId;
pub type MeasureFn<'a> = Box<dyn Fn(Option<f32>, Option<f32>) -> Size + 'a>;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Side {
Left,
Right,
Top,
Bottom,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Overflowing {
pub rect: Rect,
pub side: Side,
pub amount: f32,
}
pub struct Layout<'a, T> {
tree: TaffyTree<T>,
measures: HashMap<NodeId, MeasureFn<'a>>,
}
#[inline]
fn debug_assert_measurement(measured: f32, bound: Option<f32>, axis: &str) {
#[cfg(debug_assertions)]
{
const EPSILON: f32 = 1e-3;
if bound.is_some_and(|bound| (measured - bound).abs() <= EPSILON) {
return;
}
debug_assert!(
(measured - measured.ceil()).abs() <= EPSILON,
"a measured {axis} of {measured} is not a whole number of pixels, so the box \
it is rounded to is one it no longer fits in — go through `frus_core::fits` \
(milestone 289, issue #54)"
);
}
#[cfg(not(debug_assertions))]
{
let _ = (measured, bound, axis);
}
}
impl<'a, T> Layout<'a, T> {
pub fn new() -> Self {
let tree = TaffyTree::new();
Self {
tree,
measures: HashMap::new(),
}
}
pub fn leaf(&mut self, style: Style, data: T) -> NodeId {
self.tree
.new_leaf_with_context(style.to_taffy(), data)
.expect("creating a layout leaf")
}
pub fn measured_leaf(&mut self, style: Style, data: T, measure: MeasureFn<'a>) -> NodeId {
let node = self.leaf(style, data);
self.measures.insert(node, measure);
node
}
pub fn add_margin_top(&mut self, node: NodeId, dy: f32) {
if dy <= 0.0 {
return;
}
let mut style = self.tree.style(node).expect("a node we made").clone();
let base = match style.margin.top {
taffy::LengthPercentageAuto::Length(v) => v,
_ => 0.0,
};
style.margin.top = taffy::LengthPercentageAuto::Length(base + dy);
self.tree.set_style(node, style).expect("setting a style");
}
pub fn clear_size(&mut self, node: NodeId, horizontal: bool) {
let mut style = self.tree.style(node).expect("a node we made").clone();
if horizontal {
style.size.width = taffy::Dimension::Auto;
} else {
style.size.height = taffy::Dimension::Auto;
}
self.tree.set_style(node, style).expect("setting a style");
}
pub fn fill_parent(&mut self, node: NodeId, horizontal: bool, grow: bool) {
let mut style = self.tree.style(node).expect("a node we made").clone();
let size = if horizontal {
style.size.width
} else {
style.size.height
};
if !matches!(size, taffy::Dimension::Auto) {
return;
}
if grow {
if style.flex_grow != 0.0 {
return;
}
style.flex_grow = 1.0;
} else {
if style.align_self.is_some() {
return;
}
style.align_self = Some(taffy::AlignItems::Stretch);
}
self.tree.set_style(node, style).expect("setting a style");
}
pub fn allow_shrink(&mut self, node: NodeId) {
let mut style = self.tree.style(node).expect("a node we made").clone();
let mut changed = false;
if style.flex_shrink == 0.0 {
style.flex_shrink = 1.0;
changed = true;
}
if matches!(style.min_size.width, taffy::Dimension::Auto) {
style.min_size.width = taffy::style_helpers::length(0.0_f32);
changed = true;
}
if changed {
self.tree.set_style(node, style).expect("setting a style");
}
}
pub fn set_tile_shape(&mut self, node: NodeId, ratio: f32) {
let mut style = self.tree.style(node).expect("a node we made").clone();
if !matches!(style.size.width, taffy::Dimension::Auto)
|| !matches!(style.size.height, taffy::Dimension::Auto)
{
return;
}
style.aspect_ratio = Some(ratio);
self.tree.set_style(node, style).expect("setting a style");
}
pub fn set_min_width(&mut self, node: NodeId, width: f32) {
let mut style = self.tree.style(node).expect("a node we made").clone();
if !matches!(style.min_size.width, taffy::Dimension::Auto) {
return;
}
style.min_size.width = taffy::Dimension::Length(width);
self.tree.set_style(node, style).expect("setting a style");
}
pub fn fill_root(&mut self, node: NodeId, horizontal: bool) {
let mut style = self.tree.style(node).expect("a node we made").clone();
let axis = if horizontal {
&mut style.size.width
} else {
&mut style.size.height
};
if !matches!(axis, taffy::Dimension::Auto) {
return;
}
*axis = taffy::Dimension::Percent(1.0);
self.tree.set_style(node, style).expect("setting a style");
}
pub fn container(&mut self, style: Style, children: &[NodeId]) -> NodeId {
if style.overlap {
use taffy::style_helpers::{line, span};
for &child in children {
let mut placed = self.tree.style(child).expect("a node we made").clone();
placed.grid_row = taffy::Line {
start: line(1),
end: span(1),
};
placed.grid_column = taffy::Line {
start: line(1),
end: span(1),
};
self.tree.set_style(child, placed).expect("setting a style");
}
}
self.tree
.new_with_children(style.to_taffy(), children)
.expect("creating a layout container")
}
pub fn compute(&mut self, root: NodeId, available: Size) {
self.compute_in(
root,
taffy::Size {
width: AvailableSpace::Definite(available.width),
height: AvailableSpace::Definite(available.height),
},
);
}
pub fn compute_filled(&mut self, root: NodeId, width: f32, height: f32) {
if let Ok(current) = self.tree.style(root) {
let mut style = current.clone();
let mut changed = false;
if matches!(style.size.width, taffy::Dimension::Auto) {
style.size.width = taffy::Dimension::Length(width);
changed = true;
}
if matches!(style.size.height, taffy::Dimension::Auto) {
style.size.height = taffy::Dimension::Length(height);
changed = true;
}
if changed {
let _ = self.tree.set_style(root, style);
}
}
self.compute(root, Size::new(width, height));
}
pub fn compute_tight(
&mut self,
root: NodeId,
width: f32,
height: f32,
free_x: bool,
free_y: bool,
) {
if let Ok(current) = self.tree.style(root) {
let mut style = current.clone();
if !free_x {
style.size.width = taffy::Dimension::Length(width);
}
if !free_y {
style.size.height = taffy::Dimension::Length(height);
}
let _ = self.tree.set_style(root, style);
}
self.compute_scroll(root, width, height, free_x, free_y);
}
pub fn compute_scroll(
&mut self,
root: NodeId,
width: f32,
height: f32,
free_x: bool,
free_y: bool,
) {
let fill_w = !free_x && free_y;
let fill_h = !free_y && free_x;
self.compute_each(root, width, height, [free_x, free_y], [fill_w, fill_h]);
}
pub fn compute_axes(
&mut self,
root: NodeId,
width: f32,
height: f32,
free: [bool; 2],
fill: [bool; 2],
) {
self.compute_each(root, width, height, free, fill);
let size = self.size_of(root);
let past =
|free: bool, fill: bool, came: f32, bound: f32| !free && !fill && came > bound + 0.5;
let over = [
past(free[0], fill[0], size.width, width),
past(free[1], fill[1], size.height, height),
];
if over[0] || over[1] {
let fill = [fill[0] || over[0], fill[1] || over[1]];
self.compute_each(root, width, height, free, fill);
}
}
fn compute_each(
&mut self,
root: NodeId,
width: f32,
height: f32,
free: [bool; 2],
fill: [bool; 2],
) {
let [free_x, free_y] = free;
let fill_w = fill[0] && !free_x;
let fill_h = fill[1] && !free_y;
if fill_w || fill_h {
if let Ok(current) = self.tree.style(root) {
let mut style = current.clone();
let mut changed = false;
if fill_w && matches!(style.size.width, taffy::Dimension::Auto) {
style.size.width = taffy::Dimension::Length(width);
changed = true;
}
if fill_h && matches!(style.size.height, taffy::Dimension::Auto) {
style.size.height = taffy::Dimension::Length(height);
changed = true;
}
if changed {
let _ = self.tree.set_style(root, style);
}
}
}
let axis = |free: bool, size: f32| {
if free {
AvailableSpace::MaxContent
} else {
AvailableSpace::Definite(size)
}
};
self.compute_in(
root,
taffy::Size {
width: axis(free_x, width),
height: axis(free_y, height),
},
);
}
fn compute_in(&mut self, root: NodeId, available: taffy::Size<AvailableSpace>) {
let measures = &self.measures;
self.tree
.compute_layout_with_measure(
root,
available,
|known: taffy::Size<Option<f32>>,
space: taffy::Size<AvailableSpace>,
node,
_ctx,
_style| {
let Some(measure) = measures.get(&node) else {
return taffy::Size::ZERO;
};
let bound = |known: Option<f32>, space: AvailableSpace| {
known.or(match space {
AvailableSpace::Definite(v) => Some(v),
AvailableSpace::MinContent => Some(0.0),
AvailableSpace::MaxContent => None,
})
};
let (bw, bh) = (
bound(known.width, space.width),
bound(known.height, space.height),
);
let size = measure(bw, bh);
debug_assert_measurement(size.width, bw, "width");
debug_assert_measurement(size.height, bh, "height");
taffy::Size {
width: size.width,
height: size.height,
}
},
)
.expect("computing the layout");
}
pub fn size_of(&self, node: NodeId) -> Size {
self.tree
.layout(node)
.map(|l| Size::new(l.size.width, l.size.height))
.unwrap_or(Size::new(0.0, 0.0))
}
pub fn absolute_rects(&self, root: NodeId) -> Vec<(Rect, Option<&T>)> {
let mut out = Vec::new();
self.collect(root, 0.0, 0.0, &mut out);
out
}
pub fn overflows(&self, root: NodeId) -> Vec<Overflowing> {
let mut out = Vec::new();
self.collect_overflow(root, 0.0, 0.0, &mut out);
out
}
fn collect_overflow(
&self,
node: NodeId,
offset_x: f32,
offset_y: f32,
out: &mut Vec<Overflowing>,
) {
let layout = self.tree.unrounded_layout(node);
let x = offset_x + layout.location.x;
let y = offset_y + layout.location.y;
let child_count = self.tree.child_count(node);
if child_count > 0 {
let left = layout.border.left + layout.padding.left;
let top = layout.border.top + layout.padding.top;
let right = layout.size.width - layout.border.right - layout.padding.right;
let bottom = layout.size.height - layout.border.bottom - layout.padding.bottom;
let (mut over_l, mut over_t, mut over_r, mut over_b) = (0.0f32, 0.0f32, 0.0f32, 0.0f32);
for i in 0..child_count {
let child = self.tree.child_at_index(node, i).expect("the node's child");
let c = self.tree.unrounded_layout(child);
over_l = over_l.max(left - c.location.x);
over_t = over_t.max(top - c.location.y);
over_r = over_r.max(c.location.x + c.size.width - right);
over_b = over_b.max(c.location.y + c.size.height - bottom);
}
const TOLERANCE: f32 = 0.5;
let rect = Rect::new(x, y, layout.size.width, layout.size.height);
for (amount, side) in [
(over_l, Side::Left),
(over_t, Side::Top),
(over_r, Side::Right),
(over_b, Side::Bottom),
] {
if amount > TOLERANCE {
out.push(Overflowing { rect, side, amount });
}
}
}
for i in 0..child_count {
let child = self.tree.child_at_index(node, i).expect("the node's child");
self.collect_overflow(child, x, y, out);
}
}
fn collect<'s>(
&'s self,
node: NodeId,
offset_x: f32,
offset_y: f32,
out: &mut Vec<(Rect, Option<&'s T>)>,
) {
let layout = self.tree.layout(node).expect("the node's layout");
let x = offset_x + layout.location.x;
let y = offset_y + layout.location.y;
let rect = Rect::new(x, y, layout.size.width, layout.size.height);
out.push((rect, self.tree.get_node_context(node)));
let child_count = self.tree.child_count(node);
for i in 0..child_count {
let child = self.tree.child_at_index(node, i).expect("the node's child");
self.collect(child, x, y, out);
}
}
}
impl<T> Default for Layout<'_, T> {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
#[test]
fn a_row_too_small_for_its_children_reports_it() {
let mut layout: Layout<'_, ()> = Layout::new();
let a = layout.leaf(
Style {
width: Dimension::Length(80.0),
flex_shrink: 0.0,
..Default::default()
},
(),
);
let b = layout.leaf(
Style {
width: Dimension::Length(80.0),
flex_shrink: 0.0,
..Default::default()
},
(),
);
let row = layout.container(
Style {
width: Dimension::Length(100.0),
height: Dimension::Length(20.0),
..Default::default()
},
&[a, b],
);
layout.compute(row, Size::new(200.0, 200.0));
let over = layout.overflows(row);
assert_eq!(over.len(), 1, "one edge, once: {over:?}");
assert_eq!(over[0].side, Side::Right);
assert_eq!(over[0].amount, 60.0);
assert_eq!(over[0].rect.width, 100.0, "the box named is the parent's");
}
#[test]
fn padding_counts_as_room_taken() {
let mut layout: Layout<'_, ()> = Layout::new();
let child = layout.leaf(
Style {
width: Dimension::Length(100.0),
flex_shrink: 0.0,
..Default::default()
},
(),
);
let boxed = layout.container(
Style {
width: Dimension::Length(100.0),
height: Dimension::Length(20.0),
padding: frus_core::Insets::uniform(10.0),
..Default::default()
},
&[child],
);
layout.compute(boxed, Size::new(200.0, 200.0));
let over = layout.overflows(boxed);
assert_eq!(over.len(), 1);
assert_eq!(over[0].amount, 20.0, "10 px of padding on either side");
}
#[test]
fn a_row_that_fits_is_silent() {
let mut layout: Layout<'_, ()> = Layout::new();
let a = layout.leaf(
Style {
width: Dimension::Length(50.2),
flex_shrink: 0.0,
..Default::default()
},
(),
);
let b = layout.leaf(
Style {
width: Dimension::Length(50.1),
flex_shrink: 0.0,
..Default::default()
},
(),
);
let row = layout.container(
Style {
width: Dimension::Length(100.0),
height: Dimension::Length(20.0),
..Default::default()
},
&[a, b],
);
layout.compute(row, Size::new(200.0, 200.0));
assert!(layout.overflows(row).is_empty(), "0.3 px is nobody's bug");
}
use super::*;
use crate::{Align, Dimension, FlexDirection, Justify, Style};
use frus_core::Insets;
#[test]
fn flex_row_computes_absolute_positions() {
let mut layout: Layout<'_, ()> = Layout::new();
let a = layout.leaf(
Style {
width: Dimension::Length(120.0),
..Default::default()
},
(),
);
let b = layout.leaf(
Style {
flex_grow: 1.0,
..Default::default()
},
(),
);
let root = layout.container(
Style {
width: Dimension::Length(400.0),
height: Dimension::Length(100.0),
flex_direction: FlexDirection::Row,
padding: Insets::uniform(10.0),
gap: 8.0,
..Default::default()
},
&[a, b],
);
layout.compute(root, Size::new(400.0, 100.0));
let rects = layout.absolute_rects(root);
let (root_rect, _) = rects[0];
let (a_rect, _) = rects[1];
let (b_rect, _) = rects[2];
assert_eq!(root_rect, Rect::new(0.0, 0.0, 400.0, 100.0));
assert_eq!(a_rect, Rect::new(10.0, 10.0, 120.0, 80.0));
assert_eq!(b_rect, Rect::new(138.0, 10.0, 252.0, 80.0));
}
#[test]
fn flex_wrap_moves_overflowing_child_to_next_line() {
let mut layout: Layout<'_, ()> = Layout::new();
let kids: Vec<_> = (0..3)
.map(|_| {
layout.leaf(
Style {
width: Dimension::Length(80.0),
height: Dimension::Length(40.0),
..Default::default()
},
(),
)
})
.collect();
let root = layout.container(
Style {
width: Dimension::Length(200.0),
height: Dimension::Length(200.0),
flex_direction: FlexDirection::Row,
align: Align::Start,
flex_wrap: true,
..Default::default()
},
&kids,
);
layout.compute(root, Size::new(200.0, 200.0));
let rects = layout.absolute_rects(root);
let (k0, _) = rects[1];
let (k1, _) = rects[2];
let (k2, _) = rects[3];
assert_eq!(k0.y, k1.y);
assert_eq!(k2.x, k0.x);
assert!(k2.y >= k0.y + 40.0, "k2.y = {}, expected >= 40", k2.y);
}
#[test]
fn measured_leaf_wraps_to_the_offered_width() {
let measure: crate::MeasureFn<'_> = Box::new(|w, _| {
let w = w.unwrap_or(250.0).min(250.0);
let lines = (250.0 / w).ceil();
Size::new(w, lines * 20.0)
});
let mut layout: Layout<'_, ()> = Layout::new();
let text = layout.measured_leaf(Style::default(), (), measure);
let root = layout.container(
Style {
width: Dimension::Length(100.0),
flex_direction: FlexDirection::Column,
align: Align::Start,
..Default::default()
},
&[text],
);
layout.compute(root, Size::new(100.0, 600.0));
let rects = layout.absolute_rects(root);
let (text_rect, _) = rects[1];
assert!(
text_rect.width <= 100.0,
"wrapped to the offered width: {text_rect:?}"
);
assert!(
text_rect.height >= 60.0,
"3 lines expected (250/100 → 3 × 20): {text_rect:?}"
);
}
#[test]
fn a_centred_measured_leaf_reports_the_height_of_the_width_it_got() {
let measure: crate::MeasureFn<'_> = Box::new(|w, _| {
let w = w.unwrap_or(250.0).min(250.0);
let lines = (250.0 / w).ceil();
Size::new(w, lines * 20.0)
});
let mut layout: Layout<'_, ()> = Layout::new();
let text = layout.measured_leaf(Style::default(), (), measure);
let root = layout.container(
Style {
width: Dimension::Length(100.0),
flex_direction: FlexDirection::Column,
align: Align::Center,
..Default::default()
},
&[text],
);
layout.compute(root, Size::new(100.0, 600.0));
let rects = layout.absolute_rects(root);
let (text_rect, _) = rects[1];
assert!(
text_rect.width <= 100.0,
"wrapped to the offered width: {text_rect:?}"
);
assert!(
text_rect.height >= 60.0,
"3 lines expected (250/100 -> 3 x 20): {text_rect:?}"
);
}
#[test]
fn justify_center_centers_child() {
let mut layout: Layout<'_, ()> = Layout::new();
let child = layout.leaf(
Style {
width: Dimension::Length(100.0),
height: Dimension::Length(40.0),
align: Align::Start,
..Default::default()
},
(),
);
let root = layout.container(
Style {
width: Dimension::Length(400.0),
height: Dimension::Length(100.0),
flex_direction: FlexDirection::Row,
justify: Justify::Center,
..Default::default()
},
&[child],
);
layout.compute(root, Size::new(400.0, 100.0));
let rects = layout.absolute_rects(root);
assert_eq!(rects[1].0, Rect::new(150.0, 0.0, 100.0, 40.0));
}
#[test]
#[should_panic(expected = "not a whole number of pixels")]
fn a_measurement_that_is_not_whole_is_caught() {
let mut layout: Layout<'_, ()> = Layout::new();
let node = layout.measured_leaf(
Style::default(),
(),
Box::new(|_, _| Size::new(146.4, 20.0)),
);
layout.compute(node, Size::new(400.0, 400.0));
}
#[test]
fn a_measurement_put_through_the_helper_passes() {
let mut layout: Layout<'_, ()> = Layout::new();
let node = layout.measured_leaf(
Style::default(),
(),
Box::new(|_, _| Size::new(frus_core::fits(146.4), frus_core::fits(20.0))),
);
layout.compute(node, Size::new(400.0, 400.0));
let size = layout.size_of(node);
assert_eq!(size.width, 147.0, "rounded up, so the box still fits it");
}
#[test]
fn a_measurement_clamped_to_a_fractional_bound_is_not_a_violation() {
let mut layout: Layout<'_, ()> = Layout::new();
let node = layout.measured_leaf(
Style::default(),
(),
Box::new(|w, _| Size::new(frus_core::fits(500.0).min(w.unwrap_or(0.0)), 20.0)),
);
layout.compute(node, Size::new(146.4, 400.0));
}
#[test]
fn a_measurement_bigger_than_the_offer_is_reported_not_forbidden() {
let mut layout: Layout<'_, ()> = Layout::new();
let node = layout.measured_leaf(
Style::default(),
(),
Box::new(|_, _| Size::new(500.0, 20.0)),
);
layout.compute(node, Size::new(100.0, 100.0));
assert_eq!(layout.size_of(node).width, 500.0);
}
#[test]
fn overlapping_children_share_one_cell_the_size_of_the_largest() {
let place = |width: Dimension| {
let mut layout: Layout<'_, u8> = Layout::new();
let wide = layout.leaf(
Style {
width: Dimension::Length(80.0),
height: Dimension::Length(20.0),
..Default::default()
},
1,
);
let tall = layout.leaf(
Style {
width: Dimension::Length(30.0),
height: Dimension::Length(50.0),
..Default::default()
},
2,
);
let cell = layout.container(
Style {
width,
overlap: true,
align: crate::style::Align::Center,
..Default::default()
},
&[wide, tall],
);
layout.compute(cell, Size::new(400.0, 400.0));
let rects = layout.absolute_rects(cell);
let at = |tag: u8| {
let r = rects.iter().find(|(_, d)| *d == Some(&tag)).unwrap().0;
(r.x, r.y)
};
(layout.size_of(cell), at(1), at(2))
};
let (size, wide, tall) = place(Dimension::Auto);
assert_eq!(
(size.width, size.height),
(80.0, 50.0),
"the largest, per axis"
);
assert_eq!(wide, (0.0, 15.0), "the wide one centred down the cell");
assert_eq!(tall, (25.0, 0.0), "the tall one centred across it");
let (size, wide, tall) = place(Dimension::Length(200.0));
assert_eq!(size.width, 200.0);
assert_eq!(wide.0, 60.0, "a box handed over is filled, and centred in");
assert_eq!(tall.0, 85.0);
}
}