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 resolve_constraints_top_down(graph, graph.root, &mut bounds, viewport);
53
54 recompute_group_auto_sizes(graph, graph.root, &mut bounds);
57
58 bounds
59}
60
61pub fn resolve_subtree(
67 graph: &SceneGraph,
68 parent_idx: NodeIndex,
69 bounds: &mut HashMap<NodeIndex, ResolvedBounds>,
70 viewport: Viewport,
71) {
72 resolve_children(graph, parent_idx, bounds, viewport);
73 resolve_constraints_top_down(graph, parent_idx, bounds, viewport);
74 recompute_group_auto_sizes(graph, parent_idx, bounds);
75}
76
77fn resolve_constraints_top_down(
78 graph: &SceneGraph,
79 node_idx: NodeIndex,
80 bounds: &mut HashMap<NodeIndex, ResolvedBounds>,
81 viewport: Viewport,
82) {
83 let node = &graph.graph[node_idx];
84 let parent_managed = is_parent_managed(graph, node_idx);
85 for constraint in &node.constraints {
86 if parent_managed && matches!(constraint, Constraint::Position { .. }) {
89 continue;
90 }
91 apply_constraint(graph, node_idx, constraint, bounds, viewport);
92 }
93
94 for child_idx in graph.children(node_idx) {
95 resolve_constraints_top_down(graph, child_idx, bounds, viewport);
96 }
97}
98
99pub fn is_parent_managed(graph: &SceneGraph, node_idx: NodeIndex) -> bool {
101 let parent_idx = match graph.parent(node_idx) {
102 Some(p) => p,
103 None => return false,
104 };
105 let parent_node = &graph.graph[parent_idx];
106 match &parent_node.kind {
107 NodeKind::Frame { layout, .. } => !matches!(layout, LayoutMode::Free),
108 _ => false,
109 }
110}
111
112fn recompute_group_auto_sizes(
114 graph: &SceneGraph,
115 node_idx: NodeIndex,
116 bounds: &mut HashMap<NodeIndex, ResolvedBounds>,
117) {
118 for child_idx in graph.children(node_idx) {
120 recompute_group_auto_sizes(graph, child_idx, bounds);
121 }
122
123 let node = &graph.graph[node_idx];
124 if !matches!(node.kind, NodeKind::Group) {
126 return;
127 }
128
129 let children = graph.children(node_idx);
130 if children.is_empty() {
131 return;
132 }
133
134 let mut min_x = f32::MAX;
135 let mut min_y = f32::MAX;
136 let mut max_x = f32::MIN;
137 let mut max_y = f32::MIN;
138
139 for &child_idx in &children {
140 if let Some(cb) = bounds.get(&child_idx) {
141 min_x = min_x.min(cb.x);
142 min_y = min_y.min(cb.y);
143 max_x = max_x.max(cb.x + cb.width);
144 max_y = max_y.max(cb.y + cb.height);
145 }
146 }
147
148 if min_x < f32::MAX {
149 bounds.insert(
150 node_idx,
151 ResolvedBounds {
152 x: min_x,
153 y: min_y,
154 width: max_x - min_x,
155 height: max_y - min_y,
156 },
157 );
158 }
159}
160
161#[allow(clippy::only_used_in_recursion)]
162fn resolve_children(
163 graph: &SceneGraph,
164 parent_idx: NodeIndex,
165 bounds: &mut HashMap<NodeIndex, ResolvedBounds>,
166 viewport: Viewport,
167) {
168 let parent_bounds = bounds[&parent_idx];
169 let parent_node = &graph.graph[parent_idx];
170
171 let children: Vec<NodeIndex> = graph.children(parent_idx);
172 if children.is_empty() {
173 return;
174 }
175
176 let layout = match &parent_node.kind {
178 NodeKind::Group => LayoutMode::Free, NodeKind::Frame { layout, .. } => layout.clone(),
180 _ => LayoutMode::Free,
181 };
182
183 match layout {
184 LayoutMode::Column { gap, pad } => {
185 let content_width = parent_bounds.width - 2.0 * pad;
186 for &child_idx in &children {
188 let child_node = &graph.graph[child_idx];
189 let child_size = intrinsic_size(child_node);
190 let w = if matches!(child_node.kind, NodeKind::Text { .. }) {
192 content_width.max(child_size.0)
193 } else {
194 child_size.0
195 };
196 bounds.insert(
197 child_idx,
198 ResolvedBounds {
199 x: parent_bounds.x + pad,
200 y: parent_bounds.y + pad,
201 width: w,
202 height: child_size.1,
203 },
204 );
205 resolve_children(graph, child_idx, bounds, viewport);
206 }
207 let mut y = parent_bounds.y + pad;
209 for &child_idx in &children {
210 let resolved = bounds[&child_idx];
211 let dx = (parent_bounds.x + pad) - resolved.x;
212 let dy = y - resolved.y;
213 if dx.abs() > 0.001 || dy.abs() > 0.001 {
214 shift_subtree(graph, child_idx, dx, dy, bounds);
215 }
216 y += bounds[&child_idx].height + gap;
217 }
218 }
219 LayoutMode::Row { gap, pad } => {
220 for &child_idx in &children {
222 let child_size = intrinsic_size(&graph.graph[child_idx]);
223 bounds.insert(
224 child_idx,
225 ResolvedBounds {
226 x: parent_bounds.x + pad,
227 y: parent_bounds.y + pad,
228 width: child_size.0,
229 height: child_size.1,
230 },
231 );
232 resolve_children(graph, child_idx, bounds, viewport);
233 }
234 let mut x = parent_bounds.x + pad;
236 for &child_idx in &children {
237 let resolved = bounds[&child_idx];
238 let dx = x - resolved.x;
239 let dy = (parent_bounds.y + pad) - resolved.y;
240 if dx.abs() > 0.001 || dy.abs() > 0.001 {
241 shift_subtree(graph, child_idx, dx, dy, bounds);
242 }
243 x += bounds[&child_idx].width + gap;
244 }
245 }
246 LayoutMode::Grid { cols, gap, pad } => {
247 for &child_idx in &children {
249 let child_size = intrinsic_size(&graph.graph[child_idx]);
250 bounds.insert(
251 child_idx,
252 ResolvedBounds {
253 x: parent_bounds.x + pad,
254 y: parent_bounds.y + pad,
255 width: child_size.0,
256 height: child_size.1,
257 },
258 );
259 resolve_children(graph, child_idx, bounds, viewport);
260 }
261 let mut x = parent_bounds.x + pad;
263 let mut y = parent_bounds.y + pad;
264 let mut col = 0u32;
265 let mut row_height = 0.0f32;
266
267 for &child_idx in &children {
268 let resolved = bounds[&child_idx];
269 let dx = x - resolved.x;
270 let dy = y - resolved.y;
271 if dx.abs() > 0.001 || dy.abs() > 0.001 {
272 shift_subtree(graph, child_idx, dx, dy, bounds);
273 }
274
275 let resolved = bounds[&child_idx];
276 row_height = row_height.max(resolved.height);
277 col += 1;
278 if col >= cols {
279 col = 0;
280 x = parent_bounds.x + pad;
281 y += row_height + gap;
282 row_height = 0.0;
283 } else {
284 x += resolved.width + gap;
285 }
286 }
287 }
288 LayoutMode::Free => {
289 for &child_idx in &children {
291 let child_size = intrinsic_size(&graph.graph[child_idx]);
292 bounds.insert(
293 child_idx,
294 ResolvedBounds {
295 x: parent_bounds.x,
296 y: parent_bounds.y,
297 width: child_size.0,
298 height: child_size.1,
299 },
300 );
301 }
302
303 let parent_is_shape = matches!(
304 parent_node.kind,
305 NodeKind::Rect { .. } | NodeKind::Ellipse { .. } | NodeKind::Frame { .. }
306 );
307
308 for &child_idx in &children {
309 let child_node = &graph.graph[child_idx];
310 let has_position = child_node
311 .constraints
312 .iter()
313 .any(|c| matches!(c, Constraint::Position { .. }));
314
315 if let Some((h, v)) = child_node.place {
317 if !has_position && let Some(cb) = bounds.get(&child_idx).copied() {
318 let x = match h {
319 HPlace::Left => parent_bounds.x,
320 HPlace::Center => {
321 parent_bounds.x + (parent_bounds.width - cb.width) / 2.0
322 }
323 HPlace::Right => parent_bounds.x + parent_bounds.width - cb.width,
324 };
325 let y = match v {
326 VPlace::Top => parent_bounds.y,
327 VPlace::Middle => {
328 parent_bounds.y + (parent_bounds.height - cb.height) / 2.0
329 }
330 VPlace::Bottom => parent_bounds.y + parent_bounds.height - cb.height,
331 };
332 bounds.insert(child_idx, ResolvedBounds { x, y, ..cb });
333 }
334 continue;
335 }
336
337 if parent_is_shape
340 && matches!(child_node.kind, NodeKind::Text { .. })
341 && !has_position
342 && let Some(child_b) = bounds.get(&child_idx).copied()
343 {
344 let cx = parent_bounds.x + (parent_bounds.width - child_b.width) / 2.0;
345 let cy = parent_bounds.y + (parent_bounds.height - child_b.height) / 2.0;
346 bounds.insert(
347 child_idx,
348 ResolvedBounds {
349 x: cx,
350 y: cy,
351 width: child_b.width,
352 height: child_b.height,
353 },
354 );
355 }
356 }
357 }
358 }
359
360 if matches!(layout, LayoutMode::Free) {
362 for &child_idx in &children {
363 resolve_children(graph, child_idx, bounds, viewport);
364 }
365 }
366
367 if matches!(parent_node.kind, NodeKind::Group) && !children.is_empty() {
369 let mut min_x = f32::MAX;
370 let mut min_y = f32::MAX;
371 let mut max_x = f32::MIN;
372 let mut max_y = f32::MIN;
373
374 for &child_idx in &children {
375 if let Some(cb) = bounds.get(&child_idx) {
376 min_x = min_x.min(cb.x);
377 min_y = min_y.min(cb.y);
378 max_x = max_x.max(cb.x + cb.width);
379 max_y = max_y.max(cb.y + cb.height);
380 }
381 }
382
383 if min_x < f32::MAX {
384 bounds.insert(
385 parent_idx,
386 ResolvedBounds {
387 x: min_x,
388 y: min_y,
389 width: max_x - min_x,
390 height: max_y - min_y,
391 },
392 );
393 }
394 }
395}
396
397fn shift_subtree(
400 graph: &SceneGraph,
401 node_idx: NodeIndex,
402 dx: f32,
403 dy: f32,
404 bounds: &mut HashMap<NodeIndex, ResolvedBounds>,
405) {
406 if let Some(b) = bounds.get(&node_idx).copied() {
407 bounds.insert(
408 node_idx,
409 ResolvedBounds {
410 x: b.x + dx,
411 y: b.y + dy,
412 ..b
413 },
414 );
415 }
416 for child_idx in graph.children(node_idx) {
417 shift_subtree(graph, child_idx, dx, dy, bounds);
418 }
419}
420
421fn intrinsic_size(node: &SceneNode) -> (f32, f32) {
423 match &node.kind {
424 NodeKind::Rect { width, height } => (*width, *height),
425 NodeKind::Ellipse { rx, ry } => (*rx * 2.0, *ry * 2.0),
426 NodeKind::Text { content, .. } => {
427 let font_size = node.style.font.as_ref().map_or(14.0, |f| f.size);
428 let char_width = font_size * 0.6;
429 (content.chars().count() as f32 * char_width, font_size * 1.4)
430 }
431 NodeKind::Group => (0.0, 0.0), NodeKind::Frame { width, height, .. } => (*width, *height),
433 NodeKind::Path { .. } => (100.0, 100.0), NodeKind::Generic => (120.0, 40.0), NodeKind::Root => (0.0, 0.0),
436 }
437}
438
439fn apply_constraint(
440 graph: &SceneGraph,
441 node_idx: NodeIndex,
442 constraint: &Constraint,
443 bounds: &mut HashMap<NodeIndex, ResolvedBounds>,
444 viewport: Viewport,
445) {
446 let node_bounds = match bounds.get(&node_idx) {
447 Some(b) => *b,
448 None => return,
449 };
450
451 match constraint {
452 Constraint::CenterIn(target_id) => {
453 let container = if target_id.as_str() == "canvas" {
454 ResolvedBounds {
455 x: 0.0,
456 y: 0.0,
457 width: viewport.width,
458 height: viewport.height,
459 }
460 } else {
461 match graph.index_of(*target_id).and_then(|i| bounds.get(&i)) {
462 Some(b) => *b,
463 None => return,
464 }
465 };
466
467 let cx = container.x + (container.width - node_bounds.width) / 2.0;
468 let cy = container.y + (container.height - node_bounds.height) / 2.0;
469 let dx = cx - node_bounds.x;
470 let dy = cy - node_bounds.y;
471
472 shift_subtree(graph, node_idx, dx, dy, bounds);
473 }
474 Constraint::Offset { from, dx, dy } => {
475 let from_bounds = match graph.index_of(*from).and_then(|i| bounds.get(&i)) {
476 Some(b) => *b,
477 None => return,
478 };
479 let target_x = from_bounds.x + dx;
480 let target_y = from_bounds.y + dy;
481 let sdx = target_x - node_bounds.x;
482 let sdy = target_y - node_bounds.y;
483
484 shift_subtree(graph, node_idx, sdx, sdy, bounds);
485 }
486 Constraint::FillParent { pad } => {
487 let parent_idx = graph
489 .graph
490 .neighbors_directed(node_idx, petgraph::Direction::Incoming)
491 .next();
492
493 if let Some(parent) = parent_idx.and_then(|p| bounds.get(&p).copied()) {
494 let target_x = parent.x + pad;
495 let target_y = parent.y + pad;
496 let new_w = parent.width - 2.0 * pad;
497 let new_h = parent.height - 2.0 * pad;
498 let dx = target_x - node_bounds.x;
499 let dy = target_y - node_bounds.y;
500
501 shift_subtree(graph, node_idx, dx, dy, bounds);
503
504 if let Some(nb) = bounds.get_mut(&node_idx) {
506 nb.width = new_w;
507 nb.height = new_h;
508 }
509 }
510 }
511 Constraint::Position { x, y } => {
512 let (px, py) = match graph.parent(node_idx).and_then(|p| bounds.get(&p)) {
513 Some(p_bounds) => (p_bounds.x, p_bounds.y),
514 None => (0.0, 0.0),
515 };
516 let target_x = px + *x;
517 let target_y = py + *y;
518 let dx = target_x - node_bounds.x;
519 let dy = target_y - node_bounds.y;
520
521 shift_subtree(graph, node_idx, dx, dy, bounds);
522 }
523 }
524}
525
526#[cfg(test)]
527#[path = "layout_tests.rs"]
528mod tests;