Skip to main content

rustmotion_core/engine/
layout_pass.rs

1//! Layout pass — runs taffy on the BoxTree to produce per-node geometry.
2
3use std::collections::HashMap;
4
5use taffy::prelude as tf;
6use taffy::TaffyTree;
7
8use crate::css::taffy_bridge::{to_taffy_style, ConversionContext};
9use crate::engine::box_tree::{BoxNode, IntrinsicMeasure, NodeId};
10
11/// Resolved geometry for a single node, in absolute viewport coordinates.
12#[derive(Debug, Clone, Copy, Default)]
13pub struct BoxLayout {
14    pub x: f32,
15    pub y: f32,
16    pub width: f32,
17    pub height: f32,
18    pub border: Insets,
19    pub padding: Insets,
20}
21
22#[derive(Debug, Clone, Copy, Default)]
23pub struct Insets {
24    pub top: f32,
25    pub right: f32,
26    pub bottom: f32,
27    pub left: f32,
28}
29
30impl BoxLayout {
31    pub fn content_box(&self) -> (f32, f32, f32, f32) {
32        let x = self.x + self.border.left + self.padding.left;
33        let y = self.y + self.border.top + self.padding.top;
34        let w = (self.width
35            - self.border.left
36            - self.border.right
37            - self.padding.left
38            - self.padding.right)
39            .max(0.0);
40        let h = (self.height
41            - self.border.top
42            - self.border.bottom
43            - self.padding.top
44            - self.padding.bottom)
45            .max(0.0);
46        (x, y, w, h)
47    }
48
49    pub fn padding_box(&self) -> (f32, f32, f32, f32) {
50        let x = self.x + self.border.left;
51        let y = self.y + self.border.top;
52        let w = (self.width - self.border.left - self.border.right).max(0.0);
53        let h = (self.height - self.border.top - self.border.bottom).max(0.0);
54        (x, y, w, h)
55    }
56}
57
58/// Result of a layout pass: layout per node, indexed by `NodeId`.
59#[derive(Debug, Clone, Default)]
60pub struct LayoutResult {
61    pub layouts: HashMap<NodeId, BoxLayout>,
62}
63
64impl LayoutResult {
65    pub fn get(&self, id: NodeId) -> Option<&BoxLayout> {
66        self.layouts.get(&id)
67    }
68}
69
70/// Per-node user data stored in the taffy tree to keep the link between
71/// taffy nodes and our `BoxNode` ids + intrinsic measurers.
72struct NodeData {
73    #[allow(dead_code)]
74    box_id: NodeId,
75    intrinsic: Option<std::sync::Arc<dyn IntrinsicMeasure>>,
76}
77
78/// Run taffy on a [`BoxNode`] tree and return the resolved layouts.
79pub fn run_layout(root: &BoxNode, viewport: (f32, f32), ctx: &ConversionContext) -> LayoutResult {
80    let mut tree: TaffyTree<NodeData> = TaffyTree::new();
81    // Disable taffy's pixel rounding: it floors widths/heights to integers,
82    // but our painters use sub-pixel Skia metrics. A width of 710.376 rounded
83    // to 710 makes the painter re-wrap onto an extra line.
84    tree.disable_rounding();
85    let mut node_map: HashMap<NodeId, tf::NodeId> = HashMap::new();
86
87    // Build the taffy tree top-down.
88    let root_tf = build(&mut tree, &mut node_map, root, ctx);
89
90    let viewport_size = tf::Size {
91        width: tf::AvailableSpace::Definite(viewport.0),
92        height: tf::AvailableSpace::Definite(viewport.1),
93    };
94    let _ = tree.compute_layout_with_measure(
95        root_tf,
96        viewport_size,
97        |known, available, _node, ctx_data, _style| {
98            let Some(ctx) = ctx_data else {
99                return tf::Size::ZERO;
100            };
101            let Some(intr) = ctx.intrinsic.as_ref() else {
102                return tf::Size::ZERO;
103            };
104            let (w, h) = intr.measure(
105                (known.width, known.height),
106                (available.width.into(), available.height.into()),
107            );
108            tf::Size {
109                width: w,
110                height: h,
111            }
112        },
113    );
114
115    // Walk the tree to collect absolute layouts.
116    let mut layouts: HashMap<NodeId, BoxLayout> = HashMap::new();
117    collect(&tree, root, &node_map, 0.0, 0.0, &mut layouts);
118
119    LayoutResult { layouts }
120}
121
122fn build(
123    tree: &mut TaffyTree<NodeData>,
124    map: &mut HashMap<NodeId, tf::NodeId>,
125    node: &BoxNode,
126    ctx: &ConversionContext,
127) -> tf::NodeId {
128    let style = to_taffy_style(&node.css, ctx);
129    let data = NodeData {
130        box_id: node.id,
131        intrinsic: node.intrinsic.clone(),
132    };
133    let tf_id = if node.intrinsic.is_some() {
134        // Leaf with intrinsic measurement.
135        tree.new_leaf_with_context(style, data)
136            .expect("taffy new_leaf")
137    } else if node.children.is_empty() {
138        tree.new_leaf_with_context(style, data)
139            .expect("taffy new_leaf")
140    } else {
141        let mut child_ids = Vec::with_capacity(node.children.len());
142        for c in &node.children {
143            child_ids.push(build(tree, map, c, ctx));
144        }
145        let id = tree
146            .new_with_children(style, &child_ids)
147            .expect("taffy new_with_children");
148        // We still want context on internal nodes (for box_id mapping).
149        tree.set_node_context(id, Some(data)).ok();
150        id
151    };
152    map.insert(node.id, tf_id);
153    tf_id
154}
155
156fn collect(
157    tree: &TaffyTree<NodeData>,
158    node: &BoxNode,
159    map: &HashMap<NodeId, tf::NodeId>,
160    parent_x: f32,
161    parent_y: f32,
162    out: &mut HashMap<NodeId, BoxLayout>,
163) {
164    let Some(&tf_id) = map.get(&node.id) else {
165        return;
166    };
167    let Ok(layout) = tree.layout(tf_id) else {
168        return;
169    };
170
171    let abs_x = parent_x + layout.location.x;
172    let abs_y = parent_y + layout.location.y;
173    let bx = BoxLayout {
174        x: abs_x,
175        y: abs_y,
176        width: layout.size.width,
177        height: layout.size.height,
178        border: Insets {
179            top: layout.border.top,
180            right: layout.border.right,
181            bottom: layout.border.bottom,
182            left: layout.border.left,
183        },
184        padding: Insets {
185            top: layout.padding.top,
186            right: layout.padding.right,
187            bottom: layout.padding.bottom,
188            left: layout.padding.left,
189        },
190    };
191    out.insert(node.id, bx);
192
193    for c in &node.children {
194        collect(tree, c, map, abs_x, abs_y, out);
195    }
196}
197
198#[cfg(test)]
199mod tests {
200    use super::*;
201    use crate::css::style::*;
202    use crate::css::units::LengthPercentage;
203
204    fn ctx() -> ConversionContext {
205        ConversionContext::default()
206    }
207
208    #[test]
209    fn single_block_takes_viewport() {
210        let mut root = BoxNode::container(
211            CssStyle {
212                width: Some(Size::Length(LengthPercentage::String("100%".into()))),
213                height: Some(Size::Length(LengthPercentage::String("100%".into()))),
214                ..Default::default()
215            },
216            vec![],
217        );
218        root.assign_ids(1);
219        let res = run_layout(&root, (1920.0, 1080.0), &ctx());
220        let l = res.get(1).expect("root laid out");
221        assert_eq!(l.width, 1920.0);
222        assert_eq!(l.height, 1080.0);
223        assert_eq!(l.x, 0.0);
224        assert_eq!(l.y, 0.0);
225    }
226
227    #[test]
228    fn flex_column_with_gap_stacks_children() {
229        let mut root = BoxNode::container(
230            CssStyle {
231                display: Some(Display::Flex),
232                flex_direction: Some(FlexDirection::Column),
233                width: Some(Size::Length(LengthPercentage::Px(200.0))),
234                height: Some(Size::Length(LengthPercentage::Px(400.0))),
235                gap: Some(Gap::Uniform(LengthPercentage::Px(10.0))),
236                ..Default::default()
237            },
238            vec![
239                BoxNode::container(
240                    CssStyle {
241                        width: Some(Size::Length(LengthPercentage::Px(100.0))),
242                        height: Some(Size::Length(LengthPercentage::Px(50.0))),
243                        ..Default::default()
244                    },
245                    vec![],
246                ),
247                BoxNode::container(
248                    CssStyle {
249                        width: Some(Size::Length(LengthPercentage::Px(100.0))),
250                        height: Some(Size::Length(LengthPercentage::Px(50.0))),
251                        ..Default::default()
252                    },
253                    vec![],
254                ),
255            ],
256        );
257        root.assign_ids(1);
258        let res = run_layout(&root, (200.0, 400.0), &ctx());
259        let c1 = res.get(2).expect("c1");
260        let c2 = res.get(3).expect("c2");
261        assert_eq!(c1.x, 0.0);
262        assert_eq!(c1.y, 0.0);
263        assert_eq!(c2.x, 0.0);
264        // 50px first child + 10px gap = 60
265        assert_eq!(c2.y, 60.0);
266    }
267
268    #[test]
269    fn padding_extends_content_box_inwards() {
270        let mut root = BoxNode::container(
271            CssStyle {
272                width: Some(Size::Length(LengthPercentage::Px(200.0))),
273                height: Some(Size::Length(LengthPercentage::Px(200.0))),
274                padding: Some(Edges::Uniform(LengthPercentage::Px(20.0))),
275                ..Default::default()
276            },
277            vec![],
278        );
279        root.assign_ids(1);
280        let res = run_layout(&root, (1000.0, 1000.0), &ctx());
281        let l = res.get(1).expect("layout");
282        let (cx, cy, cw, ch) = l.content_box();
283        assert_eq!(cx, 20.0);
284        assert_eq!(cy, 20.0);
285        assert_eq!(cw, 160.0);
286        assert_eq!(ch, 160.0);
287    }
288
289    #[test]
290    fn center_align_items_horizontally() {
291        let mut root = BoxNode::container(
292            CssStyle {
293                display: Some(Display::Flex),
294                flex_direction: Some(FlexDirection::Column),
295                align_items: Some(AlignItems::Center),
296                width: Some(Size::Length(LengthPercentage::Px(200.0))),
297                height: Some(Size::Length(LengthPercentage::Px(200.0))),
298                ..Default::default()
299            },
300            vec![BoxNode::container(
301                CssStyle {
302                    width: Some(Size::Length(LengthPercentage::Px(50.0))),
303                    height: Some(Size::Length(LengthPercentage::Px(20.0))),
304                    ..Default::default()
305                },
306                vec![],
307            )],
308        );
309        root.assign_ids(1);
310        let res = run_layout(&root, (200.0, 200.0), &ctx());
311        let child = res.get(2).expect("child");
312        // (200 - 50) / 2 = 75
313        assert_eq!(child.x, 75.0);
314    }
315
316    #[test]
317    fn position_absolute_inset() {
318        let mut root = BoxNode::container(
319            CssStyle {
320                width: Some(Size::Length(LengthPercentage::Px(400.0))),
321                height: Some(Size::Length(LengthPercentage::Px(400.0))),
322                ..Default::default()
323            },
324            vec![BoxNode::container(
325                CssStyle {
326                    position: Some(Position::Absolute),
327                    top: Some(LengthPercentage::Px(30.0)),
328                    left: Some(LengthPercentage::Px(40.0)),
329                    width: Some(Size::Length(LengthPercentage::Px(100.0))),
330                    height: Some(Size::Length(LengthPercentage::Px(80.0))),
331                    ..Default::default()
332                },
333                vec![],
334            )],
335        );
336        root.assign_ids(1);
337        let res = run_layout(&root, (400.0, 400.0), &ctx());
338        let child = res.get(2).expect("child");
339        assert_eq!(child.x, 40.0);
340        assert_eq!(child.y, 30.0);
341        assert_eq!(child.width, 100.0);
342        assert_eq!(child.height, 80.0);
343    }
344}