use std::collections::{HashMap, HashSet};
use std::fmt;
use std::sync::atomic::{AtomicU64, Ordering};
use crate::event::Rect;
pub use taffy::geometry::{Line, Rect as Edges, Size};
pub use taffy::style::{
AlignContent, AlignItems, AlignSelf, Dimension, Display, FlexDirection, FlexWrap, GridAutoFlow,
GridPlacement, GridTemplateComponent, JustifyContent, JustifyItems, JustifySelf,
LengthPercentage, LengthPercentageAuto, Overflow, Position, Style, TrackSizingFunction,
};
pub use taffy::style_helpers::{
auto, evenly_sized_tracks, fit_content, fr, length, max_content, min_content, minmax, percent,
repeat, span, zero,
};
static NEXT_TREE_ID: AtomicU64 = AtomicU64::new(0);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct NodeId(u64, taffy::NodeId);
#[derive(Debug)]
pub enum FlexLayoutError {
ForeignNode(NodeId),
AlreadyParented(NodeId),
Taffy(taffy::TaffyError),
}
impl fmt::Display for FlexLayoutError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::ForeignNode(node) => {
write!(f, "{node:?} belongs to a different FlexLayout's tree")
}
Self::AlreadyParented(node) => {
write!(f, "{node:?} is already a child of another container")
}
Self::Taffy(err) => fmt::Display::fmt(err, f),
}
}
}
impl std::error::Error for FlexLayoutError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Taffy(err) => Some(err),
Self::ForeignNode(_) | Self::AlreadyParented(_) => None,
}
}
}
impl From<taffy::TaffyError> for FlexLayoutError {
fn from(err: taffy::TaffyError) -> Self {
Self::Taffy(err)
}
}
pub type FlexResult<T> = std::result::Result<T, FlexLayoutError>;
pub struct FlexLayout {
id: u64,
tree: taffy::TaffyTree<()>,
parented: HashSet<taffy::NodeId>,
}
impl Default for FlexLayout {
fn default() -> Self {
Self::new()
}
}
impl FlexLayout {
pub fn new() -> Self {
Self {
id: NEXT_TREE_ID.fetch_add(1, Ordering::Relaxed),
tree: taffy::TaffyTree::new(),
parented: HashSet::new(),
}
}
#[must_use]
pub fn without_rounding(mut self) -> Self {
self.tree.disable_rounding();
self
}
pub fn add_leaf(&mut self, style: Style) -> FlexResult<NodeId> {
let id = self.tree.new_leaf(style)?;
Ok(NodeId(self.id, id))
}
pub fn add_container(&mut self, style: Style, children: &[NodeId]) -> FlexResult<NodeId> {
for child in children {
if child.0 != self.id {
return Err(FlexLayoutError::ForeignNode(*child));
}
if self.parented.contains(&child.1) {
return Err(FlexLayoutError::AlreadyParented(*child));
}
}
let kids: Vec<taffy::NodeId> = children.iter().map(|n| n.1).collect();
let id = self.tree.new_with_children(style, &kids)?;
for child in children {
self.parented.insert(child.1);
}
Ok(NodeId(self.id, id))
}
pub fn compute(&mut self, root: NodeId, available: Rect) -> FlexResult<HashMap<NodeId, Rect>> {
if root.0 != self.id {
return Err(FlexLayoutError::ForeignNode(root));
}
let available_space = taffy::Size {
width: taffy::AvailableSpace::Definite(available.width),
height: taffy::AvailableSpace::Definite(available.height),
};
self.tree.compute_layout(root.1, available_space)?;
let mut out = HashMap::new();
self.collect(root, available.x, available.y, &mut out)?;
Ok(out)
}
fn collect(
&self,
root: NodeId,
origin_x: f32,
origin_y: f32,
out: &mut HashMap<NodeId, Rect>,
) -> FlexResult<()> {
let mut stack = vec![(root, origin_x, origin_y)];
while let Some((node, parent_x, parent_y)) = stack.pop() {
let layout = self.tree.layout(node.1)?;
let x = parent_x + layout.location.x;
let y = parent_y + layout.location.y;
out.insert(node, Rect::new(x, y, layout.size.width, layout.size.height));
for child in self.tree.children(node.1)? {
stack.push((NodeId(self.id, child), x, y));
}
}
Ok(())
}
}
pub(crate) fn pack_1d(
direction: FlexDirection,
origin: f32,
sizes: &[f32],
gap: f32,
cross: f32,
) -> Vec<f32> {
if sizes.is_empty() {
return Vec::new();
}
let is_row = matches!(direction, FlexDirection::Row | FlexDirection::RowReverse);
let mut flex = FlexLayout::new().without_rounding();
let leaves: Vec<NodeId> = sizes
.iter()
.map(|&s| {
let size = if is_row {
Size {
width: length(s),
height: length(cross),
}
} else {
Size {
width: length(cross),
height: length(s),
}
};
flex.add_leaf(Style {
size,
flex_shrink: 0.0,
flex_grow: 0.0,
..Default::default()
})
.expect("pack_1d leaf node")
})
.collect();
let gap_size = if is_row {
Size {
width: length(gap),
height: length(0.0),
}
} else {
Size {
width: length(0.0),
height: length(gap),
}
};
let root = flex
.add_container(
Style {
flex_direction: direction,
gap: gap_size,
..Default::default()
},
&leaves,
)
.expect("pack_1d container node");
let total: f32 = sizes.iter().sum::<f32>() + gap * sizes.len().saturating_sub(1) as f32;
let available = if is_row {
Rect::new(origin, 0.0, total, cross)
} else {
Rect::new(0.0, origin, cross, total)
};
let computed = flex.compute(root, available).expect("pack_1d flex layout");
leaves
.iter()
.map(|id| {
if is_row {
computed[id].x
} else {
computed[id].y
}
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn row_of_two_equal_leaves_splits_available_width() {
let mut flex = FlexLayout::new();
let left = flex
.add_leaf(Style {
size: Size {
width: percent(0.5),
height: percent(1.0),
},
..Default::default()
})
.unwrap();
let right = flex
.add_leaf(Style {
size: Size {
width: percent(0.5),
height: percent(1.0),
},
..Default::default()
})
.unwrap();
let root = flex
.add_container(
Style {
flex_direction: FlexDirection::Row,
size: Size {
width: percent(1.0),
height: percent(1.0),
},
..Default::default()
},
&[left, right],
)
.unwrap();
let rects = flex
.compute(root, Rect::new(0.0, 0.0, 100.0, 40.0))
.unwrap();
assert_eq!(rects.len(), 3);
assert_eq!(rects[&left], Rect::new(0.0, 0.0, 50.0, 40.0));
assert_eq!(rects[&right], Rect::new(50.0, 0.0, 50.0, 40.0));
assert_eq!(rects[&root], Rect::new(0.0, 0.0, 100.0, 40.0));
}
#[test]
fn column_stack_offsets_are_absolute_not_panel_relative() {
let mut flex = FlexLayout::new();
let top = flex
.add_leaf(Style {
size: Size {
width: percent(1.0),
height: length(10.0),
},
..Default::default()
})
.unwrap();
let bottom = flex
.add_leaf(Style {
size: Size {
width: percent(1.0),
height: length(10.0),
},
..Default::default()
})
.unwrap();
let root = flex
.add_container(
Style {
flex_direction: FlexDirection::Column,
size: Size {
width: percent(1.0),
height: length(20.0),
},
..Default::default()
},
&[top, bottom],
)
.unwrap();
let rects = flex.compute(root, Rect::new(5.0, 7.0, 30.0, 20.0)).unwrap();
assert_eq!(rects[&top], Rect::new(5.0, 7.0, 30.0, 10.0));
assert_eq!(rects[&bottom], Rect::new(5.0, 17.0, 30.0, 10.0));
}
#[test]
fn nested_three_level_tree_threads_absolute_offsets_correctly() {
let mut flex = FlexLayout::new();
let spacer = flex
.add_leaf(Style {
size: Size {
width: percent(1.0),
height: length(5.0),
},
..Default::default()
})
.unwrap();
let leaf_one = flex
.add_leaf(Style {
size: Size {
width: percent(1.0),
height: length(10.0),
},
..Default::default()
})
.unwrap();
let leaf_two = flex
.add_leaf(Style {
size: Size {
width: percent(1.0),
height: length(10.0),
},
..Default::default()
})
.unwrap();
let inner = flex
.add_container(
Style {
flex_direction: FlexDirection::Column,
size: Size {
width: percent(1.0),
height: length(20.0),
},
..Default::default()
},
&[leaf_one, leaf_two],
)
.unwrap();
let outer = flex
.add_container(
Style {
flex_direction: FlexDirection::Column,
size: Size {
width: percent(1.0),
height: length(25.0),
},
..Default::default()
},
&[spacer, inner],
)
.unwrap();
let rects = flex
.compute(outer, Rect::new(5.0, 7.0, 30.0, 25.0))
.unwrap();
assert_eq!(rects[&spacer], Rect::new(5.0, 7.0, 30.0, 5.0));
assert_eq!(rects[&inner], Rect::new(5.0, 12.0, 30.0, 20.0));
assert_eq!(rects[&leaf_one], Rect::new(5.0, 12.0, 30.0, 10.0));
assert_eq!(rects[&leaf_two], Rect::new(5.0, 22.0, 30.0, 10.0));
}
#[test]
fn grid_two_by_two_divides_both_axes() {
let mut flex = FlexLayout::new();
let cells: Vec<NodeId> = (0..4)
.map(|_| flex.add_leaf(Style::default()).unwrap())
.collect();
let root = flex
.add_container(
Style {
display: Display::Grid,
size: Size {
width: length(100.0),
height: length(100.0),
},
grid_template_columns: vec![fr(1.0), fr(1.0)],
grid_template_rows: vec![fr(1.0), fr(1.0)],
..Default::default()
},
&cells,
)
.unwrap();
let rects = flex
.compute(root, Rect::new(0.0, 0.0, 100.0, 100.0))
.unwrap();
let mut origins: Vec<(f32, f32)> = cells
.iter()
.map(|c| {
let r = rects[c];
assert_eq!((r.width, r.height), (50.0, 50.0));
(r.x, r.y)
})
.collect();
origins.sort_by(|a, b| a.partial_cmp(b).unwrap());
assert_eq!(
origins,
vec![(0.0, 0.0), (0.0, 50.0), (50.0, 0.0), (50.0, 50.0)]
);
}
#[test]
fn unknown_node_id_from_another_tree_errors_not_panics() {
let mut a = FlexLayout::new();
let leaf_a = a.add_leaf(Style::default()).unwrap();
let root_a = a.add_container(Style::default(), &[leaf_a]).unwrap();
let mut b = FlexLayout::new();
let _ = b.add_leaf(Style::default()).unwrap();
assert!(b.add_container(Style::default(), &[leaf_a]).is_err());
assert!(b.compute(leaf_a, Rect::new(0.0, 0.0, 10.0, 10.0)).is_err());
let rects = a.compute(root_a, Rect::new(0.0, 0.0, 10.0, 10.0)).unwrap();
assert!(rects.contains_key(&root_a));
}
#[test]
fn reparenting_a_node_errors_not_silently_moves_it() {
let mut flex = FlexLayout::new();
let leaf = flex.add_leaf(Style::default()).unwrap();
let _first_parent = flex.add_container(Style::default(), &[leaf]).unwrap();
assert!(matches!(
flex.add_container(Style::default(), &[leaf]),
Err(FlexLayoutError::AlreadyParented(node)) if node == leaf
));
}
#[test]
fn without_rounding_preserves_fractional_values() {
let mut flex = FlexLayout::new().without_rounding();
let a = flex
.add_leaf(Style {
size: Size {
width: percent(1.0 / 3.0),
height: percent(1.0),
},
..Default::default()
})
.unwrap();
let root = flex
.add_container(
Style {
flex_direction: FlexDirection::Row,
size: Size {
width: percent(1.0),
height: percent(1.0),
},
..Default::default()
},
&[a],
)
.unwrap();
let rects = flex.compute(root, Rect::new(0.0, 0.0, 10.0, 1.0)).unwrap();
assert!((rects[&a].width - 10.0 / 3.0).abs() < 0.01);
assert_ne!(rects[&a].width, rects[&a].width.round());
}
#[test]
fn non_shrinking_fixed_leaves_pack_left_to_right_even_past_container_width() {
let mut flex = FlexLayout::new().without_rounding();
let widths = [6.0_f32, 6.0, 6.0];
let leaves: Vec<NodeId> = widths
.iter()
.map(|&w| {
flex.add_leaf(Style {
size: Size {
width: length(w),
height: percent(1.0),
},
flex_shrink: 0.0,
..Default::default()
})
.unwrap()
})
.collect();
let root = flex
.add_container(
Style {
flex_direction: FlexDirection::Row,
size: Size {
width: length(9.0),
height: length(1.0),
},
..Default::default()
},
&leaves,
)
.unwrap();
let rects = flex.compute(root, Rect::new(0.0, 0.0, 9.0, 1.0)).unwrap();
assert_eq!(rects[&leaves[0]].x, 0.0);
assert_eq!(rects[&leaves[1]].x, 6.0);
assert_eq!(rects[&leaves[2]].x, 12.0);
for leaf in &leaves {
assert_eq!(rects[leaf].width, 6.0);
}
}
#[test]
fn pack_1d_row_cumulative_positions_with_origin() {
let positions = pack_1d(FlexDirection::Row, 5.0, &[10.0, 20.0, 30.0], 0.0, 4.0);
assert_eq!(positions, vec![5.0, 15.0, 35.0]);
}
#[test]
fn pack_1d_column_cumulative_positions_with_origin() {
let positions = pack_1d(FlexDirection::Column, 2.0, &[10.0, 20.0, 30.0], 0.0, 4.0);
assert_eq!(positions, vec![2.0, 12.0, 32.0]);
}
#[test]
fn pack_1d_inserts_gap_between_children_only() {
let positions = pack_1d(FlexDirection::Row, 0.0, &[10.0, 10.0, 10.0], 2.0, 4.0);
assert_eq!(positions, vec![0.0, 12.0, 24.0]);
}
#[test]
fn pack_1d_empty_sizes_returns_empty() {
assert_eq!(
pack_1d(FlexDirection::Row, 0.0, &[], 0.0, 4.0),
Vec::<f32>::new()
);
}
#[test]
fn pack_1d_single_child_ignores_gap() {
let positions = pack_1d(FlexDirection::Row, 3.0, &[7.0], 5.0, 4.0);
assert_eq!(positions, vec![3.0]);
}
}