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 Shader {
191 code: crate::shader::ShaderCode,
192 preset: crate::shader::Preset,
194 width: Signal<f64>,
195 height: Signal<f64>,
196 uniforms: Vec<crate::shader::Uniform>,
197 inputs: Vec<(String, crate::post::Input)>,
199 },
200}
201
202impl NodeKind {
203 pub fn name(&self) -> &'static str {
204 match self {
205 Self::Group => "Group",
206 Self::Camera { .. } => "Camera",
207 Self::Rect { .. } => "Rect",
208 Self::Circle { .. } => "Circle",
209 Self::Polygon { .. } => "Polygon",
210 Self::Star { .. } => "Star",
211 Self::Path { .. } => "Path",
212 Self::Line { .. } => "Line",
213 Self::Txt { .. } => "Txt",
214 Self::Code { .. } => "Code",
215 Self::Img { .. } => "Img",
216 Self::Layout { .. } => "Layout",
217 Self::Grid { .. } => "Grid",
218 Self::Svg { .. } => "Svg",
219 Self::Custom { .. } => "Custom",
220 Self::Particles { .. } => "Particles",
221 Self::Shader { .. } => "Shader",
222 }
223 }
224}
225
226#[doc(hidden)]
228pub struct NodeData {
229 pub id: NodeId,
230 pub name: Option<String>,
231 pub source: Option<String>,
233 pub parent: Option<NodeId>,
234 pub children: Vec<NodeId>,
235 pub kind: NodeKind,
236 pub x: Signal<f64>,
237 pub y: Signal<f64>,
238 pub rotation: Signal<f64>,
240 pub scale: Signal<Vec2>,
241 pub opacity: Signal<f64>,
242 pub fill: Signal<Color>,
243 pub stroke: Signal<Color>,
244 pub line_width: Signal<f64>,
245 pub start: Signal<f64>,
247 pub end: Signal<f64>,
248 pub fill_gradient: Signal<Option<Gradient>>,
250 pub stroke_gradient: Signal<Option<Gradient>>,
251 pub dash: Signal<Vec<f64>>,
253 pub dash_offset: Signal<f64>,
254 pub line_cap: Signal<LineCap>,
255 pub line_join: Signal<LineJoin>,
256 pub blur: Signal<f64>,
258 pub shadow_color: Signal<Color>,
259 pub shadow_blur: Signal<f64>,
260 pub shadow_offset: Signal<Vec2>,
261 pub clip: Signal<bool>,
263 pub blend: Signal<BlendMode>,
264 pub z_index: Signal<f64>,
266 pub anchor: Signal<Vec2>,
270 pub grow: Signal<f64>,
272 pub shrink: Signal<f64>,
273 pub basis: Signal<Option<f64>>,
274 pub margin: Signal<f64>,
275 pub align_self: Signal<Option<LayoutAlign>>,
276 pub place: Option<crate::place::Place>,
278 pub placed: Cell<(f64, f64)>,
280 pub effect: Option<crate::post::EffectData>,
282}
283
284impl NodeData {
285 pub fn local_transform(&self, offset: Vec2, size: (f64, f64)) -> Affine {
288 if let NodeKind::Camera { zoom } = &self.kind {
289 let (x, y) = self.placed.get();
293 return Affine::translate(offset)
294 * Affine::rotate(-self.rotation.get().to_radians())
295 * Affine::scale(zoom.get().max(1e-6))
296 * Affine::translate((-x, -y));
297 }
298 let s = self.scale.get();
299 let a = self.anchor.get();
300 let (x, y) = self.placed.get();
301 Affine::translate(offset + Vec2::new(x, y))
302 * Affine::rotate(self.rotation.get().to_radians())
303 * Affine::scale_non_uniform(s.x, s.y)
304 * Affine::translate((-a.x * size.0 / 2.0, -a.y * size.1 / 2.0))
305 }
306
307 pub(crate) fn text_draw(&self) -> Option<TextDraw> {
309 match &self.kind {
310 NodeKind::Txt {
311 text,
312 spans,
313 font_size,
314 weight,
315 style,
316 morph,
317 morph_t,
318 } => {
319 let style = TextStyle {
320 size: font_size.get().max(0.1),
321 weight: weight.get().clamp(1.0, 1000.0),
322 ..style.clone()
323 };
324 if let Some((from, to)) = morph.get() {
325 let a = oxframe_text::layout(&from, &style);
326 let b = oxframe_text::layout(&to, &style);
327 let (glyphs, size) = crate::text::morph(&a, &b, morph_t.get().clamp(0.0, 1.0));
328 return Some(TextDraw::Morph { glyphs, size });
329 }
330 let spans = spans.get();
331 Some(TextDraw::Styled(if spans.is_empty() {
332 crate::text::rich(&[RichSpan::new(text.get())], &style)
333 } else {
334 crate::text::rich(&spans, &style)
335 }))
336 }
337 NodeKind::Code {
338 code,
339 edit_from,
340 edit_to,
341 editing,
342 edit_t,
343 edit_flash,
344 lang,
345 theme,
346 font_size,
347 style,
348 focus,
349 dim,
350 } => {
351 let style = TextStyle {
352 size: font_size.get().max(0.1),
353 ..style.clone()
354 };
355 if editing.get() {
356 let (frame, _) = crate::text::layout_diff(
357 &edit_from.get(),
358 &edit_to.get(),
359 *lang,
360 crate::text::DiffView {
361 theme,
362 style: &style,
363 focus: focus.get(),
364 dim: dim.get(),
365 t: edit_t.get(),
366 flash: edit_flash.get(),
367 },
368 );
369 return Some(TextDraw::Diff(frame));
370 }
371 Some(TextDraw::Styled(crate::text::code(
372 &code.get(),
373 *lang,
374 theme,
375 &style,
376 focus.get(),
377 dim.get(),
378 )))
379 }
380 _ => None,
381 }
382 }
383
384 pub fn outline(&self, size: (f64, f64)) -> Option<BezPath> {
386 const TOLERANCE: f64 = 0.1;
387 match &self.kind {
388 NodeKind::Group
389 | NodeKind::Svg { .. }
390 | NodeKind::Camera { .. }
391 | NodeKind::Img { .. }
392 | NodeKind::Txt { .. }
393 | NodeKind::Code { .. }
394 | NodeKind::Grid { .. }
395 | NodeKind::Custom { .. }
396 | NodeKind::Particles { .. }
397 | NodeKind::Shader { .. } => None,
398 NodeKind::Rect { radius, .. } | NodeKind::Layout { radius, .. } => {
399 let (w, h) = (size.0.max(0.0), size.1.max(0.0));
400 let r = radius.get().clamp(0.0, w.min(h) / 2.0);
401 let rect = kurbo::Rect::new(-w / 2.0, -h / 2.0, w / 2.0, h / 2.0);
402 Some(if r > 0.0 {
403 rect.to_rounded_rect(r).to_path(TOLERANCE)
404 } else {
405 rect.to_path(TOLERANCE)
406 })
407 }
408 NodeKind::Circle {
409 start_angle,
410 end_angle,
411 pie,
412 ..
413 } => {
414 let r = size.0.max(0.0) / 2.0;
417 let (a0, a1) = (start_angle.get(), end_angle.get());
418 let sweep = (a1 - a0).clamp(-360.0, 360.0);
419 if sweep.abs() >= 360.0 - 1e-9 {
420 let circle = kurbo::Circle::new((0.0, 0.0), r).to_path(TOLERANCE);
421 return Some(Affine::rotate((a0 - 90.0).to_radians()) * circle);
422 }
423 let arc = kurbo::Arc::new(
424 (0.0, 0.0),
425 (r, r),
426 (a0 - 90.0).to_radians(),
427 sweep.to_radians(),
428 0.0,
429 );
430 let mut p = BezPath::new();
431 if *pie {
432 p.move_to((0.0, 0.0));
433 p.line_to(arc.center + kurbo::Vec2::from_angle(arc.start_angle) * r);
434 arc.to_cubic_beziers(TOLERANCE, |a, b, c| p.curve_to(a, b, c));
435 p.close_path();
436 } else {
437 p.move_to(arc.center + kurbo::Vec2::from_angle(arc.start_angle) * r);
438 arc.to_cubic_beziers(TOLERANCE, |a, b, c| p.curve_to(a, b, c));
439 }
440 Some(p)
441 }
442 NodeKind::Polygon { sides, .. } => {
443 let n = sides.get().round().max(3.0) as usize;
444 Some(regular(n, size.0.max(0.0) / 2.0, None))
445 }
446 NodeKind::Star { points, inner, .. } => {
447 let n = points.get().round().max(2.0) as usize;
448 let r = size.0.max(0.0) / 2.0;
449 Some(regular(n, r, Some(r * inner.get().clamp(0.0, 1.0))))
450 }
451 NodeKind::Path { data } => Some(data.get()),
452 NodeKind::Line {
453 points,
454 closed,
455 smooth,
456 ..
457 } => {
458 let pts = points.get();
459 if *smooth && pts.len() > 2 {
460 return Some(catmull_rom(&pts, *closed));
461 }
462 let mut p = BezPath::new();
463 for (i, v) in pts.iter().enumerate() {
464 if i == 0 {
465 p.move_to(v.to_point());
466 } else {
467 p.line_to(v.to_point());
468 }
469 }
470 if *closed && pts.len() > 2 {
471 p.close_path();
472 }
473 Some(p)
474 }
475 }
476 }
477}
478
479fn regular(n: usize, r: f64, inner: Option<f64>) -> BezPath {
481 let count = if inner.is_some() { 2 * n } else { n };
482 let mut p = BezPath::new();
483 for i in 0..count {
484 let radius = match inner {
485 Some(ri) if i % 2 == 1 => ri,
486 _ => r,
487 };
488 let a = -std::f64::consts::FRAC_PI_2 + std::f64::consts::TAU * i as f64 / count as f64;
489 let pt = Point::new(a.cos() * radius, a.sin() * radius);
490 if i == 0 {
491 p.move_to(pt);
492 } else {
493 p.line_to(pt);
494 }
495 }
496 p.close_path();
497 p
498}
499
500fn catmull_rom(pts: &[Vec2], closed: bool) -> BezPath {
502 let n = pts.len();
503 let at = |i: isize| -> Vec2 {
504 if closed {
505 pts[i.rem_euclid(n as isize) as usize]
506 } else {
507 pts[i.clamp(0, n as isize - 1) as usize]
508 }
509 };
510 let mut p = BezPath::new();
511 p.move_to(pts[0].to_point());
512 let segs = if closed { n } else { n - 1 };
513 for i in 0..segs as isize {
514 let (p0, p1, p2, p3) = (at(i - 1), at(i), at(i + 1), at(i + 2));
515 let c1 = p1 + (p2 - p0) / 6.0;
516 let c2 = p2 - (p3 - p1) / 6.0;
517 p.curve_to(c1.to_point(), c2.to_point(), p2.to_point());
518 }
519 if closed {
520 p.close_path();
521 }
522 p
523}
524
525pub(crate) enum TextDraw {
526 Styled(Styled),
527 Morph {
529 glyphs: Vec<(BezPath, f64)>,
530 size: (f64, f64),
531 },
532 Diff(DiffFrame),
534}
535
536pub struct SceneGraph {
538 nodes: RefCell<Vec<NodeData>>,
539 size: (u32, u32),
540 time: Cell<f64>,
542 shader_diags: RefCell<Vec<crate::shader::ShaderDiag>>,
544}
545
546impl SceneGraph {
547 pub const ROOT: NodeId = NodeId(0);
549
550 pub fn new(width: u32, height: u32) -> Rc<Self> {
552 let graph = Rc::new(Self {
553 nodes: RefCell::new(Vec::new()),
554 size: (width, height),
555 time: Cell::new(0.0),
556 shader_diags: RefCell::new(Vec::new()),
557 });
558 graph.insert(
559 None,
560 NodeKind::Group,
561 crate::BaseProps {
562 name: Some("view".into()),
563 ..Default::default()
564 },
565 );
566 graph
567 }
568
569 pub fn size(&self) -> (u32, u32) {
571 self.size
572 }
573
574 pub fn set_time(&self, seconds: f64) {
577 self.time.set(seconds.max(0.0));
578 }
579
580 pub fn time(&self) -> f64 {
582 self.time.get()
583 }
584
585 pub fn len(&self) -> usize {
587 self.nodes.borrow().len()
588 }
589
590 pub fn is_empty(&self) -> bool {
592 self.len() == 0
593 }
594
595 pub fn node(&self, id: NodeId) -> Ref<'_, NodeData> {
601 Ref::map(self.nodes.borrow(), |n| &n[id.0 as usize])
602 }
603
604 pub(crate) fn next_id(&self) -> u32 {
605 self.len() as u32
606 }
607
608 pub(crate) fn insert(
609 &self,
610 parent: Option<NodeId>,
611 kind: NodeKind,
612 base: crate::BaseProps,
613 ) -> NodeId {
614 let mut nodes = self.nodes.borrow_mut();
615 let id = NodeId(u32::try_from(nodes.len()).expect("too many nodes"));
616 let label = format!("{}#{}", kind.name(), id.0);
617 let sig = |field: &str| format!("{label}.{field}");
618 nodes.push(NodeData {
619 id,
620 name: base.name,
621 source: current_add_at().map(|(file, line)| source_label(file, line)),
622 parent,
623 children: Vec::new(),
624 x: Signal::new(base.x).with_name(sig("x")),
625 y: Signal::new(base.y).with_name(sig("y")),
626 rotation: Signal::new(base.rotation).with_name(sig("rotation")),
627 scale: Signal::new(base.scale).with_name(sig("scale")),
628 opacity: Signal::new(base.opacity).with_name(sig("opacity")),
629 fill: Signal::new(base.fill).with_name(sig("fill")),
630 stroke: Signal::new(base.stroke).with_name(sig("stroke")),
631 line_width: Signal::new(base.line_width).with_name(sig("line_width")),
632 start: Signal::new(base.start).with_name(sig("start")),
633 end: Signal::new(base.end).with_name(sig("end")),
634 fill_gradient: Signal::new(base.fill_gradient).with_name(sig("fill_gradient")),
635 stroke_gradient: Signal::new(base.stroke_gradient).with_name(sig("stroke_gradient")),
636 dash: Signal::new(base.dash).with_name(sig("dash")),
637 dash_offset: Signal::new(base.dash_offset).with_name(sig("dash_offset")),
638 line_cap: Signal::new(base.line_cap).with_name(sig("line_cap")),
639 line_join: Signal::new(base.line_join).with_name(sig("line_join")),
640 blur: Signal::new(base.blur).with_name(sig("blur")),
641 shadow_color: Signal::new(base.shadow_color).with_name(sig("shadow_color")),
642 shadow_blur: Signal::new(base.shadow_blur).with_name(sig("shadow_blur")),
643 shadow_offset: Signal::new(base.shadow_offset).with_name(sig("shadow_offset")),
644 clip: Signal::new(base.clip).with_name(sig("clip")),
645 blend: Signal::new(base.blend).with_name(sig("blend")),
646 z_index: Signal::new(base.z_index).with_name(sig("z_index")),
647 anchor: Signal::new(base.anchor).with_name(sig("anchor")),
648 grow: Signal::new(base.grow).with_name(sig("grow")),
649 shrink: Signal::new(base.shrink).with_name(sig("shrink")),
650 basis: Signal::new(base.basis).with_name(sig("basis")),
651 margin: Signal::new(base.margin).with_name(sig("margin")),
652 align_self: Signal::new(base.align_self).with_name(sig("align_self")),
653 place: base.place,
654 placed: Cell::new((base.x, base.y)),
655 effect: None,
656 kind,
657 });
658 if let Some(p) = parent {
659 nodes[p.0 as usize].children.push(id);
660 }
661 id
662 }
663
664 pub(crate) fn detach(&self, id: NodeId) {
665 let mut nodes = self.nodes.borrow_mut();
666 if let Some(p) = nodes[id.0 as usize].parent.take() {
667 nodes[p.0 as usize].children.retain(|c| *c != id);
668 }
669 }
670
671 pub(crate) fn reparent(&self, id: NodeId, parent: NodeId) {
672 self.detach(id);
673 let mut nodes = self.nodes.borrow_mut();
674 nodes[id.0 as usize].parent = Some(parent);
675 nodes[parent.0 as usize].children.push(id);
676 }
677
678 pub fn measure(&self, id: NodeId) -> (f64, f64) {
680 self.resolve_places();
681 let nodes = self.nodes.borrow();
682 measure(&nodes, id)
683 }
684
685 pub(crate) fn set_effect(&self, id: NodeId, effect: crate::post::EffectData) {
686 self.nodes.borrow_mut()[id.0 as usize].effect = Some(effect);
687 }
688
689 pub fn has_post(&self) -> bool {
693 self.nodes.borrow().iter().any(|n| {
694 n.effect.is_some()
695 || matches!(
696 n.kind,
697 NodeKind::Shader {
698 preset: crate::shader::Preset::LensDrops,
699 ..
700 }
701 )
702 })
703 }
704
705 pub(crate) fn set_place(&self, id: NodeId, place: crate::place::Place) {
706 self.nodes.borrow_mut()[id.0 as usize].place = Some(place);
707 }
708
709 fn resolve_places(&self) {
710 let nodes = self.nodes.borrow();
711 crate::place::resolve(&nodes, (f64::from(self.size.0), f64::from(self.size.1)));
712 }
713
714 pub fn draw_list(&self) -> Vec<DrawItem> {
716 self.resolve_places();
717 let nodes = self.nodes.borrow();
718 let mut out = Vec::new();
719 crate::shader::clear_diags();
720 for n in nodes.iter() {
721 for diag in crate::shader::problems(n) {
722 crate::shader::push_diag(diag);
723 }
724 }
725 visit(
726 &nodes,
727 Self::ROOT,
728 Affine::IDENTITY,
729 None,
730 None,
731 self.time.get(),
732 false,
733 &mut out,
734 );
735 *self.shader_diags.borrow_mut() = crate::shader::take_diags();
736 out
737 }
738
739 pub fn shader_diags(&self) -> Vec<crate::shader::ShaderDiag> {
741 self.shader_diags.borrow().clone()
742 }
743
744 pub fn draw_drift(&self) -> Option<(String, Option<String>)> {
747 let a = self.draw_list();
748 let b = self.draw_list();
749 let id = first_drift(&a, &b)?;
750 let n = self.node(id);
751 let label = n
752 .name
753 .clone()
754 .or_else(|| n.source.clone())
755 .unwrap_or_else(|| n.kind.name().to_owned());
756 Some((label, n.source.clone()))
757 }
758
759 pub fn pick(&self, p: Point) -> Option<NodeId> {
761 self.draw_list()
762 .iter()
763 .rev()
764 .find(|item| item.node != Self::ROOT && item.hit(p))
765 .map(|item| item.node)
766 }
767
768 pub fn world_transform(&self, id: NodeId) -> Option<Affine> {
770 if let Some(item) = self.draw_list().into_iter().find(|i| i.node == id) {
773 return Some(item.transform);
774 }
775 let nodes = self.nodes.borrow();
776 let mut tf = Affine::IDENTITY;
777 let mut cur = Some(id);
778 while let Some(c) = cur {
779 let n = nodes.get(c.0 as usize)?;
780 tf = n.local_transform(Vec2::ZERO, measure(&nodes, c)) * tf;
781 cur = n.parent;
782 }
783 Some(tf)
784 }
785
786 pub fn box_in(&self, id: NodeId, frame: NodeId) -> Option<kurbo::Rect> {
790 let node_tf = self.world_transform(id)?;
791 let frame_tf = self.world_transform(frame)?;
792 let (w, h) = measure(&self.nodes.borrow(), id);
793 let [x0, y0, x1, y1] = aabb(frame_tf.inverse() * node_tf, w, h);
794 Some(kurbo::Rect::new(x0, y0, x1, y1))
795 }
796
797 pub fn frame_size(&self) -> (f64, f64) {
799 (f64::from(self.size.0), f64::from(self.size.1))
800 }
801
802 pub(crate) fn descendant_named(&self, root: NodeId, name: &str) -> Option<NodeId> {
804 fn walk(nodes: &[NodeData], id: NodeId, name: &str) -> Option<NodeId> {
805 let n = nodes.get(id.0 as usize)?;
806 if n.name.as_deref() == Some(name) {
807 return Some(id);
808 }
809 for &c in &n.children {
810 if let Some(hit) = walk(nodes, c, name) {
811 return Some(hit);
812 }
813 }
814 None
815 }
816 walk(&self.nodes.borrow(), root, name)
817 }
818
819 pub(crate) fn descendant_paths(&self, root: NodeId) -> Vec<NodeId> {
821 fn walk(nodes: &[NodeData], id: NodeId, out: &mut Vec<NodeId>) {
822 let Some(n) = nodes.get(id.0 as usize) else {
823 return;
824 };
825 if matches!(n.kind, NodeKind::Path { .. }) {
826 out.push(id);
827 }
828 for &c in &n.children {
829 walk(nodes, c, out);
830 }
831 }
832 let mut out = Vec::new();
833 walk(&self.nodes.borrow(), root, &mut out);
834 out
835 }
836
837 pub fn tree(&self) -> Vec<(NodeId, Option<NodeId>, &'static str, Option<String>)> {
839 let nodes = self.nodes.borrow();
840 let mut out = Vec::new();
841 let mut stack = vec![Self::ROOT];
842 while let Some(id) = stack.pop() {
843 let n = &nodes[id.0 as usize];
844 out.push((id, n.parent, n.kind.name(), n.name.clone()));
845 stack.extend(n.children.iter().rev());
846 }
847 out
848 }
849
850 pub fn describe(&self, id: NodeId) -> Vec<(String, String)> {
852 let nodes = self.nodes.borrow();
853 let Some(n) = nodes.get(id.0 as usize) else {
854 return Vec::new();
855 };
856 let f = |v: f64| format!("{v:.3}");
857 let s = n.scale.get();
858 let mut out = vec![
859 ("x".into(), f(n.x.get())),
860 ("y".into(), f(n.y.get())),
861 ("rotation".into(), f(n.rotation.get())),
862 ("scale".into(), format!("{:.3}, {:.3}", s.x, s.y)),
863 ("opacity".into(), f(n.opacity.get())),
864 ("fill".into(), n.fill.get().to_string()),
865 ("stroke".into(), n.stroke.get().to_string()),
866 ("line_width".into(), f(n.line_width.get())),
867 ("start".into(), f(n.start.get())),
868 ("end".into(), f(n.end.get())),
869 ];
870 let (w, h) = measure(&nodes, id);
871 out.push(("size".into(), format!("{w:.1} × {h:.1}")));
872 match &n.kind {
873 NodeKind::Rect { radius, .. } => out.push(("radius".into(), f(radius.get()))),
874 NodeKind::Code { code, .. } => out.push(("code".into(), code.get())),
875 NodeKind::Txt {
876 text,
877 font_size,
878 weight,
879 ..
880 } => {
881 out.push(("text".into(), text.get()));
882 out.push(("font_size".into(), f(font_size.get())));
883 out.push(("weight".into(), f(weight.get())));
884 }
885 NodeKind::Line { points, .. } => {
886 out.push(("points".into(), points.get().len().to_string()))
887 }
888 NodeKind::Layout { gap, padding, .. } => {
889 out.push(("gap".into(), f(gap.get())));
890 out.push(("padding".into(), f(padding.get())));
891 }
892 NodeKind::Grid { spacing, .. } => out.push(("spacing".into(), f(spacing.get()))),
893 _ => {}
894 }
895 out
896 }
897
898 pub fn inspect_nodes(&self) -> Vec<InspectNode> {
903 let list = self.draw_list();
904 let mut drawn: Vec<Option<(Affine, u32)>> = vec![None; self.len()];
905 let mut order = 0u32;
906 for item in &list {
907 let slot = &mut drawn[item.node.0 as usize];
908 if slot.is_none() {
909 *slot = Some((item.transform, order));
910 order += 1;
911 }
912 }
913 let nodes = self.nodes.borrow();
914 let mut out = Vec::new();
915 let mut stack = vec![Self::ROOT];
916 while let Some(id) = stack.pop() {
917 let n = &nodes[id.0 as usize];
918 stack.extend(n.children.iter().rev());
919 let (w, h) = measure(&nodes, id);
920 let (tf, order) = drawn[id.0 as usize]
921 .unwrap_or_else(|| (transform_without_layout(&nodes, id), u32::MAX));
922 let (text, text_overflow) = text_info(n);
923 out.push(InspectNode {
924 id: id.0,
925 parent: n.parent.map(|p| p.0),
926 kind: n.kind.name(),
927 name: n.name.clone().unwrap_or_else(|| {
928 n.source
929 .clone()
930 .unwrap_or_else(|| format!("{}#{}", n.kind.name(), id.0))
931 }),
932 source: n.source.clone(),
933 bounds: aabb(tf, w, h),
934 opacity: n.opacity.get().clamp(0.0, 1.0),
935 order,
936 text,
937 fill: n.fill.get(),
938 text_overflow,
939 parent_kind: n.parent.map(|p| nodes[p.0 as usize].kind.name()),
940 in_camera: {
941 let mut up = n.parent;
942 let mut found = false;
943 while let Some(p) = up {
944 let pn = &nodes[p.0 as usize];
945 if matches!(pn.kind, NodeKind::Camera { .. }) {
946 found = true;
947 break;
948 }
949 up = pn.parent;
950 }
951 found
952 },
953 shader: match &n.kind {
954 NodeKind::Shader { preset, .. } => Some(preset.name()),
955 _ => None,
956 },
957 params: params_of(n),
958 });
959 }
960 out
961 }
962}
963
964#[derive(Debug, Clone, PartialEq)]
966pub struct InspectNode {
967 pub id: u32,
969 pub parent: Option<u32>,
971 pub kind: &'static str,
973 pub name: String,
975 pub source: Option<String>,
977 pub bounds: [f64; 4],
980 pub opacity: f64,
982 pub order: u32,
984 pub text: Option<String>,
986 pub fill: Color,
988 pub text_overflow: f64,
991 pub parent_kind: Option<&'static str>,
993 pub in_camera: bool,
996 pub shader: Option<&'static str>,
998 pub params: Vec<(String, f64)>,
1001}
1002
1003fn params_of(n: &NodeData) -> Vec<(String, f64)> {
1005 let mut out = Vec::new();
1006 if let NodeKind::Shader { uniforms, .. } = &n.kind {
1007 for u in uniforms {
1008 match &u.value {
1009 crate::shader::UniformValue::Num(v) | crate::shader::UniformValue::Bool(v) => {
1010 out.push((u.name.clone(), v.get()));
1011 }
1012 _ => {}
1013 }
1014 }
1015 }
1016 if let Some(e) = &n.effect {
1017 out.push(("effect.strength".into(), e.strength.get()));
1018 out.push(("effect.speed".into(), e.speed.get()));
1019 }
1020 out
1021}
1022
1023thread_local! {
1024 static ADD_AT: Cell<Option<(&'static str, u32)>> = const { Cell::new(None) };
1025}
1026
1027pub(crate) fn push_add_at(file: &'static str, line: u32) -> Option<(&'static str, u32)> {
1031 ADD_AT.with(|c| {
1032 let prev = c.get();
1033 if prev.is_none() {
1034 c.set(Some((file, line)));
1035 }
1036 prev
1037 })
1038}
1039
1040#[track_caller]
1046pub fn add_site<R>(f: impl FnOnce() -> R) -> R {
1047 let loc = std::panic::Location::caller();
1048 let prev = push_add_at(loc.file(), loc.line());
1049 let out = f();
1050 pop_add_at(prev);
1051 out
1052}
1053
1054pub(crate) fn pop_add_at(prev: Option<(&'static str, u32)>) {
1055 ADD_AT.with(|c| c.set(prev));
1056}
1057
1058fn current_add_at() -> Option<(&'static str, u32)> {
1059 ADD_AT.with(|c| c.get())
1060}
1061
1062fn source_label(file: &str, line: u32) -> String {
1064 let file = file.replace('\\', "/");
1065 let rel = file
1066 .rfind("/src/")
1067 .map_or(file.as_str(), |i| &file[i + 1..]);
1068 format!("{rel}:{line}")
1069}
1070
1071fn aabb(tf: Affine, w: f64, h: f64) -> [f64; 4] {
1072 let pts = [
1073 (-w / 2.0, -h / 2.0),
1074 (w / 2.0, -h / 2.0),
1075 (w / 2.0, h / 2.0),
1076 (-w / 2.0, h / 2.0),
1077 ];
1078 let mut b = [
1079 f64::INFINITY,
1080 f64::INFINITY,
1081 f64::NEG_INFINITY,
1082 f64::NEG_INFINITY,
1083 ];
1084 for (x, y) in pts {
1085 let p = tf * Point::new(x, y);
1086 b[0] = b[0].min(p.x);
1087 b[1] = b[1].min(p.y);
1088 b[2] = b[2].max(p.x);
1089 b[3] = b[3].max(p.y);
1090 }
1091 b
1092}
1093
1094fn transform_without_layout(nodes: &[NodeData], id: NodeId) -> Affine {
1095 let mut chain = Vec::new();
1096 let mut cur = Some(id);
1097 while let Some(c) = cur {
1098 chain.push(c);
1099 cur = nodes.get(c.0 as usize).and_then(|n| n.parent);
1100 }
1101 let mut tf = Affine::IDENTITY;
1102 for c in chain.into_iter().rev() {
1103 let Some(n) = nodes.get(c.0 as usize) else {
1104 break;
1105 };
1106 tf *= n.local_transform(Vec2::ZERO, measure(nodes, c));
1107 }
1108 tf
1109}
1110
1111fn text_info(n: &NodeData) -> (Option<String>, f64) {
1112 let (body, max_width) = match &n.kind {
1113 NodeKind::Txt {
1114 text, spans, style, ..
1115 } => {
1116 let spans = spans.get();
1117 let body = if spans.is_empty() {
1118 text.get()
1119 } else {
1120 spans.into_iter().map(|s| s.text).collect()
1121 };
1122 (body, style.max_width)
1123 }
1124 NodeKind::Code { code, style, .. } => (code.get(), style.max_width),
1125 _ => return (None, 0.0),
1126 };
1127 let overflow = match (max_width, n.text_draw()) {
1128 (Some(max), Some(TextDraw::Styled(s))) => (s.layout.width - max).max(0.0),
1129 (Some(max), Some(TextDraw::Morph { size, .. })) => (size.0 - max).max(0.0),
1130 (Some(max), Some(TextDraw::Diff(frame))) => (frame.size.0 - max).max(0.0),
1131 _ => 0.0,
1132 };
1133 (Some(body), overflow)
1134}
1135
1136pub(crate) fn measure(nodes: &[NodeData], id: NodeId) -> (f64, f64) {
1137 let n = &nodes[id.0 as usize];
1138 match &n.kind {
1139 NodeKind::Rect { width, height, .. } => (width.get(), height.get()),
1140 NodeKind::Circle { size, .. }
1141 | NodeKind::Polygon { size, .. }
1142 | NodeKind::Star { size, .. } => (size.get(), size.get()),
1143 NodeKind::Img { width, height, .. }
1144 | NodeKind::Grid { width, height, .. }
1145 | NodeKind::Svg { width, height } => (width.get(), height.get()),
1146 NodeKind::Txt { .. } | NodeKind::Code { .. } => match n.text_draw() {
1147 Some(TextDraw::Styled(s)) => (s.layout.width, s.layout.height),
1148 Some(TextDraw::Morph { size, .. }) => size,
1149 Some(TextDraw::Diff(frame)) => frame.size,
1150 None => (0.0, 0.0),
1151 },
1152 NodeKind::Path { .. } | NodeKind::Line { .. } => {
1153 let bb = n
1154 .outline((0.0, 0.0))
1155 .map_or(kurbo::Rect::ZERO, |p| p.bounding_box());
1156 (bb.width(), bb.height())
1157 }
1158 NodeKind::Layout { .. } => crate::flex::compute(nodes, id, &|c| measure(nodes, c)).0,
1159 NodeKind::Camera { .. } | NodeKind::Particles { .. } => (0.0, 0.0),
1161 NodeKind::Custom { width, height, .. } => (*width, *height),
1162 NodeKind::Shader { width, height, .. } => (width.get(), height.get()),
1163 NodeKind::Group => {
1164 let mut bb: Option<kurbo::Rect> = None;
1165 for &c in &n.children {
1166 let child = &nodes[c.0 as usize];
1167 let (w, h) = measure(nodes, c);
1168 let r = child
1169 .local_transform(Vec2::ZERO, (w, h))
1170 .transform_rect_bbox(kurbo::Rect::new(-w / 2.0, -h / 2.0, w / 2.0, h / 2.0));
1171 bb = Some(bb.map_or(r, |b| b.union(r)));
1172 }
1173 bb.map_or((0.0, 0.0), |b| (b.width(), b.height()))
1174 }
1175 }
1176}
1177
1178fn resizable(kind: &NodeKind) -> bool {
1180 matches!(
1181 kind,
1182 NodeKind::Rect { .. }
1183 | NodeKind::Layout { .. }
1184 | NodeKind::Img { .. }
1185 | NodeKind::Grid { .. }
1186 | NodeKind::Svg { .. }
1187 | NodeKind::Shader { .. }
1188 )
1189}
1190
1191fn first_drift(a: &[DrawItem], b: &[DrawItem]) -> Option<NodeId> {
1192 let n = a.len().max(b.len());
1193 for i in 0..n {
1194 match (a.get(i), b.get(i)) {
1195 (Some(x), Some(y)) if x.node == y.node && prim_same(&x.prim, &y.prim) => {}
1196 (Some(x), _) => return Some(x.node),
1197 (_, Some(y)) => return Some(y.node),
1198 (None, None) => {}
1199 }
1200 }
1201 None
1202}
1203
1204fn prim_same(a: &Primitive, b: &Primitive) -> bool {
1205 use Primitive::{Fill, Image, PopLayer, PushLayer, Stroke};
1206 match (a, b) {
1207 (Fill { path: p, paint: c }, Fill { path: q, paint: d }) => p == q && c == d,
1208 (
1209 Stroke {
1210 path: p,
1211 paint: c,
1212 style: s,
1213 },
1214 Stroke {
1215 path: q,
1216 paint: d,
1217 style: t,
1218 },
1219 ) => p == q && c == d && s == t,
1220 (
1221 Image {
1222 rect: r,
1223 opacity: o,
1224 ..
1225 },
1226 Image {
1227 rect: s,
1228 opacity: p,
1229 ..
1230 },
1231 ) => r == s && o == p,
1232 (PushLayer(x), PushLayer(y)) => x == y,
1233 (PopLayer, PopLayer) => true,
1234 (Primitive::ReadFrame(x), Primitive::ReadFrame(y))
1235 | (Primitive::BeginEffect(x), Primitive::BeginEffect(y)) => x == y,
1236 (Primitive::EndEffect, Primitive::EndEffect) => true,
1237 _ => false,
1238 }
1239}
1240
1241#[allow(clippy::too_many_arguments)]
1244fn visit(
1245 nodes: &[NodeData],
1246 id: NodeId,
1247 parent_tf: Affine,
1248 placement: Option<FlexBox>,
1249 flex: Option<&FlexMap>,
1250 time: f64,
1251 inside: bool,
1252 out: &mut Vec<DrawItem>,
1253) {
1254 let n = &nodes[id.0 as usize];
1255 let opacity = n.opacity.get().clamp(0.0, 1.0);
1256 if opacity <= 0.0 {
1257 return;
1258 }
1259 let size = match placement {
1260 Some(b) if resizable(&n.kind) => {
1261 let s = n.scale.get();
1262 (
1263 b.size.0 / s.x.abs().max(1e-9),
1264 b.size.1 / s.y.abs().max(1e-9),
1265 )
1266 }
1267 _ => measure(nodes, id),
1268 };
1269 let offset = placement.map_or(Vec2::ZERO, |b| b.center);
1270 let tf = parent_tf * n.local_transform(offset, size);
1271
1272 let blend = n.blend.get();
1275 let blur = n.blur.get().max(0.0);
1276 let shadow_color = n.shadow_color.get();
1277 let shadow = (shadow_color.a > 0.0).then(|| Shadow {
1278 color: shadow_color,
1279 blur: n.shadow_blur.get().max(0.0),
1280 offset: n.shadow_offset.get(),
1281 });
1282 let layered = blend != BlendMode::Normal
1283 || blur > 0.0
1284 || shadow.is_some()
1285 || (opacity < 1.0 && !n.children.is_empty());
1286 let effect = n.effect.as_ref().and_then(|e| {
1287 if inside {
1288 cycle(n, "This effect is inside a layer or another effect");
1289 return None;
1290 }
1291 Some(e.shade(tf * Point::ZERO, time))
1292 });
1293 if let Some(shade) = &effect {
1294 out.push(DrawItem {
1295 node: id,
1296 transform: tf,
1297 prim: Primitive::BeginEffect(shade.clone()),
1298 });
1299 }
1300 if let Some(shade) = crate::shader::read_frame(n, size, tf, time, opacity) {
1301 if inside || effect.is_some() {
1302 cycle(
1303 n,
1304 "lens_drops reads the frame, but it is inside a layer or an effect",
1305 );
1306 } else {
1307 out.push(DrawItem {
1308 node: id,
1309 transform: tf,
1310 prim: Primitive::ReadFrame(shade),
1311 });
1312 }
1313 }
1314 if layered {
1315 out.push(DrawItem {
1316 node: id,
1317 transform: tf,
1318 prim: Primitive::PushLayer(Layer {
1319 opacity,
1320 blend,
1321 clip: None,
1322 blur,
1323 shadow,
1324 }),
1325 });
1326 }
1327 let paint_opacity = if layered { 1.0 } else { opacity };
1328 for prim in crate::draw::primitives(n, size, paint_opacity, time) {
1329 out.push(DrawItem {
1330 node: id,
1331 transform: tf,
1332 prim,
1333 });
1334 }
1335 if let NodeKind::Shader { .. } = &n.kind
1336 && crate::shader::problems(n).is_empty()
1337 && let Some(prim) = crate::shader::raster(n, size, tf, time, paint_opacity)
1338 {
1339 out.push(DrawItem {
1340 node: id,
1341 transform: tf,
1342 prim,
1343 });
1344 }
1345
1346 if !n.children.is_empty() {
1347 let clip = n.clip.get().then(|| {
1349 n.outline(size).unwrap_or_else(|| {
1350 kurbo::Rect::new(-size.0 / 2.0, -size.1 / 2.0, size.0 / 2.0, size.1 / 2.0)
1351 .to_path(0.1)
1352 })
1353 });
1354 let clipped = clip.is_some();
1355 if let Some(path) = clip {
1356 out.push(DrawItem {
1357 node: id,
1358 transform: tf,
1359 prim: Primitive::PushLayer(Layer::clip(path)),
1360 });
1361 }
1362 let own;
1365 let map: Option<&FlexMap> = match (&n.kind, flex) {
1366 (NodeKind::Layout { .. }, Some(m)) => Some(m),
1367 (NodeKind::Layout { .. }, None) => {
1368 own = crate::flex::compute(nodes, id, &|c| measure(nodes, c)).1;
1369 Some(&own)
1370 }
1371 _ => None,
1372 };
1373 let mut order: Vec<(usize, NodeId)> = n.children.iter().copied().enumerate().collect();
1374 order.sort_by(|a, b| {
1376 let za = nodes[a.1.0 as usize].z_index.get();
1377 let zb = nodes[b.1.0 as usize].z_index.get();
1378 za.total_cmp(&zb)
1379 });
1380 for (_, c) in order {
1381 let placement = map.and_then(|m| m.get(&c).copied());
1382 let inside = inside || layered || clipped || effect.is_some();
1383 visit(nodes, c, tf, placement, map, time, inside, out);
1384 }
1385 if clipped {
1386 out.push(DrawItem {
1387 node: id,
1388 transform: tf,
1389 prim: Primitive::PopLayer,
1390 });
1391 }
1392 }
1393 if layered {
1394 out.push(DrawItem {
1395 node: id,
1396 transform: tf,
1397 prim: Primitive::PopLayer,
1398 });
1399 }
1400 if effect.is_some() {
1401 out.push(DrawItem {
1402 node: id,
1403 transform: tf,
1404 prim: Primitive::EndEffect,
1405 });
1406 }
1407}
1408
1409fn cycle(n: &NodeData, what: &str) {
1411 crate::shader::push_diag(crate::shader::ShaderDiag {
1412 node: n
1413 .name
1414 .clone()
1415 .unwrap_or_else(|| format!("{}#{}", n.kind.name(), n.id.0)),
1416 source: n.source.clone(),
1417 rule: "input_cycle",
1418 hint: format!(
1419 "{what} (a group with opacity below 1, a blur, a clip, a blend or a shadow). \
1420 Its pixels are not finished there, so the shader is skipped. \
1421 Put the effect on that group, or move the node out of it."
1422 ),
1423 });
1424}
1425
1426impl DrawItem {
1427 fn hit(&self, p: Point) -> bool {
1428 let local = self.transform.inverse() * p;
1429 match &self.prim {
1430 Primitive::Fill { path, .. } => {
1431 (is_text_like(path) && path.bounding_box().contains(local))
1432 || closed(path).contains(local)
1433 }
1434 Primitive::Stroke { path, style, .. } => {
1435 let tol = (style.width / 2.0)
1436 .max(4.0 / self.transform.determinant().abs().sqrt().max(1e-9));
1437 path.segments().any(|s| {
1438 kurbo::ParamCurveNearest::nearest(&s, local, 0.1).distance_sq <= tol * tol
1439 })
1440 }
1441 Primitive::Image { rect, .. } => rect.contains(local),
1442 Primitive::PushLayer(_)
1443 | Primitive::PopLayer
1444 | Primitive::ReadFrame(_)
1445 | Primitive::BeginEffect(_)
1446 | Primitive::EndEffect => false,
1447 }
1448 }
1449}
1450
1451fn closed(path: &BezPath) -> BezPath {
1453 let mut out = BezPath::new();
1454 let mut open = false;
1455 for el in path.elements() {
1456 match el {
1457 kurbo::PathEl::MoveTo(_) => {
1458 if open {
1459 out.close_path();
1460 }
1461 open = true;
1462 }
1463 kurbo::PathEl::ClosePath => open = false,
1464 _ => {}
1465 }
1466 out.push(*el);
1467 }
1468 if open {
1469 out.close_path();
1470 }
1471 out
1472}
1473
1474fn is_text_like(path: &BezPath) -> bool {
1477 path.elements()
1478 .iter()
1479 .filter(|e| matches!(e, kurbo::PathEl::MoveTo(_)))
1480 .count()
1481 > 3
1482}
1483
1484thread_local! {
1485 static CURRENT: RefCell<Option<Rc<SceneGraph>>> = const { RefCell::new(None) };
1486}
1487
1488impl SceneGraph {
1489 pub fn enter<R>(self: &Rc<Self>, f: impl FnOnce() -> R) -> R {
1491 let prev = CURRENT.with(|c| c.borrow_mut().replace(self.clone()));
1492 struct Restore(Option<Rc<SceneGraph>>);
1493 impl Drop for Restore {
1494 fn drop(&mut self) {
1495 let prev = self.0.take();
1496 CURRENT.with(|c| *c.borrow_mut() = prev);
1497 }
1498 }
1499 let _restore = Restore(prev);
1500 f()
1501 }
1502}
1503
1504pub(crate) fn with_graph<R>(f: impl FnOnce(&SceneGraph) -> R) -> R {
1505 CURRENT.with(|c| {
1506 let c = c.borrow();
1507 f(c.as_ref()
1508 .expect("oxframe: node handles can only be used while a scene is running"))
1509 })
1510}