Skip to main content

rustmotion_core/engine/
layout_pass.rs

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