1use 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#[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#[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
70struct NodeData {
73 #[allow(dead_code)]
74 box_id: NodeId,
75 intrinsic: Option<std::sync::Arc<dyn IntrinsicMeasure>>,
76}
77
78pub fn run_layout(root: &BoxNode, viewport: (f32, f32), ctx: &ConversionContext) -> LayoutResult {
80 let mut tree: TaffyTree<NodeData> = TaffyTree::new();
81 tree.disable_rounding();
85 let mut node_map: HashMap<NodeId, tf::NodeId> = HashMap::new();
86
87 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 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 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 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 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 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}