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