Skip to main content

oxframe_2d/
graph.rs

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/// Index of a node in its [`SceneGraph`]. Assigned in creation order, so it
15/// is deterministic too.
16#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
17pub struct NodeId(
18    /// Position in creation order, starting at 0 for the root view.
19    pub u32,
20);
21
22/// Main axis of a [`Layout`](crate::Layout).
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
24pub enum Direction {
25    /// Left to right.
26    #[default]
27    Row,
28    /// Top to bottom.
29    Column,
30}
31
32/// Cross-axis alignment of children in a [`Layout`](crate::Layout).
33#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
34pub enum LayoutAlign {
35    /// Top (in a row) or left (in a column).
36    Start,
37    /// Centered.
38    #[default]
39    Center,
40    /// Bottom (in a row) or right (in a column).
41    End,
42    /// Fill the cross axis (resizes rects, images and nested layouts).
43    Stretch,
44}
45
46/// Main-axis distribution of children; only matters with a fixed size.
47#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
48pub enum Justify {
49    /// Packed at the start.
50    Start,
51    /// Packed in the middle.
52    #[default]
53    Center,
54    /// Packed at the end.
55    End,
56    /// First and last at the edges, equal space between.
57    SpaceBetween,
58    /// Equal space around each child (half-size at the edges).
59    SpaceAround,
60    /// Equal space between children and at the edges.
61    SpaceEvenly,
62}
63
64/// Node type and its type-specific properties (internal representation).
65#[doc(hidden)]
66pub enum NodeKind {
67    Group,
68    /// Looks at `x`/`y` with `zoom` and `rotation`; its children are drawn
69    /// through it (see [`NodeData::local_transform`]).
70    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        /// Degrees clockwise from 12 o'clock; a full turn draws a circle.
81        start_angle: Signal<f64>,
82        end_angle: Signal<f64>,
83        /// Close an arc through the center (pie slice).
84        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 radius as a fraction of the outer one.
94        inner: Signal<f64>,
95    },
96    Path {
97        data: Signal<BezPath>,
98    },
99    Line {
100        points: Signal<Vec<Vec2>>,
101        closed: bool,
102        /// Smooth curve through the points (Catmull-Rom).
103        smooth: bool,
104        start_arrow: bool,
105        end_arrow: bool,
106        arrow_size: Signal<f64>,
107    },
108    Txt {
109        text: Signal<String>,
110        /// When non-empty, replaces `text` with styled runs.
111        spans: Signal<Vec<RichSpan>>,
112        font_size: Signal<f64>,
113        weight: Signal<f64>,
114        style: TextStyle,
115        /// Active glyph morph: (from, to) and progress 0..1.
116        morph: Signal<Option<(String, String)>>,
117        morph_t: Signal<f64>,
118    },
119    Code {
120        code: Signal<String>,
121        /// Source a running diff started from. Ignored unless `editing` is set.
122        edit_from: Signal<String>,
123        /// Source a running diff is going to.
124        edit_to: Signal<String>,
125        /// A diff tween is in progress.
126        editing: Signal<bool>,
127        /// Progress of the diff, 0 = `edit_from`, 1 = `edit_to`.
128        edit_t: Signal<f64>,
129        /// Flash a background behind text the diff inserts.
130        edit_flash: Signal<bool>,
131        lang: Lang,
132        theme: CodeTheme,
133        font_size: Signal<f64>,
134        style: TextStyle,
135        /// Focused lines (1-based, inclusive); (0, 0) focuses all.
136        focus: Signal<Vec2>,
137        /// Alpha of unfocused lines.
138        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 children onto several lines.
150        wrap: bool,
151        /// CSS grid with this many equal columns instead of flexbox.
152        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    /// An imported SVG. Draws nothing itself; its children are the file's
165    /// groups, paths and images. `width` and `height` are the fitted size.
166    Svg {
167        width: Signal<f64>,
168        height: Signal<f64>,
169    },
170    /// Geometry drawn by a closure on every paint. `width` and `height` are
171    /// the layout box; the closure's shapes are not measured.
172    Custom {
173        draw: crate::draw::DrawFn,
174        width: f64,
175        height: f64,
176        /// The box from the node's current values ([`Draw::size`](crate::Draw::size)).
177        /// `None` keeps `width` and `height`.
178        measure: Option<crate::node_api::SizeFn>,
179        /// The signals of a `#[derive(Node)]` node, for its handle.
180        state: Option<Rc<dyn std::any::Any>>,
181        /// `"Custom"`, or the type name of a derived node, for `check`.
182        type_name: &'static str,
183    },
184    /// Particles integrated from bursts. The box is a point; the dots are
185    /// drawn in the node's coordinates.
186    Particles {
187        seed: u64,
188        gravity: Signal<f64>,
189        toward_camera: bool,
190        life: f64,
191        speed: f64,
192        radius: f64,
193        state: crate::sim::ParticlesState,
194    },
195    /// A video clip: the frame at `time`, in a box like an image.
196    Video {
197        data: Rc<crate::video::VideoData>,
198        width: Signal<f64>,
199        height: Signal<f64>,
200        /// Seconds into the clip.
201        time: Signal<f64>,
202        /// Clip seconds per scene second while playing.
203        rate: Signal<f64>,
204        /// Corner radius of the box.
205        radius: Signal<f64>,
206    },
207    /// Pixels from a WGSL `shade` function. No children in this version.
208    /// Presets are run here; a custom `shade` is type-checked.
209    Shader {
210        code: crate::shader::ShaderCode,
211        /// Built-in that runs. A custom `shade` is checked, not executed.
212        preset: crate::shader::Preset,
213        width: Signal<f64>,
214        height: Signal<f64>,
215        uniforms: Vec<crate::shader::Uniform>,
216        /// Named inputs from `.input(..)`.
217        inputs: Vec<(String, crate::post::Input)>,
218    },
219}
220
221impl NodeKind {
222    pub fn name(&self) -> &'static str {
223        match self {
224            Self::Group => "Group",
225            Self::Camera { .. } => "Camera",
226            Self::Rect { .. } => "Rect",
227            Self::Circle { .. } => "Circle",
228            Self::Polygon { .. } => "Polygon",
229            Self::Star { .. } => "Star",
230            Self::Path { .. } => "Path",
231            Self::Line { .. } => "Line",
232            Self::Txt { .. } => "Txt",
233            Self::Code { .. } => "Code",
234            Self::Img { .. } => "Img",
235            Self::Layout { .. } => "Layout",
236            Self::Grid { .. } => "Grid",
237            Self::Svg { .. } => "Svg",
238            Self::Custom { type_name, .. } => type_name,
239            Self::Particles { .. } => "Particles",
240            Self::Shader { .. } => "Shader",
241            Self::Video { .. } => "Video",
242        }
243    }
244}
245
246/// Stored state of one node (internal representation).
247#[doc(hidden)]
248pub struct NodeData {
249    pub id: NodeId,
250    pub name: Option<String>,
251    /// `file:line` of the `add` call that created the node, when known.
252    pub source: Option<String>,
253    pub parent: Option<NodeId>,
254    pub children: Vec<NodeId>,
255    pub kind: NodeKind,
256    pub x: Signal<f64>,
257    pub y: Signal<f64>,
258    /// Degrees, clockwise (y points down).
259    pub rotation: Signal<f64>,
260    pub scale: Signal<Vec2>,
261    pub opacity: Signal<f64>,
262    pub fill: Signal<Color>,
263    pub stroke: Signal<Color>,
264    pub line_width: Signal<f64>,
265    /// Drawn part of the outline, as fractions of its length.
266    pub start: Signal<f64>,
267    pub end: Signal<f64>,
268    /// Overrides `fill` when set (local coordinates).
269    pub fill_gradient: Signal<Option<Gradient>>,
270    pub stroke_gradient: Signal<Option<Gradient>>,
271    /// Dash pattern (on, off, on, …) in local units; empty = solid.
272    pub dash: Signal<Vec<f64>>,
273    pub dash_offset: Signal<f64>,
274    pub line_cap: Signal<LineCap>,
275    pub line_join: Signal<LineJoin>,
276    /// Gaussian blur of the node and its children (local units).
277    pub blur: Signal<f64>,
278    pub shadow_color: Signal<Color>,
279    pub shadow_blur: Signal<f64>,
280    pub shadow_offset: Signal<Vec2>,
281    /// Children are clipped to this node's outline.
282    pub clip: Signal<bool>,
283    pub blend: Signal<BlendMode>,
284    /// Paint order among siblings (higher on top; ties keep insertion order).
285    pub z_index: Signal<f64>,
286    /// Which point of the node's box sits at (x, y) and is the pivot of
287    /// rotation and scale: (-1, -1) top-left, (0, 0) center, (1, 1)
288    /// bottom-right.
289    pub anchor: Signal<Vec2>,
290    // Flex item properties (used when the parent is a `Layout`).
291    pub grow: Signal<f64>,
292    pub shrink: Signal<f64>,
293    pub basis: Signal<Option<f64>>,
294    pub margin: Signal<f64>,
295    pub align_self: Signal<Option<LayoutAlign>>,
296    /// Recomputed every frame. `None` means `x` and `y` are used as they are.
297    pub place: Option<crate::place::Place>,
298    /// Position after [`place`](Self::place) is resolved. Draw uses this.
299    pub placed: Cell<(f64, f64)>,
300    /// Shader effect on the node's own pixels, from [`Node::effect`](crate::Node::effect).
301    pub effect: Option<crate::post::EffectData>,
302}
303
304impl NodeData {
305    /// Transform relative to the parent, plus a layout-assigned offset.
306    /// `size` is the node's box, needed for the anchor.
307    pub fn local_transform(&self, offset: Vec2, size: (f64, f64)) -> Affine {
308        if let NodeKind::Camera { zoom } = &self.kind {
309            // The view through the camera: the point it looks at comes to
310            // the camera's origin, scaled by zoom and turned against its
311            // rotation.
312            let (x, y) = self.placed.get();
313            return Affine::translate(offset)
314                * Affine::rotate(-self.rotation.get().to_radians())
315                * Affine::scale(zoom.get().max(1e-6))
316                * Affine::translate((-x, -y));
317        }
318        let s = self.scale.get();
319        let a = self.anchor.get();
320        let (x, y) = self.placed.get();
321        Affine::translate(offset + Vec2::new(x, y))
322            * Affine::rotate(self.rotation.get().to_radians())
323            * Affine::scale_non_uniform(s.x, s.y)
324            * Affine::translate((-a.x * size.0 / 2.0, -a.y * size.1 / 2.0))
325    }
326
327    /// Text to draw for `Txt` and `Code` nodes.
328    pub(crate) fn text_draw(&self) -> Option<TextDraw> {
329        match &self.kind {
330            NodeKind::Txt {
331                text,
332                spans,
333                font_size,
334                weight,
335                style,
336                morph,
337                morph_t,
338            } => {
339                let style = TextStyle {
340                    size: font_size.get().max(0.1),
341                    weight: weight.get().clamp(1.0, 1000.0),
342                    ..style.clone()
343                };
344                if let Some((from, to)) = morph.get() {
345                    let a = oxframe_text::layout(&from, &style);
346                    let b = oxframe_text::layout(&to, &style);
347                    let (glyphs, size) = crate::text::morph(&a, &b, morph_t.get().clamp(0.0, 1.0));
348                    return Some(TextDraw::Morph { glyphs, size });
349                }
350                let spans = spans.get();
351                Some(TextDraw::Styled(if spans.is_empty() {
352                    crate::text::rich(&[RichSpan::new(text.get())], &style)
353                } else {
354                    crate::text::rich(&spans, &style)
355                }))
356            }
357            NodeKind::Code {
358                code,
359                edit_from,
360                edit_to,
361                editing,
362                edit_t,
363                edit_flash,
364                lang,
365                theme,
366                font_size,
367                style,
368                focus,
369                dim,
370            } => {
371                let style = TextStyle {
372                    size: font_size.get().max(0.1),
373                    ..style.clone()
374                };
375                if editing.get() {
376                    let (frame, _) = crate::text::layout_diff(
377                        &edit_from.get(),
378                        &edit_to.get(),
379                        *lang,
380                        crate::text::DiffView {
381                            theme,
382                            style: &style,
383                            focus: focus.get(),
384                            dim: dim.get(),
385                            t: edit_t.get(),
386                            flash: edit_flash.get(),
387                        },
388                    );
389                    return Some(TextDraw::Diff(frame));
390                }
391                Some(TextDraw::Styled(crate::text::code(
392                    &code.get(),
393                    *lang,
394                    theme,
395                    &style,
396                    focus.get(),
397                    dim.get(),
398                )))
399            }
400            _ => None,
401        }
402    }
403
404    /// Untrimmed outline in local coordinates, centered on the origin.
405    pub fn outline(&self, size: (f64, f64)) -> Option<BezPath> {
406        const TOLERANCE: f64 = 0.1;
407        match &self.kind {
408            NodeKind::Video { radius, .. } => {
409                let (w, h) = size;
410                Some(
411                    kurbo::RoundedRect::new(
412                        -w / 2.0,
413                        -h / 2.0,
414                        w / 2.0,
415                        h / 2.0,
416                        radius.get().max(0.0),
417                    )
418                    .to_path(TOLERANCE),
419                )
420            }
421            NodeKind::Group
422            | NodeKind::Svg { .. }
423            | NodeKind::Camera { .. }
424            | NodeKind::Img { .. }
425            | NodeKind::Txt { .. }
426            | NodeKind::Code { .. }
427            | NodeKind::Grid { .. }
428            | NodeKind::Custom { .. }
429            | NodeKind::Particles { .. }
430            | NodeKind::Shader { .. } => None,
431            NodeKind::Rect { radius, .. } | NodeKind::Layout { radius, .. } => {
432                let (w, h) = (size.0.max(0.0), size.1.max(0.0));
433                let r = radius.get().clamp(0.0, w.min(h) / 2.0);
434                let rect = kurbo::Rect::new(-w / 2.0, -h / 2.0, w / 2.0, h / 2.0);
435                Some(if r > 0.0 {
436                    rect.to_rounded_rect(r).to_path(TOLERANCE)
437                } else {
438                    rect.to_path(TOLERANCE)
439                })
440            }
441            NodeKind::Circle {
442                start_angle,
443                end_angle,
444                pie,
445                ..
446            } => {
447                // Start at the top, clockwise — like a clock hand, which is
448                // what `end` animations expect.
449                let r = size.0.max(0.0) / 2.0;
450                let (a0, a1) = (start_angle.get(), end_angle.get());
451                let sweep = (a1 - a0).clamp(-360.0, 360.0);
452                if sweep.abs() >= 360.0 - 1e-9 {
453                    let circle = kurbo::Circle::new((0.0, 0.0), r).to_path(TOLERANCE);
454                    return Some(Affine::rotate((a0 - 90.0).to_radians()) * circle);
455                }
456                let arc = kurbo::Arc::new(
457                    (0.0, 0.0),
458                    (r, r),
459                    (a0 - 90.0).to_radians(),
460                    sweep.to_radians(),
461                    0.0,
462                );
463                let mut p = BezPath::new();
464                if *pie {
465                    p.move_to((0.0, 0.0));
466                    p.line_to(arc.center + kurbo::Vec2::from_angle(arc.start_angle) * r);
467                    arc.to_cubic_beziers(TOLERANCE, |a, b, c| p.curve_to(a, b, c));
468                    p.close_path();
469                } else {
470                    p.move_to(arc.center + kurbo::Vec2::from_angle(arc.start_angle) * r);
471                    arc.to_cubic_beziers(TOLERANCE, |a, b, c| p.curve_to(a, b, c));
472                }
473                Some(p)
474            }
475            NodeKind::Polygon { sides, .. } => {
476                let n = sides.get().round().max(3.0) as usize;
477                Some(regular(n, size.0.max(0.0) / 2.0, None))
478            }
479            NodeKind::Star { points, inner, .. } => {
480                let n = points.get().round().max(2.0) as usize;
481                let r = size.0.max(0.0) / 2.0;
482                Some(regular(n, r, Some(r * inner.get().clamp(0.0, 1.0))))
483            }
484            NodeKind::Path { data } => Some(data.get()),
485            NodeKind::Line {
486                points,
487                closed,
488                smooth,
489                ..
490            } => {
491                let pts = points.get();
492                if *smooth && pts.len() > 2 {
493                    return Some(catmull_rom(&pts, *closed));
494                }
495                let mut p = BezPath::new();
496                for (i, v) in pts.iter().enumerate() {
497                    if i == 0 {
498                        p.move_to(v.to_point());
499                    } else {
500                        p.line_to(v.to_point());
501                    }
502                }
503                if *closed && pts.len() > 2 {
504                    p.close_path();
505                }
506                Some(p)
507            }
508        }
509    }
510}
511
512/// Regular polygon (or star with an inner radius), first vertex at the top.
513fn regular(n: usize, r: f64, inner: Option<f64>) -> BezPath {
514    let count = if inner.is_some() { 2 * n } else { n };
515    let mut p = BezPath::new();
516    for i in 0..count {
517        let radius = match inner {
518            Some(ri) if i % 2 == 1 => ri,
519            _ => r,
520        };
521        let a = -std::f64::consts::FRAC_PI_2 + std::f64::consts::TAU * i as f64 / count as f64;
522        let pt = Point::new(a.cos() * radius, a.sin() * radius);
523        if i == 0 {
524            p.move_to(pt);
525        } else {
526            p.line_to(pt);
527        }
528    }
529    p.close_path();
530    p
531}
532
533/// Smooth curve through the points (uniform Catmull-Rom as cubic Béziers).
534fn catmull_rom(pts: &[Vec2], closed: bool) -> BezPath {
535    let n = pts.len();
536    let at = |i: isize| -> Vec2 {
537        if closed {
538            pts[i.rem_euclid(n as isize) as usize]
539        } else {
540            pts[i.clamp(0, n as isize - 1) as usize]
541        }
542    };
543    let mut p = BezPath::new();
544    p.move_to(pts[0].to_point());
545    let segs = if closed { n } else { n - 1 };
546    for i in 0..segs as isize {
547        let (p0, p1, p2, p3) = (at(i - 1), at(i), at(i + 1), at(i + 2));
548        let c1 = p1 + (p2 - p0) / 6.0;
549        let c2 = p2 - (p3 - p1) / 6.0;
550        p.curve_to(c1.to_point(), c2.to_point(), p2.to_point());
551    }
552    if closed {
553        p.close_path();
554    }
555    p
556}
557
558pub(crate) enum TextDraw {
559    Styled(Styled),
560    /// Glyphs with their alpha, already centered.
561    Morph {
562        glyphs: Vec<(BezPath, f64)>,
563        size: (f64, f64),
564    },
565    /// A code diff, glyphs in a top-left layout box.
566    Diff(DiffFrame),
567}
568
569/// Arena of nodes for one scene run. Node 0 is the view (root).
570pub struct SceneGraph {
571    nodes: RefCell<Vec<NodeData>>,
572    size: (u32, u32),
573    /// Seconds of the frame last stepped. [`draw_list`](Self::draw_list) paints this time.
574    time: Cell<f64>,
575    /// WGSL problems from the last [`draw_list`](Self::draw_list).
576    shader_diags: RefCell<Vec<crate::shader::ShaderDiag>>,
577}
578
579impl SceneGraph {
580    /// The root node (the scene's view).
581    pub const ROOT: NodeId = NodeId(0);
582
583    /// An empty scene of the given size in pixels, with only the root.
584    pub fn new(width: u32, height: u32) -> Rc<Self> {
585        let graph = Rc::new(Self {
586            nodes: RefCell::new(Vec::new()),
587            size: (width, height),
588            time: Cell::new(0.0),
589            shader_diags: RefCell::new(Vec::new()),
590        });
591        graph.insert(
592            None,
593            NodeKind::Group,
594            crate::BaseProps {
595                name: Some("view".into()),
596                ..Default::default()
597            },
598        );
599        graph
600    }
601
602    /// Scene size in pixels.
603    pub fn size(&self) -> (u32, u32) {
604        self.size
605    }
606
607    /// Time, in seconds, that the next [`draw_list`](Self::draw_list) paints.
608    /// The engine sets this after each frame.
609    pub fn set_time(&self, seconds: f64) {
610        self.time.set(seconds.max(0.0));
611    }
612
613    /// Time the last [`set_time`](Self::set_time) stored.
614    pub fn time(&self) -> f64 {
615        self.time.get()
616    }
617
618    /// Number of nodes, including the root.
619    pub fn len(&self) -> usize {
620        self.nodes.borrow().len()
621    }
622
623    /// Never true: the root always exists.
624    pub fn is_empty(&self) -> bool {
625        self.len() == 0
626    }
627
628    /// Borrows a node's stored state.
629    ///
630    /// # Panics
631    ///
632    /// If `id` is not a node of this graph.
633    pub fn node(&self, id: NodeId) -> Ref<'_, NodeData> {
634        Ref::map(self.nodes.borrow(), |n| &n[id.0 as usize])
635    }
636
637    pub(crate) fn next_id(&self) -> u32 {
638        self.len() as u32
639    }
640
641    pub(crate) fn insert(
642        &self,
643        parent: Option<NodeId>,
644        kind: NodeKind,
645        base: crate::BaseProps,
646    ) -> NodeId {
647        let mut nodes = self.nodes.borrow_mut();
648        let id = NodeId(u32::try_from(nodes.len()).expect("too many nodes"));
649        let label = format!("{}#{}", kind.name(), id.0);
650        let sig = |field: &str| format!("{label}.{field}");
651        nodes.push(NodeData {
652            id,
653            name: base.name,
654            source: current_add_at().map(|(file, line)| source_label(file, line)),
655            parent,
656            children: Vec::new(),
657            x: Signal::new(base.x).with_name(sig("x")),
658            y: Signal::new(base.y).with_name(sig("y")),
659            rotation: Signal::new(base.rotation).with_name(sig("rotation")),
660            scale: Signal::new(base.scale).with_name(sig("scale")),
661            opacity: Signal::new(base.opacity).with_name(sig("opacity")),
662            fill: Signal::new(base.fill).with_name(sig("fill")),
663            stroke: Signal::new(base.stroke).with_name(sig("stroke")),
664            line_width: Signal::new(base.line_width).with_name(sig("line_width")),
665            start: Signal::new(base.start).with_name(sig("start")),
666            end: Signal::new(base.end).with_name(sig("end")),
667            fill_gradient: Signal::new(base.fill_gradient).with_name(sig("fill_gradient")),
668            stroke_gradient: Signal::new(base.stroke_gradient).with_name(sig("stroke_gradient")),
669            dash: Signal::new(base.dash).with_name(sig("dash")),
670            dash_offset: Signal::new(base.dash_offset).with_name(sig("dash_offset")),
671            line_cap: Signal::new(base.line_cap).with_name(sig("line_cap")),
672            line_join: Signal::new(base.line_join).with_name(sig("line_join")),
673            blur: Signal::new(base.blur).with_name(sig("blur")),
674            shadow_color: Signal::new(base.shadow_color).with_name(sig("shadow_color")),
675            shadow_blur: Signal::new(base.shadow_blur).with_name(sig("shadow_blur")),
676            shadow_offset: Signal::new(base.shadow_offset).with_name(sig("shadow_offset")),
677            clip: Signal::new(base.clip).with_name(sig("clip")),
678            blend: Signal::new(base.blend).with_name(sig("blend")),
679            z_index: Signal::new(base.z_index).with_name(sig("z_index")),
680            anchor: Signal::new(base.anchor).with_name(sig("anchor")),
681            grow: Signal::new(base.grow).with_name(sig("grow")),
682            shrink: Signal::new(base.shrink).with_name(sig("shrink")),
683            basis: Signal::new(base.basis).with_name(sig("basis")),
684            margin: Signal::new(base.margin).with_name(sig("margin")),
685            align_self: Signal::new(base.align_self).with_name(sig("align_self")),
686            place: base.place,
687            placed: Cell::new((base.x, base.y)),
688            effect: None,
689            kind,
690        });
691        if let Some(p) = parent {
692            nodes[p.0 as usize].children.push(id);
693        }
694        id
695    }
696
697    pub(crate) fn detach(&self, id: NodeId) {
698        let mut nodes = self.nodes.borrow_mut();
699        if let Some(p) = nodes[id.0 as usize].parent.take() {
700            nodes[p.0 as usize].children.retain(|c| *c != id);
701        }
702    }
703
704    pub(crate) fn reparent(&self, id: NodeId, parent: NodeId) {
705        self.detach(id);
706        let mut nodes = self.nodes.borrow_mut();
707        nodes[id.0 as usize].parent = Some(parent);
708        nodes[parent.0 as usize].children.push(id);
709    }
710
711    /// Size of a node's box in its own (unscaled) coordinates.
712    pub fn measure(&self, id: NodeId) -> (f64, f64) {
713        self.resolve_places();
714        let nodes = self.nodes.borrow();
715        measure(&nodes, id)
716    }
717
718    pub(crate) fn set_effect(&self, id: NodeId, effect: crate::post::EffectData) {
719        self.nodes.borrow_mut()[id.0 as usize].effect = Some(effect);
720    }
721
722    /// Some node reads or changes drawn pixels (an effect, or a shader
723    /// that reads the frame). When false, the draw list has no markers and
724    /// a renderer can draw it in one go.
725    pub fn has_post(&self) -> bool {
726        self.nodes.borrow().iter().any(|n| {
727            n.effect.is_some()
728                || matches!(
729                    n.kind,
730                    NodeKind::Shader {
731                        preset: crate::shader::Preset::LensDrops,
732                        ..
733                    }
734                )
735        })
736    }
737
738    pub(crate) fn set_place(&self, id: NodeId, place: crate::place::Place) {
739        self.nodes.borrow_mut()[id.0 as usize].place = Some(place);
740    }
741
742    fn resolve_places(&self) {
743        let nodes = self.nodes.borrow();
744        crate::place::resolve(&nodes, (f64::from(self.size.0), f64::from(self.size.1)));
745    }
746
747    /// Flattened, paint-ordered list of everything to draw.
748    pub fn draw_list(&self) -> Vec<DrawItem> {
749        self.resolve_places();
750        let nodes = self.nodes.borrow();
751        let mut out = Vec::new();
752        crate::shader::clear_diags();
753        for n in nodes.iter() {
754            for diag in crate::shader::problems(n) {
755                crate::shader::push_diag(diag);
756            }
757        }
758        visit(
759            &nodes,
760            Self::ROOT,
761            Affine::IDENTITY,
762            None,
763            None,
764            self.time.get(),
765            false,
766            &mut out,
767        );
768        *self.shader_diags.borrow_mut() = crate::shader::take_diags();
769        out
770    }
771
772    /// WGSL problems from the last [`draw_list`](Self::draw_list).
773    pub fn shader_diags(&self) -> Vec<crate::shader::ShaderDiag> {
774        self.shader_diags.borrow().clone()
775    }
776
777    /// `Some` when painting this frame twice does not produce the same
778    /// primitives. The string is the node's name, or its `add` call site.
779    pub fn draw_drift(&self) -> Option<(String, Option<String>)> {
780        let a = self.draw_list();
781        let b = self.draw_list();
782        let id = first_drift(&a, &b)?;
783        let n = self.node(id);
784        let label = n
785            .name
786            .clone()
787            .or_else(|| n.source.clone())
788            .unwrap_or_else(|| n.kind.name().to_owned());
789        Some((label, n.source.clone()))
790    }
791
792    /// Topmost node under `p` (scene coordinates), excluding the view.
793    pub fn pick(&self, p: Point) -> Option<NodeId> {
794        self.draw_list()
795            .iter()
796            .rev()
797            .find(|item| item.node != Self::ROOT && item.hit(p))
798            .map(|item| item.node)
799    }
800
801    /// World transform of a node (including layout offsets).
802    pub fn world_transform(&self, id: NodeId) -> Option<Affine> {
803        // The draw list already composes transforms; reuse it for nodes
804        // that draw. Those that don't (a Layout row, a transparent node) are
805        // placed by the same walk, layout included; a node outside the tree
806        // by its parents alone.
807        if let Some(item) = self.draw_list().into_iter().find(|i| i.node == id) {
808            return Some(item.transform);
809        }
810        self.resolve_places();
811        let nodes = self.nodes.borrow();
812        if let Some(Some((tf, _))) = placements(&nodes).get(id.0 as usize) {
813            return Some(*tf);
814        }
815        let mut tf = Affine::IDENTITY;
816        let mut cur = Some(id);
817        while let Some(c) = cur {
818            let n = nodes.get(c.0 as usize)?;
819            tf = n.local_transform(Vec2::ZERO, measure(&nodes, c)) * tf;
820            cur = n.parent;
821        }
822        Some(tf)
823    }
824
825    /// The box of `id` (its size around its origin) in the coordinate frame
826    /// of `frame`'s children: for a camera, where the node sits in the
827    /// world the camera looks at. Axis-aligned.
828    pub fn box_in(&self, id: NodeId, frame: NodeId) -> Option<kurbo::Rect> {
829        let node_tf = self.world_transform(id)?;
830        let frame_tf = self.world_transform(frame)?;
831        let (w, h) = measure(&self.nodes.borrow(), id);
832        let [x0, y0, x1, y1] = aabb(frame_tf.inverse() * node_tf, w, h);
833        Some(kurbo::Rect::new(x0, y0, x1, y1))
834    }
835
836    /// Frame size in pixels.
837    pub fn frame_size(&self) -> (f64, f64) {
838        (f64::from(self.size.0), f64::from(self.size.1))
839    }
840
841    /// First descendant of `root` (including `root`) whose name is `name`.
842    pub(crate) fn descendant_named(&self, root: NodeId, name: &str) -> Option<NodeId> {
843        fn walk(nodes: &[NodeData], id: NodeId, name: &str) -> Option<NodeId> {
844            let n = nodes.get(id.0 as usize)?;
845            if n.name.as_deref() == Some(name) {
846                return Some(id);
847            }
848            for &c in &n.children {
849                if let Some(hit) = walk(nodes, c, name) {
850                    return Some(hit);
851                }
852            }
853            None
854        }
855        walk(&self.nodes.borrow(), root, name)
856    }
857
858    /// Path nodes under `root`, in paint order.
859    pub(crate) fn descendant_paths(&self, root: NodeId) -> Vec<NodeId> {
860        fn walk(nodes: &[NodeData], id: NodeId, out: &mut Vec<NodeId>) {
861            let Some(n) = nodes.get(id.0 as usize) else {
862                return;
863            };
864            if matches!(n.kind, NodeKind::Path { .. }) {
865                out.push(id);
866            }
867            for &c in &n.children {
868                walk(nodes, c, out);
869            }
870        }
871        let mut out = Vec::new();
872        walk(&self.nodes.borrow(), root, &mut out);
873        out
874    }
875
876    /// `(id, parent, kind, name)` of every attached node, depth-first.
877    pub fn tree(&self) -> Vec<(NodeId, Option<NodeId>, &'static str, Option<String>)> {
878        let nodes = self.nodes.borrow();
879        let mut out = Vec::new();
880        let mut stack = vec![Self::ROOT];
881        while let Some(id) = stack.pop() {
882            let n = &nodes[id.0 as usize];
883            out.push((id, n.parent, n.kind.name(), n.name.clone()));
884            stack.extend(n.children.iter().rev());
885        }
886        out
887    }
888
889    /// Current signal values of a node, for the inspector.
890    pub fn describe(&self, id: NodeId) -> Vec<(String, String)> {
891        let nodes = self.nodes.borrow();
892        let Some(n) = nodes.get(id.0 as usize) else {
893            return Vec::new();
894        };
895        let f = |v: f64| format!("{v:.3}");
896        let s = n.scale.get();
897        let mut out = vec![
898            ("x".into(), f(n.x.get())),
899            ("y".into(), f(n.y.get())),
900            ("rotation".into(), f(n.rotation.get())),
901            ("scale".into(), format!("{:.3}, {:.3}", s.x, s.y)),
902            ("opacity".into(), f(n.opacity.get())),
903            ("fill".into(), n.fill.get().to_string()),
904            ("stroke".into(), n.stroke.get().to_string()),
905            ("line_width".into(), f(n.line_width.get())),
906            ("start".into(), f(n.start.get())),
907            ("end".into(), f(n.end.get())),
908        ];
909        let (w, h) = measure(&nodes, id);
910        out.push(("size".into(), format!("{w:.1} × {h:.1}")));
911        match &n.kind {
912            NodeKind::Rect { radius, .. } => out.push(("radius".into(), f(radius.get()))),
913            NodeKind::Code { code, .. } => out.push(("code".into(), code.get())),
914            NodeKind::Txt {
915                text,
916                font_size,
917                weight,
918                ..
919            } => {
920                out.push(("text".into(), text.get()));
921                out.push(("font_size".into(), f(font_size.get())));
922                out.push(("weight".into(), f(weight.get())));
923            }
924            NodeKind::Line { points, .. } => {
925                out.push(("points".into(), points.get().len().to_string()))
926            }
927            NodeKind::Layout { gap, padding, .. } => {
928                out.push(("gap".into(), f(gap.get())));
929                out.push(("padding".into(), f(padding.get())));
930            }
931            NodeKind::Grid { spacing, .. } => out.push(("spacing".into(), f(spacing.get()))),
932            _ => {}
933        }
934        out
935    }
936
937    /// One record per attached node, in tree order, with scene-space bounds.
938    ///
939    /// `order` is paint order (0 is the back). Nodes that are not drawn
940    /// (fully transparent) get `u32::MAX`.
941    pub fn inspect_nodes(&self) -> Vec<InspectNode> {
942        let list = self.draw_list();
943        let mut drawn: Vec<Option<(Affine, u32)>> = vec![None; self.len()];
944        let mut order = 0u32;
945        for item in &list {
946            let slot = &mut drawn[item.node.0 as usize];
947            if slot.is_none() {
948                *slot = Some((item.transform, order));
949                order += 1;
950            }
951        }
952        let nodes = self.nodes.borrow();
953        // Where every node is, drawn or not (a Layout row draws nothing).
954        let placed = placements(&nodes);
955        let mut out = Vec::new();
956        let mut stack = vec![Self::ROOT];
957        while let Some(id) = stack.pop() {
958            let n = &nodes[id.0 as usize];
959            stack.extend(n.children.iter().rev());
960            let order = drawn[id.0 as usize].map_or(u32::MAX, |(_, o)| o);
961            let (tf, (w, h)) = placed[id.0 as usize]
962                .unwrap_or_else(|| (transform_without_layout(&nodes, id), measure(&nodes, id)));
963            let (text, text_overflow) = text_info(n);
964            out.push(InspectNode {
965                id: id.0,
966                parent: n.parent.map(|p| p.0),
967                kind: n.kind.name(),
968                name: n.name.clone().unwrap_or_else(|| {
969                    n.source
970                        .clone()
971                        .unwrap_or_else(|| format!("{}#{}", n.kind.name(), id.0))
972                }),
973                source: n.source.clone(),
974                bounds: aabb(tf, w, h),
975                opacity: n.opacity.get().clamp(0.0, 1.0),
976                order,
977                text,
978                fill: n.fill.get(),
979                text_overflow,
980                parent_kind: n.parent.map(|p| nodes[p.0 as usize].kind.name()),
981                in_camera: {
982                    let mut up = n.parent;
983                    let mut found = false;
984                    while let Some(p) = up {
985                        let pn = &nodes[p.0 as usize];
986                        if matches!(pn.kind, NodeKind::Camera { .. }) {
987                            found = true;
988                            break;
989                        }
990                        up = pn.parent;
991                    }
992                    found
993                },
994                shader: match &n.kind {
995                    NodeKind::Shader { preset, .. } => Some(preset.name()),
996                    _ => None,
997                },
998                params: params_of(n),
999            });
1000        }
1001        out
1002    }
1003}
1004
1005/// A node as `oxframe inspect` and the visual linter see it.
1006#[derive(Debug, Clone, PartialEq)]
1007pub struct InspectNode {
1008    /// Node index.
1009    pub id: u32,
1010    /// Parent index; `None` for the root.
1011    pub parent: Option<u32>,
1012    /// Node type, e.g. `"Rect"`.
1013    pub kind: &'static str,
1014    /// `.name(..)`, or the `add` call site, or `Kind#id`.
1015    pub name: String,
1016    /// `file:line` of the `add` call, when it was recorded.
1017    pub source: Option<String>,
1018    /// Axis-aligned bounds in scene coordinates: min x, min y, max x, max y.
1019    /// The origin is the center of the frame, y points down.
1020    pub bounds: [f64; 4],
1021    /// 0 (invisible) to 1.
1022    pub opacity: f64,
1023    /// Paint order among drawn nodes; `u32::MAX` if the node is not drawn.
1024    pub order: u32,
1025    /// Text of a `Txt` or `Code` node.
1026    pub text: Option<String>,
1027    /// Fill color.
1028    pub fill: Color,
1029    /// How many pixels the text is wider than its `max_width` (0 if it fits
1030    /// or `max_width` is unset).
1031    pub text_overflow: f64,
1032    /// Type of the parent, if any.
1033    pub parent_kind: Option<&'static str>,
1034    /// Seen through a [`Camera`](crate::Camera): leaving the frame is then
1035    /// usually the camera's framing, not a layout mistake.
1036    pub in_camera: bool,
1037    /// Preset of a `Shader` node: `"waves"`, `"lens_drops"` or `"custom"`.
1038    pub shader: Option<&'static str>,
1039    /// Numeric shader parameters (`amp`, `wetness`, …), the strength and
1040    /// speed of an effect on the node (`effect.strength`), how much of it is
1041    /// drawn (`start` above 0, `end` below 1) and its glow or shadow
1042    /// (`shadow`: blur times opacity). Empty otherwise.
1043    pub params: Vec<(String, f64)>,
1044}
1045
1046/// Values that change a shader's picture without moving its box.
1047fn params_of(n: &NodeData) -> Vec<(String, f64)> {
1048    let mut out = Vec::new();
1049    if let NodeKind::Shader { uniforms, .. } = &n.kind {
1050        for u in uniforms {
1051            match &u.value {
1052                crate::shader::UniformValue::Num(v) | crate::shader::UniformValue::Bool(v) => {
1053                    out.push((u.name.clone(), v.get()));
1054                }
1055                _ => {}
1056            }
1057        }
1058    }
1059    if let Some(e) = &n.effect {
1060        out.push(("effect.strength".into(), e.strength.get()));
1061        out.push(("effect.speed".into(), e.speed.get()));
1062    }
1063    // What changes the picture without moving the box: the drawn part of an
1064    // outline or a text (`start`/`end`), and a glow or shadow.
1065    let (start, end) = (n.start.get(), n.end.get());
1066    if start > 0.0 {
1067        out.push(("start".into(), start));
1068    }
1069    if end < 1.0 {
1070        out.push(("end".into(), end));
1071    }
1072    let shadow = n.shadow_color.get();
1073    if shadow.a > 0.0 {
1074        out.push(("shadow".into(), n.shadow_blur.get() * f64::from(shadow.a)));
1075    }
1076    out
1077}
1078
1079thread_local! {
1080    static ADD_AT: Cell<Option<(&'static str, u32)>> = const { Cell::new(None) };
1081}
1082
1083/// Records where an `add` happens. An add inside another one (a component
1084/// building its parts) keeps the outer site: `check` points at the user's
1085/// line, not at the component's code.
1086pub(crate) fn push_add_at(file: &'static str, line: u32) -> Option<(&'static str, u32)> {
1087    ADD_AT.with(|c| {
1088        let prev = c.get();
1089        if prev.is_none() {
1090            c.set(Some((file, line)));
1091        }
1092        prev
1093    })
1094}
1095
1096/// Runs `f` so that the nodes it adds report the caller's file and line in
1097/// `check` and `inspect`.
1098///
1099/// For methods that add nodes after the node itself was added, such as a
1100/// plot on a set of axes. Mark the method `#[track_caller]`.
1101#[track_caller]
1102pub fn add_site<R>(f: impl FnOnce() -> R) -> R {
1103    let loc = std::panic::Location::caller();
1104    let prev = push_add_at(loc.file(), loc.line());
1105    let out = f();
1106    pop_add_at(prev);
1107    out
1108}
1109
1110pub(crate) fn pop_add_at(prev: Option<(&'static str, u32)>) {
1111    ADD_AT.with(|c| c.set(prev));
1112}
1113
1114fn current_add_at() -> Option<(&'static str, u32)> {
1115    ADD_AT.with(|c| c.get())
1116}
1117
1118/// `src/main.rs:42` rather than an absolute build path.
1119fn source_label(file: &str, line: u32) -> String {
1120    let file = file.replace('\\', "/");
1121    let rel = file
1122        .rfind("/src/")
1123        .map_or(file.as_str(), |i| &file[i + 1..]);
1124    format!("{rel}:{line}")
1125}
1126
1127fn aabb(tf: Affine, w: f64, h: f64) -> [f64; 4] {
1128    let pts = [
1129        (-w / 2.0, -h / 2.0),
1130        (w / 2.0, -h / 2.0),
1131        (w / 2.0, h / 2.0),
1132        (-w / 2.0, h / 2.0),
1133    ];
1134    let mut b = [
1135        f64::INFINITY,
1136        f64::INFINITY,
1137        f64::NEG_INFINITY,
1138        f64::NEG_INFINITY,
1139    ];
1140    for (x, y) in pts {
1141        let p = tf * Point::new(x, y);
1142        b[0] = b[0].min(p.x);
1143        b[1] = b[1].min(p.y);
1144        b[2] = b[2].max(p.x);
1145        b[3] = b[3].max(p.y);
1146    }
1147    b
1148}
1149
1150/// World transform and size of every node as [`visit`] places it, layout
1151/// boxes included, whether or not the node draws anything: an empty row of
1152/// a `Layout` sits where its children are drawn. `None` for detached nodes.
1153fn placements(nodes: &[NodeData]) -> Vec<Option<(Affine, (f64, f64))>> {
1154    fn walk(
1155        nodes: &[NodeData],
1156        id: NodeId,
1157        parent_tf: Affine,
1158        placement: Option<FlexBox>,
1159        flex: Option<&FlexMap>,
1160        out: &mut [Option<(Affine, (f64, f64))>],
1161    ) {
1162        let n = &nodes[id.0 as usize];
1163        // The same size and offset as `visit`.
1164        let size = match placement {
1165            Some(b) if resizable(&n.kind) => {
1166                let s = n.scale.get();
1167                (
1168                    b.size.0 / s.x.abs().max(1e-9),
1169                    b.size.1 / s.y.abs().max(1e-9),
1170                )
1171            }
1172            _ => measure(nodes, id),
1173        };
1174        let offset = placement.map_or(Vec2::ZERO, |b| b.center);
1175        let tf = parent_tf * n.local_transform(offset, size);
1176        out[id.0 as usize] = Some((tf, size));
1177        let own;
1178        let map: Option<&FlexMap> = match (&n.kind, flex) {
1179            (NodeKind::Layout { .. }, Some(m)) => Some(m),
1180            (NodeKind::Layout { .. }, None) => {
1181                own = crate::flex::compute(nodes, id, &|c| measure(nodes, c)).1;
1182                Some(&own)
1183            }
1184            _ => None,
1185        };
1186        for &c in &n.children {
1187            let placement = map.and_then(|m| m.get(&c).copied());
1188            walk(nodes, c, tf, placement, map, out);
1189        }
1190    }
1191    let mut out = vec![None; nodes.len()];
1192    walk(
1193        nodes,
1194        SceneGraph::ROOT,
1195        Affine::IDENTITY,
1196        None,
1197        None,
1198        &mut out,
1199    );
1200    out
1201}
1202
1203fn transform_without_layout(nodes: &[NodeData], id: NodeId) -> Affine {
1204    let mut chain = Vec::new();
1205    let mut cur = Some(id);
1206    while let Some(c) = cur {
1207        chain.push(c);
1208        cur = nodes.get(c.0 as usize).and_then(|n| n.parent);
1209    }
1210    let mut tf = Affine::IDENTITY;
1211    for c in chain.into_iter().rev() {
1212        let Some(n) = nodes.get(c.0 as usize) else {
1213            break;
1214        };
1215        tf *= n.local_transform(Vec2::ZERO, measure(nodes, c));
1216    }
1217    tf
1218}
1219
1220fn text_info(n: &NodeData) -> (Option<String>, f64) {
1221    let (body, max_width) = match &n.kind {
1222        NodeKind::Txt {
1223            text, spans, style, ..
1224        } => {
1225            let spans = spans.get();
1226            let body = if spans.is_empty() {
1227                text.get()
1228            } else {
1229                spans.into_iter().map(|s| s.text).collect()
1230            };
1231            (body, style.max_width)
1232        }
1233        NodeKind::Code { code, style, .. } => (code.get(), style.max_width),
1234        _ => return (None, 0.0),
1235    };
1236    let overflow = match (max_width, n.text_draw()) {
1237        (Some(max), Some(TextDraw::Styled(s))) => (s.layout.width - max).max(0.0),
1238        (Some(max), Some(TextDraw::Morph { size, .. })) => (size.0 - max).max(0.0),
1239        (Some(max), Some(TextDraw::Diff(frame))) => (frame.size.0 - max).max(0.0),
1240        _ => 0.0,
1241    };
1242    (Some(body), overflow)
1243}
1244
1245pub(crate) fn measure(nodes: &[NodeData], id: NodeId) -> (f64, f64) {
1246    let n = &nodes[id.0 as usize];
1247    match &n.kind {
1248        NodeKind::Rect { width, height, .. } => (width.get(), height.get()),
1249        NodeKind::Circle { size, .. }
1250        | NodeKind::Polygon { size, .. }
1251        | NodeKind::Star { size, .. } => (size.get(), size.get()),
1252        NodeKind::Img { width, height, .. }
1253        | NodeKind::Grid { width, height, .. }
1254        | NodeKind::Svg { width, height } => (width.get(), height.get()),
1255        NodeKind::Txt { .. } | NodeKind::Code { .. } => match n.text_draw() {
1256            Some(TextDraw::Styled(s)) => (s.layout.width, s.layout.height),
1257            Some(TextDraw::Morph { size, .. }) => size,
1258            Some(TextDraw::Diff(frame)) => frame.size,
1259            None => (0.0, 0.0),
1260        },
1261        NodeKind::Path { .. } | NodeKind::Line { .. } => {
1262            let bb = n
1263                .outline((0.0, 0.0))
1264                .map_or(kurbo::Rect::ZERO, |p| p.bounding_box());
1265            (bb.width(), bb.height())
1266        }
1267        NodeKind::Layout { .. } => crate::flex::compute(nodes, id, &|c| measure(nodes, c)).0,
1268        // A window onto its children, not a shape: no box of its own.
1269        NodeKind::Camera { .. } | NodeKind::Particles { .. } => (0.0, 0.0),
1270        NodeKind::Custom {
1271            measure: Some(m), ..
1272        } => m(),
1273        NodeKind::Custom { width, height, .. } => (*width, *height),
1274        NodeKind::Shader { width, height, .. } => (width.get(), height.get()),
1275        NodeKind::Video { width, height, .. } => (width.get(), height.get()),
1276        NodeKind::Group => {
1277            let mut bb: Option<kurbo::Rect> = None;
1278            for &c in &n.children {
1279                let child = &nodes[c.0 as usize];
1280                let (w, h) = measure(nodes, c);
1281                let r = child
1282                    .local_transform(Vec2::ZERO, (w, h))
1283                    .transform_rect_bbox(kurbo::Rect::new(-w / 2.0, -h / 2.0, w / 2.0, h / 2.0));
1284                bb = Some(bb.map_or(r, |b| b.union(r)));
1285            }
1286            bb.map_or((0.0, 0.0), |b| (b.width(), b.height()))
1287        }
1288    }
1289}
1290
1291/// Kinds whose drawn size follows the box a layout gives them.
1292fn resizable(kind: &NodeKind) -> bool {
1293    matches!(
1294        kind,
1295        NodeKind::Rect { .. }
1296            | NodeKind::Layout { .. }
1297            | NodeKind::Img { .. }
1298            | NodeKind::Grid { .. }
1299            | NodeKind::Svg { .. }
1300            | NodeKind::Shader { .. }
1301            | NodeKind::Video { .. }
1302    )
1303}
1304
1305fn first_drift(a: &[DrawItem], b: &[DrawItem]) -> Option<NodeId> {
1306    let n = a.len().max(b.len());
1307    for i in 0..n {
1308        match (a.get(i), b.get(i)) {
1309            (Some(x), Some(y)) if x.node == y.node && prim_same(&x.prim, &y.prim) => {}
1310            (Some(x), _) => return Some(x.node),
1311            (_, Some(y)) => return Some(y.node),
1312            (None, None) => {}
1313        }
1314    }
1315    None
1316}
1317
1318fn prim_same(a: &Primitive, b: &Primitive) -> bool {
1319    use Primitive::{Fill, Image, PopLayer, PushLayer, Stroke};
1320    match (a, b) {
1321        (Fill { path: p, paint: c }, Fill { path: q, paint: d }) => p == q && c == d,
1322        (
1323            Stroke {
1324                path: p,
1325                paint: c,
1326                style: s,
1327            },
1328            Stroke {
1329                path: q,
1330                paint: d,
1331                style: t,
1332            },
1333        ) => p == q && c == d && s == t,
1334        (
1335            Image {
1336                rect: r,
1337                opacity: o,
1338                ..
1339            },
1340            Image {
1341                rect: s,
1342                opacity: p,
1343                ..
1344            },
1345        ) => r == s && o == p,
1346        (PushLayer(x), PushLayer(y)) => x == y,
1347        (PopLayer, PopLayer) => true,
1348        (Primitive::ReadFrame(x), Primitive::ReadFrame(y))
1349        | (Primitive::BeginEffect(x), Primitive::BeginEffect(y)) => x == y,
1350        (Primitive::EndEffect, Primitive::EndEffect) => true,
1351        _ => false,
1352    }
1353}
1354
1355/// `inside` is true under an open layer or effect. A marker there could not
1356/// be drawn in parts, so the node reports `input_cycle` and draws without it.
1357#[allow(clippy::too_many_arguments)]
1358fn visit(
1359    nodes: &[NodeData],
1360    id: NodeId,
1361    parent_tf: Affine,
1362    placement: Option<FlexBox>,
1363    flex: Option<&FlexMap>,
1364    time: f64,
1365    inside: bool,
1366    out: &mut Vec<DrawItem>,
1367) {
1368    let n = &nodes[id.0 as usize];
1369    let opacity = n.opacity.get().clamp(0.0, 1.0);
1370    if opacity <= 0.0 {
1371        return;
1372    }
1373    let size = match placement {
1374        Some(b) if resizable(&n.kind) => {
1375            let s = n.scale.get();
1376            (
1377                b.size.0 / s.x.abs().max(1e-9),
1378                b.size.1 / s.y.abs().max(1e-9),
1379            )
1380        }
1381        _ => measure(nodes, id),
1382    };
1383    let offset = placement.map_or(Vec2::ZERO, |b| b.center);
1384    let tf = parent_tf * n.local_transform(offset, size);
1385
1386    // A layer is needed when the node's effects apply to it and its children
1387    // as a whole; plain leaves just fold opacity into their paints.
1388    let blend = n.blend.get();
1389    let blur = n.blur.get().max(0.0);
1390    let shadow_color = n.shadow_color.get();
1391    let shadow = (shadow_color.a > 0.0).then(|| Shadow {
1392        color: shadow_color,
1393        blur: n.shadow_blur.get().max(0.0),
1394        offset: n.shadow_offset.get(),
1395    });
1396    let layered = blend != BlendMode::Normal
1397        || blur > 0.0
1398        || shadow.is_some()
1399        || (opacity < 1.0 && !n.children.is_empty());
1400    let effect = n.effect.as_ref().and_then(|e| {
1401        if inside {
1402            cycle(n, "This effect is inside a layer or another effect");
1403            return None;
1404        }
1405        Some(e.shade(tf * Point::ZERO, time))
1406    });
1407    if let Some(shade) = &effect {
1408        out.push(DrawItem {
1409            node: id,
1410            transform: tf,
1411            prim: Primitive::BeginEffect(shade.clone()),
1412        });
1413    }
1414    if let Some(shade) = crate::shader::read_frame(n, size, tf, time, opacity) {
1415        if inside || effect.is_some() {
1416            cycle(
1417                n,
1418                "lens_drops reads the frame, but it is inside a layer or an effect",
1419            );
1420        } else {
1421            out.push(DrawItem {
1422                node: id,
1423                transform: tf,
1424                prim: Primitive::ReadFrame(shade),
1425            });
1426        }
1427    }
1428    if layered {
1429        out.push(DrawItem {
1430            node: id,
1431            transform: tf,
1432            prim: Primitive::PushLayer(Layer {
1433                opacity,
1434                blend,
1435                clip: None,
1436                blur,
1437                shadow,
1438            }),
1439        });
1440    }
1441    let paint_opacity = if layered { 1.0 } else { opacity };
1442    for prim in crate::draw::primitives(n, size, paint_opacity, time) {
1443        out.push(DrawItem {
1444            node: id,
1445            transform: tf,
1446            prim,
1447        });
1448    }
1449    if let NodeKind::Shader { .. } = &n.kind
1450        && crate::shader::problems(n).is_empty()
1451        && let Some(prim) = crate::shader::raster(n, size, tf, time, paint_opacity)
1452    {
1453        out.push(DrawItem {
1454            node: id,
1455            transform: tf,
1456            prim,
1457        });
1458    }
1459
1460    if !n.children.is_empty() {
1461        // Nodes without an outline (groups, text, images) clip to their box.
1462        let clip = n.clip.get().then(|| {
1463            n.outline(size).unwrap_or_else(|| {
1464                kurbo::Rect::new(-size.0 / 2.0, -size.1 / 2.0, size.0 / 2.0, size.1 / 2.0)
1465                    .to_path(0.1)
1466            })
1467        });
1468        let clipped = clip.is_some();
1469        if let Some(path) = clip {
1470            out.push(DrawItem {
1471                node: id,
1472                transform: tf,
1473                prim: Primitive::PushLayer(Layer::clip(path)),
1474            });
1475        }
1476        // Boxes of the children: from the enclosing layout tree, or a new
1477        // tree when this is a top-level layout.
1478        let own;
1479        let map: Option<&FlexMap> = match (&n.kind, flex) {
1480            (NodeKind::Layout { .. }, Some(m)) => Some(m),
1481            (NodeKind::Layout { .. }, None) => {
1482                own = crate::flex::compute(nodes, id, &|c| measure(nodes, c)).1;
1483                Some(&own)
1484            }
1485            _ => None,
1486        };
1487        let mut order: Vec<(usize, NodeId)> = n.children.iter().copied().enumerate().collect();
1488        // Stable: equal z keeps insertion order.
1489        order.sort_by(|a, b| {
1490            let za = nodes[a.1.0 as usize].z_index.get();
1491            let zb = nodes[b.1.0 as usize].z_index.get();
1492            za.total_cmp(&zb)
1493        });
1494        for (_, c) in order {
1495            let placement = map.and_then(|m| m.get(&c).copied());
1496            let inside = inside || layered || clipped || effect.is_some();
1497            visit(nodes, c, tf, placement, map, time, inside, out);
1498        }
1499        if clipped {
1500            out.push(DrawItem {
1501                node: id,
1502                transform: tf,
1503                prim: Primitive::PopLayer,
1504            });
1505        }
1506    }
1507    if layered {
1508        out.push(DrawItem {
1509            node: id,
1510            transform: tf,
1511            prim: Primitive::PopLayer,
1512        });
1513    }
1514    if effect.is_some() {
1515        out.push(DrawItem {
1516            node: id,
1517            transform: tf,
1518            prim: Primitive::EndEffect,
1519        });
1520    }
1521}
1522
1523/// Reports `input_cycle` for `n`.
1524fn cycle(n: &NodeData, what: &str) {
1525    crate::shader::push_diag(crate::shader::ShaderDiag {
1526        node: n
1527            .name
1528            .clone()
1529            .unwrap_or_else(|| format!("{}#{}", n.kind.name(), n.id.0)),
1530        source: n.source.clone(),
1531        rule: "input_cycle",
1532        hint: format!(
1533            "{what} (a group with opacity below 1, a blur, a clip, a blend or a shadow). \
1534             Its pixels are not finished there, so the shader is skipped. \
1535             Put the effect on that group, or move the node out of it."
1536        ),
1537    });
1538}
1539
1540impl DrawItem {
1541    fn hit(&self, p: Point) -> bool {
1542        let local = self.transform.inverse() * p;
1543        match &self.prim {
1544            Primitive::Fill { path, .. } => {
1545                (is_text_like(path) && path.bounding_box().contains(local))
1546                    || closed(path).contains(local)
1547            }
1548            Primitive::Stroke { path, style, .. } => {
1549                let tol = (style.width / 2.0)
1550                    .max(4.0 / self.transform.determinant().abs().sqrt().max(1e-9));
1551                path.segments().any(|s| {
1552                    kurbo::ParamCurveNearest::nearest(&s, local, 0.1).distance_sq <= tol * tol
1553                })
1554            }
1555            Primitive::Image { rect, .. } => rect.contains(local),
1556            Primitive::PushLayer(_)
1557            | Primitive::PopLayer
1558            | Primitive::ReadFrame(_)
1559            | Primitive::BeginEffect(_)
1560            | Primitive::EndEffect => false,
1561        }
1562    }
1563}
1564
1565/// Fills implicitly close open subpaths; kurbo's winding does not.
1566fn closed(path: &BezPath) -> BezPath {
1567    let mut out = BezPath::new();
1568    let mut open = false;
1569    for el in path.elements() {
1570        match el {
1571            kurbo::PathEl::MoveTo(_) => {
1572                if open {
1573                    out.close_path();
1574                }
1575                open = true;
1576            }
1577            kurbo::PathEl::ClosePath => open = false,
1578            _ => {}
1579        }
1580        out.push(*el);
1581    }
1582    if open {
1583        out.close_path();
1584    }
1585    out
1586}
1587
1588/// Glyph runs are hard to hit exactly; treat many-subpath fills (text) by
1589/// their bounding box.
1590fn is_text_like(path: &BezPath) -> bool {
1591    path.elements()
1592        .iter()
1593        .filter(|e| matches!(e, kurbo::PathEl::MoveTo(_)))
1594        .count()
1595        > 3
1596}
1597
1598thread_local! {
1599    static CURRENT: RefCell<Option<Rc<SceneGraph>>> = const { RefCell::new(None) };
1600}
1601
1602impl SceneGraph {
1603    /// Makes this graph the target of node handles for the duration of `f`.
1604    pub fn enter<R>(self: &Rc<Self>, f: impl FnOnce() -> R) -> R {
1605        let prev = CURRENT.with(|c| c.borrow_mut().replace(self.clone()));
1606        struct Restore(Option<Rc<SceneGraph>>);
1607        impl Drop for Restore {
1608            fn drop(&mut self) {
1609                let prev = self.0.take();
1610                CURRENT.with(|c| *c.borrow_mut() = prev);
1611            }
1612        }
1613        let _restore = Restore(prev);
1614        f()
1615    }
1616}
1617
1618pub(crate) fn with_graph<R>(f: impl FnOnce(&SceneGraph) -> R) -> R {
1619    CURRENT.with(|c| {
1620        let c = c.borrow();
1621        f(c.as_ref()
1622            .expect("oxframe: node handles can only be used while a scene is running"))
1623    })
1624}