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}