1use crate::model::*;
8use petgraph::graph::NodeIndex;
9use std::collections::HashMap;
10
11#[derive(Debug, Clone, Copy)]
13pub struct Viewport {
14 pub width: f32,
15 pub height: f32,
16}
17
18impl Default for Viewport {
19 fn default() -> Self {
20 Self {
21 width: 800.0,
22 height: 600.0,
23 }
24 }
25}
26
27pub fn resolve_layout(
31 graph: &SceneGraph,
32 viewport: Viewport,
33) -> HashMap<NodeIndex, ResolvedBounds> {
34 let mut bounds: HashMap<NodeIndex, ResolvedBounds> = HashMap::new();
35
36 bounds.insert(
38 graph.root,
39 ResolvedBounds {
40 x: 0.0,
41 y: 0.0,
42 width: viewport.width,
43 height: viewport.height,
44 },
45 );
46
47 resolve_children(graph, graph.root, &mut bounds, viewport);
49
50 for idx in graph.graph.node_indices() {
52 let node = &graph.graph[idx];
53 for constraint in &node.constraints {
54 apply_constraint(graph, idx, constraint, &mut bounds, viewport);
55 }
56 }
57
58 bounds
59}
60
61#[allow(clippy::only_used_in_recursion)]
62fn resolve_children(
63 graph: &SceneGraph,
64 parent_idx: NodeIndex,
65 bounds: &mut HashMap<NodeIndex, ResolvedBounds>,
66 viewport: Viewport,
67) {
68 let parent_bounds = bounds[&parent_idx];
69 let parent_node = &graph.graph[parent_idx];
70
71 let children: Vec<NodeIndex> = graph.children(parent_idx);
72 if children.is_empty() {
73 return;
74 }
75
76 let layout = match &parent_node.kind {
78 NodeKind::Group { layout } => layout.clone(),
79 _ => LayoutMode::Free,
80 };
81
82 match layout {
83 LayoutMode::Column { gap, pad } => {
84 let mut y = parent_bounds.y + pad;
85 for &child_idx in &children {
86 let child_size = intrinsic_size(&graph.graph[child_idx]);
87 bounds.insert(
88 child_idx,
89 ResolvedBounds {
90 x: parent_bounds.x + pad,
91 y,
92 width: child_size.0,
93 height: child_size.1,
94 },
95 );
96 y += child_size.1 + gap;
97 }
98 }
99 LayoutMode::Row { gap, pad } => {
100 let mut x = parent_bounds.x + pad;
101 for &child_idx in &children {
102 let child_size = intrinsic_size(&graph.graph[child_idx]);
103 bounds.insert(
104 child_idx,
105 ResolvedBounds {
106 x,
107 y: parent_bounds.y + pad,
108 width: child_size.0,
109 height: child_size.1,
110 },
111 );
112 x += child_size.0 + gap;
113 }
114 }
115 LayoutMode::Grid { cols, gap, pad } => {
116 let mut x = parent_bounds.x + pad;
117 let mut y = parent_bounds.y + pad;
118 let mut col = 0u32;
119 let mut row_height = 0.0f32;
120
121 for &child_idx in &children {
122 let child_size = intrinsic_size(&graph.graph[child_idx]);
123 bounds.insert(
124 child_idx,
125 ResolvedBounds {
126 x,
127 y,
128 width: child_size.0,
129 height: child_size.1,
130 },
131 );
132
133 row_height = row_height.max(child_size.1);
134 col += 1;
135 if col >= cols {
136 col = 0;
137 x = parent_bounds.x + pad;
138 y += row_height + gap;
139 row_height = 0.0;
140 } else {
141 x += child_size.0 + gap;
142 }
143 }
144 }
145 LayoutMode::Free => {
146 for &child_idx in &children {
148 let child_size = intrinsic_size(&graph.graph[child_idx]);
149 bounds.insert(
150 child_idx,
151 ResolvedBounds {
152 x: parent_bounds.x,
153 y: parent_bounds.y,
154 width: child_size.0,
155 height: child_size.1,
156 },
157 );
158 }
159 }
160 }
161
162 for &child_idx in &children {
164 resolve_children(graph, child_idx, bounds, viewport);
165 }
166}
167
168fn intrinsic_size(node: &SceneNode) -> (f32, f32) {
170 match &node.kind {
171 NodeKind::Rect { width, height } => (*width, *height),
172 NodeKind::Ellipse { rx, ry } => (*rx * 2.0, *ry * 2.0),
173 NodeKind::Text { content } => {
174 (content.len() as f32 * 8.0, 20.0)
176 }
177 NodeKind::Group { .. } => (200.0, 200.0), NodeKind::Path { .. } => (100.0, 100.0), NodeKind::Root => (0.0, 0.0),
180 }
181}
182
183fn apply_constraint(
184 graph: &SceneGraph,
185 node_idx: NodeIndex,
186 constraint: &Constraint,
187 bounds: &mut HashMap<NodeIndex, ResolvedBounds>,
188 viewport: Viewport,
189) {
190 let node_bounds = match bounds.get(&node_idx) {
191 Some(b) => *b,
192 None => return,
193 };
194
195 match constraint {
196 Constraint::CenterIn(target_id) => {
197 let container = if target_id.as_str() == "canvas" {
198 ResolvedBounds {
199 x: 0.0,
200 y: 0.0,
201 width: viewport.width,
202 height: viewport.height,
203 }
204 } else {
205 match graph.index_of(*target_id).and_then(|i| bounds.get(&i)) {
206 Some(b) => *b,
207 None => return,
208 }
209 };
210
211 let cx = container.x + (container.width - node_bounds.width) / 2.0;
212 let cy = container.y + (container.height - node_bounds.height) / 2.0;
213
214 bounds.insert(
215 node_idx,
216 ResolvedBounds {
217 x: cx,
218 y: cy,
219 ..node_bounds
220 },
221 );
222 }
223 Constraint::Offset { from, dx, dy } => {
224 let from_bounds = match graph.index_of(*from).and_then(|i| bounds.get(&i)) {
225 Some(b) => *b,
226 None => return,
227 };
228 bounds.insert(
229 node_idx,
230 ResolvedBounds {
231 x: from_bounds.x + dx,
232 y: from_bounds.y + dy,
233 ..node_bounds
234 },
235 );
236 }
237 Constraint::FillParent { pad } => {
238 let parent_idx = graph
240 .graph
241 .neighbors_directed(node_idx, petgraph::Direction::Incoming)
242 .next();
243
244 if let Some(parent) = parent_idx.and_then(|p| bounds.get(&p)) {
245 bounds.insert(
246 node_idx,
247 ResolvedBounds {
248 x: parent.x + pad,
249 y: parent.y + pad,
250 width: parent.width - 2.0 * pad,
251 height: parent.height - 2.0 * pad,
252 },
253 );
254 }
255 }
256 Constraint::Absolute { x, y } => {
257 bounds.insert(
258 node_idx,
259 ResolvedBounds {
260 x: *x,
261 y: *y,
262 ..node_bounds
263 },
264 );
265 }
266 }
267}
268
269#[cfg(test)]
270mod tests {
271 use super::*;
272 use crate::id::NodeId;
273 use crate::parser::parse_document;
274
275 #[test]
276 fn layout_column() {
277 let input = r#"
278group @form {
279 layout: column gap=10 pad=20
280
281 rect @a { w: 100 h: 40 }
282 rect @b { w: 100 h: 30 }
283}
284"#;
285 let graph = parse_document(input).unwrap();
286 let viewport = Viewport {
287 width: 800.0,
288 height: 600.0,
289 };
290 let bounds = resolve_layout(&graph, viewport);
291
292 let a_idx = graph.index_of(NodeId::intern("a")).unwrap();
293 let b_idx = graph.index_of(NodeId::intern("b")).unwrap();
294
295 let a = bounds[&a_idx];
296 let b = bounds[&b_idx];
297
298 assert!(
300 (a.x - 20.0).abs() < 0.01,
301 "a.x should be 20 (pad), got {}",
302 a.x
303 );
304 assert!(
305 (b.x - 20.0).abs() < 0.01,
306 "b.x should be 20 (pad), got {}",
307 b.x
308 );
309
310 let gap_plus_height = (b.y - a.y).abs();
312 assert!(
314 (gap_plus_height - 50.0).abs() < 0.01 || (gap_plus_height - 40.0).abs() < 0.01,
315 "children should be height+gap apart, got diff = {gap_plus_height}"
316 );
317 }
318
319 #[test]
320 fn layout_center_in_canvas() {
321 let input = r#"
322rect @box {
323 w: 200
324 h: 100
325}
326
327@box -> center_in: canvas
328"#;
329 let graph = parse_document(input).unwrap();
330 let viewport = Viewport {
331 width: 800.0,
332 height: 600.0,
333 };
334 let bounds = resolve_layout(&graph, viewport);
335
336 let idx = graph.index_of(NodeId::intern("box")).unwrap();
337 let b = bounds[&idx];
338
339 assert!((b.x - 300.0).abs() < 0.01); assert!((b.y - 250.0).abs() < 0.01); }
342}