use std::collections::HashMap;
use taffy::{
NodeId, TaffyTree, TraversePartialTree,
geometry::{Point, Rect as TaffyRect, Size},
style::{
AlignContent, AlignItems, AlignSelf, AvailableSpace, Dimension, Display as TaffyDisplay,
FlexDirection, FlexWrap as TaffyFlexWrap, JustifyContent, LengthPercentage,
LengthPercentageAuto, Overflow as TaffyOverflow, Position as TaffyPosition,
Style as TaffyStyle,
},
};
use crate::dom::{
Align, ContentAlign, Dim, Display, FlexDir, FlexWrap, Lp, Overflow, Position, Style,
};
use super::engine::{LayoutEngine, MeasureFn, Rect};
pub fn style_to_taffy(s: &Style) -> TaffyStyle {
TaffyStyle {
position: map_position(s.position),
inset: TaffyRect {
top: map_dim_lpa(s.top.as_ref()),
right: map_dim_lpa(s.right.as_ref()),
bottom: map_dim_lpa(s.bottom.as_ref()),
left: map_dim_lpa(s.left.as_ref()),
},
margin: TaffyRect {
top: map_lpa(
s.margin_top
.as_ref()
.or(s.margin_y.as_ref())
.or(s.margin.as_ref()),
),
bottom: map_lpa(
s.margin_bottom
.as_ref()
.or(s.margin_y.as_ref())
.or(s.margin.as_ref()),
),
left: map_lpa(
s.margin_left
.as_ref()
.or(s.margin_x.as_ref())
.or(s.margin.as_ref()),
),
right: map_lpa(
s.margin_right
.as_ref()
.or(s.margin_x.as_ref())
.or(s.margin.as_ref()),
),
},
padding: TaffyRect {
top: map_lp(
s.padding_top
.as_ref()
.or(s.padding_y.as_ref())
.or(s.padding.as_ref()),
),
bottom: map_lp(
s.padding_bottom
.as_ref()
.or(s.padding_y.as_ref())
.or(s.padding.as_ref()),
),
left: map_lp(
s.padding_left
.as_ref()
.or(s.padding_x.as_ref())
.or(s.padding.as_ref()),
),
right: map_lp(
s.padding_right
.as_ref()
.or(s.padding_x.as_ref())
.or(s.padding.as_ref()),
),
},
border: {
let [top, right, bottom, left] = s.border_edges();
TaffyRect {
top: LengthPercentage::length(top as f32),
right: LengthPercentage::length(right as f32),
bottom: LengthPercentage::length(bottom as f32),
left: LengthPercentage::length(left as f32),
}
},
flex_direction: map_flex_dir(s.flex_direction),
flex_wrap: map_flex_wrap(s.flex_wrap),
flex_grow: s.flex_grow.unwrap_or(0.0),
flex_shrink: s.flex_shrink.unwrap_or(1.0),
flex_basis: map_dim(s.flex_basis.as_ref()),
align_items: s.align_items.map(map_align_items),
align_self: s.align_self.map(map_align_self),
align_content: Some(
s.align_content
.map(map_content_align_content)
.unwrap_or(AlignContent::FlexStart),
),
justify_content: s.justify_content.map(map_content_align_justify),
size: Size {
width: map_dim(s.width.as_ref()),
height: map_dim(s.height.as_ref()),
},
min_size: Size {
width: map_dim(s.min_width.as_ref()),
height: map_dim(s.min_height.as_ref()),
},
max_size: Size {
width: map_dim(s.max_width.as_ref()),
height: map_dim(s.max_height.as_ref()),
},
aspect_ratio: s.aspect_ratio,
display: map_display(s.display),
gap: Size {
width: map_gap(s.column_gap.or(s.gap)),
height: map_gap(s.row_gap.or(s.gap)),
},
overflow: Point {
x: map_overflow(s.overflow_x),
y: map_overflow(s.overflow_y),
},
..Default::default()
}
}
fn map_position(p: Option<Position>) -> TaffyPosition {
match p.unwrap_or(Position::Relative) {
Position::Absolute => TaffyPosition::Absolute,
Position::Relative | Position::Static => TaffyPosition::Relative,
}
}
fn map_dim(d: Option<&Dim>) -> Dimension {
match d {
None | Some(Dim::Auto) => Dimension::auto(),
Some(Dim::Points(v)) => Dimension::length(*v),
Some(Dim::Percent(p)) => Dimension::percent(p / 100.0),
}
}
fn map_dim_lpa(d: Option<&Dim>) -> LengthPercentageAuto {
match d {
None | Some(Dim::Auto) => LengthPercentageAuto::auto(),
Some(Dim::Points(v)) => LengthPercentageAuto::length(*v),
Some(Dim::Percent(p)) => LengthPercentageAuto::percent(p / 100.0),
}
}
fn map_lp(lp: Option<&Lp>) -> LengthPercentage {
match lp {
None => LengthPercentage::length(0.0),
Some(Lp::Points(v)) => LengthPercentage::length(*v),
Some(Lp::Percent(p)) => LengthPercentage::percent(p / 100.0),
}
}
fn map_lpa(lp: Option<&Lp>) -> LengthPercentageAuto {
match lp {
None => LengthPercentageAuto::length(0.0),
Some(Lp::Points(v)) => LengthPercentageAuto::length(*v),
Some(Lp::Percent(p)) => LengthPercentageAuto::percent(p / 100.0),
}
}
fn map_flex_dir(d: Option<FlexDir>) -> FlexDirection {
match d.unwrap_or(FlexDir::Row) {
FlexDir::Row => FlexDirection::Row,
FlexDir::Column => FlexDirection::Column,
FlexDir::RowReverse => FlexDirection::RowReverse,
FlexDir::ColumnReverse => FlexDirection::ColumnReverse,
}
}
fn map_flex_wrap(w: Option<FlexWrap>) -> TaffyFlexWrap {
match w.unwrap_or(FlexWrap::NoWrap) {
FlexWrap::NoWrap => TaffyFlexWrap::NoWrap,
FlexWrap::Wrap => TaffyFlexWrap::Wrap,
FlexWrap::WrapReverse => TaffyFlexWrap::WrapReverse,
}
}
fn map_align_items(a: Align) -> AlignItems {
match a {
Align::Stretch => AlignItems::Stretch,
Align::FlexStart => AlignItems::FlexStart,
Align::Center => AlignItems::Center,
Align::FlexEnd => AlignItems::FlexEnd,
Align::Baseline => AlignItems::Baseline,
}
}
fn map_align_self(a: Align) -> AlignSelf {
match a {
Align::Stretch => AlignSelf::Stretch,
Align::FlexStart => AlignSelf::FlexStart,
Align::Center => AlignSelf::Center,
Align::FlexEnd => AlignSelf::FlexEnd,
Align::Baseline => AlignSelf::Baseline,
}
}
fn map_content_align_content(c: ContentAlign) -> AlignContent {
match c {
ContentAlign::FlexStart => AlignContent::FlexStart,
ContentAlign::Center => AlignContent::Center,
ContentAlign::FlexEnd => AlignContent::FlexEnd,
ContentAlign::SpaceBetween => AlignContent::SpaceBetween,
ContentAlign::SpaceAround => AlignContent::SpaceAround,
ContentAlign::SpaceEvenly => AlignContent::SpaceEvenly,
ContentAlign::Stretch => AlignContent::Stretch,
}
}
fn map_content_align_justify(c: ContentAlign) -> JustifyContent {
match c {
ContentAlign::FlexStart => JustifyContent::FlexStart,
ContentAlign::Center => JustifyContent::Center,
ContentAlign::FlexEnd => JustifyContent::FlexEnd,
ContentAlign::SpaceBetween => JustifyContent::SpaceBetween,
ContentAlign::SpaceAround => JustifyContent::SpaceAround,
ContentAlign::SpaceEvenly => JustifyContent::SpaceEvenly,
ContentAlign::Stretch => JustifyContent::Stretch,
}
}
fn map_display(d: Option<Display>) -> TaffyDisplay {
match d.unwrap_or(Display::Flex) {
Display::Flex => TaffyDisplay::Flex,
Display::None => TaffyDisplay::None,
}
}
fn map_gap(g: Option<f32>) -> LengthPercentage {
match g {
None => LengthPercentage::length(0.0),
Some(v) => LengthPercentage::length(v),
}
}
fn map_overflow(o: Option<Overflow>) -> TaffyOverflow {
match o.unwrap_or(Overflow::Visible) {
Overflow::Visible => TaffyOverflow::Visible,
Overflow::Hidden => TaffyOverflow::Hidden,
}
}
pub struct TaffyEngine {
tree: TaffyTree<u32>,
id_map: HashMap<u32, NodeId>,
measures: HashMap<u32, Box<MeasureFn>>,
rounded: HashMap<u32, Rect>,
rounded_absolute: HashMap<u32, Rect>,
}
impl TaffyEngine {
pub fn new() -> Self {
let mut tree = TaffyTree::new();
tree.disable_rounding();
Self {
tree,
id_map: HashMap::new(),
measures: HashMap::new(),
rounded: HashMap::new(),
rounded_absolute: HashMap::new(),
}
}
fn taffy_id(&self, dom_id: u32) -> Result<NodeId, String> {
self.id_map
.get(&dom_id)
.copied()
.ok_or_else(|| format!("layout: unknown dom id {dom_id}"))
}
pub fn computed_absolute(&self, id: u32) -> Option<Rect> {
self.rounded_absolute.get(&id).copied()
}
}
fn inexact_equals(a: f64, b: f64) -> bool {
(a - b).abs() < 0.0001
}
fn round_value_to_pixel_grid(value: f64, force_ceil: bool, force_floor: bool) -> f64 {
let mut scaled = value;
let mut fractial = scaled % 1.0;
if fractial < 0.0 {
fractial += 1.0;
}
let delta = if inexact_equals(fractial, 0.0) {
-fractial
} else if inexact_equals(fractial, 1.0) || force_ceil {
1.0 - fractial
} else if force_floor {
-fractial
} else if fractial > 0.5 || inexact_equals(fractial, 0.5) {
1.0 - fractial
} else {
-fractial
};
scaled += delta;
scaled
}
struct RoundOrigin {
absolute_left: f64,
absolute_top: f64,
rounded_left: i32,
rounded_top: i32,
}
fn round_node(
tree: &TaffyTree<u32>,
is_text: &dyn Fn(u32) -> bool,
nid: NodeId,
dom_id: u32,
origin: RoundOrigin,
out: &mut HashMap<u32, Rect>,
out_absolute: &mut HashMap<u32, Rect>,
) {
let RoundOrigin {
absolute_left,
absolute_top,
rounded_left,
rounded_top,
} = origin;
let Ok(layout) = tree.layout(nid) else {
return;
};
let node_left = f64::from(layout.location.x);
let node_top = f64::from(layout.location.y);
let node_width = f64::from(layout.size.width);
let node_height = f64::from(layout.size.height);
let absolute_node_left = absolute_left + node_left;
let absolute_node_top = absolute_top + node_top;
let absolute_node_right = absolute_node_left + node_width;
let absolute_node_bottom = absolute_node_top + node_height;
let text_rounding = is_text(dom_id);
let rx = round_value_to_pixel_grid(node_left, false, text_rounding);
let ry = round_value_to_pixel_grid(node_top, false, text_rounding);
let has_fractional_width =
!inexact_equals(node_width % 1.0, 0.0) && !inexact_equals(node_width % 1.0, 1.0);
let has_fractional_height =
!inexact_equals(node_height % 1.0, 0.0) && !inexact_equals(node_height % 1.0, 1.0);
let rw = round_value_to_pixel_grid(
absolute_node_right,
text_rounding && has_fractional_width,
text_rounding && !has_fractional_width,
) - round_value_to_pixel_grid(absolute_node_left, false, text_rounding);
let rh = round_value_to_pixel_grid(
absolute_node_bottom,
text_rounding && has_fractional_height,
text_rounding && !has_fractional_height,
) - round_value_to_pixel_grid(absolute_node_top, false, text_rounding);
let rect = Rect {
x: rx.round() as i32,
y: ry.round() as i32,
width: rw.max(0.0).round() as u16,
height: rh.max(0.0).round() as u16,
};
out.insert(dom_id, rect);
let abs_x = rounded_left + rect.x;
let abs_y = rounded_top + rect.y;
out_absolute.insert(
dom_id,
Rect {
x: abs_x,
y: abs_y,
..rect
},
);
let child_count = tree.child_count(nid);
for index in 0..child_count {
let Ok(child_nid) = tree.child_at_index(nid, index) else {
continue;
};
let Some(child_dom) = tree.get_node_context(child_nid).copied() else {
continue;
};
round_node(
tree,
is_text,
child_nid,
child_dom,
RoundOrigin {
absolute_left: absolute_node_left,
absolute_top: absolute_node_top,
rounded_left: abs_x,
rounded_top: abs_y,
},
out,
out_absolute,
);
}
}
impl Default for TaffyEngine {
fn default() -> Self {
Self::new()
}
}
impl LayoutEngine for TaffyEngine {
fn create(&mut self, id: u32) -> Result<(), String> {
if self.id_map.contains_key(&id) {
return Ok(());
}
let nid = self
.tree
.new_leaf_with_context(TaffyStyle::default(), id)
.map_err(|e| e.to_string())?;
self.id_map.insert(id, nid);
Ok(())
}
fn apply_style(&mut self, id: u32, style: &Style) -> Result<(), String> {
let nid = self.taffy_id(id)?;
self.tree
.set_style(nid, style_to_taffy(style))
.map_err(|e| e.to_string())
}
fn set_measure(&mut self, id: u32, f: Box<MeasureFn>) {
self.measures.insert(id, f);
}
fn insert_child(&mut self, parent: u32, child: u32, index: usize) -> Result<(), String> {
let pnid = self.taffy_id(parent)?;
let cnid = self.taffy_id(child)?;
let child_count = self.tree.child_count(pnid);
if index >= child_count {
self.tree.add_child(pnid, cnid).map_err(|e| e.to_string())
} else {
self.tree
.insert_child_at_index(pnid, index, cnid)
.map_err(|e| e.to_string())
}
}
fn remove_child(&mut self, parent: u32, child: u32) -> Result<(), String> {
let pnid = self.taffy_id(parent)?;
let cnid = self.taffy_id(child)?;
self.tree
.remove_child(pnid, cnid)
.map(|_| ())
.map_err(|e| e.to_string())
}
fn destroy(&mut self, id: u32) {
let Some(nid) = self.id_map.remove(&id) else {
return;
};
self.measures.remove(&id);
self.rounded.remove(&id);
self.rounded_absolute.remove(&id);
let _ = self.tree.remove(nid);
}
fn mark_dirty(&mut self, id: u32) -> Result<(), String> {
let nid = self.taffy_id(id)?;
self.tree.mark_dirty(nid).map_err(|e| e.to_string())
}
fn calculate(
&mut self,
root_id: u32,
viewport_width: f32,
viewport_height: Option<f32>,
) -> Result<(), String> {
let root_nid = self.taffy_id(root_id)?;
if let Ok(mut s) = self.tree.style(root_nid).cloned() {
s.size.width = Dimension::length(viewport_width);
let _ = self.tree.set_style(root_nid, s);
}
let available = Size {
width: AvailableSpace::Definite(viewport_width),
height: match viewport_height {
Some(h) => AvailableSpace::Definite(h),
None => AvailableSpace::MaxContent,
},
};
self.tree
.compute_layout_with_measure(root_nid, available, |known, avail, _nid, ctx, _style| {
if let Some(&mut did) = ctx
&& let Some(f) = self.measures.get_mut(&did)
{
return f(known, avail);
}
Size::ZERO
})
.map_err(|e| e.to_string())?;
self.rounded.clear();
self.rounded_absolute.clear();
let is_text = |dom_id: u32| self.measures.contains_key(&dom_id);
round_node(
&self.tree,
&is_text,
root_nid,
root_id,
RoundOrigin {
absolute_left: 0.0,
absolute_top: 0.0,
rounded_left: 0,
rounded_top: 0,
},
&mut self.rounded,
&mut self.rounded_absolute,
);
Ok(())
}
fn computed(&self, id: u32) -> Option<Rect> {
if let Some(r) = self.rounded.get(&id) {
return Some(*r);
}
let nid = self.id_map.get(&id).copied()?;
let lay = self.tree.layout(nid).ok()?;
Some(Rect {
x: lay.location.x as i32,
y: lay.location.y as i32,
width: lay.size.width as u16,
height: lay.size.height as u16,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::dom::BorderStyle;
use taffy::style::FlexDirection as TFD;
fn pt(v: f32) -> Dimension {
Dimension::length(v)
}
fn engine_with(ids: &[u32]) -> TaffyEngine {
let mut e = TaffyEngine::new();
for &id in ids {
e.create(id).unwrap();
}
e
}
#[test]
fn map_position_absolute() {
let s = Style {
position: Some(Position::Absolute),
..Default::default()
};
assert_eq!(style_to_taffy(&s).position, TaffyPosition::Absolute);
}
#[test]
fn map_position_relative() {
let s = Style {
position: Some(Position::Relative),
..Default::default()
};
assert_eq!(style_to_taffy(&s).position, TaffyPosition::Relative);
}
#[test]
fn map_position_static_maps_to_relative() {
let s = Style {
position: Some(Position::Static),
..Default::default()
};
assert_eq!(style_to_taffy(&s).position, TaffyPosition::Relative);
}
#[test]
fn map_inset_points() {
let s = Style {
top: Some(Dim::Points(2.0)),
left: Some(Dim::Points(5.0)),
..Default::default()
};
let t = style_to_taffy(&s);
assert_eq!(t.inset.top, LengthPercentageAuto::length(2.0));
assert_eq!(t.inset.left, LengthPercentageAuto::length(5.0));
assert_eq!(t.inset.right, LengthPercentageAuto::auto());
assert_eq!(t.inset.bottom, LengthPercentageAuto::auto());
}
#[test]
fn map_inset_percent() {
let s = Style {
top: Some(Dim::Percent(50.0)),
..Default::default()
};
assert_eq!(
style_to_taffy(&s).inset.top,
LengthPercentageAuto::percent(0.5)
);
}
#[test]
fn map_margin_cascade() {
let s = Style {
margin: Some(Lp::Points(2.0)),
margin_x: Some(Lp::Points(4.0)),
margin_left: Some(Lp::Points(7.0)),
..Default::default()
};
let t = style_to_taffy(&s);
assert_eq!(t.margin.top, LengthPercentageAuto::length(2.0));
assert_eq!(t.margin.bottom, LengthPercentageAuto::length(2.0));
assert_eq!(t.margin.right, LengthPercentageAuto::length(4.0));
assert_eq!(t.margin.left, LengthPercentageAuto::length(7.0));
}
#[test]
fn map_margin_y_shorthand() {
let s = Style {
margin: Some(Lp::Points(1.0)),
margin_y: Some(Lp::Points(3.0)),
..Default::default()
};
let t = style_to_taffy(&s);
assert_eq!(t.margin.top, LengthPercentageAuto::length(3.0));
assert_eq!(t.margin.bottom, LengthPercentageAuto::length(3.0));
assert_eq!(t.margin.left, LengthPercentageAuto::length(1.0));
assert_eq!(t.margin.right, LengthPercentageAuto::length(1.0));
}
#[test]
fn map_margin_percent() {
let s = Style {
margin: Some(Lp::Percent(50.0)),
..Default::default()
};
let t = style_to_taffy(&s);
assert_eq!(t.margin.top, LengthPercentageAuto::percent(0.5));
}
#[test]
fn map_padding_cascade() {
let s = Style {
padding: Some(Lp::Points(2.0)),
padding_x: Some(Lp::Points(4.0)),
padding_top: Some(Lp::Points(1.0)),
..Default::default()
};
let t = style_to_taffy(&s);
assert_eq!(t.padding.top, LengthPercentage::length(1.0));
assert_eq!(t.padding.bottom, LengthPercentage::length(2.0));
assert_eq!(t.padding.left, LengthPercentage::length(4.0));
assert_eq!(t.padding.right, LengthPercentage::length(4.0));
}
#[test]
fn map_flex_direction_column() {
let s = Style {
flex_direction: Some(FlexDir::Column),
..Default::default()
};
assert_eq!(style_to_taffy(&s).flex_direction, FlexDirection::Column);
}
#[test]
fn map_flex_direction_row_reverse() {
let s = Style {
flex_direction: Some(FlexDir::RowReverse),
..Default::default()
};
assert_eq!(style_to_taffy(&s).flex_direction, FlexDirection::RowReverse);
}
#[test]
fn map_flex_wrap() {
let s = Style {
flex_wrap: Some(FlexWrap::Wrap),
..Default::default()
};
assert_eq!(style_to_taffy(&s).flex_wrap, TaffyFlexWrap::Wrap);
}
#[test]
fn map_flex_grow_shrink() {
let s = Style {
flex_grow: Some(2.0),
flex_shrink: Some(0.5),
..Default::default()
};
let t = style_to_taffy(&s);
assert_eq!(t.flex_grow, 2.0);
assert_eq!(t.flex_shrink, 0.5);
}
#[test]
fn map_flex_basis_points() {
let s = Style {
flex_basis: Some(Dim::Points(40.0)),
..Default::default()
};
assert_eq!(style_to_taffy(&s).flex_basis, Dimension::length(40.0));
}
#[test]
fn map_flex_basis_percent() {
let s = Style {
flex_basis: Some(Dim::Percent(50.0)),
..Default::default()
};
assert_eq!(style_to_taffy(&s).flex_basis, Dimension::percent(0.5));
}
#[test]
fn map_flex_basis_auto() {
let s = Style {
flex_basis: Some(Dim::Auto),
..Default::default()
};
assert_eq!(style_to_taffy(&s).flex_basis, Dimension::auto());
}
#[test]
fn map_align_items_center() {
let s = Style {
align_items: Some(Align::Center),
..Default::default()
};
assert_eq!(style_to_taffy(&s).align_items, Some(AlignItems::Center));
}
#[test]
fn map_align_self_none_is_auto() {
let s = Style {
align_self: None,
..Default::default()
};
assert_eq!(style_to_taffy(&s).align_self, None);
}
#[test]
fn map_align_content_space_between() {
let s = Style {
align_content: Some(ContentAlign::SpaceBetween),
..Default::default()
};
assert_eq!(
style_to_taffy(&s).align_content,
Some(AlignContent::SpaceBetween)
);
}
#[test]
fn map_justify_content_center() {
let s = Style {
justify_content: Some(ContentAlign::Center),
..Default::default()
};
assert_eq!(
style_to_taffy(&s).justify_content,
Some(JustifyContent::Center)
);
}
#[test]
fn map_width_height_points() {
let s = Style {
width: Some(Dim::Points(80.0)),
height: Some(Dim::Points(24.0)),
..Default::default()
};
let t = style_to_taffy(&s);
assert_eq!(t.size.width, Dimension::length(80.0));
assert_eq!(t.size.height, Dimension::length(24.0));
}
#[test]
fn map_width_percent() {
let s = Style {
width: Some(Dim::Percent(50.0)),
..Default::default()
};
assert_eq!(style_to_taffy(&s).size.width, Dimension::percent(0.5));
}
#[test]
fn map_width_auto() {
let s = Style {
width: Some(Dim::Auto),
..Default::default()
};
assert_eq!(style_to_taffy(&s).size.width, Dimension::auto());
}
#[test]
fn map_min_max_size() {
let s = Style {
min_width: Some(Dim::Points(10.0)),
max_width: Some(Dim::Points(100.0)),
min_height: Some(Dim::Points(5.0)),
max_height: Some(Dim::Points(50.0)),
..Default::default()
};
let t = style_to_taffy(&s);
assert_eq!(t.min_size.width, Dimension::length(10.0));
assert_eq!(t.max_size.width, Dimension::length(100.0));
assert_eq!(t.min_size.height, Dimension::length(5.0));
assert_eq!(t.max_size.height, Dimension::length(50.0));
}
#[test]
fn map_aspect_ratio() {
let s = Style {
aspect_ratio: Some(16.0 / 9.0),
..Default::default()
};
let t = style_to_taffy(&s);
assert!((t.aspect_ratio.unwrap() - 16.0 / 9.0).abs() < f32::EPSILON);
}
#[test]
fn map_display_flex() {
let s = Style {
display: Some(Display::Flex),
..Default::default()
};
assert_eq!(style_to_taffy(&s).display, TaffyDisplay::Flex);
}
#[test]
fn map_display_none() {
let s = Style {
display: Some(Display::None),
..Default::default()
};
assert_eq!(style_to_taffy(&s).display, TaffyDisplay::None);
}
#[test]
fn map_border_all_edges_active() {
let s = Style {
border_style: Some(BorderStyle::Named("single".into())),
..Default::default()
};
let t = style_to_taffy(&s);
assert_eq!(t.border.top, LengthPercentage::length(1.0));
assert_eq!(t.border.right, LengthPercentage::length(1.0));
assert_eq!(t.border.bottom, LengthPercentage::length(1.0));
assert_eq!(t.border.left, LengthPercentage::length(1.0));
}
#[test]
fn map_border_top_disabled() {
let s = Style {
border_style: Some(BorderStyle::Named("single".into())),
border_top: Some(false),
..Default::default()
};
let t = style_to_taffy(&s);
assert_eq!(t.border.top, LengthPercentage::length(0.0));
assert_eq!(t.border.bottom, LengthPercentage::length(1.0));
assert_eq!(t.border.left, LengthPercentage::length(1.0));
assert_eq!(t.border.right, LengthPercentage::length(1.0));
}
#[test]
fn map_border_none_when_no_border_style() {
let s = Style {
border_style: None,
..Default::default()
};
let t = style_to_taffy(&s);
assert_eq!(t.border.top, LengthPercentage::length(0.0));
assert_eq!(t.border.right, LengthPercentage::length(0.0));
assert_eq!(t.border.bottom, LengthPercentage::length(0.0));
assert_eq!(t.border.left, LengthPercentage::length(0.0));
}
#[test]
fn map_gap_all() {
let s = Style {
gap: Some(2.0),
..Default::default()
};
let t = style_to_taffy(&s);
assert_eq!(t.gap.width, LengthPercentage::length(2.0));
assert_eq!(t.gap.height, LengthPercentage::length(2.0));
}
#[test]
fn map_gap_per_axis() {
let s = Style {
column_gap: Some(4.0),
row_gap: Some(1.0),
..Default::default()
};
let t = style_to_taffy(&s);
assert_eq!(t.gap.width, LengthPercentage::length(4.0));
assert_eq!(t.gap.height, LengthPercentage::length(1.0));
}
#[test]
fn map_gap_per_axis_overrides_all() {
let s = Style {
gap: Some(2.0),
column_gap: Some(4.0),
..Default::default()
};
let t = style_to_taffy(&s);
assert_eq!(t.gap.width, LengthPercentage::length(4.0));
assert_eq!(t.gap.height, LengthPercentage::length(2.0));
}
#[test]
fn map_overflow_hidden() {
let s = Style {
overflow_x: Some(Overflow::Hidden),
..Default::default()
};
assert_eq!(style_to_taffy(&s).overflow.x, TaffyOverflow::Hidden);
assert_eq!(style_to_taffy(&s).overflow.y, TaffyOverflow::Visible);
}
#[test]
fn apply_style_width_height() {
let mut e = TaffyEngine::new();
e.create(0).unwrap();
let s = Style {
width: Some(Dim::Points(80.0)),
height: Some(Dim::Points(24.0)),
..Default::default()
};
e.apply_style(0, &s).unwrap();
e.calculate(0, 80.0, Some(24.0)).unwrap();
assert_eq!(
e.computed(0).unwrap(),
Rect {
x: 0,
y: 0,
width: 80,
height: 24
}
);
}
#[test]
fn apply_style_flex_direction_column() {
let mut e = engine_with(&[0, 1, 2]);
e.insert_child(0, 1, 0).unwrap();
e.insert_child(0, 2, 1).unwrap();
e.apply_style(
0,
&Style {
width: Some(Dim::Points(80.0)),
height: Some(Dim::Points(24.0)),
flex_direction: Some(FlexDir::Column),
..Default::default()
},
)
.unwrap();
e.apply_style(
1,
&Style {
width: Some(Dim::Points(80.0)),
height: Some(Dim::Points(12.0)),
..Default::default()
},
)
.unwrap();
e.apply_style(
2,
&Style {
width: Some(Dim::Points(80.0)),
height: Some(Dim::Points(12.0)),
..Default::default()
},
)
.unwrap();
e.calculate(0, 80.0, Some(24.0)).unwrap();
let r1 = e.computed(1).unwrap();
let r2 = e.computed(2).unwrap();
assert_eq!(
r1,
Rect {
x: 0,
y: 0,
width: 80,
height: 12
}
);
assert_eq!(
r2,
Rect {
x: 0,
y: 12,
width: 80,
height: 12
}
);
}
#[test]
fn apply_style_gap() {
let mut e = engine_with(&[0, 1, 2]);
e.insert_child(0, 1, 0).unwrap();
e.insert_child(0, 2, 1).unwrap();
e.apply_style(
0,
&Style {
width: Some(Dim::Points(80.0)),
height: Some(Dim::Points(24.0)),
gap: Some(2.0),
align_items: Some(Align::FlexStart),
..Default::default()
},
)
.unwrap();
e.apply_style(
1,
&Style {
width: Some(Dim::Points(10.0)),
height: Some(Dim::Points(5.0)),
..Default::default()
},
)
.unwrap();
e.apply_style(
2,
&Style {
width: Some(Dim::Points(10.0)),
height: Some(Dim::Points(5.0)),
..Default::default()
},
)
.unwrap();
e.calculate(0, 80.0, Some(24.0)).unwrap();
let c1 = e.computed(1).unwrap();
let c2 = e.computed(2).unwrap();
assert_eq!(c1.x, 0);
assert_eq!(c2.x, 12); }
#[test]
fn apply_style_padding() {
let mut e = engine_with(&[0, 1]);
e.insert_child(0, 1, 0).unwrap();
e.apply_style(
0,
&Style {
width: Some(Dim::Points(80.0)),
height: Some(Dim::Points(24.0)),
padding: Some(Lp::Points(2.0)),
align_items: Some(Align::FlexStart),
..Default::default()
},
)
.unwrap();
e.apply_style(
1,
&Style {
width: Some(Dim::Points(10.0)),
height: Some(Dim::Points(5.0)),
..Default::default()
},
)
.unwrap();
e.calculate(0, 80.0, Some(24.0)).unwrap();
let child = e.computed(1).unwrap();
assert_eq!(child.x, 2);
assert_eq!(child.y, 2);
}
#[test]
fn apply_style_border() {
let mut e = engine_with(&[0, 1]);
e.insert_child(0, 1, 0).unwrap();
e.apply_style(
0,
&Style {
width: Some(Dim::Points(10.0)),
height: Some(Dim::Points(5.0)),
border_style: Some(BorderStyle::Named("single".into())),
align_items: Some(Align::FlexStart),
..Default::default()
},
)
.unwrap();
e.apply_style(
1,
&Style {
width: Some(Dim::Points(6.0)),
height: Some(Dim::Points(3.0)),
..Default::default()
},
)
.unwrap();
e.calculate(0, 10.0, Some(5.0)).unwrap();
let child = e.computed(1).unwrap();
assert_eq!(child.x, 1);
assert_eq!(child.y, 1);
}
#[test]
fn computed_absolute_nested_offsets() {
let mut e = engine_with(&[0, 1, 2]);
e.insert_child(0, 1, 0).unwrap();
e.insert_child(1, 2, 0).unwrap();
e.apply_style(
0,
&Style {
width: Some(Dim::Points(40.0)),
height: Some(Dim::Points(10.0)),
padding: Some(Lp::Points(2.0)),
align_items: Some(Align::FlexStart),
..Default::default()
},
)
.unwrap();
e.apply_style(
1,
&Style {
width: Some(Dim::Points(20.0)),
height: Some(Dim::Points(6.0)),
margin_left: Some(Lp::Points(3.0)),
margin_top: Some(Lp::Points(2.0)),
align_items: Some(Align::FlexStart),
..Default::default()
},
)
.unwrap();
e.apply_style(
2,
&Style {
width: Some(Dim::Points(6.0)),
height: Some(Dim::Points(2.0)),
margin_left: Some(Lp::Points(4.0)),
margin_top: Some(Lp::Points(1.0)),
..Default::default()
},
)
.unwrap();
e.calculate(0, 40.0, Some(10.0)).unwrap();
assert_eq!(
e.computed(1).unwrap(),
Rect {
x: 5,
y: 4,
width: 20,
height: 6
}
);
assert_eq!(
e.computed(2).unwrap(),
Rect {
x: 4,
y: 1,
width: 6,
height: 2
}
);
assert_eq!(
e.computed_absolute(0).unwrap(),
Rect {
x: 0,
y: 0,
width: 40,
height: 10
}
);
assert_eq!(
e.computed_absolute(1).unwrap(),
Rect {
x: 5,
y: 4,
width: 20,
height: 6
}
);
assert_eq!(
e.computed_absolute(2).unwrap(),
Rect {
x: 9,
y: 5,
width: 6,
height: 2
}
);
assert_ne!(e.computed_absolute(2), e.computed(2));
assert_eq!(e.computed_absolute(99), None);
}
#[test]
fn single_root_fills_viewport() {
let mut e = TaffyEngine::new();
e.create(0).unwrap();
let nid = e.id_map[&0];
e.tree
.set_style(
nid,
TaffyStyle {
size: taffy::geometry::Size {
width: pt(80.0),
height: pt(24.0),
},
..Default::default()
},
)
.unwrap();
e.calculate(0, 80.0, Some(24.0)).unwrap();
let r = e.computed(0).unwrap();
assert_eq!(
r,
Rect {
x: 0,
y: 0,
width: 80,
height: 24
}
);
}
#[test]
fn root_two_default_children() {
let mut e = engine_with(&[0, 1, 2]);
e.insert_child(0, 1, 0).unwrap();
e.insert_child(0, 2, 1).unwrap();
let root_nid = e.id_map[&0];
let c1_nid = e.id_map[&1];
let c2_nid = e.id_map[&2];
e.tree
.set_style(
root_nid,
TaffyStyle {
size: taffy::geometry::Size {
width: pt(80.0),
height: pt(24.0),
},
..Default::default()
},
)
.unwrap();
e.tree
.set_style(
c1_nid,
TaffyStyle {
size: taffy::geometry::Size {
width: pt(10.0),
height: pt(24.0),
},
..Default::default()
},
)
.unwrap();
e.tree
.set_style(
c2_nid,
TaffyStyle {
size: taffy::geometry::Size {
width: pt(20.0),
height: pt(24.0),
},
..Default::default()
},
)
.unwrap();
e.calculate(0, 80.0, Some(24.0)).unwrap();
assert_eq!(
e.computed(0).unwrap(),
Rect {
x: 0,
y: 0,
width: 80,
height: 24
}
);
let c1 = e.computed(1).unwrap();
let c2 = e.computed(2).unwrap();
assert_eq!(c1.x, 0);
assert_eq!(c1.y, 0);
assert_eq!(c2.y, 0); assert_eq!(c1.width, 10);
assert_eq!(c2.width, 20);
assert_eq!(c2.x, c1.x + i32::from(c1.width));
}
#[test]
fn nested_box() {
let mut e = engine_with(&[0, 1, 2]);
e.insert_child(0, 1, 0).unwrap();
e.insert_child(1, 2, 0).unwrap();
let root_nid = e.id_map[&0];
let outer_nid = e.id_map[&1];
let inner_nid = e.id_map[&2];
e.tree
.set_style(
root_nid,
TaffyStyle {
size: taffy::geometry::Size {
width: pt(80.0),
height: pt(24.0),
},
..Default::default()
},
)
.unwrap();
e.tree
.set_style(
outer_nid,
TaffyStyle {
size: taffy::geometry::Size {
width: pt(40.0),
height: pt(24.0),
},
..Default::default()
},
)
.unwrap();
e.tree
.set_style(
inner_nid,
TaffyStyle {
size: taffy::geometry::Size {
width: pt(20.0),
height: pt(24.0),
},
..Default::default()
},
)
.unwrap();
e.calculate(0, 80.0, Some(24.0)).unwrap();
assert_eq!(
e.computed(0).unwrap(),
Rect {
x: 0,
y: 0,
width: 80,
height: 24
}
);
assert_eq!(
e.computed(1).unwrap(),
Rect {
x: 0,
y: 0,
width: 40,
height: 24
}
);
assert_eq!(
e.computed(2).unwrap(),
Rect {
x: 0,
y: 0,
width: 20,
height: 24
}
);
}
#[test]
fn recalculate_after_style_change() {
let mut e = TaffyEngine::new();
e.create(0).unwrap();
let initial = Style {
width: Some(Dim::Points(80.0)),
height: Some(Dim::Points(24.0)),
..Default::default()
};
e.apply_style(0, &initial).unwrap();
e.calculate(0, 80.0, Some(24.0)).unwrap();
assert_eq!(
e.computed(0).unwrap(),
Rect {
x: 0,
y: 0,
width: 80,
height: 24
}
);
let resized = Style {
width: Some(Dim::Points(40.0)),
height: Some(Dim::Points(12.0)),
..Default::default()
};
e.apply_style(0, &resized).unwrap();
e.mark_dirty(0).unwrap();
e.calculate(0, 40.0, Some(12.0)).unwrap();
assert_eq!(
e.computed(0).unwrap(),
Rect {
x: 0,
y: 0,
width: 40,
height: 12
}
);
}
#[test]
fn computed_unknown_id_returns_none() {
let e = TaffyEngine::new();
assert_eq!(e.computed(999), None);
}
#[test]
fn create_duplicate_is_noop() {
let mut e = TaffyEngine::new();
e.create(0).unwrap();
let nid_first = e.id_map[&0];
e.create(0).unwrap();
assert_eq!(e.id_map[&0], nid_first);
}
#[test]
fn measure_fn_is_invoked() {
let mut e = TaffyEngine::new();
e.create(0).unwrap();
e.create(1).unwrap();
let root_nid = e.id_map[&0];
e.tree
.set_style(
root_nid,
TaffyStyle {
size: taffy::geometry::Size {
width: pt(80.0),
height: pt(24.0),
},
align_items: Some(AlignItems::FlexStart),
..Default::default()
},
)
.unwrap();
e.insert_child(0, 1, 0).unwrap();
e.set_measure(
1,
Box::new(|_known, _avail| Size {
width: 10.0,
height: 3.0,
}),
);
e.calculate(0, 80.0, Some(24.0)).unwrap();
let leaf = e.computed(1).unwrap();
assert_eq!(leaf.width, 10);
assert_eq!(leaf.height, 3);
}
#[test]
fn insert_child_index_clamp_appends() {
let mut e = engine_with(&[0, 1, 2]);
e.insert_child(0, 1, 0).unwrap();
e.insert_child(0, 2, 9999).unwrap();
let root_nid = e.id_map[&0];
let root_nid_children = e.tree.children(root_nid).unwrap();
assert_eq!(root_nid_children, vec![e.id_map[&1], e.id_map[&2]]);
}
#[test]
fn destroy_removes_node() {
let mut e = TaffyEngine::new();
e.create(0).unwrap();
e.create(1).unwrap();
e.set_measure(
1,
Box::new(|_, _| Size {
width: 5.0,
height: 1.0,
}),
);
assert!(e.computed(1).is_some());
e.insert_child(0, 1, 0).unwrap();
let root_s = crate::dom::Style {
width: Some(crate::dom::Dim::Points(80.0)),
height: Some(crate::dom::Dim::Points(24.0)),
..Default::default()
};
e.apply_style(0, &root_s).unwrap();
e.calculate(0, 80.0, Some(24.0)).unwrap();
assert!(e.computed(1).is_some());
e.destroy(1);
assert!(e.computed(1).is_none());
assert!(!e.id_map.contains_key(&1));
assert!(!e.measures.contains_key(&1));
}
#[test]
fn destroy_unknown_id_is_noop() {
let mut e = TaffyEngine::new();
e.create(0).unwrap();
e.destroy(999); assert!(e.computed(0).is_some());
}
#[test]
fn destroy_then_recreate_clean_state() {
let mut e = TaffyEngine::new();
e.create(0).unwrap();
e.create(1).unwrap();
e.insert_child(0, 1, 0).unwrap();
let root_s = crate::dom::Style {
width: Some(crate::dom::Dim::Points(80.0)),
height: Some(crate::dom::Dim::Points(24.0)),
align_items: Some(crate::dom::Align::FlexStart),
..Default::default()
};
e.apply_style(0, &root_s).unwrap();
e.set_measure(
1,
Box::new(|_, _| Size {
width: 5.0,
height: 1.0,
}),
);
e.calculate(0, 80.0, Some(24.0)).unwrap();
assert_eq!(e.computed(1).unwrap().width, 5);
e.destroy(1);
e.destroy(0);
e.create(0).unwrap();
e.create(1).unwrap();
e.insert_child(0, 1, 0).unwrap();
e.apply_style(0, &root_s).unwrap();
e.set_measure(
1,
Box::new(|_, _| Size {
width: 7.0,
height: 1.0,
}),
);
e.calculate(0, 80.0, Some(24.0)).unwrap();
assert_eq!(e.computed(1).unwrap().width, 7);
}
#[test]
fn remove_child_detaches() {
let mut e = engine_with(&[0, 1]);
e.insert_child(0, 1, 0).unwrap();
e.remove_child(0, 1).unwrap();
assert!(e.id_map.contains_key(&1));
let root_nid = e.id_map[&0];
assert_eq!(e.tree.child_count(root_nid), 0);
}
#[test]
fn flex_column_two_children() {
let mut e = engine_with(&[0, 1, 2]);
e.insert_child(0, 1, 0).unwrap();
e.insert_child(0, 2, 1).unwrap();
let root_nid = e.id_map[&0];
let c1_nid = e.id_map[&1];
let c2_nid = e.id_map[&2];
e.tree
.set_style(
root_nid,
TaffyStyle {
size: taffy::geometry::Size {
width: pt(80.0),
height: pt(24.0),
},
flex_direction: TFD::Column,
..Default::default()
},
)
.unwrap();
e.tree
.set_style(
c1_nid,
TaffyStyle {
size: taffy::geometry::Size {
width: pt(80.0),
height: pt(12.0),
},
..Default::default()
},
)
.unwrap();
e.tree
.set_style(
c2_nid,
TaffyStyle {
size: taffy::geometry::Size {
width: pt(80.0),
height: pt(12.0),
},
..Default::default()
},
)
.unwrap();
e.calculate(0, 80.0, Some(24.0)).unwrap();
let r1 = e.computed(1).unwrap();
let r2 = e.computed(2).unwrap();
assert_eq!(
r1,
Rect {
x: 0,
y: 0,
width: 80,
height: 12
}
);
assert_eq!(
r2,
Rect {
x: 0,
y: 12,
width: 80,
height: 12
}
);
}
#[test]
fn kernel_fractional_near_zero_floors_to_whole() {
assert_eq!(round_value_to_pixel_grid(5.00005, false, false), 5.0);
assert_eq!(round_value_to_pixel_grid(5.00005, true, false), 5.0);
assert_eq!(round_value_to_pixel_grid(5.00005, false, true), 5.0);
}
#[test]
fn kernel_fractional_near_one_ceils_before_force_floor() {
assert_eq!(round_value_to_pixel_grid(4.99995, false, false), 5.0);
assert_eq!(round_value_to_pixel_grid(4.99995, false, true), 5.0);
}
#[test]
fn kernel_force_ceil_rounds_up() {
assert_eq!(round_value_to_pixel_grid(4.3, true, false), 5.0);
}
#[test]
fn kernel_force_floor_rounds_down() {
assert_eq!(round_value_to_pixel_grid(4.7, false, true), 4.0);
}
#[test]
fn kernel_round_half_up_tie() {
assert_eq!(round_value_to_pixel_grid(4.5, false, false), 5.0);
assert_eq!(round_value_to_pixel_grid(4.49, false, false), 4.0);
assert_eq!(round_value_to_pixel_grid(4.51, false, false), 5.0);
}
#[test]
fn kernel_half_epsilon_tie_ceils() {
assert_eq!(round_value_to_pixel_grid(4.49995, false, false), 5.0);
}
#[test]
fn kernel_negative_values() {
assert_eq!(round_value_to_pixel_grid(-4.5, false, false), -4.0);
assert_eq!(round_value_to_pixel_grid(-4.3, false, false), -4.0);
assert_eq!(round_value_to_pixel_grid(-4.7, false, false), -5.0);
}
#[test]
fn inexact_equals_at_threshold_is_not_equal() {
assert!(!inexact_equals(0.0001, 0.0));
}
#[test]
fn inexact_equals_just_under_threshold_is_equal() {
assert!(inexact_equals(0.00009, 0.0));
}
}