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 for constraint in &node.constraints {
85 apply_constraint(graph, node_idx, constraint, bounds, viewport);
89 }
90
91 for child_idx in graph.children(node_idx) {
92 resolve_constraints_top_down(graph, child_idx, bounds, viewport);
93 }
94}
95
96pub fn is_parent_managed(graph: &SceneGraph, node_idx: NodeIndex) -> bool {
98 let parent_idx = match graph.parent(node_idx) {
99 Some(p) => p,
100 None => return false,
101 };
102 let parent_node = &graph.graph[parent_idx];
103 match &parent_node.kind {
104 NodeKind::Frame { layout, .. } => !matches!(layout, LayoutMode::Free { .. }),
105 _ => false,
106 }
107}
108
109fn recompute_group_auto_sizes(
111 graph: &SceneGraph,
112 node_idx: NodeIndex,
113 bounds: &mut HashMap<NodeIndex, ResolvedBounds>,
114) {
115 for child_idx in graph.children(node_idx) {
117 recompute_group_auto_sizes(graph, child_idx, bounds);
118 }
119
120 let node = &graph.graph[node_idx];
121 if !matches!(node.kind, NodeKind::Group) {
123 return;
124 }
125
126 let children = graph.children(node_idx);
127 if children.is_empty() {
128 return;
129 }
130
131 let mut min_x = f32::MAX;
132 let mut min_y = f32::MAX;
133 let mut max_x = f32::MIN;
134 let mut max_y = f32::MIN;
135
136 for &child_idx in &children {
137 if let Some(cb) = bounds.get(&child_idx) {
138 min_x = min_x.min(cb.x);
139 min_y = min_y.min(cb.y);
140 max_x = max_x.max(cb.x + cb.width);
141 max_y = max_y.max(cb.y + cb.height);
142 }
143 }
144
145 if min_x < f32::MAX {
146 bounds.insert(
147 node_idx,
148 ResolvedBounds {
149 x: min_x,
150 y: min_y,
151 width: max_x - min_x,
152 height: max_y - min_y,
153 },
154 );
155 }
156}
157
158#[allow(clippy::only_used_in_recursion)]
159fn resolve_children(
160 graph: &SceneGraph,
161 parent_idx: NodeIndex,
162 bounds: &mut HashMap<NodeIndex, ResolvedBounds>,
163 viewport: Viewport,
164) {
165 let parent_bounds = bounds[&parent_idx];
166 let parent_node = &graph.graph[parent_idx];
167
168 let children: Vec<NodeIndex> = graph.children(parent_idx);
169 if children.is_empty() {
170 return;
171 }
172
173 let layout = match &parent_node.kind {
175 NodeKind::Group => LayoutMode::Free { pad: 0.0 }, NodeKind::Frame { layout, .. } => layout.clone(),
177 _ => LayoutMode::Free { pad: 0.0 },
178 };
179
180 match layout {
181 LayoutMode::Column { gap, pad } => {
182 let content_width = parent_bounds.width - 2.0 * pad;
183 let flow_children: Vec<NodeIndex> = children
186 .iter()
187 .copied()
188 .filter(|&ci| {
189 !graph.graph[ci]
190 .constraints
191 .iter()
192 .any(|c| matches!(c, Constraint::Position { .. }))
193 })
194 .collect();
195 for &child_idx in &flow_children {
197 let child_node = &graph.graph[child_idx];
198 let child_size = intrinsic_size(child_node);
199 let w = if matches!(child_node.kind, NodeKind::Text { .. }) {
201 content_width.max(child_size.0)
202 } else {
203 child_size.0
204 };
205 bounds.insert(
206 child_idx,
207 ResolvedBounds {
208 x: parent_bounds.x + pad,
209 y: parent_bounds.y + pad,
210 width: w,
211 height: child_size.1,
212 },
213 );
214 resolve_children(graph, child_idx, bounds, viewport);
215 }
216 for &child_idx in &children {
218 if !flow_children.contains(&child_idx) {
219 let child_size = intrinsic_size(&graph.graph[child_idx]);
220 bounds.entry(child_idx).or_insert(ResolvedBounds {
221 x: parent_bounds.x,
222 y: parent_bounds.y,
223 width: child_size.0,
224 height: child_size.1,
225 });
226 resolve_children(graph, child_idx, bounds, viewport);
227 }
228 }
229 let mut y = parent_bounds.y + pad;
231 for &child_idx in &flow_children {
232 let resolved = bounds[&child_idx];
233 let dx = (parent_bounds.x + pad) - resolved.x;
234 let dy = y - resolved.y;
235 if dx.abs() > 0.001 || dy.abs() > 0.001 {
236 shift_subtree(graph, child_idx, dx, dy, bounds);
237 }
238 y += bounds[&child_idx].height + gap;
239 }
240 }
241 LayoutMode::Row { gap, pad } => {
242 let flow_children: Vec<NodeIndex> = children
244 .iter()
245 .copied()
246 .filter(|&ci| {
247 !graph.graph[ci]
248 .constraints
249 .iter()
250 .any(|c| matches!(c, Constraint::Position { .. }))
251 })
252 .collect();
253 for &child_idx in &flow_children {
255 let child_size = intrinsic_size(&graph.graph[child_idx]);
256 bounds.insert(
257 child_idx,
258 ResolvedBounds {
259 x: parent_bounds.x + pad,
260 y: parent_bounds.y + pad,
261 width: child_size.0,
262 height: child_size.1,
263 },
264 );
265 resolve_children(graph, child_idx, bounds, viewport);
266 }
267 for &child_idx in &children {
269 if !flow_children.contains(&child_idx) {
270 let child_size = intrinsic_size(&graph.graph[child_idx]);
271 bounds.entry(child_idx).or_insert(ResolvedBounds {
272 x: parent_bounds.x,
273 y: parent_bounds.y,
274 width: child_size.0,
275 height: child_size.1,
276 });
277 resolve_children(graph, child_idx, bounds, viewport);
278 }
279 }
280 let mut x = parent_bounds.x + pad;
282 for &child_idx in &flow_children {
283 let resolved = bounds[&child_idx];
284 let dx = x - resolved.x;
285 let dy = (parent_bounds.y + pad) - resolved.y;
286 if dx.abs() > 0.001 || dy.abs() > 0.001 {
287 shift_subtree(graph, child_idx, dx, dy, bounds);
288 }
289 x += bounds[&child_idx].width + gap;
290 }
291 }
292 LayoutMode::Grid { cols, gap, pad } => {
293 let flow_children: Vec<NodeIndex> = children
295 .iter()
296 .copied()
297 .filter(|&ci| {
298 !graph.graph[ci]
299 .constraints
300 .iter()
301 .any(|c| matches!(c, Constraint::Position { .. }))
302 })
303 .collect();
304 for &child_idx in &flow_children {
306 let child_size = intrinsic_size(&graph.graph[child_idx]);
307 bounds.insert(
308 child_idx,
309 ResolvedBounds {
310 x: parent_bounds.x + pad,
311 y: parent_bounds.y + pad,
312 width: child_size.0,
313 height: child_size.1,
314 },
315 );
316 resolve_children(graph, child_idx, bounds, viewport);
317 }
318 for &child_idx in &children {
320 if !flow_children.contains(&child_idx) {
321 let child_size = intrinsic_size(&graph.graph[child_idx]);
322 bounds.entry(child_idx).or_insert(ResolvedBounds {
323 x: parent_bounds.x,
324 y: parent_bounds.y,
325 width: child_size.0,
326 height: child_size.1,
327 });
328 resolve_children(graph, child_idx, bounds, viewport);
329 }
330 }
331 let mut x = parent_bounds.x + pad;
333 let mut y = parent_bounds.y + pad;
334 let mut col = 0u32;
335 let mut row_height = 0.0f32;
336
337 for &child_idx in &flow_children {
338 let resolved = bounds[&child_idx];
339 let dx = x - resolved.x;
340 let dy = y - resolved.y;
341 if dx.abs() > 0.001 || dy.abs() > 0.001 {
342 shift_subtree(graph, child_idx, dx, dy, bounds);
343 }
344
345 let resolved = bounds[&child_idx];
346 row_height = row_height.max(resolved.height);
347 col += 1;
348 if col >= cols {
349 col = 0;
350 x = parent_bounds.x + pad;
351 y += row_height + gap;
352 row_height = 0.0;
353 } else {
354 x += resolved.width + gap;
355 }
356 }
357 }
358 LayoutMode::Free { pad } => {
359 let content_x = parent_bounds.x + pad;
361 let content_y = parent_bounds.y + pad;
362 let content_w = (parent_bounds.width - 2.0 * pad).max(0.0);
363 let content_h = (parent_bounds.height - 2.0 * pad).max(0.0);
364
365 for &child_idx in &children {
369 let child_size = intrinsic_size(&graph.graph[child_idx]);
370 bounds.entry(child_idx).or_insert(ResolvedBounds {
371 x: content_x,
372 y: content_y,
373 width: child_size.0,
374 height: child_size.1,
375 });
376 }
377
378 let parent_is_shape = matches!(
379 parent_node.kind,
380 NodeKind::Rect { .. } | NodeKind::Ellipse { .. } | NodeKind::Frame { .. }
381 );
382
383 for &child_idx in &children {
384 let child_node = &graph.graph[child_idx];
385 let has_position = child_node
386 .constraints
387 .iter()
388 .any(|c| matches!(c, Constraint::Position { .. }));
389
390 if let Some((h, v)) = child_node.place {
392 if !has_position && let Some(cb) = bounds.get(&child_idx).copied() {
393 let x = match h {
394 HPlace::Left => content_x,
395 HPlace::Center => content_x + (content_w - cb.width) / 2.0,
396 HPlace::Right => content_x + content_w - cb.width,
397 };
398 let y = match v {
399 VPlace::Top => content_y,
400 VPlace::Middle => content_y + (content_h - cb.height) / 2.0,
401 VPlace::Bottom => content_y + content_h - cb.height,
402 };
403 bounds.insert(child_idx, ResolvedBounds { x, y, ..cb });
404 }
405 continue;
406 }
407
408 if parent_is_shape
412 && matches!(child_node.kind, NodeKind::Text { .. })
413 && !has_position
414 && let Some(child_b) = bounds.get(&child_idx).copied()
415 {
416 let cx = content_x + (content_w - child_b.width) / 2.0;
417 let cy = content_y + (content_h - child_b.height) / 2.0;
418 bounds.insert(
419 child_idx,
420 ResolvedBounds {
421 x: cx,
422 y: cy,
423 width: child_b.width,
424 height: child_b.height,
425 },
426 );
427 }
428 }
429 }
430 }
431
432 if matches!(layout, LayoutMode::Free { .. }) {
434 for &child_idx in &children {
435 resolve_children(graph, child_idx, bounds, viewport);
436 }
437 }
438
439 if matches!(parent_node.kind, NodeKind::Group) && !children.is_empty() {
441 let mut min_x = f32::MAX;
442 let mut min_y = f32::MAX;
443 let mut max_x = f32::MIN;
444 let mut max_y = f32::MIN;
445
446 for &child_idx in &children {
447 if let Some(cb) = bounds.get(&child_idx) {
448 min_x = min_x.min(cb.x);
449 min_y = min_y.min(cb.y);
450 max_x = max_x.max(cb.x + cb.width);
451 max_y = max_y.max(cb.y + cb.height);
452 }
453 }
454
455 if min_x < f32::MAX {
456 bounds.insert(
457 parent_idx,
458 ResolvedBounds {
459 x: min_x,
460 y: min_y,
461 width: max_x - min_x,
462 height: max_y - min_y,
463 },
464 );
465 }
466 }
467}
468
469fn shift_subtree(
472 graph: &SceneGraph,
473 node_idx: NodeIndex,
474 dx: f32,
475 dy: f32,
476 bounds: &mut HashMap<NodeIndex, ResolvedBounds>,
477) {
478 if let Some(b) = bounds.get(&node_idx).copied() {
479 bounds.insert(
480 node_idx,
481 ResolvedBounds {
482 x: b.x + dx,
483 y: b.y + dy,
484 ..b
485 },
486 );
487 }
488 for child_idx in graph.children(node_idx) {
489 shift_subtree(graph, child_idx, dx, dy, bounds);
490 }
491}
492
493fn intrinsic_size(node: &SceneNode) -> (f32, f32) {
495 match &node.kind {
496 NodeKind::Rect { width, height } => (*width, *height),
497 NodeKind::Ellipse { rx, ry } => (*rx * 2.0, *ry * 2.0),
498 NodeKind::Text {
499 content, max_width, ..
500 } => {
501 let font_size = node.props.font.as_ref().map_or(14.0, |f| f.size);
502 let char_width = font_size * 0.6;
503 let total_w = content.chars().count() as f32 * char_width;
504 let line_height = font_size * 1.4;
505 match max_width {
506 Some(mw) => (*mw, line_height),
511 None => (total_w, line_height),
512 }
513 }
514 NodeKind::Group => (0.0, 0.0), NodeKind::Frame { width, height, .. } => (*width, *height),
516 NodeKind::Path { .. } => (100.0, 100.0), NodeKind::Image { width, height, .. } => (*width, *height),
518 NodeKind::Generic => (120.0, 40.0), NodeKind::Root => (0.0, 0.0),
520 }
521}
522
523fn apply_constraint(
524 graph: &SceneGraph,
525 node_idx: NodeIndex,
526 constraint: &Constraint,
527 bounds: &mut HashMap<NodeIndex, ResolvedBounds>,
528 viewport: Viewport,
529) {
530 let node_bounds = match bounds.get(&node_idx) {
531 Some(b) => *b,
532 None => return,
533 };
534
535 match constraint {
536 Constraint::CenterIn(target_id) => {
537 let container = if target_id.as_str() == "canvas" {
538 ResolvedBounds {
539 x: 0.0,
540 y: 0.0,
541 width: viewport.width,
542 height: viewport.height,
543 }
544 } else {
545 match graph.index_of(*target_id).and_then(|i| bounds.get(&i)) {
546 Some(b) => *b,
547 None => return,
548 }
549 };
550
551 let cx = container.x + (container.width - node_bounds.width) / 2.0;
552 let cy = container.y + (container.height - node_bounds.height) / 2.0;
553 let dx = cx - node_bounds.x;
554 let dy = cy - node_bounds.y;
555
556 shift_subtree(graph, node_idx, dx, dy, bounds);
557 }
558 Constraint::Offset { from, dx, dy } => {
559 let from_bounds = match graph.index_of(*from).and_then(|i| bounds.get(&i)) {
560 Some(b) => *b,
561 None => return,
562 };
563 let target_x = from_bounds.x + dx;
564 let target_y = from_bounds.y + dy;
565 let sdx = target_x - node_bounds.x;
566 let sdy = target_y - node_bounds.y;
567
568 shift_subtree(graph, node_idx, sdx, sdy, bounds);
569 }
570 Constraint::FillParent { pad } => {
571 let parent_idx = graph
573 .graph
574 .neighbors_directed(node_idx, petgraph::Direction::Incoming)
575 .next();
576
577 if let Some(parent) = parent_idx.and_then(|p| bounds.get(&p).copied()) {
578 let target_x = parent.x + pad;
579 let target_y = parent.y + pad;
580 let new_w = parent.width - 2.0 * pad;
581 let new_h = parent.height - 2.0 * pad;
582 let dx = target_x - node_bounds.x;
583 let dy = target_y - node_bounds.y;
584
585 shift_subtree(graph, node_idx, dx, dy, bounds);
587
588 if let Some(nb) = bounds.get_mut(&node_idx) {
590 nb.width = new_w;
591 nb.height = new_h;
592 }
593 }
594 }
595 Constraint::Position { x, y } => {
596 let (px, py) = match graph.parent(node_idx).and_then(|p| bounds.get(&p)) {
597 Some(p_bounds) => (p_bounds.x, p_bounds.y),
598 None => (0.0, 0.0),
599 };
600 let target_x = px + *x;
601 let target_y = py + *y;
602 let dx = target_x - node_bounds.x;
603 let dy = target_y - node_bounds.y;
604
605 shift_subtree(graph, node_idx, dx, dy, bounds);
606 }
607 }
608}
609
610#[cfg(test)]
611#[path = "layout_tests.rs"]
612mod tests;