Skip to main content

foundry_runtime/
layout.rs

1use std::collections::HashMap;
2use taffy::prelude::*;
3
4use crate::scene::{
5    AlignItems, Display, ElementKind, FlexDirection, FlexWrap, JustifyContent, LayoutRect, NodeId,
6    Overflow, Position, ResolvedStyle, SceneGraph, SceneNode, SizeValue,
7};
8
9pub struct LayoutEngine {
10    tree: TaffyTree,
11    node_map: HashMap<NodeId, taffy::NodeId>,
12    reverse_map: HashMap<taffy::NodeId, NodeId>,
13}
14
15impl Default for LayoutEngine {
16    fn default() -> Self {
17        Self::new()
18    }
19}
20
21impl LayoutEngine {
22    pub fn new() -> Self {
23        Self {
24            tree: TaffyTree::new(),
25            node_map: HashMap::new(),
26            reverse_map: HashMap::new(),
27        }
28    }
29
30    pub fn compute(&mut self, scene: &mut SceneGraph, viewport_width: f32, viewport_height: f32) {
31        self.tree = TaffyTree::new();
32        self.node_map.clear();
33        self.reverse_map.clear();
34
35        if let Some(root_id) = scene.root {
36            // Force html and body nodes to fill viewport (like browsers do)
37            for i in 0..scene.nodes.len() {
38                let tag = scene.nodes[i].tag.clone();
39                if tag == "html" || tag == "body" {
40                    if scene.nodes[i].style.width == SizeValue::Auto {
41                        scene.nodes[i].style.width = SizeValue::Px(viewport_width);
42                    }
43                    if scene.nodes[i].style.height == SizeValue::Auto {
44                        scene.nodes[i].style.min_height = SizeValue::Px(viewport_height);
45                    }
46                    // Body scrolls by default when content overflows
47                    if tag == "body" && scene.nodes[i].style.overflow == Overflow::Visible {
48                        scene.nodes[i].style.overflow = Overflow::Scroll;
49                    }
50                }
51            }
52
53            let taffy_root = self.build_taffy_tree(scene, root_id, viewport_width, viewport_height);
54
55            self.tree
56                .compute_layout(
57                    taffy_root,
58                    Size {
59                        width: AvailableSpace::Definite(viewport_width),
60                        height: AvailableSpace::Definite(viewport_height),
61                    },
62                )
63                .ok();
64
65            self.apply_layout(scene, root_id, 0.0, 0.0);
66        }
67    }
68
69    fn build_taffy_tree(
70        &mut self,
71        scene: &SceneGraph,
72        node_id: NodeId,
73        vw: f32,
74        vh: f32,
75    ) -> taffy::NodeId {
76        let node = scene.get(node_id);
77        let style = &node.style;
78
79        let taffy_style = self.convert_style(style, node, vw, vh, scene);
80
81        let children: Vec<taffy::NodeId> = node
82            .children
83            .iter()
84            .map(|&child_id| self.build_taffy_tree(scene, child_id, vw, vh))
85            .collect();
86
87        let taffy_node = self.tree.new_with_children(taffy_style, &children).unwrap();
88        self.node_map.insert(node_id, taffy_node);
89        self.reverse_map.insert(taffy_node, node_id);
90        taffy_node
91    }
92
93    fn convert_style(
94        &self,
95        style: &ResolvedStyle,
96        node: &SceneNode,
97        vw: f32,
98        vh: f32,
99        scene_graph: &SceneGraph,
100    ) -> Style {
101        self.build_taffy_style(style, node, vw, vh, scene_graph)
102    }
103
104    #[allow(clippy::field_reassign_with_default)]
105    fn build_taffy_style(
106        &self,
107        style: &ResolvedStyle,
108        node: &SceneNode,
109        vw: f32,
110        vh: f32,
111        scene_graph: &SceneGraph,
112    ) -> Style {
113        let mut ts = Style::default();
114
115        // Display
116        ts.display = match style.display {
117            Display::Flex => taffy::Display::Flex,
118            Display::Block => taffy::Display::Block,
119            Display::None => taffy::Display::None,
120            Display::Inline => taffy::Display::Flex, // approximate inline as flex
121        };
122
123        // Flex properties
124        ts.flex_direction = match style.flex_direction {
125            FlexDirection::Row => taffy::FlexDirection::Row,
126            FlexDirection::Column => taffy::FlexDirection::Column,
127            FlexDirection::RowReverse => taffy::FlexDirection::RowReverse,
128            FlexDirection::ColumnReverse => taffy::FlexDirection::ColumnReverse,
129        };
130
131        ts.justify_content = Some(match style.justify_content {
132            JustifyContent::Start => taffy::JustifyContent::FlexStart,
133            JustifyContent::Center => taffy::JustifyContent::Center,
134            JustifyContent::End => taffy::JustifyContent::FlexEnd,
135            JustifyContent::SpaceBetween => taffy::JustifyContent::SpaceBetween,
136            JustifyContent::SpaceAround => taffy::JustifyContent::SpaceAround,
137            JustifyContent::SpaceEvenly => taffy::JustifyContent::SpaceEvenly,
138        });
139
140        ts.align_items = Some(match style.align_items {
141            AlignItems::Stretch => taffy::AlignItems::Stretch,
142            AlignItems::Start => taffy::AlignItems::FlexStart,
143            AlignItems::Center => taffy::AlignItems::Center,
144            AlignItems::End => taffy::AlignItems::FlexEnd,
145        });
146
147        ts.flex_wrap = match style.flex_wrap {
148            FlexWrap::NoWrap => taffy::FlexWrap::NoWrap,
149            FlexWrap::Wrap => taffy::FlexWrap::Wrap,
150        };
151
152        ts.flex_grow = style.flex_grow;
153        ts.flex_shrink = style.flex_shrink;
154        ts.gap = Size {
155            width: LengthPercentage::Length(style.gap),
156            height: LengthPercentage::Length(style.gap),
157        };
158
159        // Position
160        ts.position = match style.position {
161            Position::Relative => taffy::Position::Relative,
162            Position::Absolute | Position::Fixed => taffy::Position::Absolute,
163        };
164
165        ts.inset = Rect {
166            top: self.convert_length_auto(style.top, vw, vh),
167            right: self.convert_length_auto(style.right, vw, vh),
168            bottom: self.convert_length_auto(style.bottom, vw, vh),
169            left: self.convert_length_auto(style.left, vw, vh),
170        };
171
172        // Size
173        ts.size = Size {
174            width: self.convert_dimension(style.font_size, style.width, vw, vh),
175            height: self.convert_dimension(style.font_size, style.height, vw, vh),
176        };
177        ts.min_size = Size {
178            width: self.convert_dimension(style.font_size, style.min_width, vw, vh),
179            height: self.convert_dimension(style.font_size, style.min_height, vw, vh),
180        };
181        ts.max_size = Size {
182            width: self.convert_dimension(style.font_size, style.max_width, vw, vh),
183            height: self.convert_dimension(style.font_size, style.max_height, vw, vh),
184        };
185
186        // Margin
187        ts.margin = Rect {
188            top: LengthPercentageAuto::Length(style.margin[0]),
189            right: LengthPercentageAuto::Length(style.margin[1]),
190            bottom: LengthPercentageAuto::Length(style.margin[2]),
191            left: LengthPercentageAuto::Length(style.margin[3]),
192        };
193
194        // Padding
195        ts.padding = Rect {
196            top: LengthPercentage::Length(style.padding[0]),
197            right: LengthPercentage::Length(style.padding[1]),
198            bottom: LengthPercentage::Length(style.padding[2]),
199            left: LengthPercentage::Length(style.padding[3]),
200        };
201
202        // Border
203        ts.border = Rect {
204            top: LengthPercentage::Length(style.border_width[0]),
205            right: LengthPercentage::Length(style.border_width[1]),
206            bottom: LengthPercentage::Length(style.border_width[2]),
207            left: LengthPercentage::Length(style.border_width[3]),
208        };
209
210        // Text nodes: inherit font properties from parent for sizing
211        if node.kind == ElementKind::Text {
212            if let Some(text) = &node.text_content {
213                // Use parent's font size since CSS is applied to the container, not the text node
214                let font_size = if let Some(parent_id) = node.parent {
215                    scene_graph.get(parent_id).style.font_size
216                } else {
217                    style.font_size
218                };
219                let font_weight = if let Some(parent_id) = node.parent {
220                    scene_graph.get(parent_id).style.font_weight
221                } else {
222                    style.font_weight
223                };
224                let line_height = if let Some(parent_id) = node.parent {
225                    scene_graph.get(parent_id).style.line_height
226                } else {
227                    style.line_height
228                };
229
230                let char_count = text.chars().count() as f32;
231                let ratio = if font_weight >= 700 { 0.72 } else { 0.62 };
232                let text_width = char_count * font_size * ratio;
233                let text_height = font_size * line_height;
234                ts.size = Size {
235                    width: Dimension::Length(text_width),
236                    height: Dimension::Length(text_height),
237                };
238            }
239        }
240
241        // Block display: default to column layout for children
242        if style.display == Display::Block {
243            ts.display = taffy::Display::Flex;
244            ts.flex_direction = taffy::FlexDirection::Column;
245        }
246
247        ts
248    }
249
250    fn convert_dimension(&self, font_size: f32, val: SizeValue, vw: f32, vh: f32) -> Dimension {
251        match val {
252            SizeValue::Px(v) => Dimension::Length(v),
253            SizeValue::Percent(v) => Dimension::Percent(v / 100.0),
254            SizeValue::Em(v) => Dimension::Length(v * font_size),
255            SizeValue::Rem(v) => Dimension::Length(v * 16.0),
256            SizeValue::Vh(v) => Dimension::Length(v / 100.0 * vh),
257            SizeValue::Vw(v) => Dimension::Length(v / 100.0 * vw),
258            SizeValue::Auto => Dimension::Auto,
259        }
260    }
261
262    fn convert_length_auto(&self, val: SizeValue, vw: f32, vh: f32) -> LengthPercentageAuto {
263        match val {
264            SizeValue::Px(v) => LengthPercentageAuto::Length(v),
265            SizeValue::Percent(v) => LengthPercentageAuto::Percent(v / 100.0),
266            SizeValue::Vh(v) => LengthPercentageAuto::Length(v / 100.0 * vh),
267            SizeValue::Vw(v) => LengthPercentageAuto::Length(v / 100.0 * vw),
268            _ => LengthPercentageAuto::Auto,
269        }
270    }
271
272    fn apply_layout(&self, scene: &mut SceneGraph, node_id: NodeId, parent_x: f32, parent_y: f32) {
273        if let Some(&taffy_node) = self.node_map.get(&node_id) {
274            if let Ok(layout) = self.tree.layout(taffy_node) {
275                let x = parent_x + layout.location.x;
276                let y = parent_y + layout.location.y;
277
278                let scene_node = scene.get_mut(node_id);
279                scene_node.layout = LayoutRect {
280                    x,
281                    y,
282                    width: layout.size.width,
283                    height: layout.size.height,
284                };
285                scene_node.dirty = false;
286
287                let children: Vec<NodeId> = scene_node.children.clone();
288                for child_id in children {
289                    self.apply_layout(scene, child_id, x, y);
290                }
291
292                // Compute content_height from children (for scroll)
293                let mut max_bottom: f32 = 0.0;
294                for &child_id in &scene.get(node_id).children {
295                    let child = scene.get(child_id);
296                    let child_bottom = child.layout.y + child.layout.height - y;
297                    if child_bottom > max_bottom {
298                        max_bottom = child_bottom;
299                    }
300                }
301                scene.get_mut(node_id).content_height = max_bottom;
302            }
303        }
304    }
305}