1use std::cell::{Cell, Ref, RefCell};
2use std::rc::Rc;
3
4use kurbo::{Affine, BezPath, Point, Shape, Vec2};
5use oxframe_core::{BlendMode, Color, Gradient, Signal};
6use oxframe_text::{Lang, TextStyle};
7
8use crate::text::{CodeTheme, DiffFrame, RichSpan, Styled};
9
10use crate::draw::{DrawItem, Layer, LineCap, LineJoin, Primitive, Shadow};
11use crate::flex::{FlexBox, FlexMap};
12use crate::image::ImageSource;
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
17pub struct NodeId(
18 pub u32,
20);
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
24pub enum Direction {
25 #[default]
27 Row,
28 Column,
30}
31
32#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
34pub enum LayoutAlign {
35 Start,
37 #[default]
39 Center,
40 End,
42 Stretch,
44}
45
46#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
48pub enum Justify {
49 Start,
51 #[default]
53 Center,
54 End,
56 SpaceBetween,
58 SpaceAround,
60 SpaceEvenly,
62}
63
64#[doc(hidden)]
66pub enum NodeKind {
67 Group,
68 Camera {
71 zoom: Signal<f64>,
72 },
73 Rect {
74 width: Signal<f64>,
75 height: Signal<f64>,
76 radius: Signal<f64>,
77 },
78 Circle {
79 size: Signal<f64>,
80 start_angle: Signal<f64>,
82 end_angle: Signal<f64>,
83 pie: bool,
85 },
86 Polygon {
87 sides: Signal<f64>,
88 size: Signal<f64>,
89 },
90 Star {
91 points: Signal<f64>,
92 size: Signal<f64>,
93 inner: Signal<f64>,
95 },
96 Path {
97 data: Signal<BezPath>,
98 },
99 Line {
100 points: Signal<Vec<Vec2>>,
101 closed: bool,
102 smooth: bool,
104 start_arrow: bool,
105 end_arrow: bool,
106 arrow_size: Signal<f64>,
107 },
108 Txt {
109 text: Signal<String>,
110 spans: Signal<Vec<RichSpan>>,
112 font_size: Signal<f64>,
113 weight: Signal<f64>,
114 style: TextStyle,
115 morph: Signal<Option<(String, String)>>,
117 morph_t: Signal<f64>,
118 },
119 Code {
120 code: Signal<String>,
121 edit_from: Signal<String>,
123 edit_to: Signal<String>,
125 editing: Signal<bool>,
127 edit_t: Signal<f64>,
129 edit_flash: Signal<bool>,
131 lang: Lang,
132 theme: CodeTheme,
133 font_size: Signal<f64>,
134 style: TextStyle,
135 focus: Signal<Vec2>,
137 dim: Signal<f64>,
139 },
140 Img {
141 source: ImageSource,
142 width: Signal<f64>,
143 height: Signal<f64>,
144 },
145 Layout {
146 direction: Direction,
147 align: LayoutAlign,
148 justify: Justify,
149 wrap: bool,
151 columns: Option<u16>,
153 gap: Signal<f64>,
154 padding: Signal<f64>,
155 width: Signal<Option<f64>>,
156 height: Signal<Option<f64>>,
157 radius: Signal<f64>,
158 },
159 Grid {
160 width: Signal<f64>,
161 height: Signal<f64>,
162 spacing: Signal<f64>,
163 },
164 Svg {
167 width: Signal<f64>,
168 height: Signal<f64>,
169 },
170}
171
172impl NodeKind {
173 pub fn name(&self) -> &'static str {
174 match self {
175 Self::Group => "Group",
176 Self::Camera { .. } => "Camera",
177 Self::Rect { .. } => "Rect",
178 Self::Circle { .. } => "Circle",
179 Self::Polygon { .. } => "Polygon",
180 Self::Star { .. } => "Star",
181 Self::Path { .. } => "Path",
182 Self::Line { .. } => "Line",
183 Self::Txt { .. } => "Txt",
184 Self::Code { .. } => "Code",
185 Self::Img { .. } => "Img",
186 Self::Layout { .. } => "Layout",
187 Self::Grid { .. } => "Grid",
188 Self::Svg { .. } => "Svg",
189 }
190 }
191}
192
193#[doc(hidden)]
195pub struct NodeData {
196 pub id: NodeId,
197 pub name: Option<String>,
198 pub source: Option<String>,
200 pub parent: Option<NodeId>,
201 pub children: Vec<NodeId>,
202 pub kind: NodeKind,
203 pub x: Signal<f64>,
204 pub y: Signal<f64>,
205 pub rotation: Signal<f64>,
207 pub scale: Signal<Vec2>,
208 pub opacity: Signal<f64>,
209 pub fill: Signal<Color>,
210 pub stroke: Signal<Color>,
211 pub line_width: Signal<f64>,
212 pub start: Signal<f64>,
214 pub end: Signal<f64>,
215 pub fill_gradient: Signal<Option<Gradient>>,
217 pub stroke_gradient: Signal<Option<Gradient>>,
218 pub dash: Signal<Vec<f64>>,
220 pub dash_offset: Signal<f64>,
221 pub line_cap: Signal<LineCap>,
222 pub line_join: Signal<LineJoin>,
223 pub blur: Signal<f64>,
225 pub shadow_color: Signal<Color>,
226 pub shadow_blur: Signal<f64>,
227 pub shadow_offset: Signal<Vec2>,
228 pub clip: Signal<bool>,
230 pub blend: Signal<BlendMode>,
231 pub z_index: Signal<f64>,
233 pub anchor: Signal<Vec2>,
237 pub grow: Signal<f64>,
239 pub shrink: Signal<f64>,
240 pub basis: Signal<Option<f64>>,
241 pub margin: Signal<f64>,
242 pub align_self: Signal<Option<LayoutAlign>>,
243 pub place: Option<crate::place::Place>,
245 pub placed: Cell<(f64, f64)>,
247}
248
249impl NodeData {
250 pub fn local_transform(&self, offset: Vec2, size: (f64, f64)) -> Affine {
253 if let NodeKind::Camera { zoom } = &self.kind {
254 let (x, y) = self.placed.get();
258 return Affine::translate(offset)
259 * Affine::rotate(-self.rotation.get().to_radians())
260 * Affine::scale(zoom.get().max(1e-6))
261 * Affine::translate((-x, -y));
262 }
263 let s = self.scale.get();
264 let a = self.anchor.get();
265 let (x, y) = self.placed.get();
266 Affine::translate(offset + Vec2::new(x, y))
267 * Affine::rotate(self.rotation.get().to_radians())
268 * Affine::scale_non_uniform(s.x, s.y)
269 * Affine::translate((-a.x * size.0 / 2.0, -a.y * size.1 / 2.0))
270 }
271
272 pub(crate) fn text_draw(&self) -> Option<TextDraw> {
274 match &self.kind {
275 NodeKind::Txt {
276 text,
277 spans,
278 font_size,
279 weight,
280 style,
281 morph,
282 morph_t,
283 } => {
284 let style = TextStyle {
285 size: font_size.get().max(0.1),
286 weight: weight.get().clamp(1.0, 1000.0),
287 ..style.clone()
288 };
289 if let Some((from, to)) = morph.get() {
290 let a = oxframe_text::layout(&from, &style);
291 let b = oxframe_text::layout(&to, &style);
292 let (glyphs, size) = crate::text::morph(&a, &b, morph_t.get().clamp(0.0, 1.0));
293 return Some(TextDraw::Morph { glyphs, size });
294 }
295 let spans = spans.get();
296 Some(TextDraw::Styled(if spans.is_empty() {
297 crate::text::rich(&[RichSpan::new(text.get())], &style)
298 } else {
299 crate::text::rich(&spans, &style)
300 }))
301 }
302 NodeKind::Code {
303 code,
304 edit_from,
305 edit_to,
306 editing,
307 edit_t,
308 edit_flash,
309 lang,
310 theme,
311 font_size,
312 style,
313 focus,
314 dim,
315 } => {
316 let style = TextStyle {
317 size: font_size.get().max(0.1),
318 ..style.clone()
319 };
320 if editing.get() {
321 let (frame, _) = crate::text::layout_diff(
322 &edit_from.get(),
323 &edit_to.get(),
324 *lang,
325 crate::text::DiffView {
326 theme,
327 style: &style,
328 focus: focus.get(),
329 dim: dim.get(),
330 t: edit_t.get(),
331 flash: edit_flash.get(),
332 },
333 );
334 return Some(TextDraw::Diff(frame));
335 }
336 Some(TextDraw::Styled(crate::text::code(
337 &code.get(),
338 *lang,
339 theme,
340 &style,
341 focus.get(),
342 dim.get(),
343 )))
344 }
345 _ => None,
346 }
347 }
348
349 pub fn outline(&self, size: (f64, f64)) -> Option<BezPath> {
351 const TOLERANCE: f64 = 0.1;
352 match &self.kind {
353 NodeKind::Group
354 | NodeKind::Svg { .. }
355 | NodeKind::Camera { .. }
356 | NodeKind::Img { .. }
357 | NodeKind::Txt { .. }
358 | NodeKind::Code { .. }
359 | NodeKind::Grid { .. } => None,
360 NodeKind::Rect { radius, .. } | NodeKind::Layout { radius, .. } => {
361 let (w, h) = (size.0.max(0.0), size.1.max(0.0));
362 let r = radius.get().clamp(0.0, w.min(h) / 2.0);
363 let rect = kurbo::Rect::new(-w / 2.0, -h / 2.0, w / 2.0, h / 2.0);
364 Some(if r > 0.0 {
365 rect.to_rounded_rect(r).to_path(TOLERANCE)
366 } else {
367 rect.to_path(TOLERANCE)
368 })
369 }
370 NodeKind::Circle {
371 start_angle,
372 end_angle,
373 pie,
374 ..
375 } => {
376 let r = size.0.max(0.0) / 2.0;
379 let (a0, a1) = (start_angle.get(), end_angle.get());
380 let sweep = (a1 - a0).clamp(-360.0, 360.0);
381 if sweep.abs() >= 360.0 - 1e-9 {
382 let circle = kurbo::Circle::new((0.0, 0.0), r).to_path(TOLERANCE);
383 return Some(Affine::rotate((a0 - 90.0).to_radians()) * circle);
384 }
385 let arc = kurbo::Arc::new(
386 (0.0, 0.0),
387 (r, r),
388 (a0 - 90.0).to_radians(),
389 sweep.to_radians(),
390 0.0,
391 );
392 let mut p = BezPath::new();
393 if *pie {
394 p.move_to((0.0, 0.0));
395 p.line_to(arc.center + kurbo::Vec2::from_angle(arc.start_angle) * r);
396 arc.to_cubic_beziers(TOLERANCE, |a, b, c| p.curve_to(a, b, c));
397 p.close_path();
398 } else {
399 p.move_to(arc.center + kurbo::Vec2::from_angle(arc.start_angle) * r);
400 arc.to_cubic_beziers(TOLERANCE, |a, b, c| p.curve_to(a, b, c));
401 }
402 Some(p)
403 }
404 NodeKind::Polygon { sides, .. } => {
405 let n = sides.get().round().max(3.0) as usize;
406 Some(regular(n, size.0.max(0.0) / 2.0, None))
407 }
408 NodeKind::Star { points, inner, .. } => {
409 let n = points.get().round().max(2.0) as usize;
410 let r = size.0.max(0.0) / 2.0;
411 Some(regular(n, r, Some(r * inner.get().clamp(0.0, 1.0))))
412 }
413 NodeKind::Path { data } => Some(data.get()),
414 NodeKind::Line {
415 points,
416 closed,
417 smooth,
418 ..
419 } => {
420 let pts = points.get();
421 if *smooth && pts.len() > 2 {
422 return Some(catmull_rom(&pts, *closed));
423 }
424 let mut p = BezPath::new();
425 for (i, v) in pts.iter().enumerate() {
426 if i == 0 {
427 p.move_to(v.to_point());
428 } else {
429 p.line_to(v.to_point());
430 }
431 }
432 if *closed && pts.len() > 2 {
433 p.close_path();
434 }
435 Some(p)
436 }
437 }
438 }
439}
440
441fn regular(n: usize, r: f64, inner: Option<f64>) -> BezPath {
443 let count = if inner.is_some() { 2 * n } else { n };
444 let mut p = BezPath::new();
445 for i in 0..count {
446 let radius = match inner {
447 Some(ri) if i % 2 == 1 => ri,
448 _ => r,
449 };
450 let a = -std::f64::consts::FRAC_PI_2 + std::f64::consts::TAU * i as f64 / count as f64;
451 let pt = Point::new(a.cos() * radius, a.sin() * radius);
452 if i == 0 {
453 p.move_to(pt);
454 } else {
455 p.line_to(pt);
456 }
457 }
458 p.close_path();
459 p
460}
461
462fn catmull_rom(pts: &[Vec2], closed: bool) -> BezPath {
464 let n = pts.len();
465 let at = |i: isize| -> Vec2 {
466 if closed {
467 pts[i.rem_euclid(n as isize) as usize]
468 } else {
469 pts[i.clamp(0, n as isize - 1) as usize]
470 }
471 };
472 let mut p = BezPath::new();
473 p.move_to(pts[0].to_point());
474 let segs = if closed { n } else { n - 1 };
475 for i in 0..segs as isize {
476 let (p0, p1, p2, p3) = (at(i - 1), at(i), at(i + 1), at(i + 2));
477 let c1 = p1 + (p2 - p0) / 6.0;
478 let c2 = p2 - (p3 - p1) / 6.0;
479 p.curve_to(c1.to_point(), c2.to_point(), p2.to_point());
480 }
481 if closed {
482 p.close_path();
483 }
484 p
485}
486
487pub(crate) enum TextDraw {
488 Styled(Styled),
489 Morph {
491 glyphs: Vec<(BezPath, f64)>,
492 size: (f64, f64),
493 },
494 Diff(DiffFrame),
496}
497
498pub struct SceneGraph {
500 nodes: RefCell<Vec<NodeData>>,
501 size: (u32, u32),
502}
503
504impl SceneGraph {
505 pub const ROOT: NodeId = NodeId(0);
507
508 pub fn new(width: u32, height: u32) -> Rc<Self> {
510 let graph = Rc::new(Self {
511 nodes: RefCell::new(Vec::new()),
512 size: (width, height),
513 });
514 graph.insert(
515 None,
516 NodeKind::Group,
517 crate::BaseProps {
518 name: Some("view".into()),
519 ..Default::default()
520 },
521 );
522 graph
523 }
524
525 pub fn size(&self) -> (u32, u32) {
527 self.size
528 }
529
530 pub fn len(&self) -> usize {
532 self.nodes.borrow().len()
533 }
534
535 pub fn is_empty(&self) -> bool {
537 self.len() == 0
538 }
539
540 pub fn node(&self, id: NodeId) -> Ref<'_, NodeData> {
546 Ref::map(self.nodes.borrow(), |n| &n[id.0 as usize])
547 }
548
549 pub(crate) fn next_id(&self) -> u32 {
550 self.len() as u32
551 }
552
553 pub(crate) fn insert(
554 &self,
555 parent: Option<NodeId>,
556 kind: NodeKind,
557 base: crate::BaseProps,
558 ) -> NodeId {
559 let mut nodes = self.nodes.borrow_mut();
560 let id = NodeId(u32::try_from(nodes.len()).expect("too many nodes"));
561 let label = format!("{}#{}", kind.name(), id.0);
562 let sig = |field: &str| format!("{label}.{field}");
563 nodes.push(NodeData {
564 id,
565 name: base.name,
566 source: current_add_at().map(|(file, line)| source_label(file, line)),
567 parent,
568 children: Vec::new(),
569 x: Signal::new(base.x).with_name(sig("x")),
570 y: Signal::new(base.y).with_name(sig("y")),
571 rotation: Signal::new(base.rotation).with_name(sig("rotation")),
572 scale: Signal::new(base.scale).with_name(sig("scale")),
573 opacity: Signal::new(base.opacity).with_name(sig("opacity")),
574 fill: Signal::new(base.fill).with_name(sig("fill")),
575 stroke: Signal::new(base.stroke).with_name(sig("stroke")),
576 line_width: Signal::new(base.line_width).with_name(sig("line_width")),
577 start: Signal::new(base.start).with_name(sig("start")),
578 end: Signal::new(base.end).with_name(sig("end")),
579 fill_gradient: Signal::new(base.fill_gradient).with_name(sig("fill_gradient")),
580 stroke_gradient: Signal::new(base.stroke_gradient).with_name(sig("stroke_gradient")),
581 dash: Signal::new(base.dash).with_name(sig("dash")),
582 dash_offset: Signal::new(base.dash_offset).with_name(sig("dash_offset")),
583 line_cap: Signal::new(base.line_cap).with_name(sig("line_cap")),
584 line_join: Signal::new(base.line_join).with_name(sig("line_join")),
585 blur: Signal::new(base.blur).with_name(sig("blur")),
586 shadow_color: Signal::new(base.shadow_color).with_name(sig("shadow_color")),
587 shadow_blur: Signal::new(base.shadow_blur).with_name(sig("shadow_blur")),
588 shadow_offset: Signal::new(base.shadow_offset).with_name(sig("shadow_offset")),
589 clip: Signal::new(base.clip).with_name(sig("clip")),
590 blend: Signal::new(base.blend).with_name(sig("blend")),
591 z_index: Signal::new(base.z_index).with_name(sig("z_index")),
592 anchor: Signal::new(base.anchor).with_name(sig("anchor")),
593 grow: Signal::new(base.grow).with_name(sig("grow")),
594 shrink: Signal::new(base.shrink).with_name(sig("shrink")),
595 basis: Signal::new(base.basis).with_name(sig("basis")),
596 margin: Signal::new(base.margin).with_name(sig("margin")),
597 align_self: Signal::new(base.align_self).with_name(sig("align_self")),
598 place: base.place,
599 placed: Cell::new((base.x, base.y)),
600 kind,
601 });
602 if let Some(p) = parent {
603 nodes[p.0 as usize].children.push(id);
604 }
605 id
606 }
607
608 pub(crate) fn detach(&self, id: NodeId) {
609 let mut nodes = self.nodes.borrow_mut();
610 if let Some(p) = nodes[id.0 as usize].parent.take() {
611 nodes[p.0 as usize].children.retain(|c| *c != id);
612 }
613 }
614
615 pub(crate) fn reparent(&self, id: NodeId, parent: NodeId) {
616 self.detach(id);
617 let mut nodes = self.nodes.borrow_mut();
618 nodes[id.0 as usize].parent = Some(parent);
619 nodes[parent.0 as usize].children.push(id);
620 }
621
622 pub fn measure(&self, id: NodeId) -> (f64, f64) {
624 self.resolve_places();
625 let nodes = self.nodes.borrow();
626 measure(&nodes, id)
627 }
628
629 pub(crate) fn set_place(&self, id: NodeId, place: crate::place::Place) {
630 self.nodes.borrow_mut()[id.0 as usize].place = Some(place);
631 }
632
633 fn resolve_places(&self) {
634 let nodes = self.nodes.borrow();
635 crate::place::resolve(&nodes, (f64::from(self.size.0), f64::from(self.size.1)));
636 }
637
638 pub fn draw_list(&self) -> Vec<DrawItem> {
640 self.resolve_places();
641 let nodes = self.nodes.borrow();
642 let mut out = Vec::new();
643 visit(&nodes, Self::ROOT, Affine::IDENTITY, None, None, &mut out);
644 out
645 }
646
647 pub fn pick(&self, p: Point) -> Option<NodeId> {
649 self.draw_list()
650 .iter()
651 .rev()
652 .find(|item| item.node != Self::ROOT && item.hit(p))
653 .map(|item| item.node)
654 }
655
656 pub fn world_transform(&self, id: NodeId) -> Option<Affine> {
658 if let Some(item) = self.draw_list().into_iter().find(|i| i.node == id) {
661 return Some(item.transform);
662 }
663 let nodes = self.nodes.borrow();
664 let mut tf = Affine::IDENTITY;
665 let mut cur = Some(id);
666 while let Some(c) = cur {
667 let n = nodes.get(c.0 as usize)?;
668 tf = n.local_transform(Vec2::ZERO, measure(&nodes, c)) * tf;
669 cur = n.parent;
670 }
671 Some(tf)
672 }
673
674 pub fn box_in(&self, id: NodeId, frame: NodeId) -> Option<kurbo::Rect> {
678 let node_tf = self.world_transform(id)?;
679 let frame_tf = self.world_transform(frame)?;
680 let (w, h) = measure(&self.nodes.borrow(), id);
681 let [x0, y0, x1, y1] = aabb(frame_tf.inverse() * node_tf, w, h);
682 Some(kurbo::Rect::new(x0, y0, x1, y1))
683 }
684
685 pub fn frame_size(&self) -> (f64, f64) {
687 (f64::from(self.size.0), f64::from(self.size.1))
688 }
689
690 pub(crate) fn descendant_named(&self, root: NodeId, name: &str) -> Option<NodeId> {
692 fn walk(nodes: &[NodeData], id: NodeId, name: &str) -> Option<NodeId> {
693 let n = nodes.get(id.0 as usize)?;
694 if n.name.as_deref() == Some(name) {
695 return Some(id);
696 }
697 for &c in &n.children {
698 if let Some(hit) = walk(nodes, c, name) {
699 return Some(hit);
700 }
701 }
702 None
703 }
704 walk(&self.nodes.borrow(), root, name)
705 }
706
707 pub(crate) fn descendant_paths(&self, root: NodeId) -> Vec<NodeId> {
709 fn walk(nodes: &[NodeData], id: NodeId, out: &mut Vec<NodeId>) {
710 let Some(n) = nodes.get(id.0 as usize) else {
711 return;
712 };
713 if matches!(n.kind, NodeKind::Path { .. }) {
714 out.push(id);
715 }
716 for &c in &n.children {
717 walk(nodes, c, out);
718 }
719 }
720 let mut out = Vec::new();
721 walk(&self.nodes.borrow(), root, &mut out);
722 out
723 }
724
725 pub fn tree(&self) -> Vec<(NodeId, Option<NodeId>, &'static str, Option<String>)> {
727 let nodes = self.nodes.borrow();
728 let mut out = Vec::new();
729 let mut stack = vec![Self::ROOT];
730 while let Some(id) = stack.pop() {
731 let n = &nodes[id.0 as usize];
732 out.push((id, n.parent, n.kind.name(), n.name.clone()));
733 stack.extend(n.children.iter().rev());
734 }
735 out
736 }
737
738 pub fn describe(&self, id: NodeId) -> Vec<(String, String)> {
740 let nodes = self.nodes.borrow();
741 let Some(n) = nodes.get(id.0 as usize) else {
742 return Vec::new();
743 };
744 let f = |v: f64| format!("{v:.3}");
745 let s = n.scale.get();
746 let mut out = vec![
747 ("x".into(), f(n.x.get())),
748 ("y".into(), f(n.y.get())),
749 ("rotation".into(), f(n.rotation.get())),
750 ("scale".into(), format!("{:.3}, {:.3}", s.x, s.y)),
751 ("opacity".into(), f(n.opacity.get())),
752 ("fill".into(), n.fill.get().to_string()),
753 ("stroke".into(), n.stroke.get().to_string()),
754 ("line_width".into(), f(n.line_width.get())),
755 ("start".into(), f(n.start.get())),
756 ("end".into(), f(n.end.get())),
757 ];
758 let (w, h) = measure(&nodes, id);
759 out.push(("size".into(), format!("{w:.1} × {h:.1}")));
760 match &n.kind {
761 NodeKind::Rect { radius, .. } => out.push(("radius".into(), f(radius.get()))),
762 NodeKind::Code { code, .. } => out.push(("code".into(), code.get())),
763 NodeKind::Txt {
764 text,
765 font_size,
766 weight,
767 ..
768 } => {
769 out.push(("text".into(), text.get()));
770 out.push(("font_size".into(), f(font_size.get())));
771 out.push(("weight".into(), f(weight.get())));
772 }
773 NodeKind::Line { points, .. } => {
774 out.push(("points".into(), points.get().len().to_string()))
775 }
776 NodeKind::Layout { gap, padding, .. } => {
777 out.push(("gap".into(), f(gap.get())));
778 out.push(("padding".into(), f(padding.get())));
779 }
780 NodeKind::Grid { spacing, .. } => out.push(("spacing".into(), f(spacing.get()))),
781 _ => {}
782 }
783 out
784 }
785
786 pub fn inspect_nodes(&self) -> Vec<InspectNode> {
791 let list = self.draw_list();
792 let mut drawn: Vec<Option<(Affine, u32)>> = vec![None; self.len()];
793 let mut order = 0u32;
794 for item in &list {
795 let slot = &mut drawn[item.node.0 as usize];
796 if slot.is_none() {
797 *slot = Some((item.transform, order));
798 order += 1;
799 }
800 }
801 let nodes = self.nodes.borrow();
802 let mut out = Vec::new();
803 let mut stack = vec![Self::ROOT];
804 while let Some(id) = stack.pop() {
805 let n = &nodes[id.0 as usize];
806 stack.extend(n.children.iter().rev());
807 let (w, h) = measure(&nodes, id);
808 let (tf, order) = drawn[id.0 as usize]
809 .unwrap_or_else(|| (transform_without_layout(&nodes, id), u32::MAX));
810 let (text, text_overflow) = text_info(n);
811 out.push(InspectNode {
812 id: id.0,
813 parent: n.parent.map(|p| p.0),
814 kind: n.kind.name(),
815 name: n.name.clone().unwrap_or_else(|| {
816 n.source
817 .clone()
818 .unwrap_or_else(|| format!("{}#{}", n.kind.name(), id.0))
819 }),
820 source: n.source.clone(),
821 bounds: aabb(tf, w, h),
822 opacity: n.opacity.get().clamp(0.0, 1.0),
823 order,
824 text,
825 fill: n.fill.get(),
826 text_overflow,
827 parent_kind: n.parent.map(|p| nodes[p.0 as usize].kind.name()),
828 in_camera: {
829 let mut up = n.parent;
830 let mut found = false;
831 while let Some(p) = up {
832 let pn = &nodes[p.0 as usize];
833 if matches!(pn.kind, NodeKind::Camera { .. }) {
834 found = true;
835 break;
836 }
837 up = pn.parent;
838 }
839 found
840 },
841 });
842 }
843 out
844 }
845}
846
847#[derive(Debug, Clone, PartialEq)]
849pub struct InspectNode {
850 pub id: u32,
852 pub parent: Option<u32>,
854 pub kind: &'static str,
856 pub name: String,
858 pub source: Option<String>,
860 pub bounds: [f64; 4],
863 pub opacity: f64,
865 pub order: u32,
867 pub text: Option<String>,
869 pub fill: Color,
871 pub text_overflow: f64,
874 pub parent_kind: Option<&'static str>,
876 pub in_camera: bool,
879}
880
881thread_local! {
882 static ADD_AT: Cell<Option<(&'static str, u32)>> = const { Cell::new(None) };
883}
884
885pub(crate) fn push_add_at(file: &'static str, line: u32) -> Option<(&'static str, u32)> {
886 ADD_AT.with(|c| c.replace(Some((file, line))))
887}
888
889pub(crate) fn pop_add_at(prev: Option<(&'static str, u32)>) {
890 ADD_AT.with(|c| c.set(prev));
891}
892
893fn current_add_at() -> Option<(&'static str, u32)> {
894 ADD_AT.with(|c| c.get())
895}
896
897fn source_label(file: &str, line: u32) -> String {
899 let file = file.replace('\\', "/");
900 let rel = file
901 .rfind("/src/")
902 .map_or(file.as_str(), |i| &file[i + 1..]);
903 format!("{rel}:{line}")
904}
905
906fn aabb(tf: Affine, w: f64, h: f64) -> [f64; 4] {
907 let pts = [
908 (-w / 2.0, -h / 2.0),
909 (w / 2.0, -h / 2.0),
910 (w / 2.0, h / 2.0),
911 (-w / 2.0, h / 2.0),
912 ];
913 let mut b = [
914 f64::INFINITY,
915 f64::INFINITY,
916 f64::NEG_INFINITY,
917 f64::NEG_INFINITY,
918 ];
919 for (x, y) in pts {
920 let p = tf * Point::new(x, y);
921 b[0] = b[0].min(p.x);
922 b[1] = b[1].min(p.y);
923 b[2] = b[2].max(p.x);
924 b[3] = b[3].max(p.y);
925 }
926 b
927}
928
929fn transform_without_layout(nodes: &[NodeData], id: NodeId) -> Affine {
930 let mut chain = Vec::new();
931 let mut cur = Some(id);
932 while let Some(c) = cur {
933 chain.push(c);
934 cur = nodes.get(c.0 as usize).and_then(|n| n.parent);
935 }
936 let mut tf = Affine::IDENTITY;
937 for c in chain.into_iter().rev() {
938 let Some(n) = nodes.get(c.0 as usize) else {
939 break;
940 };
941 tf *= n.local_transform(Vec2::ZERO, measure(nodes, c));
942 }
943 tf
944}
945
946fn text_info(n: &NodeData) -> (Option<String>, f64) {
947 let (body, max_width) = match &n.kind {
948 NodeKind::Txt {
949 text, spans, style, ..
950 } => {
951 let spans = spans.get();
952 let body = if spans.is_empty() {
953 text.get()
954 } else {
955 spans.into_iter().map(|s| s.text).collect()
956 };
957 (body, style.max_width)
958 }
959 NodeKind::Code { code, style, .. } => (code.get(), style.max_width),
960 _ => return (None, 0.0),
961 };
962 let overflow = match (max_width, n.text_draw()) {
963 (Some(max), Some(TextDraw::Styled(s))) => (s.layout.width - max).max(0.0),
964 (Some(max), Some(TextDraw::Morph { size, .. })) => (size.0 - max).max(0.0),
965 (Some(max), Some(TextDraw::Diff(frame))) => (frame.size.0 - max).max(0.0),
966 _ => 0.0,
967 };
968 (Some(body), overflow)
969}
970
971pub(crate) fn measure(nodes: &[NodeData], id: NodeId) -> (f64, f64) {
972 let n = &nodes[id.0 as usize];
973 match &n.kind {
974 NodeKind::Rect { width, height, .. } => (width.get(), height.get()),
975 NodeKind::Circle { size, .. }
976 | NodeKind::Polygon { size, .. }
977 | NodeKind::Star { size, .. } => (size.get(), size.get()),
978 NodeKind::Img { width, height, .. }
979 | NodeKind::Grid { width, height, .. }
980 | NodeKind::Svg { width, height } => (width.get(), height.get()),
981 NodeKind::Txt { .. } | NodeKind::Code { .. } => match n.text_draw() {
982 Some(TextDraw::Styled(s)) => (s.layout.width, s.layout.height),
983 Some(TextDraw::Morph { size, .. }) => size,
984 Some(TextDraw::Diff(frame)) => frame.size,
985 None => (0.0, 0.0),
986 },
987 NodeKind::Path { .. } | NodeKind::Line { .. } => {
988 let bb = n
989 .outline((0.0, 0.0))
990 .map_or(kurbo::Rect::ZERO, |p| p.bounding_box());
991 (bb.width(), bb.height())
992 }
993 NodeKind::Layout { .. } => crate::flex::compute(nodes, id, &|c| measure(nodes, c)).0,
994 NodeKind::Camera { .. } => (0.0, 0.0),
996 NodeKind::Group => {
997 let mut bb: Option<kurbo::Rect> = None;
998 for &c in &n.children {
999 let child = &nodes[c.0 as usize];
1000 let (w, h) = measure(nodes, c);
1001 let r = child
1002 .local_transform(Vec2::ZERO, (w, h))
1003 .transform_rect_bbox(kurbo::Rect::new(-w / 2.0, -h / 2.0, w / 2.0, h / 2.0));
1004 bb = Some(bb.map_or(r, |b| b.union(r)));
1005 }
1006 bb.map_or((0.0, 0.0), |b| (b.width(), b.height()))
1007 }
1008 }
1009}
1010
1011fn resizable(kind: &NodeKind) -> bool {
1013 matches!(
1014 kind,
1015 NodeKind::Rect { .. }
1016 | NodeKind::Layout { .. }
1017 | NodeKind::Img { .. }
1018 | NodeKind::Grid { .. }
1019 | NodeKind::Svg { .. }
1020 )
1021}
1022
1023fn visit(
1024 nodes: &[NodeData],
1025 id: NodeId,
1026 parent_tf: Affine,
1027 placement: Option<FlexBox>,
1028 flex: Option<&FlexMap>,
1029 out: &mut Vec<DrawItem>,
1030) {
1031 let n = &nodes[id.0 as usize];
1032 let opacity = n.opacity.get().clamp(0.0, 1.0);
1033 if opacity <= 0.0 {
1034 return;
1035 }
1036 let size = match placement {
1037 Some(b) if resizable(&n.kind) => {
1038 let s = n.scale.get();
1039 (
1040 b.size.0 / s.x.abs().max(1e-9),
1041 b.size.1 / s.y.abs().max(1e-9),
1042 )
1043 }
1044 _ => measure(nodes, id),
1045 };
1046 let offset = placement.map_or(Vec2::ZERO, |b| b.center);
1047 let tf = parent_tf * n.local_transform(offset, size);
1048
1049 let blend = n.blend.get();
1052 let blur = n.blur.get().max(0.0);
1053 let shadow_color = n.shadow_color.get();
1054 let shadow = (shadow_color.a > 0.0).then(|| Shadow {
1055 color: shadow_color,
1056 blur: n.shadow_blur.get().max(0.0),
1057 offset: n.shadow_offset.get(),
1058 });
1059 let layered = blend != BlendMode::Normal
1060 || blur > 0.0
1061 || shadow.is_some()
1062 || (opacity < 1.0 && !n.children.is_empty());
1063 if layered {
1064 out.push(DrawItem {
1065 node: id,
1066 transform: tf,
1067 prim: Primitive::PushLayer(Layer {
1068 opacity,
1069 blend,
1070 clip: None,
1071 blur,
1072 shadow,
1073 }),
1074 });
1075 }
1076 let paint_opacity = if layered { 1.0 } else { opacity };
1077 for prim in crate::draw::primitives(n, size, paint_opacity) {
1078 out.push(DrawItem {
1079 node: id,
1080 transform: tf,
1081 prim,
1082 });
1083 }
1084
1085 if !n.children.is_empty() {
1086 let clip = n.clip.get().then(|| {
1088 n.outline(size).unwrap_or_else(|| {
1089 kurbo::Rect::new(-size.0 / 2.0, -size.1 / 2.0, size.0 / 2.0, size.1 / 2.0)
1090 .to_path(0.1)
1091 })
1092 });
1093 let clipped = clip.is_some();
1094 if let Some(path) = clip {
1095 out.push(DrawItem {
1096 node: id,
1097 transform: tf,
1098 prim: Primitive::PushLayer(Layer::clip(path)),
1099 });
1100 }
1101 let own;
1104 let map: Option<&FlexMap> = match (&n.kind, flex) {
1105 (NodeKind::Layout { .. }, Some(m)) => Some(m),
1106 (NodeKind::Layout { .. }, None) => {
1107 own = crate::flex::compute(nodes, id, &|c| measure(nodes, c)).1;
1108 Some(&own)
1109 }
1110 _ => None,
1111 };
1112 let mut order: Vec<(usize, NodeId)> = n.children.iter().copied().enumerate().collect();
1113 order.sort_by(|a, b| {
1115 let za = nodes[a.1.0 as usize].z_index.get();
1116 let zb = nodes[b.1.0 as usize].z_index.get();
1117 za.total_cmp(&zb)
1118 });
1119 for (_, c) in order {
1120 let placement = map.and_then(|m| m.get(&c).copied());
1121 visit(nodes, c, tf, placement, map, out);
1122 }
1123 if clipped {
1124 out.push(DrawItem {
1125 node: id,
1126 transform: tf,
1127 prim: Primitive::PopLayer,
1128 });
1129 }
1130 }
1131 if layered {
1132 out.push(DrawItem {
1133 node: id,
1134 transform: tf,
1135 prim: Primitive::PopLayer,
1136 });
1137 }
1138}
1139
1140impl DrawItem {
1141 fn hit(&self, p: Point) -> bool {
1142 let local = self.transform.inverse() * p;
1143 match &self.prim {
1144 Primitive::Fill { path, .. } => {
1145 (is_text_like(path) && path.bounding_box().contains(local))
1146 || closed(path).contains(local)
1147 }
1148 Primitive::Stroke { path, style, .. } => {
1149 let tol = (style.width / 2.0)
1150 .max(4.0 / self.transform.determinant().abs().sqrt().max(1e-9));
1151 path.segments().any(|s| {
1152 kurbo::ParamCurveNearest::nearest(&s, local, 0.1).distance_sq <= tol * tol
1153 })
1154 }
1155 Primitive::Image { rect, .. } => rect.contains(local),
1156 Primitive::PushLayer(_) | Primitive::PopLayer => false,
1157 }
1158 }
1159}
1160
1161fn closed(path: &BezPath) -> BezPath {
1163 let mut out = BezPath::new();
1164 let mut open = false;
1165 for el in path.elements() {
1166 match el {
1167 kurbo::PathEl::MoveTo(_) => {
1168 if open {
1169 out.close_path();
1170 }
1171 open = true;
1172 }
1173 kurbo::PathEl::ClosePath => open = false,
1174 _ => {}
1175 }
1176 out.push(*el);
1177 }
1178 if open {
1179 out.close_path();
1180 }
1181 out
1182}
1183
1184fn is_text_like(path: &BezPath) -> bool {
1187 path.elements()
1188 .iter()
1189 .filter(|e| matches!(e, kurbo::PathEl::MoveTo(_)))
1190 .count()
1191 > 3
1192}
1193
1194thread_local! {
1195 static CURRENT: RefCell<Option<Rc<SceneGraph>>> = const { RefCell::new(None) };
1196}
1197
1198impl SceneGraph {
1199 pub fn enter<R>(self: &Rc<Self>, f: impl FnOnce() -> R) -> R {
1201 let prev = CURRENT.with(|c| c.borrow_mut().replace(self.clone()));
1202 struct Restore(Option<Rc<SceneGraph>>);
1203 impl Drop for Restore {
1204 fn drop(&mut self) {
1205 let prev = self.0.take();
1206 CURRENT.with(|c| *c.borrow_mut() = prev);
1207 }
1208 }
1209 let _restore = Restore(prev);
1210 f()
1211 }
1212}
1213
1214pub(crate) fn with_graph<R>(f: impl FnOnce(&SceneGraph) -> R) -> R {
1215 CURRENT.with(|c| {
1216 let c = c.borrow();
1217 f(c.as_ref()
1218 .expect("oxframe: node handles can only be used while a scene is running"))
1219 })
1220}