Skip to main content

oxframe_2d/
graph.rs

1use std::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, 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    Rect {
69        width: Signal<f64>,
70        height: Signal<f64>,
71        radius: Signal<f64>,
72    },
73    Circle {
74        size: Signal<f64>,
75        /// Degrees clockwise from 12 o'clock; a full turn draws a circle.
76        start_angle: Signal<f64>,
77        end_angle: Signal<f64>,
78        /// Close an arc through the center (pie slice).
79        pie: bool,
80    },
81    Polygon {
82        sides: Signal<f64>,
83        size: Signal<f64>,
84    },
85    Star {
86        points: Signal<f64>,
87        size: Signal<f64>,
88        /// Inner radius as a fraction of the outer one.
89        inner: Signal<f64>,
90    },
91    Path {
92        data: Signal<BezPath>,
93    },
94    Line {
95        points: Signal<Vec<Vec2>>,
96        closed: bool,
97        /// Smooth curve through the points (Catmull-Rom).
98        smooth: bool,
99        start_arrow: bool,
100        end_arrow: bool,
101        arrow_size: Signal<f64>,
102    },
103    Txt {
104        text: Signal<String>,
105        /// When non-empty, replaces `text` with styled runs.
106        spans: Signal<Vec<RichSpan>>,
107        font_size: Signal<f64>,
108        weight: Signal<f64>,
109        style: TextStyle,
110        /// Active glyph morph: (from, to) and progress 0..1.
111        morph: Signal<Option<(String, String)>>,
112        morph_t: Signal<f64>,
113    },
114    Code {
115        code: Signal<String>,
116        lang: Lang,
117        theme: CodeTheme,
118        font_size: Signal<f64>,
119        style: TextStyle,
120        /// Focused lines (1-based, inclusive); (0, 0) focuses all.
121        focus: Signal<Vec2>,
122        /// Alpha of unfocused lines.
123        dim: Signal<f64>,
124    },
125    Img {
126        source: ImageSource,
127        width: Signal<f64>,
128        height: Signal<f64>,
129    },
130    Layout {
131        direction: Direction,
132        align: LayoutAlign,
133        justify: Justify,
134        /// Wrap children onto several lines.
135        wrap: bool,
136        /// CSS grid with this many equal columns instead of flexbox.
137        columns: Option<u16>,
138        gap: Signal<f64>,
139        padding: Signal<f64>,
140        width: Signal<Option<f64>>,
141        height: Signal<Option<f64>>,
142        radius: Signal<f64>,
143    },
144    Grid {
145        width: Signal<f64>,
146        height: Signal<f64>,
147        spacing: Signal<f64>,
148    },
149}
150
151impl NodeKind {
152    pub fn name(&self) -> &'static str {
153        match self {
154            Self::Group => "Group",
155            Self::Rect { .. } => "Rect",
156            Self::Circle { .. } => "Circle",
157            Self::Polygon { .. } => "Polygon",
158            Self::Star { .. } => "Star",
159            Self::Path { .. } => "Path",
160            Self::Line { .. } => "Line",
161            Self::Txt { .. } => "Txt",
162            Self::Code { .. } => "Code",
163            Self::Img { .. } => "Img",
164            Self::Layout { .. } => "Layout",
165            Self::Grid { .. } => "Grid",
166        }
167    }
168}
169
170/// Stored state of one node (internal representation).
171#[doc(hidden)]
172pub struct NodeData {
173    pub id: NodeId,
174    pub name: Option<String>,
175    pub parent: Option<NodeId>,
176    pub children: Vec<NodeId>,
177    pub kind: NodeKind,
178    pub x: Signal<f64>,
179    pub y: Signal<f64>,
180    /// Degrees, clockwise (y points down).
181    pub rotation: Signal<f64>,
182    pub scale: Signal<Vec2>,
183    pub opacity: Signal<f64>,
184    pub fill: Signal<Color>,
185    pub stroke: Signal<Color>,
186    pub line_width: Signal<f64>,
187    /// Drawn part of the outline, as fractions of its length.
188    pub start: Signal<f64>,
189    pub end: Signal<f64>,
190    /// Overrides `fill` when set (local coordinates).
191    pub fill_gradient: Signal<Option<Gradient>>,
192    pub stroke_gradient: Signal<Option<Gradient>>,
193    /// Dash pattern (on, off, on, …) in local units; empty = solid.
194    pub dash: Signal<Vec<f64>>,
195    pub dash_offset: Signal<f64>,
196    pub line_cap: Signal<LineCap>,
197    pub line_join: Signal<LineJoin>,
198    /// Gaussian blur of the node and its children (local units).
199    pub blur: Signal<f64>,
200    pub shadow_color: Signal<Color>,
201    pub shadow_blur: Signal<f64>,
202    pub shadow_offset: Signal<Vec2>,
203    /// Children are clipped to this node's outline.
204    pub clip: Signal<bool>,
205    pub blend: Signal<BlendMode>,
206    /// Paint order among siblings (higher on top; ties keep insertion order).
207    pub z_index: Signal<f64>,
208    /// Which point of the node's box sits at (x, y) and is the pivot of
209    /// rotation and scale: (-1, -1) top-left, (0, 0) center, (1, 1)
210    /// bottom-right.
211    pub anchor: Signal<Vec2>,
212    // Flex item properties (used when the parent is a `Layout`).
213    pub grow: Signal<f64>,
214    pub shrink: Signal<f64>,
215    pub basis: Signal<Option<f64>>,
216    pub margin: Signal<f64>,
217    pub align_self: Signal<Option<LayoutAlign>>,
218}
219
220impl NodeData {
221    /// Transform relative to the parent, plus a layout-assigned offset.
222    /// `size` is the node's box, needed for the anchor.
223    pub fn local_transform(&self, offset: Vec2, size: (f64, f64)) -> Affine {
224        let s = self.scale.get();
225        let a = self.anchor.get();
226        Affine::translate(offset + Vec2::new(self.x.get(), self.y.get()))
227            * Affine::rotate(self.rotation.get().to_radians())
228            * Affine::scale_non_uniform(s.x, s.y)
229            * Affine::translate((-a.x * size.0 / 2.0, -a.y * size.1 / 2.0))
230    }
231
232    /// Text to draw for `Txt` and `Code` nodes.
233    pub(crate) fn text_draw(&self) -> Option<TextDraw> {
234        match &self.kind {
235            NodeKind::Txt {
236                text,
237                spans,
238                font_size,
239                weight,
240                style,
241                morph,
242                morph_t,
243            } => {
244                let style = TextStyle {
245                    size: font_size.get().max(0.1),
246                    weight: weight.get().clamp(1.0, 1000.0),
247                    ..style.clone()
248                };
249                if let Some((from, to)) = morph.get() {
250                    let a = oxframe_text::layout(&from, &style);
251                    let b = oxframe_text::layout(&to, &style);
252                    let (glyphs, size) = crate::text::morph(&a, &b, morph_t.get().clamp(0.0, 1.0));
253                    return Some(TextDraw::Morph { glyphs, size });
254                }
255                let spans = spans.get();
256                Some(TextDraw::Styled(if spans.is_empty() {
257                    crate::text::rich(&[RichSpan::new(text.get())], &style)
258                } else {
259                    crate::text::rich(&spans, &style)
260                }))
261            }
262            NodeKind::Code {
263                code,
264                lang,
265                theme,
266                font_size,
267                style,
268                focus,
269                dim,
270            } => {
271                let style = TextStyle {
272                    size: font_size.get().max(0.1),
273                    ..style.clone()
274                };
275                Some(TextDraw::Styled(crate::text::code(
276                    &code.get(),
277                    *lang,
278                    theme,
279                    &style,
280                    focus.get(),
281                    dim.get(),
282                )))
283            }
284            _ => None,
285        }
286    }
287
288    /// Untrimmed outline in local coordinates, centered on the origin.
289    pub fn outline(&self, size: (f64, f64)) -> Option<BezPath> {
290        const TOLERANCE: f64 = 0.1;
291        match &self.kind {
292            NodeKind::Group
293            | NodeKind::Img { .. }
294            | NodeKind::Txt { .. }
295            | NodeKind::Code { .. }
296            | NodeKind::Grid { .. } => None,
297            NodeKind::Rect { radius, .. } | NodeKind::Layout { radius, .. } => {
298                let (w, h) = (size.0.max(0.0), size.1.max(0.0));
299                let r = radius.get().clamp(0.0, w.min(h) / 2.0);
300                let rect = kurbo::Rect::new(-w / 2.0, -h / 2.0, w / 2.0, h / 2.0);
301                Some(if r > 0.0 {
302                    rect.to_rounded_rect(r).to_path(TOLERANCE)
303                } else {
304                    rect.to_path(TOLERANCE)
305                })
306            }
307            NodeKind::Circle {
308                start_angle,
309                end_angle,
310                pie,
311                ..
312            } => {
313                // Start at the top, clockwise — like a clock hand, which is
314                // what `end` animations expect.
315                let r = size.0.max(0.0) / 2.0;
316                let (a0, a1) = (start_angle.get(), end_angle.get());
317                let sweep = (a1 - a0).clamp(-360.0, 360.0);
318                if sweep.abs() >= 360.0 - 1e-9 {
319                    let circle = kurbo::Circle::new((0.0, 0.0), r).to_path(TOLERANCE);
320                    return Some(Affine::rotate((a0 - 90.0).to_radians()) * circle);
321                }
322                let arc = kurbo::Arc::new(
323                    (0.0, 0.0),
324                    (r, r),
325                    (a0 - 90.0).to_radians(),
326                    sweep.to_radians(),
327                    0.0,
328                );
329                let mut p = BezPath::new();
330                if *pie {
331                    p.move_to((0.0, 0.0));
332                    p.line_to(arc.center + kurbo::Vec2::from_angle(arc.start_angle) * r);
333                    arc.to_cubic_beziers(TOLERANCE, |a, b, c| p.curve_to(a, b, c));
334                    p.close_path();
335                } else {
336                    p.move_to(arc.center + kurbo::Vec2::from_angle(arc.start_angle) * r);
337                    arc.to_cubic_beziers(TOLERANCE, |a, b, c| p.curve_to(a, b, c));
338                }
339                Some(p)
340            }
341            NodeKind::Polygon { sides, .. } => {
342                let n = sides.get().round().max(3.0) as usize;
343                Some(regular(n, size.0.max(0.0) / 2.0, None))
344            }
345            NodeKind::Star { points, inner, .. } => {
346                let n = points.get().round().max(2.0) as usize;
347                let r = size.0.max(0.0) / 2.0;
348                Some(regular(n, r, Some(r * inner.get().clamp(0.0, 1.0))))
349            }
350            NodeKind::Path { data } => Some(data.get()),
351            NodeKind::Line {
352                points,
353                closed,
354                smooth,
355                ..
356            } => {
357                let pts = points.get();
358                if *smooth && pts.len() > 2 {
359                    return Some(catmull_rom(&pts, *closed));
360                }
361                let mut p = BezPath::new();
362                for (i, v) in pts.iter().enumerate() {
363                    if i == 0 {
364                        p.move_to(v.to_point());
365                    } else {
366                        p.line_to(v.to_point());
367                    }
368                }
369                if *closed && pts.len() > 2 {
370                    p.close_path();
371                }
372                Some(p)
373            }
374        }
375    }
376}
377
378/// Regular polygon (or star with an inner radius), first vertex at the top.
379fn regular(n: usize, r: f64, inner: Option<f64>) -> BezPath {
380    let count = if inner.is_some() { 2 * n } else { n };
381    let mut p = BezPath::new();
382    for i in 0..count {
383        let radius = match inner {
384            Some(ri) if i % 2 == 1 => ri,
385            _ => r,
386        };
387        let a = -std::f64::consts::FRAC_PI_2 + std::f64::consts::TAU * i as f64 / count as f64;
388        let pt = Point::new(a.cos() * radius, a.sin() * radius);
389        if i == 0 {
390            p.move_to(pt);
391        } else {
392            p.line_to(pt);
393        }
394    }
395    p.close_path();
396    p
397}
398
399/// Smooth curve through the points (uniform Catmull-Rom as cubic Béziers).
400fn catmull_rom(pts: &[Vec2], closed: bool) -> BezPath {
401    let n = pts.len();
402    let at = |i: isize| -> Vec2 {
403        if closed {
404            pts[i.rem_euclid(n as isize) as usize]
405        } else {
406            pts[i.clamp(0, n as isize - 1) as usize]
407        }
408    };
409    let mut p = BezPath::new();
410    p.move_to(pts[0].to_point());
411    let segs = if closed { n } else { n - 1 };
412    for i in 0..segs as isize {
413        let (p0, p1, p2, p3) = (at(i - 1), at(i), at(i + 1), at(i + 2));
414        let c1 = p1 + (p2 - p0) / 6.0;
415        let c2 = p2 - (p3 - p1) / 6.0;
416        p.curve_to(c1.to_point(), c2.to_point(), p2.to_point());
417    }
418    if closed {
419        p.close_path();
420    }
421    p
422}
423
424pub(crate) enum TextDraw {
425    Styled(Styled),
426    /// Glyphs with their alpha, already centered.
427    Morph {
428        glyphs: Vec<(BezPath, f64)>,
429        size: (f64, f64),
430    },
431}
432
433/// Arena of nodes for one scene run. Node 0 is the view (root).
434pub struct SceneGraph {
435    nodes: RefCell<Vec<NodeData>>,
436    size: (u32, u32),
437}
438
439impl SceneGraph {
440    /// The root node (the scene's view).
441    pub const ROOT: NodeId = NodeId(0);
442
443    /// An empty scene of the given size in pixels, with only the root.
444    pub fn new(width: u32, height: u32) -> Rc<Self> {
445        let graph = Rc::new(Self {
446            nodes: RefCell::new(Vec::new()),
447            size: (width, height),
448        });
449        graph.insert(
450            None,
451            NodeKind::Group,
452            crate::BaseProps {
453                name: Some("view".into()),
454                ..Default::default()
455            },
456        );
457        graph
458    }
459
460    /// Scene size in pixels.
461    pub fn size(&self) -> (u32, u32) {
462        self.size
463    }
464
465    /// Number of nodes, including the root.
466    pub fn len(&self) -> usize {
467        self.nodes.borrow().len()
468    }
469
470    /// Never true: the root always exists.
471    pub fn is_empty(&self) -> bool {
472        self.len() == 0
473    }
474
475    /// Borrows a node's stored state.
476    ///
477    /// # Panics
478    ///
479    /// If `id` is not a node of this graph.
480    pub fn node(&self, id: NodeId) -> Ref<'_, NodeData> {
481        Ref::map(self.nodes.borrow(), |n| &n[id.0 as usize])
482    }
483
484    pub(crate) fn next_id(&self) -> u32 {
485        self.len() as u32
486    }
487
488    pub(crate) fn insert(
489        &self,
490        parent: Option<NodeId>,
491        kind: NodeKind,
492        base: crate::BaseProps,
493    ) -> NodeId {
494        let mut nodes = self.nodes.borrow_mut();
495        let id = NodeId(u32::try_from(nodes.len()).expect("too many nodes"));
496        let label = format!("{}#{}", kind.name(), id.0);
497        let sig = |field: &str| format!("{label}.{field}");
498        nodes.push(NodeData {
499            id,
500            name: base.name,
501            parent,
502            children: Vec::new(),
503            x: Signal::new(base.x).with_name(sig("x")),
504            y: Signal::new(base.y).with_name(sig("y")),
505            rotation: Signal::new(base.rotation).with_name(sig("rotation")),
506            scale: Signal::new(base.scale).with_name(sig("scale")),
507            opacity: Signal::new(base.opacity).with_name(sig("opacity")),
508            fill: Signal::new(base.fill).with_name(sig("fill")),
509            stroke: Signal::new(base.stroke).with_name(sig("stroke")),
510            line_width: Signal::new(base.line_width).with_name(sig("line_width")),
511            start: Signal::new(base.start).with_name(sig("start")),
512            end: Signal::new(base.end).with_name(sig("end")),
513            fill_gradient: Signal::new(base.fill_gradient).with_name(sig("fill_gradient")),
514            stroke_gradient: Signal::new(base.stroke_gradient).with_name(sig("stroke_gradient")),
515            dash: Signal::new(base.dash).with_name(sig("dash")),
516            dash_offset: Signal::new(base.dash_offset).with_name(sig("dash_offset")),
517            line_cap: Signal::new(base.line_cap).with_name(sig("line_cap")),
518            line_join: Signal::new(base.line_join).with_name(sig("line_join")),
519            blur: Signal::new(base.blur).with_name(sig("blur")),
520            shadow_color: Signal::new(base.shadow_color).with_name(sig("shadow_color")),
521            shadow_blur: Signal::new(base.shadow_blur).with_name(sig("shadow_blur")),
522            shadow_offset: Signal::new(base.shadow_offset).with_name(sig("shadow_offset")),
523            clip: Signal::new(base.clip).with_name(sig("clip")),
524            blend: Signal::new(base.blend).with_name(sig("blend")),
525            z_index: Signal::new(base.z_index).with_name(sig("z_index")),
526            anchor: Signal::new(base.anchor).with_name(sig("anchor")),
527            grow: Signal::new(base.grow).with_name(sig("grow")),
528            shrink: Signal::new(base.shrink).with_name(sig("shrink")),
529            basis: Signal::new(base.basis).with_name(sig("basis")),
530            margin: Signal::new(base.margin).with_name(sig("margin")),
531            align_self: Signal::new(base.align_self).with_name(sig("align_self")),
532            kind,
533        });
534        if let Some(p) = parent {
535            nodes[p.0 as usize].children.push(id);
536        }
537        id
538    }
539
540    pub(crate) fn detach(&self, id: NodeId) {
541        let mut nodes = self.nodes.borrow_mut();
542        if let Some(p) = nodes[id.0 as usize].parent.take() {
543            nodes[p.0 as usize].children.retain(|c| *c != id);
544        }
545    }
546
547    pub(crate) fn reparent(&self, id: NodeId, parent: NodeId) {
548        self.detach(id);
549        let mut nodes = self.nodes.borrow_mut();
550        nodes[id.0 as usize].parent = Some(parent);
551        nodes[parent.0 as usize].children.push(id);
552    }
553
554    /// Size of a node's box in its own (unscaled) coordinates.
555    pub fn measure(&self, id: NodeId) -> (f64, f64) {
556        let nodes = self.nodes.borrow();
557        measure(&nodes, id)
558    }
559
560    /// Flattened, paint-ordered list of everything to draw.
561    pub fn draw_list(&self) -> Vec<DrawItem> {
562        let nodes = self.nodes.borrow();
563        let mut out = Vec::new();
564        visit(&nodes, Self::ROOT, Affine::IDENTITY, None, None, &mut out);
565        out
566    }
567
568    /// Topmost node under `p` (scene coordinates), excluding the view.
569    pub fn pick(&self, p: Point) -> Option<NodeId> {
570        self.draw_list()
571            .iter()
572            .rev()
573            .find(|item| item.node != Self::ROOT && item.hit(p))
574            .map(|item| item.node)
575    }
576
577    /// World transform of a node (including layout offsets).
578    pub fn world_transform(&self, id: NodeId) -> Option<Affine> {
579        // The draw list already composes transforms; reuse it for nodes
580        // that draw, and fall back to walking parents for those that don't.
581        if let Some(item) = self.draw_list().into_iter().find(|i| i.node == id) {
582            return Some(item.transform);
583        }
584        let nodes = self.nodes.borrow();
585        let mut tf = Affine::IDENTITY;
586        let mut cur = Some(id);
587        while let Some(c) = cur {
588            let n = nodes.get(c.0 as usize)?;
589            tf = n.local_transform(Vec2::ZERO, measure(&nodes, c)) * tf;
590            cur = n.parent;
591        }
592        Some(tf)
593    }
594
595    /// `(id, parent, kind, name)` of every attached node, depth-first.
596    pub fn tree(&self) -> Vec<(NodeId, Option<NodeId>, &'static str, Option<String>)> {
597        let nodes = self.nodes.borrow();
598        let mut out = Vec::new();
599        let mut stack = vec![Self::ROOT];
600        while let Some(id) = stack.pop() {
601            let n = &nodes[id.0 as usize];
602            out.push((id, n.parent, n.kind.name(), n.name.clone()));
603            stack.extend(n.children.iter().rev());
604        }
605        out
606    }
607
608    /// Current signal values of a node, for the inspector.
609    pub fn describe(&self, id: NodeId) -> Vec<(String, String)> {
610        let nodes = self.nodes.borrow();
611        let Some(n) = nodes.get(id.0 as usize) else {
612            return Vec::new();
613        };
614        let f = |v: f64| format!("{v:.3}");
615        let s = n.scale.get();
616        let mut out = vec![
617            ("x".into(), f(n.x.get())),
618            ("y".into(), f(n.y.get())),
619            ("rotation".into(), f(n.rotation.get())),
620            ("scale".into(), format!("{:.3}, {:.3}", s.x, s.y)),
621            ("opacity".into(), f(n.opacity.get())),
622            ("fill".into(), n.fill.get().to_string()),
623            ("stroke".into(), n.stroke.get().to_string()),
624            ("line_width".into(), f(n.line_width.get())),
625            ("start".into(), f(n.start.get())),
626            ("end".into(), f(n.end.get())),
627        ];
628        let (w, h) = measure(&nodes, id);
629        out.push(("size".into(), format!("{w:.1} × {h:.1}")));
630        match &n.kind {
631            NodeKind::Rect { radius, .. } => out.push(("radius".into(), f(radius.get()))),
632            NodeKind::Code { code, .. } => out.push(("code".into(), code.get())),
633            NodeKind::Txt {
634                text,
635                font_size,
636                weight,
637                ..
638            } => {
639                out.push(("text".into(), text.get()));
640                out.push(("font_size".into(), f(font_size.get())));
641                out.push(("weight".into(), f(weight.get())));
642            }
643            NodeKind::Line { points, .. } => {
644                out.push(("points".into(), points.get().len().to_string()))
645            }
646            NodeKind::Layout { gap, padding, .. } => {
647                out.push(("gap".into(), f(gap.get())));
648                out.push(("padding".into(), f(padding.get())));
649            }
650            NodeKind::Grid { spacing, .. } => out.push(("spacing".into(), f(spacing.get()))),
651            _ => {}
652        }
653        out
654    }
655}
656
657fn measure(nodes: &[NodeData], id: NodeId) -> (f64, f64) {
658    let n = &nodes[id.0 as usize];
659    match &n.kind {
660        NodeKind::Rect { width, height, .. } => (width.get(), height.get()),
661        NodeKind::Circle { size, .. }
662        | NodeKind::Polygon { size, .. }
663        | NodeKind::Star { size, .. } => (size.get(), size.get()),
664        NodeKind::Img { width, height, .. } | NodeKind::Grid { width, height, .. } => {
665            (width.get(), height.get())
666        }
667        NodeKind::Txt { .. } | NodeKind::Code { .. } => match n.text_draw() {
668            Some(TextDraw::Styled(s)) => (s.layout.width, s.layout.height),
669            Some(TextDraw::Morph { size, .. }) => size,
670            None => (0.0, 0.0),
671        },
672        NodeKind::Path { .. } | NodeKind::Line { .. } => {
673            let bb = n
674                .outline((0.0, 0.0))
675                .map_or(kurbo::Rect::ZERO, |p| p.bounding_box());
676            (bb.width(), bb.height())
677        }
678        NodeKind::Layout { .. } => crate::flex::compute(nodes, id, &|c| measure(nodes, c)).0,
679        NodeKind::Group => {
680            let mut bb: Option<kurbo::Rect> = None;
681            for &c in &n.children {
682                let child = &nodes[c.0 as usize];
683                let (w, h) = measure(nodes, c);
684                let r = child
685                    .local_transform(Vec2::ZERO, (w, h))
686                    .transform_rect_bbox(kurbo::Rect::new(-w / 2.0, -h / 2.0, w / 2.0, h / 2.0));
687                bb = Some(bb.map_or(r, |b| b.union(r)));
688            }
689            bb.map_or((0.0, 0.0), |b| (b.width(), b.height()))
690        }
691    }
692}
693
694/// Kinds whose drawn size follows the box a layout gives them.
695fn resizable(kind: &NodeKind) -> bool {
696    matches!(
697        kind,
698        NodeKind::Rect { .. }
699            | NodeKind::Layout { .. }
700            | NodeKind::Img { .. }
701            | NodeKind::Grid { .. }
702    )
703}
704
705fn visit(
706    nodes: &[NodeData],
707    id: NodeId,
708    parent_tf: Affine,
709    placement: Option<FlexBox>,
710    flex: Option<&FlexMap>,
711    out: &mut Vec<DrawItem>,
712) {
713    let n = &nodes[id.0 as usize];
714    let opacity = n.opacity.get().clamp(0.0, 1.0);
715    if opacity <= 0.0 {
716        return;
717    }
718    let size = match placement {
719        Some(b) if resizable(&n.kind) => {
720            let s = n.scale.get();
721            (
722                b.size.0 / s.x.abs().max(1e-9),
723                b.size.1 / s.y.abs().max(1e-9),
724            )
725        }
726        _ => measure(nodes, id),
727    };
728    let offset = placement.map_or(Vec2::ZERO, |b| b.center);
729    let tf = parent_tf * n.local_transform(offset, size);
730
731    // A layer is needed when the node's effects apply to it and its children
732    // as a whole; plain leaves just fold opacity into their paints.
733    let blend = n.blend.get();
734    let blur = n.blur.get().max(0.0);
735    let shadow_color = n.shadow_color.get();
736    let shadow = (shadow_color.a > 0.0).then(|| Shadow {
737        color: shadow_color,
738        blur: n.shadow_blur.get().max(0.0),
739        offset: n.shadow_offset.get(),
740    });
741    let layered = blend != BlendMode::Normal
742        || blur > 0.0
743        || shadow.is_some()
744        || (opacity < 1.0 && !n.children.is_empty());
745    if layered {
746        out.push(DrawItem {
747            node: id,
748            transform: tf,
749            prim: Primitive::PushLayer(Layer {
750                opacity,
751                blend,
752                clip: None,
753                blur,
754                shadow,
755            }),
756        });
757    }
758    let paint_opacity = if layered { 1.0 } else { opacity };
759    for prim in crate::draw::primitives(n, size, paint_opacity) {
760        out.push(DrawItem {
761            node: id,
762            transform: tf,
763            prim,
764        });
765    }
766
767    if !n.children.is_empty() {
768        // Nodes without an outline (groups, text, images) clip to their box.
769        let clip = n.clip.get().then(|| {
770            n.outline(size).unwrap_or_else(|| {
771                kurbo::Rect::new(-size.0 / 2.0, -size.1 / 2.0, size.0 / 2.0, size.1 / 2.0)
772                    .to_path(0.1)
773            })
774        });
775        let clipped = clip.is_some();
776        if let Some(path) = clip {
777            out.push(DrawItem {
778                node: id,
779                transform: tf,
780                prim: Primitive::PushLayer(Layer::clip(path)),
781            });
782        }
783        // Boxes of the children: from the enclosing layout tree, or a new
784        // tree when this is a top-level layout.
785        let own;
786        let map: Option<&FlexMap> = match (&n.kind, flex) {
787            (NodeKind::Layout { .. }, Some(m)) => Some(m),
788            (NodeKind::Layout { .. }, None) => {
789                own = crate::flex::compute(nodes, id, &|c| measure(nodes, c)).1;
790                Some(&own)
791            }
792            _ => None,
793        };
794        let mut order: Vec<(usize, NodeId)> = n.children.iter().copied().enumerate().collect();
795        // Stable: equal z keeps insertion order.
796        order.sort_by(|a, b| {
797            let za = nodes[a.1.0 as usize].z_index.get();
798            let zb = nodes[b.1.0 as usize].z_index.get();
799            za.total_cmp(&zb)
800        });
801        for (_, c) in order {
802            let placement = map.and_then(|m| m.get(&c).copied());
803            visit(nodes, c, tf, placement, map, out);
804        }
805        if clipped {
806            out.push(DrawItem {
807                node: id,
808                transform: tf,
809                prim: Primitive::PopLayer,
810            });
811        }
812    }
813    if layered {
814        out.push(DrawItem {
815            node: id,
816            transform: tf,
817            prim: Primitive::PopLayer,
818        });
819    }
820}
821
822impl DrawItem {
823    fn hit(&self, p: Point) -> bool {
824        let local = self.transform.inverse() * p;
825        match &self.prim {
826            Primitive::Fill { path, .. } => {
827                (is_text_like(path) && path.bounding_box().contains(local))
828                    || closed(path).contains(local)
829            }
830            Primitive::Stroke { path, style, .. } => {
831                let tol = (style.width / 2.0)
832                    .max(4.0 / self.transform.determinant().abs().sqrt().max(1e-9));
833                path.segments().any(|s| {
834                    kurbo::ParamCurveNearest::nearest(&s, local, 0.1).distance_sq <= tol * tol
835                })
836            }
837            Primitive::Image { rect, .. } => rect.contains(local),
838            Primitive::PushLayer(_) | Primitive::PopLayer => false,
839        }
840    }
841}
842
843/// Fills implicitly close open subpaths; kurbo's winding does not.
844fn closed(path: &BezPath) -> BezPath {
845    let mut out = BezPath::new();
846    let mut open = false;
847    for el in path.elements() {
848        match el {
849            kurbo::PathEl::MoveTo(_) => {
850                if open {
851                    out.close_path();
852                }
853                open = true;
854            }
855            kurbo::PathEl::ClosePath => open = false,
856            _ => {}
857        }
858        out.push(*el);
859    }
860    if open {
861        out.close_path();
862    }
863    out
864}
865
866/// Glyph runs are hard to hit exactly; treat many-subpath fills (text) by
867/// their bounding box.
868fn is_text_like(path: &BezPath) -> bool {
869    path.elements()
870        .iter()
871        .filter(|e| matches!(e, kurbo::PathEl::MoveTo(_)))
872        .count()
873        > 3
874}
875
876thread_local! {
877    static CURRENT: RefCell<Option<Rc<SceneGraph>>> = const { RefCell::new(None) };
878}
879
880impl SceneGraph {
881    /// Makes this graph the target of node handles for the duration of `f`.
882    pub fn enter<R>(self: &Rc<Self>, f: impl FnOnce() -> R) -> R {
883        let prev = CURRENT.with(|c| c.borrow_mut().replace(self.clone()));
884        struct Restore(Option<Rc<SceneGraph>>);
885        impl Drop for Restore {
886            fn drop(&mut self) {
887                let prev = self.0.take();
888                CURRENT.with(|c| *c.borrow_mut() = prev);
889            }
890        }
891        let _restore = Restore(prev);
892        f()
893    }
894}
895
896pub(crate) fn with_graph<R>(f: impl FnOnce(&SceneGraph) -> R) -> R {
897    CURRENT.with(|c| {
898        let c = c.borrow();
899        f(c.as_ref()
900            .expect("oxframe: node handles can only be used while a scene is running"))
901    })
902}