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 Custom {
173 draw: crate::draw::DrawFn,
174 width: f64,
175 height: f64,
176 },
177 Particles {
180 seed: u64,
181 gravity: Signal<f64>,
182 toward_camera: bool,
183 life: f64,
184 speed: f64,
185 radius: f64,
186 state: crate::sim::ParticlesState,
187 },
188}
189
190impl NodeKind {
191 pub fn name(&self) -> &'static str {
192 match self {
193 Self::Group => "Group",
194 Self::Camera { .. } => "Camera",
195 Self::Rect { .. } => "Rect",
196 Self::Circle { .. } => "Circle",
197 Self::Polygon { .. } => "Polygon",
198 Self::Star { .. } => "Star",
199 Self::Path { .. } => "Path",
200 Self::Line { .. } => "Line",
201 Self::Txt { .. } => "Txt",
202 Self::Code { .. } => "Code",
203 Self::Img { .. } => "Img",
204 Self::Layout { .. } => "Layout",
205 Self::Grid { .. } => "Grid",
206 Self::Svg { .. } => "Svg",
207 Self::Custom { .. } => "Custom",
208 Self::Particles { .. } => "Particles",
209 }
210 }
211}
212
213#[doc(hidden)]
215pub struct NodeData {
216 pub id: NodeId,
217 pub name: Option<String>,
218 pub source: Option<String>,
220 pub parent: Option<NodeId>,
221 pub children: Vec<NodeId>,
222 pub kind: NodeKind,
223 pub x: Signal<f64>,
224 pub y: Signal<f64>,
225 pub rotation: Signal<f64>,
227 pub scale: Signal<Vec2>,
228 pub opacity: Signal<f64>,
229 pub fill: Signal<Color>,
230 pub stroke: Signal<Color>,
231 pub line_width: Signal<f64>,
232 pub start: Signal<f64>,
234 pub end: Signal<f64>,
235 pub fill_gradient: Signal<Option<Gradient>>,
237 pub stroke_gradient: Signal<Option<Gradient>>,
238 pub dash: Signal<Vec<f64>>,
240 pub dash_offset: Signal<f64>,
241 pub line_cap: Signal<LineCap>,
242 pub line_join: Signal<LineJoin>,
243 pub blur: Signal<f64>,
245 pub shadow_color: Signal<Color>,
246 pub shadow_blur: Signal<f64>,
247 pub shadow_offset: Signal<Vec2>,
248 pub clip: Signal<bool>,
250 pub blend: Signal<BlendMode>,
251 pub z_index: Signal<f64>,
253 pub anchor: Signal<Vec2>,
257 pub grow: Signal<f64>,
259 pub shrink: Signal<f64>,
260 pub basis: Signal<Option<f64>>,
261 pub margin: Signal<f64>,
262 pub align_self: Signal<Option<LayoutAlign>>,
263 pub place: Option<crate::place::Place>,
265 pub placed: Cell<(f64, f64)>,
267}
268
269impl NodeData {
270 pub fn local_transform(&self, offset: Vec2, size: (f64, f64)) -> Affine {
273 if let NodeKind::Camera { zoom } = &self.kind {
274 let (x, y) = self.placed.get();
278 return Affine::translate(offset)
279 * Affine::rotate(-self.rotation.get().to_radians())
280 * Affine::scale(zoom.get().max(1e-6))
281 * Affine::translate((-x, -y));
282 }
283 let s = self.scale.get();
284 let a = self.anchor.get();
285 let (x, y) = self.placed.get();
286 Affine::translate(offset + Vec2::new(x, y))
287 * Affine::rotate(self.rotation.get().to_radians())
288 * Affine::scale_non_uniform(s.x, s.y)
289 * Affine::translate((-a.x * size.0 / 2.0, -a.y * size.1 / 2.0))
290 }
291
292 pub(crate) fn text_draw(&self) -> Option<TextDraw> {
294 match &self.kind {
295 NodeKind::Txt {
296 text,
297 spans,
298 font_size,
299 weight,
300 style,
301 morph,
302 morph_t,
303 } => {
304 let style = TextStyle {
305 size: font_size.get().max(0.1),
306 weight: weight.get().clamp(1.0, 1000.0),
307 ..style.clone()
308 };
309 if let Some((from, to)) = morph.get() {
310 let a = oxframe_text::layout(&from, &style);
311 let b = oxframe_text::layout(&to, &style);
312 let (glyphs, size) = crate::text::morph(&a, &b, morph_t.get().clamp(0.0, 1.0));
313 return Some(TextDraw::Morph { glyphs, size });
314 }
315 let spans = spans.get();
316 Some(TextDraw::Styled(if spans.is_empty() {
317 crate::text::rich(&[RichSpan::new(text.get())], &style)
318 } else {
319 crate::text::rich(&spans, &style)
320 }))
321 }
322 NodeKind::Code {
323 code,
324 edit_from,
325 edit_to,
326 editing,
327 edit_t,
328 edit_flash,
329 lang,
330 theme,
331 font_size,
332 style,
333 focus,
334 dim,
335 } => {
336 let style = TextStyle {
337 size: font_size.get().max(0.1),
338 ..style.clone()
339 };
340 if editing.get() {
341 let (frame, _) = crate::text::layout_diff(
342 &edit_from.get(),
343 &edit_to.get(),
344 *lang,
345 crate::text::DiffView {
346 theme,
347 style: &style,
348 focus: focus.get(),
349 dim: dim.get(),
350 t: edit_t.get(),
351 flash: edit_flash.get(),
352 },
353 );
354 return Some(TextDraw::Diff(frame));
355 }
356 Some(TextDraw::Styled(crate::text::code(
357 &code.get(),
358 *lang,
359 theme,
360 &style,
361 focus.get(),
362 dim.get(),
363 )))
364 }
365 _ => None,
366 }
367 }
368
369 pub fn outline(&self, size: (f64, f64)) -> Option<BezPath> {
371 const TOLERANCE: f64 = 0.1;
372 match &self.kind {
373 NodeKind::Group
374 | NodeKind::Svg { .. }
375 | NodeKind::Camera { .. }
376 | NodeKind::Img { .. }
377 | NodeKind::Txt { .. }
378 | NodeKind::Code { .. }
379 | NodeKind::Grid { .. }
380 | NodeKind::Custom { .. }
381 | NodeKind::Particles { .. } => None,
382 NodeKind::Rect { radius, .. } | NodeKind::Layout { radius, .. } => {
383 let (w, h) = (size.0.max(0.0), size.1.max(0.0));
384 let r = radius.get().clamp(0.0, w.min(h) / 2.0);
385 let rect = kurbo::Rect::new(-w / 2.0, -h / 2.0, w / 2.0, h / 2.0);
386 Some(if r > 0.0 {
387 rect.to_rounded_rect(r).to_path(TOLERANCE)
388 } else {
389 rect.to_path(TOLERANCE)
390 })
391 }
392 NodeKind::Circle {
393 start_angle,
394 end_angle,
395 pie,
396 ..
397 } => {
398 let r = size.0.max(0.0) / 2.0;
401 let (a0, a1) = (start_angle.get(), end_angle.get());
402 let sweep = (a1 - a0).clamp(-360.0, 360.0);
403 if sweep.abs() >= 360.0 - 1e-9 {
404 let circle = kurbo::Circle::new((0.0, 0.0), r).to_path(TOLERANCE);
405 return Some(Affine::rotate((a0 - 90.0).to_radians()) * circle);
406 }
407 let arc = kurbo::Arc::new(
408 (0.0, 0.0),
409 (r, r),
410 (a0 - 90.0).to_radians(),
411 sweep.to_radians(),
412 0.0,
413 );
414 let mut p = BezPath::new();
415 if *pie {
416 p.move_to((0.0, 0.0));
417 p.line_to(arc.center + kurbo::Vec2::from_angle(arc.start_angle) * r);
418 arc.to_cubic_beziers(TOLERANCE, |a, b, c| p.curve_to(a, b, c));
419 p.close_path();
420 } else {
421 p.move_to(arc.center + kurbo::Vec2::from_angle(arc.start_angle) * r);
422 arc.to_cubic_beziers(TOLERANCE, |a, b, c| p.curve_to(a, b, c));
423 }
424 Some(p)
425 }
426 NodeKind::Polygon { sides, .. } => {
427 let n = sides.get().round().max(3.0) as usize;
428 Some(regular(n, size.0.max(0.0) / 2.0, None))
429 }
430 NodeKind::Star { points, inner, .. } => {
431 let n = points.get().round().max(2.0) as usize;
432 let r = size.0.max(0.0) / 2.0;
433 Some(regular(n, r, Some(r * inner.get().clamp(0.0, 1.0))))
434 }
435 NodeKind::Path { data } => Some(data.get()),
436 NodeKind::Line {
437 points,
438 closed,
439 smooth,
440 ..
441 } => {
442 let pts = points.get();
443 if *smooth && pts.len() > 2 {
444 return Some(catmull_rom(&pts, *closed));
445 }
446 let mut p = BezPath::new();
447 for (i, v) in pts.iter().enumerate() {
448 if i == 0 {
449 p.move_to(v.to_point());
450 } else {
451 p.line_to(v.to_point());
452 }
453 }
454 if *closed && pts.len() > 2 {
455 p.close_path();
456 }
457 Some(p)
458 }
459 }
460 }
461}
462
463fn regular(n: usize, r: f64, inner: Option<f64>) -> BezPath {
465 let count = if inner.is_some() { 2 * n } else { n };
466 let mut p = BezPath::new();
467 for i in 0..count {
468 let radius = match inner {
469 Some(ri) if i % 2 == 1 => ri,
470 _ => r,
471 };
472 let a = -std::f64::consts::FRAC_PI_2 + std::f64::consts::TAU * i as f64 / count as f64;
473 let pt = Point::new(a.cos() * radius, a.sin() * radius);
474 if i == 0 {
475 p.move_to(pt);
476 } else {
477 p.line_to(pt);
478 }
479 }
480 p.close_path();
481 p
482}
483
484fn catmull_rom(pts: &[Vec2], closed: bool) -> BezPath {
486 let n = pts.len();
487 let at = |i: isize| -> Vec2 {
488 if closed {
489 pts[i.rem_euclid(n as isize) as usize]
490 } else {
491 pts[i.clamp(0, n as isize - 1) as usize]
492 }
493 };
494 let mut p = BezPath::new();
495 p.move_to(pts[0].to_point());
496 let segs = if closed { n } else { n - 1 };
497 for i in 0..segs as isize {
498 let (p0, p1, p2, p3) = (at(i - 1), at(i), at(i + 1), at(i + 2));
499 let c1 = p1 + (p2 - p0) / 6.0;
500 let c2 = p2 - (p3 - p1) / 6.0;
501 p.curve_to(c1.to_point(), c2.to_point(), p2.to_point());
502 }
503 if closed {
504 p.close_path();
505 }
506 p
507}
508
509pub(crate) enum TextDraw {
510 Styled(Styled),
511 Morph {
513 glyphs: Vec<(BezPath, f64)>,
514 size: (f64, f64),
515 },
516 Diff(DiffFrame),
518}
519
520pub struct SceneGraph {
522 nodes: RefCell<Vec<NodeData>>,
523 size: (u32, u32),
524 time: Cell<f64>,
526}
527
528impl SceneGraph {
529 pub const ROOT: NodeId = NodeId(0);
531
532 pub fn new(width: u32, height: u32) -> Rc<Self> {
534 let graph = Rc::new(Self {
535 nodes: RefCell::new(Vec::new()),
536 size: (width, height),
537 time: Cell::new(0.0),
538 });
539 graph.insert(
540 None,
541 NodeKind::Group,
542 crate::BaseProps {
543 name: Some("view".into()),
544 ..Default::default()
545 },
546 );
547 graph
548 }
549
550 pub fn size(&self) -> (u32, u32) {
552 self.size
553 }
554
555 pub fn set_time(&self, seconds: f64) {
558 self.time.set(seconds.max(0.0));
559 }
560
561 pub fn time(&self) -> f64 {
563 self.time.get()
564 }
565
566 pub fn len(&self) -> usize {
568 self.nodes.borrow().len()
569 }
570
571 pub fn is_empty(&self) -> bool {
573 self.len() == 0
574 }
575
576 pub fn node(&self, id: NodeId) -> Ref<'_, NodeData> {
582 Ref::map(self.nodes.borrow(), |n| &n[id.0 as usize])
583 }
584
585 pub(crate) fn next_id(&self) -> u32 {
586 self.len() as u32
587 }
588
589 pub(crate) fn insert(
590 &self,
591 parent: Option<NodeId>,
592 kind: NodeKind,
593 base: crate::BaseProps,
594 ) -> NodeId {
595 let mut nodes = self.nodes.borrow_mut();
596 let id = NodeId(u32::try_from(nodes.len()).expect("too many nodes"));
597 let label = format!("{}#{}", kind.name(), id.0);
598 let sig = |field: &str| format!("{label}.{field}");
599 nodes.push(NodeData {
600 id,
601 name: base.name,
602 source: current_add_at().map(|(file, line)| source_label(file, line)),
603 parent,
604 children: Vec::new(),
605 x: Signal::new(base.x).with_name(sig("x")),
606 y: Signal::new(base.y).with_name(sig("y")),
607 rotation: Signal::new(base.rotation).with_name(sig("rotation")),
608 scale: Signal::new(base.scale).with_name(sig("scale")),
609 opacity: Signal::new(base.opacity).with_name(sig("opacity")),
610 fill: Signal::new(base.fill).with_name(sig("fill")),
611 stroke: Signal::new(base.stroke).with_name(sig("stroke")),
612 line_width: Signal::new(base.line_width).with_name(sig("line_width")),
613 start: Signal::new(base.start).with_name(sig("start")),
614 end: Signal::new(base.end).with_name(sig("end")),
615 fill_gradient: Signal::new(base.fill_gradient).with_name(sig("fill_gradient")),
616 stroke_gradient: Signal::new(base.stroke_gradient).with_name(sig("stroke_gradient")),
617 dash: Signal::new(base.dash).with_name(sig("dash")),
618 dash_offset: Signal::new(base.dash_offset).with_name(sig("dash_offset")),
619 line_cap: Signal::new(base.line_cap).with_name(sig("line_cap")),
620 line_join: Signal::new(base.line_join).with_name(sig("line_join")),
621 blur: Signal::new(base.blur).with_name(sig("blur")),
622 shadow_color: Signal::new(base.shadow_color).with_name(sig("shadow_color")),
623 shadow_blur: Signal::new(base.shadow_blur).with_name(sig("shadow_blur")),
624 shadow_offset: Signal::new(base.shadow_offset).with_name(sig("shadow_offset")),
625 clip: Signal::new(base.clip).with_name(sig("clip")),
626 blend: Signal::new(base.blend).with_name(sig("blend")),
627 z_index: Signal::new(base.z_index).with_name(sig("z_index")),
628 anchor: Signal::new(base.anchor).with_name(sig("anchor")),
629 grow: Signal::new(base.grow).with_name(sig("grow")),
630 shrink: Signal::new(base.shrink).with_name(sig("shrink")),
631 basis: Signal::new(base.basis).with_name(sig("basis")),
632 margin: Signal::new(base.margin).with_name(sig("margin")),
633 align_self: Signal::new(base.align_self).with_name(sig("align_self")),
634 place: base.place,
635 placed: Cell::new((base.x, base.y)),
636 kind,
637 });
638 if let Some(p) = parent {
639 nodes[p.0 as usize].children.push(id);
640 }
641 id
642 }
643
644 pub(crate) fn detach(&self, id: NodeId) {
645 let mut nodes = self.nodes.borrow_mut();
646 if let Some(p) = nodes[id.0 as usize].parent.take() {
647 nodes[p.0 as usize].children.retain(|c| *c != id);
648 }
649 }
650
651 pub(crate) fn reparent(&self, id: NodeId, parent: NodeId) {
652 self.detach(id);
653 let mut nodes = self.nodes.borrow_mut();
654 nodes[id.0 as usize].parent = Some(parent);
655 nodes[parent.0 as usize].children.push(id);
656 }
657
658 pub fn measure(&self, id: NodeId) -> (f64, f64) {
660 self.resolve_places();
661 let nodes = self.nodes.borrow();
662 measure(&nodes, id)
663 }
664
665 pub(crate) fn set_place(&self, id: NodeId, place: crate::place::Place) {
666 self.nodes.borrow_mut()[id.0 as usize].place = Some(place);
667 }
668
669 fn resolve_places(&self) {
670 let nodes = self.nodes.borrow();
671 crate::place::resolve(&nodes, (f64::from(self.size.0), f64::from(self.size.1)));
672 }
673
674 pub fn draw_list(&self) -> Vec<DrawItem> {
676 self.resolve_places();
677 let nodes = self.nodes.borrow();
678 let mut out = Vec::new();
679 visit(
680 &nodes,
681 Self::ROOT,
682 Affine::IDENTITY,
683 None,
684 None,
685 self.time.get(),
686 &mut out,
687 );
688 out
689 }
690
691 pub fn draw_drift(&self) -> Option<(String, Option<String>)> {
694 let a = self.draw_list();
695 let b = self.draw_list();
696 let id = first_drift(&a, &b)?;
697 let n = self.node(id);
698 let label = n
699 .name
700 .clone()
701 .or_else(|| n.source.clone())
702 .unwrap_or_else(|| n.kind.name().to_owned());
703 Some((label, n.source.clone()))
704 }
705
706 pub fn pick(&self, p: Point) -> Option<NodeId> {
708 self.draw_list()
709 .iter()
710 .rev()
711 .find(|item| item.node != Self::ROOT && item.hit(p))
712 .map(|item| item.node)
713 }
714
715 pub fn world_transform(&self, id: NodeId) -> Option<Affine> {
717 if let Some(item) = self.draw_list().into_iter().find(|i| i.node == id) {
720 return Some(item.transform);
721 }
722 let nodes = self.nodes.borrow();
723 let mut tf = Affine::IDENTITY;
724 let mut cur = Some(id);
725 while let Some(c) = cur {
726 let n = nodes.get(c.0 as usize)?;
727 tf = n.local_transform(Vec2::ZERO, measure(&nodes, c)) * tf;
728 cur = n.parent;
729 }
730 Some(tf)
731 }
732
733 pub fn box_in(&self, id: NodeId, frame: NodeId) -> Option<kurbo::Rect> {
737 let node_tf = self.world_transform(id)?;
738 let frame_tf = self.world_transform(frame)?;
739 let (w, h) = measure(&self.nodes.borrow(), id);
740 let [x0, y0, x1, y1] = aabb(frame_tf.inverse() * node_tf, w, h);
741 Some(kurbo::Rect::new(x0, y0, x1, y1))
742 }
743
744 pub fn frame_size(&self) -> (f64, f64) {
746 (f64::from(self.size.0), f64::from(self.size.1))
747 }
748
749 pub(crate) fn descendant_named(&self, root: NodeId, name: &str) -> Option<NodeId> {
751 fn walk(nodes: &[NodeData], id: NodeId, name: &str) -> Option<NodeId> {
752 let n = nodes.get(id.0 as usize)?;
753 if n.name.as_deref() == Some(name) {
754 return Some(id);
755 }
756 for &c in &n.children {
757 if let Some(hit) = walk(nodes, c, name) {
758 return Some(hit);
759 }
760 }
761 None
762 }
763 walk(&self.nodes.borrow(), root, name)
764 }
765
766 pub(crate) fn descendant_paths(&self, root: NodeId) -> Vec<NodeId> {
768 fn walk(nodes: &[NodeData], id: NodeId, out: &mut Vec<NodeId>) {
769 let Some(n) = nodes.get(id.0 as usize) else {
770 return;
771 };
772 if matches!(n.kind, NodeKind::Path { .. }) {
773 out.push(id);
774 }
775 for &c in &n.children {
776 walk(nodes, c, out);
777 }
778 }
779 let mut out = Vec::new();
780 walk(&self.nodes.borrow(), root, &mut out);
781 out
782 }
783
784 pub fn tree(&self) -> Vec<(NodeId, Option<NodeId>, &'static str, Option<String>)> {
786 let nodes = self.nodes.borrow();
787 let mut out = Vec::new();
788 let mut stack = vec![Self::ROOT];
789 while let Some(id) = stack.pop() {
790 let n = &nodes[id.0 as usize];
791 out.push((id, n.parent, n.kind.name(), n.name.clone()));
792 stack.extend(n.children.iter().rev());
793 }
794 out
795 }
796
797 pub fn describe(&self, id: NodeId) -> Vec<(String, String)> {
799 let nodes = self.nodes.borrow();
800 let Some(n) = nodes.get(id.0 as usize) else {
801 return Vec::new();
802 };
803 let f = |v: f64| format!("{v:.3}");
804 let s = n.scale.get();
805 let mut out = vec![
806 ("x".into(), f(n.x.get())),
807 ("y".into(), f(n.y.get())),
808 ("rotation".into(), f(n.rotation.get())),
809 ("scale".into(), format!("{:.3}, {:.3}", s.x, s.y)),
810 ("opacity".into(), f(n.opacity.get())),
811 ("fill".into(), n.fill.get().to_string()),
812 ("stroke".into(), n.stroke.get().to_string()),
813 ("line_width".into(), f(n.line_width.get())),
814 ("start".into(), f(n.start.get())),
815 ("end".into(), f(n.end.get())),
816 ];
817 let (w, h) = measure(&nodes, id);
818 out.push(("size".into(), format!("{w:.1} × {h:.1}")));
819 match &n.kind {
820 NodeKind::Rect { radius, .. } => out.push(("radius".into(), f(radius.get()))),
821 NodeKind::Code { code, .. } => out.push(("code".into(), code.get())),
822 NodeKind::Txt {
823 text,
824 font_size,
825 weight,
826 ..
827 } => {
828 out.push(("text".into(), text.get()));
829 out.push(("font_size".into(), f(font_size.get())));
830 out.push(("weight".into(), f(weight.get())));
831 }
832 NodeKind::Line { points, .. } => {
833 out.push(("points".into(), points.get().len().to_string()))
834 }
835 NodeKind::Layout { gap, padding, .. } => {
836 out.push(("gap".into(), f(gap.get())));
837 out.push(("padding".into(), f(padding.get())));
838 }
839 NodeKind::Grid { spacing, .. } => out.push(("spacing".into(), f(spacing.get()))),
840 _ => {}
841 }
842 out
843 }
844
845 pub fn inspect_nodes(&self) -> Vec<InspectNode> {
850 let list = self.draw_list();
851 let mut drawn: Vec<Option<(Affine, u32)>> = vec![None; self.len()];
852 let mut order = 0u32;
853 for item in &list {
854 let slot = &mut drawn[item.node.0 as usize];
855 if slot.is_none() {
856 *slot = Some((item.transform, order));
857 order += 1;
858 }
859 }
860 let nodes = self.nodes.borrow();
861 let mut out = Vec::new();
862 let mut stack = vec![Self::ROOT];
863 while let Some(id) = stack.pop() {
864 let n = &nodes[id.0 as usize];
865 stack.extend(n.children.iter().rev());
866 let (w, h) = measure(&nodes, id);
867 let (tf, order) = drawn[id.0 as usize]
868 .unwrap_or_else(|| (transform_without_layout(&nodes, id), u32::MAX));
869 let (text, text_overflow) = text_info(n);
870 out.push(InspectNode {
871 id: id.0,
872 parent: n.parent.map(|p| p.0),
873 kind: n.kind.name(),
874 name: n.name.clone().unwrap_or_else(|| {
875 n.source
876 .clone()
877 .unwrap_or_else(|| format!("{}#{}", n.kind.name(), id.0))
878 }),
879 source: n.source.clone(),
880 bounds: aabb(tf, w, h),
881 opacity: n.opacity.get().clamp(0.0, 1.0),
882 order,
883 text,
884 fill: n.fill.get(),
885 text_overflow,
886 parent_kind: n.parent.map(|p| nodes[p.0 as usize].kind.name()),
887 in_camera: {
888 let mut up = n.parent;
889 let mut found = false;
890 while let Some(p) = up {
891 let pn = &nodes[p.0 as usize];
892 if matches!(pn.kind, NodeKind::Camera { .. }) {
893 found = true;
894 break;
895 }
896 up = pn.parent;
897 }
898 found
899 },
900 });
901 }
902 out
903 }
904}
905
906#[derive(Debug, Clone, PartialEq)]
908pub struct InspectNode {
909 pub id: u32,
911 pub parent: Option<u32>,
913 pub kind: &'static str,
915 pub name: String,
917 pub source: Option<String>,
919 pub bounds: [f64; 4],
922 pub opacity: f64,
924 pub order: u32,
926 pub text: Option<String>,
928 pub fill: Color,
930 pub text_overflow: f64,
933 pub parent_kind: Option<&'static str>,
935 pub in_camera: bool,
938}
939
940thread_local! {
941 static ADD_AT: Cell<Option<(&'static str, u32)>> = const { Cell::new(None) };
942}
943
944pub(crate) fn push_add_at(file: &'static str, line: u32) -> Option<(&'static str, u32)> {
945 ADD_AT.with(|c| c.replace(Some((file, line))))
946}
947
948pub(crate) fn pop_add_at(prev: Option<(&'static str, u32)>) {
949 ADD_AT.with(|c| c.set(prev));
950}
951
952fn current_add_at() -> Option<(&'static str, u32)> {
953 ADD_AT.with(|c| c.get())
954}
955
956fn source_label(file: &str, line: u32) -> String {
958 let file = file.replace('\\', "/");
959 let rel = file
960 .rfind("/src/")
961 .map_or(file.as_str(), |i| &file[i + 1..]);
962 format!("{rel}:{line}")
963}
964
965fn aabb(tf: Affine, w: f64, h: f64) -> [f64; 4] {
966 let pts = [
967 (-w / 2.0, -h / 2.0),
968 (w / 2.0, -h / 2.0),
969 (w / 2.0, h / 2.0),
970 (-w / 2.0, h / 2.0),
971 ];
972 let mut b = [
973 f64::INFINITY,
974 f64::INFINITY,
975 f64::NEG_INFINITY,
976 f64::NEG_INFINITY,
977 ];
978 for (x, y) in pts {
979 let p = tf * Point::new(x, y);
980 b[0] = b[0].min(p.x);
981 b[1] = b[1].min(p.y);
982 b[2] = b[2].max(p.x);
983 b[3] = b[3].max(p.y);
984 }
985 b
986}
987
988fn transform_without_layout(nodes: &[NodeData], id: NodeId) -> Affine {
989 let mut chain = Vec::new();
990 let mut cur = Some(id);
991 while let Some(c) = cur {
992 chain.push(c);
993 cur = nodes.get(c.0 as usize).and_then(|n| n.parent);
994 }
995 let mut tf = Affine::IDENTITY;
996 for c in chain.into_iter().rev() {
997 let Some(n) = nodes.get(c.0 as usize) else {
998 break;
999 };
1000 tf *= n.local_transform(Vec2::ZERO, measure(nodes, c));
1001 }
1002 tf
1003}
1004
1005fn text_info(n: &NodeData) -> (Option<String>, f64) {
1006 let (body, max_width) = match &n.kind {
1007 NodeKind::Txt {
1008 text, spans, style, ..
1009 } => {
1010 let spans = spans.get();
1011 let body = if spans.is_empty() {
1012 text.get()
1013 } else {
1014 spans.into_iter().map(|s| s.text).collect()
1015 };
1016 (body, style.max_width)
1017 }
1018 NodeKind::Code { code, style, .. } => (code.get(), style.max_width),
1019 _ => return (None, 0.0),
1020 };
1021 let overflow = match (max_width, n.text_draw()) {
1022 (Some(max), Some(TextDraw::Styled(s))) => (s.layout.width - max).max(0.0),
1023 (Some(max), Some(TextDraw::Morph { size, .. })) => (size.0 - max).max(0.0),
1024 (Some(max), Some(TextDraw::Diff(frame))) => (frame.size.0 - max).max(0.0),
1025 _ => 0.0,
1026 };
1027 (Some(body), overflow)
1028}
1029
1030pub(crate) fn measure(nodes: &[NodeData], id: NodeId) -> (f64, f64) {
1031 let n = &nodes[id.0 as usize];
1032 match &n.kind {
1033 NodeKind::Rect { width, height, .. } => (width.get(), height.get()),
1034 NodeKind::Circle { size, .. }
1035 | NodeKind::Polygon { size, .. }
1036 | NodeKind::Star { size, .. } => (size.get(), size.get()),
1037 NodeKind::Img { width, height, .. }
1038 | NodeKind::Grid { width, height, .. }
1039 | NodeKind::Svg { width, height } => (width.get(), height.get()),
1040 NodeKind::Txt { .. } | NodeKind::Code { .. } => match n.text_draw() {
1041 Some(TextDraw::Styled(s)) => (s.layout.width, s.layout.height),
1042 Some(TextDraw::Morph { size, .. }) => size,
1043 Some(TextDraw::Diff(frame)) => frame.size,
1044 None => (0.0, 0.0),
1045 },
1046 NodeKind::Path { .. } | NodeKind::Line { .. } => {
1047 let bb = n
1048 .outline((0.0, 0.0))
1049 .map_or(kurbo::Rect::ZERO, |p| p.bounding_box());
1050 (bb.width(), bb.height())
1051 }
1052 NodeKind::Layout { .. } => crate::flex::compute(nodes, id, &|c| measure(nodes, c)).0,
1053 NodeKind::Camera { .. } | NodeKind::Particles { .. } => (0.0, 0.0),
1055 NodeKind::Custom { width, height, .. } => (*width, *height),
1056 NodeKind::Group => {
1057 let mut bb: Option<kurbo::Rect> = None;
1058 for &c in &n.children {
1059 let child = &nodes[c.0 as usize];
1060 let (w, h) = measure(nodes, c);
1061 let r = child
1062 .local_transform(Vec2::ZERO, (w, h))
1063 .transform_rect_bbox(kurbo::Rect::new(-w / 2.0, -h / 2.0, w / 2.0, h / 2.0));
1064 bb = Some(bb.map_or(r, |b| b.union(r)));
1065 }
1066 bb.map_or((0.0, 0.0), |b| (b.width(), b.height()))
1067 }
1068 }
1069}
1070
1071fn resizable(kind: &NodeKind) -> bool {
1073 matches!(
1074 kind,
1075 NodeKind::Rect { .. }
1076 | NodeKind::Layout { .. }
1077 | NodeKind::Img { .. }
1078 | NodeKind::Grid { .. }
1079 | NodeKind::Svg { .. }
1080 )
1081}
1082
1083fn first_drift(a: &[DrawItem], b: &[DrawItem]) -> Option<NodeId> {
1084 let n = a.len().max(b.len());
1085 for i in 0..n {
1086 match (a.get(i), b.get(i)) {
1087 (Some(x), Some(y)) if x.node == y.node && prim_same(&x.prim, &y.prim) => {}
1088 (Some(x), _) => return Some(x.node),
1089 (_, Some(y)) => return Some(y.node),
1090 (None, None) => {}
1091 }
1092 }
1093 None
1094}
1095
1096fn prim_same(a: &Primitive, b: &Primitive) -> bool {
1097 use Primitive::{Fill, Image, PopLayer, PushLayer, Stroke};
1098 match (a, b) {
1099 (Fill { path: p, paint: c }, Fill { path: q, paint: d }) => p == q && c == d,
1100 (
1101 Stroke {
1102 path: p,
1103 paint: c,
1104 style: s,
1105 },
1106 Stroke {
1107 path: q,
1108 paint: d,
1109 style: t,
1110 },
1111 ) => p == q && c == d && s == t,
1112 (
1113 Image {
1114 rect: r,
1115 opacity: o,
1116 ..
1117 },
1118 Image {
1119 rect: s,
1120 opacity: p,
1121 ..
1122 },
1123 ) => r == s && o == p,
1124 (PushLayer(x), PushLayer(y)) => x == y,
1125 (PopLayer, PopLayer) => true,
1126 _ => false,
1127 }
1128}
1129
1130fn visit(
1131 nodes: &[NodeData],
1132 id: NodeId,
1133 parent_tf: Affine,
1134 placement: Option<FlexBox>,
1135 flex: Option<&FlexMap>,
1136 time: f64,
1137 out: &mut Vec<DrawItem>,
1138) {
1139 let n = &nodes[id.0 as usize];
1140 let opacity = n.opacity.get().clamp(0.0, 1.0);
1141 if opacity <= 0.0 {
1142 return;
1143 }
1144 let size = match placement {
1145 Some(b) if resizable(&n.kind) => {
1146 let s = n.scale.get();
1147 (
1148 b.size.0 / s.x.abs().max(1e-9),
1149 b.size.1 / s.y.abs().max(1e-9),
1150 )
1151 }
1152 _ => measure(nodes, id),
1153 };
1154 let offset = placement.map_or(Vec2::ZERO, |b| b.center);
1155 let tf = parent_tf * n.local_transform(offset, size);
1156
1157 let blend = n.blend.get();
1160 let blur = n.blur.get().max(0.0);
1161 let shadow_color = n.shadow_color.get();
1162 let shadow = (shadow_color.a > 0.0).then(|| Shadow {
1163 color: shadow_color,
1164 blur: n.shadow_blur.get().max(0.0),
1165 offset: n.shadow_offset.get(),
1166 });
1167 let layered = blend != BlendMode::Normal
1168 || blur > 0.0
1169 || shadow.is_some()
1170 || (opacity < 1.0 && !n.children.is_empty());
1171 if layered {
1172 out.push(DrawItem {
1173 node: id,
1174 transform: tf,
1175 prim: Primitive::PushLayer(Layer {
1176 opacity,
1177 blend,
1178 clip: None,
1179 blur,
1180 shadow,
1181 }),
1182 });
1183 }
1184 let paint_opacity = if layered { 1.0 } else { opacity };
1185 for prim in crate::draw::primitives(n, size, paint_opacity, time) {
1186 out.push(DrawItem {
1187 node: id,
1188 transform: tf,
1189 prim,
1190 });
1191 }
1192
1193 if !n.children.is_empty() {
1194 let clip = n.clip.get().then(|| {
1196 n.outline(size).unwrap_or_else(|| {
1197 kurbo::Rect::new(-size.0 / 2.0, -size.1 / 2.0, size.0 / 2.0, size.1 / 2.0)
1198 .to_path(0.1)
1199 })
1200 });
1201 let clipped = clip.is_some();
1202 if let Some(path) = clip {
1203 out.push(DrawItem {
1204 node: id,
1205 transform: tf,
1206 prim: Primitive::PushLayer(Layer::clip(path)),
1207 });
1208 }
1209 let own;
1212 let map: Option<&FlexMap> = match (&n.kind, flex) {
1213 (NodeKind::Layout { .. }, Some(m)) => Some(m),
1214 (NodeKind::Layout { .. }, None) => {
1215 own = crate::flex::compute(nodes, id, &|c| measure(nodes, c)).1;
1216 Some(&own)
1217 }
1218 _ => None,
1219 };
1220 let mut order: Vec<(usize, NodeId)> = n.children.iter().copied().enumerate().collect();
1221 order.sort_by(|a, b| {
1223 let za = nodes[a.1.0 as usize].z_index.get();
1224 let zb = nodes[b.1.0 as usize].z_index.get();
1225 za.total_cmp(&zb)
1226 });
1227 for (_, c) in order {
1228 let placement = map.and_then(|m| m.get(&c).copied());
1229 visit(nodes, c, tf, placement, map, time, out);
1230 }
1231 if clipped {
1232 out.push(DrawItem {
1233 node: id,
1234 transform: tf,
1235 prim: Primitive::PopLayer,
1236 });
1237 }
1238 }
1239 if layered {
1240 out.push(DrawItem {
1241 node: id,
1242 transform: tf,
1243 prim: Primitive::PopLayer,
1244 });
1245 }
1246}
1247
1248impl DrawItem {
1249 fn hit(&self, p: Point) -> bool {
1250 let local = self.transform.inverse() * p;
1251 match &self.prim {
1252 Primitive::Fill { path, .. } => {
1253 (is_text_like(path) && path.bounding_box().contains(local))
1254 || closed(path).contains(local)
1255 }
1256 Primitive::Stroke { path, style, .. } => {
1257 let tol = (style.width / 2.0)
1258 .max(4.0 / self.transform.determinant().abs().sqrt().max(1e-9));
1259 path.segments().any(|s| {
1260 kurbo::ParamCurveNearest::nearest(&s, local, 0.1).distance_sq <= tol * tol
1261 })
1262 }
1263 Primitive::Image { rect, .. } => rect.contains(local),
1264 Primitive::PushLayer(_) | Primitive::PopLayer => false,
1265 }
1266 }
1267}
1268
1269fn closed(path: &BezPath) -> BezPath {
1271 let mut out = BezPath::new();
1272 let mut open = false;
1273 for el in path.elements() {
1274 match el {
1275 kurbo::PathEl::MoveTo(_) => {
1276 if open {
1277 out.close_path();
1278 }
1279 open = true;
1280 }
1281 kurbo::PathEl::ClosePath => open = false,
1282 _ => {}
1283 }
1284 out.push(*el);
1285 }
1286 if open {
1287 out.close_path();
1288 }
1289 out
1290}
1291
1292fn is_text_like(path: &BezPath) -> bool {
1295 path.elements()
1296 .iter()
1297 .filter(|e| matches!(e, kurbo::PathEl::MoveTo(_)))
1298 .count()
1299 > 3
1300}
1301
1302thread_local! {
1303 static CURRENT: RefCell<Option<Rc<SceneGraph>>> = const { RefCell::new(None) };
1304}
1305
1306impl SceneGraph {
1307 pub fn enter<R>(self: &Rc<Self>, f: impl FnOnce() -> R) -> R {
1309 let prev = CURRENT.with(|c| c.borrow_mut().replace(self.clone()));
1310 struct Restore(Option<Rc<SceneGraph>>);
1311 impl Drop for Restore {
1312 fn drop(&mut self) {
1313 let prev = self.0.take();
1314 CURRENT.with(|c| *c.borrow_mut() = prev);
1315 }
1316 }
1317 let _restore = Restore(prev);
1318 f()
1319 }
1320}
1321
1322pub(crate) fn with_graph<R>(f: impl FnOnce(&SceneGraph) -> R) -> R {
1323 CURRENT.with(|c| {
1324 let c = c.borrow();
1325 f(c.as_ref()
1326 .expect("oxframe: node handles can only be used while a scene is running"))
1327 })
1328}