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, 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    /// `file:line` of the `add` call that created the node, when known.
176    pub source: Option<String>,
177    pub parent: Option<NodeId>,
178    pub children: Vec<NodeId>,
179    pub kind: NodeKind,
180    pub x: Signal<f64>,
181    pub y: Signal<f64>,
182    /// Degrees, clockwise (y points down).
183    pub rotation: Signal<f64>,
184    pub scale: Signal<Vec2>,
185    pub opacity: Signal<f64>,
186    pub fill: Signal<Color>,
187    pub stroke: Signal<Color>,
188    pub line_width: Signal<f64>,
189    /// Drawn part of the outline, as fractions of its length.
190    pub start: Signal<f64>,
191    pub end: Signal<f64>,
192    /// Overrides `fill` when set (local coordinates).
193    pub fill_gradient: Signal<Option<Gradient>>,
194    pub stroke_gradient: Signal<Option<Gradient>>,
195    /// Dash pattern (on, off, on, …) in local units; empty = solid.
196    pub dash: Signal<Vec<f64>>,
197    pub dash_offset: Signal<f64>,
198    pub line_cap: Signal<LineCap>,
199    pub line_join: Signal<LineJoin>,
200    /// Gaussian blur of the node and its children (local units).
201    pub blur: Signal<f64>,
202    pub shadow_color: Signal<Color>,
203    pub shadow_blur: Signal<f64>,
204    pub shadow_offset: Signal<Vec2>,
205    /// Children are clipped to this node's outline.
206    pub clip: Signal<bool>,
207    pub blend: Signal<BlendMode>,
208    /// Paint order among siblings (higher on top; ties keep insertion order).
209    pub z_index: Signal<f64>,
210    /// Which point of the node's box sits at (x, y) and is the pivot of
211    /// rotation and scale: (-1, -1) top-left, (0, 0) center, (1, 1)
212    /// bottom-right.
213    pub anchor: Signal<Vec2>,
214    // Flex item properties (used when the parent is a `Layout`).
215    pub grow: Signal<f64>,
216    pub shrink: Signal<f64>,
217    pub basis: Signal<Option<f64>>,
218    pub margin: Signal<f64>,
219    pub align_self: Signal<Option<LayoutAlign>>,
220    /// Recomputed every frame. `None` means `x` and `y` are used as they are.
221    pub place: Option<crate::place::Place>,
222    /// Position after [`place`](Self::place) is resolved. Draw uses this.
223    pub placed: Cell<(f64, f64)>,
224}
225
226impl NodeData {
227    /// Transform relative to the parent, plus a layout-assigned offset.
228    /// `size` is the node's box, needed for the anchor.
229    pub fn local_transform(&self, offset: Vec2, size: (f64, f64)) -> Affine {
230        let s = self.scale.get();
231        let a = self.anchor.get();
232        let (x, y) = self.placed.get();
233        Affine::translate(offset + Vec2::new(x, y))
234            * Affine::rotate(self.rotation.get().to_radians())
235            * Affine::scale_non_uniform(s.x, s.y)
236            * Affine::translate((-a.x * size.0 / 2.0, -a.y * size.1 / 2.0))
237    }
238
239    /// Text to draw for `Txt` and `Code` nodes.
240    pub(crate) fn text_draw(&self) -> Option<TextDraw> {
241        match &self.kind {
242            NodeKind::Txt {
243                text,
244                spans,
245                font_size,
246                weight,
247                style,
248                morph,
249                morph_t,
250            } => {
251                let style = TextStyle {
252                    size: font_size.get().max(0.1),
253                    weight: weight.get().clamp(1.0, 1000.0),
254                    ..style.clone()
255                };
256                if let Some((from, to)) = morph.get() {
257                    let a = oxframe_text::layout(&from, &style);
258                    let b = oxframe_text::layout(&to, &style);
259                    let (glyphs, size) = crate::text::morph(&a, &b, morph_t.get().clamp(0.0, 1.0));
260                    return Some(TextDraw::Morph { glyphs, size });
261                }
262                let spans = spans.get();
263                Some(TextDraw::Styled(if spans.is_empty() {
264                    crate::text::rich(&[RichSpan::new(text.get())], &style)
265                } else {
266                    crate::text::rich(&spans, &style)
267                }))
268            }
269            NodeKind::Code {
270                code,
271                lang,
272                theme,
273                font_size,
274                style,
275                focus,
276                dim,
277            } => {
278                let style = TextStyle {
279                    size: font_size.get().max(0.1),
280                    ..style.clone()
281                };
282                Some(TextDraw::Styled(crate::text::code(
283                    &code.get(),
284                    *lang,
285                    theme,
286                    &style,
287                    focus.get(),
288                    dim.get(),
289                )))
290            }
291            _ => None,
292        }
293    }
294
295    /// Untrimmed outline in local coordinates, centered on the origin.
296    pub fn outline(&self, size: (f64, f64)) -> Option<BezPath> {
297        const TOLERANCE: f64 = 0.1;
298        match &self.kind {
299            NodeKind::Group
300            | NodeKind::Img { .. }
301            | NodeKind::Txt { .. }
302            | NodeKind::Code { .. }
303            | NodeKind::Grid { .. } => None,
304            NodeKind::Rect { radius, .. } | NodeKind::Layout { radius, .. } => {
305                let (w, h) = (size.0.max(0.0), size.1.max(0.0));
306                let r = radius.get().clamp(0.0, w.min(h) / 2.0);
307                let rect = kurbo::Rect::new(-w / 2.0, -h / 2.0, w / 2.0, h / 2.0);
308                Some(if r > 0.0 {
309                    rect.to_rounded_rect(r).to_path(TOLERANCE)
310                } else {
311                    rect.to_path(TOLERANCE)
312                })
313            }
314            NodeKind::Circle {
315                start_angle,
316                end_angle,
317                pie,
318                ..
319            } => {
320                // Start at the top, clockwise — like a clock hand, which is
321                // what `end` animations expect.
322                let r = size.0.max(0.0) / 2.0;
323                let (a0, a1) = (start_angle.get(), end_angle.get());
324                let sweep = (a1 - a0).clamp(-360.0, 360.0);
325                if sweep.abs() >= 360.0 - 1e-9 {
326                    let circle = kurbo::Circle::new((0.0, 0.0), r).to_path(TOLERANCE);
327                    return Some(Affine::rotate((a0 - 90.0).to_radians()) * circle);
328                }
329                let arc = kurbo::Arc::new(
330                    (0.0, 0.0),
331                    (r, r),
332                    (a0 - 90.0).to_radians(),
333                    sweep.to_radians(),
334                    0.0,
335                );
336                let mut p = BezPath::new();
337                if *pie {
338                    p.move_to((0.0, 0.0));
339                    p.line_to(arc.center + kurbo::Vec2::from_angle(arc.start_angle) * r);
340                    arc.to_cubic_beziers(TOLERANCE, |a, b, c| p.curve_to(a, b, c));
341                    p.close_path();
342                } else {
343                    p.move_to(arc.center + kurbo::Vec2::from_angle(arc.start_angle) * r);
344                    arc.to_cubic_beziers(TOLERANCE, |a, b, c| p.curve_to(a, b, c));
345                }
346                Some(p)
347            }
348            NodeKind::Polygon { sides, .. } => {
349                let n = sides.get().round().max(3.0) as usize;
350                Some(regular(n, size.0.max(0.0) / 2.0, None))
351            }
352            NodeKind::Star { points, inner, .. } => {
353                let n = points.get().round().max(2.0) as usize;
354                let r = size.0.max(0.0) / 2.0;
355                Some(regular(n, r, Some(r * inner.get().clamp(0.0, 1.0))))
356            }
357            NodeKind::Path { data } => Some(data.get()),
358            NodeKind::Line {
359                points,
360                closed,
361                smooth,
362                ..
363            } => {
364                let pts = points.get();
365                if *smooth && pts.len() > 2 {
366                    return Some(catmull_rom(&pts, *closed));
367                }
368                let mut p = BezPath::new();
369                for (i, v) in pts.iter().enumerate() {
370                    if i == 0 {
371                        p.move_to(v.to_point());
372                    } else {
373                        p.line_to(v.to_point());
374                    }
375                }
376                if *closed && pts.len() > 2 {
377                    p.close_path();
378                }
379                Some(p)
380            }
381        }
382    }
383}
384
385/// Regular polygon (or star with an inner radius), first vertex at the top.
386fn regular(n: usize, r: f64, inner: Option<f64>) -> BezPath {
387    let count = if inner.is_some() { 2 * n } else { n };
388    let mut p = BezPath::new();
389    for i in 0..count {
390        let radius = match inner {
391            Some(ri) if i % 2 == 1 => ri,
392            _ => r,
393        };
394        let a = -std::f64::consts::FRAC_PI_2 + std::f64::consts::TAU * i as f64 / count as f64;
395        let pt = Point::new(a.cos() * radius, a.sin() * radius);
396        if i == 0 {
397            p.move_to(pt);
398        } else {
399            p.line_to(pt);
400        }
401    }
402    p.close_path();
403    p
404}
405
406/// Smooth curve through the points (uniform Catmull-Rom as cubic Béziers).
407fn catmull_rom(pts: &[Vec2], closed: bool) -> BezPath {
408    let n = pts.len();
409    let at = |i: isize| -> Vec2 {
410        if closed {
411            pts[i.rem_euclid(n as isize) as usize]
412        } else {
413            pts[i.clamp(0, n as isize - 1) as usize]
414        }
415    };
416    let mut p = BezPath::new();
417    p.move_to(pts[0].to_point());
418    let segs = if closed { n } else { n - 1 };
419    for i in 0..segs as isize {
420        let (p0, p1, p2, p3) = (at(i - 1), at(i), at(i + 1), at(i + 2));
421        let c1 = p1 + (p2 - p0) / 6.0;
422        let c2 = p2 - (p3 - p1) / 6.0;
423        p.curve_to(c1.to_point(), c2.to_point(), p2.to_point());
424    }
425    if closed {
426        p.close_path();
427    }
428    p
429}
430
431pub(crate) enum TextDraw {
432    Styled(Styled),
433    /// Glyphs with their alpha, already centered.
434    Morph {
435        glyphs: Vec<(BezPath, f64)>,
436        size: (f64, f64),
437    },
438}
439
440/// Arena of nodes for one scene run. Node 0 is the view (root).
441pub struct SceneGraph {
442    nodes: RefCell<Vec<NodeData>>,
443    size: (u32, u32),
444}
445
446impl SceneGraph {
447    /// The root node (the scene's view).
448    pub const ROOT: NodeId = NodeId(0);
449
450    /// An empty scene of the given size in pixels, with only the root.
451    pub fn new(width: u32, height: u32) -> Rc<Self> {
452        let graph = Rc::new(Self {
453            nodes: RefCell::new(Vec::new()),
454            size: (width, height),
455        });
456        graph.insert(
457            None,
458            NodeKind::Group,
459            crate::BaseProps {
460                name: Some("view".into()),
461                ..Default::default()
462            },
463        );
464        graph
465    }
466
467    /// Scene size in pixels.
468    pub fn size(&self) -> (u32, u32) {
469        self.size
470    }
471
472    /// Number of nodes, including the root.
473    pub fn len(&self) -> usize {
474        self.nodes.borrow().len()
475    }
476
477    /// Never true: the root always exists.
478    pub fn is_empty(&self) -> bool {
479        self.len() == 0
480    }
481
482    /// Borrows a node's stored state.
483    ///
484    /// # Panics
485    ///
486    /// If `id` is not a node of this graph.
487    pub fn node(&self, id: NodeId) -> Ref<'_, NodeData> {
488        Ref::map(self.nodes.borrow(), |n| &n[id.0 as usize])
489    }
490
491    pub(crate) fn next_id(&self) -> u32 {
492        self.len() as u32
493    }
494
495    pub(crate) fn insert(
496        &self,
497        parent: Option<NodeId>,
498        kind: NodeKind,
499        base: crate::BaseProps,
500    ) -> NodeId {
501        let mut nodes = self.nodes.borrow_mut();
502        let id = NodeId(u32::try_from(nodes.len()).expect("too many nodes"));
503        let label = format!("{}#{}", kind.name(), id.0);
504        let sig = |field: &str| format!("{label}.{field}");
505        nodes.push(NodeData {
506            id,
507            name: base.name,
508            source: current_add_at().map(|(file, line)| source_label(file, line)),
509            parent,
510            children: Vec::new(),
511            x: Signal::new(base.x).with_name(sig("x")),
512            y: Signal::new(base.y).with_name(sig("y")),
513            rotation: Signal::new(base.rotation).with_name(sig("rotation")),
514            scale: Signal::new(base.scale).with_name(sig("scale")),
515            opacity: Signal::new(base.opacity).with_name(sig("opacity")),
516            fill: Signal::new(base.fill).with_name(sig("fill")),
517            stroke: Signal::new(base.stroke).with_name(sig("stroke")),
518            line_width: Signal::new(base.line_width).with_name(sig("line_width")),
519            start: Signal::new(base.start).with_name(sig("start")),
520            end: Signal::new(base.end).with_name(sig("end")),
521            fill_gradient: Signal::new(base.fill_gradient).with_name(sig("fill_gradient")),
522            stroke_gradient: Signal::new(base.stroke_gradient).with_name(sig("stroke_gradient")),
523            dash: Signal::new(base.dash).with_name(sig("dash")),
524            dash_offset: Signal::new(base.dash_offset).with_name(sig("dash_offset")),
525            line_cap: Signal::new(base.line_cap).with_name(sig("line_cap")),
526            line_join: Signal::new(base.line_join).with_name(sig("line_join")),
527            blur: Signal::new(base.blur).with_name(sig("blur")),
528            shadow_color: Signal::new(base.shadow_color).with_name(sig("shadow_color")),
529            shadow_blur: Signal::new(base.shadow_blur).with_name(sig("shadow_blur")),
530            shadow_offset: Signal::new(base.shadow_offset).with_name(sig("shadow_offset")),
531            clip: Signal::new(base.clip).with_name(sig("clip")),
532            blend: Signal::new(base.blend).with_name(sig("blend")),
533            z_index: Signal::new(base.z_index).with_name(sig("z_index")),
534            anchor: Signal::new(base.anchor).with_name(sig("anchor")),
535            grow: Signal::new(base.grow).with_name(sig("grow")),
536            shrink: Signal::new(base.shrink).with_name(sig("shrink")),
537            basis: Signal::new(base.basis).with_name(sig("basis")),
538            margin: Signal::new(base.margin).with_name(sig("margin")),
539            align_self: Signal::new(base.align_self).with_name(sig("align_self")),
540            place: base.place,
541            placed: Cell::new((base.x, base.y)),
542            kind,
543        });
544        if let Some(p) = parent {
545            nodes[p.0 as usize].children.push(id);
546        }
547        id
548    }
549
550    pub(crate) fn detach(&self, id: NodeId) {
551        let mut nodes = self.nodes.borrow_mut();
552        if let Some(p) = nodes[id.0 as usize].parent.take() {
553            nodes[p.0 as usize].children.retain(|c| *c != id);
554        }
555    }
556
557    pub(crate) fn reparent(&self, id: NodeId, parent: NodeId) {
558        self.detach(id);
559        let mut nodes = self.nodes.borrow_mut();
560        nodes[id.0 as usize].parent = Some(parent);
561        nodes[parent.0 as usize].children.push(id);
562    }
563
564    /// Size of a node's box in its own (unscaled) coordinates.
565    pub fn measure(&self, id: NodeId) -> (f64, f64) {
566        self.resolve_places();
567        let nodes = self.nodes.borrow();
568        measure(&nodes, id)
569    }
570
571    pub(crate) fn set_place(&self, id: NodeId, place: crate::place::Place) {
572        self.nodes.borrow_mut()[id.0 as usize].place = Some(place);
573    }
574
575    fn resolve_places(&self) {
576        let nodes = self.nodes.borrow();
577        crate::place::resolve(&nodes, (f64::from(self.size.0), f64::from(self.size.1)));
578    }
579
580    /// Flattened, paint-ordered list of everything to draw.
581    pub fn draw_list(&self) -> Vec<DrawItem> {
582        self.resolve_places();
583        let nodes = self.nodes.borrow();
584        let mut out = Vec::new();
585        visit(&nodes, Self::ROOT, Affine::IDENTITY, None, None, &mut out);
586        out
587    }
588
589    /// Topmost node under `p` (scene coordinates), excluding the view.
590    pub fn pick(&self, p: Point) -> Option<NodeId> {
591        self.draw_list()
592            .iter()
593            .rev()
594            .find(|item| item.node != Self::ROOT && item.hit(p))
595            .map(|item| item.node)
596    }
597
598    /// World transform of a node (including layout offsets).
599    pub fn world_transform(&self, id: NodeId) -> Option<Affine> {
600        // The draw list already composes transforms; reuse it for nodes
601        // that draw, and fall back to walking parents for those that don't.
602        if let Some(item) = self.draw_list().into_iter().find(|i| i.node == id) {
603            return Some(item.transform);
604        }
605        let nodes = self.nodes.borrow();
606        let mut tf = Affine::IDENTITY;
607        let mut cur = Some(id);
608        while let Some(c) = cur {
609            let n = nodes.get(c.0 as usize)?;
610            tf = n.local_transform(Vec2::ZERO, measure(&nodes, c)) * tf;
611            cur = n.parent;
612        }
613        Some(tf)
614    }
615
616    /// `(id, parent, kind, name)` of every attached node, depth-first.
617    pub fn tree(&self) -> Vec<(NodeId, Option<NodeId>, &'static str, Option<String>)> {
618        let nodes = self.nodes.borrow();
619        let mut out = Vec::new();
620        let mut stack = vec![Self::ROOT];
621        while let Some(id) = stack.pop() {
622            let n = &nodes[id.0 as usize];
623            out.push((id, n.parent, n.kind.name(), n.name.clone()));
624            stack.extend(n.children.iter().rev());
625        }
626        out
627    }
628
629    /// Current signal values of a node, for the inspector.
630    pub fn describe(&self, id: NodeId) -> Vec<(String, String)> {
631        let nodes = self.nodes.borrow();
632        let Some(n) = nodes.get(id.0 as usize) else {
633            return Vec::new();
634        };
635        let f = |v: f64| format!("{v:.3}");
636        let s = n.scale.get();
637        let mut out = vec![
638            ("x".into(), f(n.x.get())),
639            ("y".into(), f(n.y.get())),
640            ("rotation".into(), f(n.rotation.get())),
641            ("scale".into(), format!("{:.3}, {:.3}", s.x, s.y)),
642            ("opacity".into(), f(n.opacity.get())),
643            ("fill".into(), n.fill.get().to_string()),
644            ("stroke".into(), n.stroke.get().to_string()),
645            ("line_width".into(), f(n.line_width.get())),
646            ("start".into(), f(n.start.get())),
647            ("end".into(), f(n.end.get())),
648        ];
649        let (w, h) = measure(&nodes, id);
650        out.push(("size".into(), format!("{w:.1} × {h:.1}")));
651        match &n.kind {
652            NodeKind::Rect { radius, .. } => out.push(("radius".into(), f(radius.get()))),
653            NodeKind::Code { code, .. } => out.push(("code".into(), code.get())),
654            NodeKind::Txt {
655                text,
656                font_size,
657                weight,
658                ..
659            } => {
660                out.push(("text".into(), text.get()));
661                out.push(("font_size".into(), f(font_size.get())));
662                out.push(("weight".into(), f(weight.get())));
663            }
664            NodeKind::Line { points, .. } => {
665                out.push(("points".into(), points.get().len().to_string()))
666            }
667            NodeKind::Layout { gap, padding, .. } => {
668                out.push(("gap".into(), f(gap.get())));
669                out.push(("padding".into(), f(padding.get())));
670            }
671            NodeKind::Grid { spacing, .. } => out.push(("spacing".into(), f(spacing.get()))),
672            _ => {}
673        }
674        out
675    }
676
677    /// One record per attached node, in tree order, with scene-space bounds.
678    ///
679    /// `order` is paint order (0 is the back). Nodes that are not drawn
680    /// (fully transparent) get `u32::MAX`.
681    pub fn inspect_nodes(&self) -> Vec<InspectNode> {
682        let list = self.draw_list();
683        let mut drawn: Vec<Option<(Affine, u32)>> = vec![None; self.len()];
684        let mut order = 0u32;
685        for item in &list {
686            let slot = &mut drawn[item.node.0 as usize];
687            if slot.is_none() {
688                *slot = Some((item.transform, order));
689                order += 1;
690            }
691        }
692        let nodes = self.nodes.borrow();
693        let mut out = Vec::new();
694        let mut stack = vec![Self::ROOT];
695        while let Some(id) = stack.pop() {
696            let n = &nodes[id.0 as usize];
697            stack.extend(n.children.iter().rev());
698            let (w, h) = measure(&nodes, id);
699            let (tf, order) = drawn[id.0 as usize]
700                .unwrap_or_else(|| (transform_without_layout(&nodes, id), u32::MAX));
701            let (text, text_overflow) = text_info(n);
702            out.push(InspectNode {
703                id: id.0,
704                parent: n.parent.map(|p| p.0),
705                kind: n.kind.name(),
706                name: n.name.clone().unwrap_or_else(|| {
707                    n.source
708                        .clone()
709                        .unwrap_or_else(|| format!("{}#{}", n.kind.name(), id.0))
710                }),
711                source: n.source.clone(),
712                bounds: aabb(tf, w, h),
713                opacity: n.opacity.get().clamp(0.0, 1.0),
714                order,
715                text,
716                fill: n.fill.get(),
717                text_overflow,
718                parent_kind: n.parent.map(|p| nodes[p.0 as usize].kind.name()),
719            });
720        }
721        out
722    }
723}
724
725/// A node as `oxframe inspect` and the visual linter see it.
726#[derive(Debug, Clone, PartialEq)]
727pub struct InspectNode {
728    /// Node index.
729    pub id: u32,
730    /// Parent index; `None` for the root.
731    pub parent: Option<u32>,
732    /// Node type, e.g. `"Rect"`.
733    pub kind: &'static str,
734    /// `.name(..)`, or the `add` call site, or `Kind#id`.
735    pub name: String,
736    /// `file:line` of the `add` call, when it was recorded.
737    pub source: Option<String>,
738    /// Axis-aligned bounds in scene coordinates: min x, min y, max x, max y.
739    /// The origin is the center of the frame, y points down.
740    pub bounds: [f64; 4],
741    /// 0 (invisible) to 1.
742    pub opacity: f64,
743    /// Paint order among drawn nodes; `u32::MAX` if the node is not drawn.
744    pub order: u32,
745    /// Text of a `Txt` or `Code` node.
746    pub text: Option<String>,
747    /// Fill color.
748    pub fill: Color,
749    /// How many pixels the text is wider than its `max_width` (0 if it fits
750    /// or `max_width` is unset).
751    pub text_overflow: f64,
752    /// Type of the parent, if any.
753    pub parent_kind: Option<&'static str>,
754}
755
756thread_local! {
757    static ADD_AT: Cell<Option<(&'static str, u32)>> = const { Cell::new(None) };
758}
759
760pub(crate) fn push_add_at(file: &'static str, line: u32) -> Option<(&'static str, u32)> {
761    ADD_AT.with(|c| c.replace(Some((file, line))))
762}
763
764pub(crate) fn pop_add_at(prev: Option<(&'static str, u32)>) {
765    ADD_AT.with(|c| c.set(prev));
766}
767
768fn current_add_at() -> Option<(&'static str, u32)> {
769    ADD_AT.with(|c| c.get())
770}
771
772/// `src/main.rs:42` rather than an absolute build path.
773fn source_label(file: &str, line: u32) -> String {
774    let file = file.replace('\\', "/");
775    let rel = file
776        .rfind("/src/")
777        .map_or(file.as_str(), |i| &file[i + 1..]);
778    format!("{rel}:{line}")
779}
780
781fn aabb(tf: Affine, w: f64, h: f64) -> [f64; 4] {
782    let pts = [
783        (-w / 2.0, -h / 2.0),
784        (w / 2.0, -h / 2.0),
785        (w / 2.0, h / 2.0),
786        (-w / 2.0, h / 2.0),
787    ];
788    let mut b = [
789        f64::INFINITY,
790        f64::INFINITY,
791        f64::NEG_INFINITY,
792        f64::NEG_INFINITY,
793    ];
794    for (x, y) in pts {
795        let p = tf * Point::new(x, y);
796        b[0] = b[0].min(p.x);
797        b[1] = b[1].min(p.y);
798        b[2] = b[2].max(p.x);
799        b[3] = b[3].max(p.y);
800    }
801    b
802}
803
804fn transform_without_layout(nodes: &[NodeData], id: NodeId) -> Affine {
805    let mut chain = Vec::new();
806    let mut cur = Some(id);
807    while let Some(c) = cur {
808        chain.push(c);
809        cur = nodes.get(c.0 as usize).and_then(|n| n.parent);
810    }
811    let mut tf = Affine::IDENTITY;
812    for c in chain.into_iter().rev() {
813        let Some(n) = nodes.get(c.0 as usize) else {
814            break;
815        };
816        tf *= n.local_transform(Vec2::ZERO, measure(nodes, c));
817    }
818    tf
819}
820
821fn text_info(n: &NodeData) -> (Option<String>, f64) {
822    let (body, max_width) = match &n.kind {
823        NodeKind::Txt {
824            text, spans, style, ..
825        } => {
826            let spans = spans.get();
827            let body = if spans.is_empty() {
828                text.get()
829            } else {
830                spans.into_iter().map(|s| s.text).collect()
831            };
832            (body, style.max_width)
833        }
834        NodeKind::Code { code, style, .. } => (code.get(), style.max_width),
835        _ => return (None, 0.0),
836    };
837    let overflow = match (max_width, n.text_draw()) {
838        (Some(max), Some(TextDraw::Styled(s))) => (s.layout.width - max).max(0.0),
839        (Some(max), Some(TextDraw::Morph { size, .. })) => (size.0 - max).max(0.0),
840        _ => 0.0,
841    };
842    (Some(body), overflow)
843}
844
845pub(crate) fn measure(nodes: &[NodeData], id: NodeId) -> (f64, f64) {
846    let n = &nodes[id.0 as usize];
847    match &n.kind {
848        NodeKind::Rect { width, height, .. } => (width.get(), height.get()),
849        NodeKind::Circle { size, .. }
850        | NodeKind::Polygon { size, .. }
851        | NodeKind::Star { size, .. } => (size.get(), size.get()),
852        NodeKind::Img { width, height, .. } | NodeKind::Grid { width, height, .. } => {
853            (width.get(), height.get())
854        }
855        NodeKind::Txt { .. } | NodeKind::Code { .. } => match n.text_draw() {
856            Some(TextDraw::Styled(s)) => (s.layout.width, s.layout.height),
857            Some(TextDraw::Morph { size, .. }) => size,
858            None => (0.0, 0.0),
859        },
860        NodeKind::Path { .. } | NodeKind::Line { .. } => {
861            let bb = n
862                .outline((0.0, 0.0))
863                .map_or(kurbo::Rect::ZERO, |p| p.bounding_box());
864            (bb.width(), bb.height())
865        }
866        NodeKind::Layout { .. } => crate::flex::compute(nodes, id, &|c| measure(nodes, c)).0,
867        NodeKind::Group => {
868            let mut bb: Option<kurbo::Rect> = None;
869            for &c in &n.children {
870                let child = &nodes[c.0 as usize];
871                let (w, h) = measure(nodes, c);
872                let r = child
873                    .local_transform(Vec2::ZERO, (w, h))
874                    .transform_rect_bbox(kurbo::Rect::new(-w / 2.0, -h / 2.0, w / 2.0, h / 2.0));
875                bb = Some(bb.map_or(r, |b| b.union(r)));
876            }
877            bb.map_or((0.0, 0.0), |b| (b.width(), b.height()))
878        }
879    }
880}
881
882/// Kinds whose drawn size follows the box a layout gives them.
883fn resizable(kind: &NodeKind) -> bool {
884    matches!(
885        kind,
886        NodeKind::Rect { .. }
887            | NodeKind::Layout { .. }
888            | NodeKind::Img { .. }
889            | NodeKind::Grid { .. }
890    )
891}
892
893fn visit(
894    nodes: &[NodeData],
895    id: NodeId,
896    parent_tf: Affine,
897    placement: Option<FlexBox>,
898    flex: Option<&FlexMap>,
899    out: &mut Vec<DrawItem>,
900) {
901    let n = &nodes[id.0 as usize];
902    let opacity = n.opacity.get().clamp(0.0, 1.0);
903    if opacity <= 0.0 {
904        return;
905    }
906    let size = match placement {
907        Some(b) if resizable(&n.kind) => {
908            let s = n.scale.get();
909            (
910                b.size.0 / s.x.abs().max(1e-9),
911                b.size.1 / s.y.abs().max(1e-9),
912            )
913        }
914        _ => measure(nodes, id),
915    };
916    let offset = placement.map_or(Vec2::ZERO, |b| b.center);
917    let tf = parent_tf * n.local_transform(offset, size);
918
919    // A layer is needed when the node's effects apply to it and its children
920    // as a whole; plain leaves just fold opacity into their paints.
921    let blend = n.blend.get();
922    let blur = n.blur.get().max(0.0);
923    let shadow_color = n.shadow_color.get();
924    let shadow = (shadow_color.a > 0.0).then(|| Shadow {
925        color: shadow_color,
926        blur: n.shadow_blur.get().max(0.0),
927        offset: n.shadow_offset.get(),
928    });
929    let layered = blend != BlendMode::Normal
930        || blur > 0.0
931        || shadow.is_some()
932        || (opacity < 1.0 && !n.children.is_empty());
933    if layered {
934        out.push(DrawItem {
935            node: id,
936            transform: tf,
937            prim: Primitive::PushLayer(Layer {
938                opacity,
939                blend,
940                clip: None,
941                blur,
942                shadow,
943            }),
944        });
945    }
946    let paint_opacity = if layered { 1.0 } else { opacity };
947    for prim in crate::draw::primitives(n, size, paint_opacity) {
948        out.push(DrawItem {
949            node: id,
950            transform: tf,
951            prim,
952        });
953    }
954
955    if !n.children.is_empty() {
956        // Nodes without an outline (groups, text, images) clip to their box.
957        let clip = n.clip.get().then(|| {
958            n.outline(size).unwrap_or_else(|| {
959                kurbo::Rect::new(-size.0 / 2.0, -size.1 / 2.0, size.0 / 2.0, size.1 / 2.0)
960                    .to_path(0.1)
961            })
962        });
963        let clipped = clip.is_some();
964        if let Some(path) = clip {
965            out.push(DrawItem {
966                node: id,
967                transform: tf,
968                prim: Primitive::PushLayer(Layer::clip(path)),
969            });
970        }
971        // Boxes of the children: from the enclosing layout tree, or a new
972        // tree when this is a top-level layout.
973        let own;
974        let map: Option<&FlexMap> = match (&n.kind, flex) {
975            (NodeKind::Layout { .. }, Some(m)) => Some(m),
976            (NodeKind::Layout { .. }, None) => {
977                own = crate::flex::compute(nodes, id, &|c| measure(nodes, c)).1;
978                Some(&own)
979            }
980            _ => None,
981        };
982        let mut order: Vec<(usize, NodeId)> = n.children.iter().copied().enumerate().collect();
983        // Stable: equal z keeps insertion order.
984        order.sort_by(|a, b| {
985            let za = nodes[a.1.0 as usize].z_index.get();
986            let zb = nodes[b.1.0 as usize].z_index.get();
987            za.total_cmp(&zb)
988        });
989        for (_, c) in order {
990            let placement = map.and_then(|m| m.get(&c).copied());
991            visit(nodes, c, tf, placement, map, out);
992        }
993        if clipped {
994            out.push(DrawItem {
995                node: id,
996                transform: tf,
997                prim: Primitive::PopLayer,
998            });
999        }
1000    }
1001    if layered {
1002        out.push(DrawItem {
1003            node: id,
1004            transform: tf,
1005            prim: Primitive::PopLayer,
1006        });
1007    }
1008}
1009
1010impl DrawItem {
1011    fn hit(&self, p: Point) -> bool {
1012        let local = self.transform.inverse() * p;
1013        match &self.prim {
1014            Primitive::Fill { path, .. } => {
1015                (is_text_like(path) && path.bounding_box().contains(local))
1016                    || closed(path).contains(local)
1017            }
1018            Primitive::Stroke { path, style, .. } => {
1019                let tol = (style.width / 2.0)
1020                    .max(4.0 / self.transform.determinant().abs().sqrt().max(1e-9));
1021                path.segments().any(|s| {
1022                    kurbo::ParamCurveNearest::nearest(&s, local, 0.1).distance_sq <= tol * tol
1023                })
1024            }
1025            Primitive::Image { rect, .. } => rect.contains(local),
1026            Primitive::PushLayer(_) | Primitive::PopLayer => false,
1027        }
1028    }
1029}
1030
1031/// Fills implicitly close open subpaths; kurbo's winding does not.
1032fn closed(path: &BezPath) -> BezPath {
1033    let mut out = BezPath::new();
1034    let mut open = false;
1035    for el in path.elements() {
1036        match el {
1037            kurbo::PathEl::MoveTo(_) => {
1038                if open {
1039                    out.close_path();
1040                }
1041                open = true;
1042            }
1043            kurbo::PathEl::ClosePath => open = false,
1044            _ => {}
1045        }
1046        out.push(*el);
1047    }
1048    if open {
1049        out.close_path();
1050    }
1051    out
1052}
1053
1054/// Glyph runs are hard to hit exactly; treat many-subpath fills (text) by
1055/// their bounding box.
1056fn is_text_like(path: &BezPath) -> bool {
1057    path.elements()
1058        .iter()
1059        .filter(|e| matches!(e, kurbo::PathEl::MoveTo(_)))
1060        .count()
1061        > 3
1062}
1063
1064thread_local! {
1065    static CURRENT: RefCell<Option<Rc<SceneGraph>>> = const { RefCell::new(None) };
1066}
1067
1068impl SceneGraph {
1069    /// Makes this graph the target of node handles for the duration of `f`.
1070    pub fn enter<R>(self: &Rc<Self>, f: impl FnOnce() -> R) -> R {
1071        let prev = CURRENT.with(|c| c.borrow_mut().replace(self.clone()));
1072        struct Restore(Option<Rc<SceneGraph>>);
1073        impl Drop for Restore {
1074            fn drop(&mut self) {
1075                let prev = self.0.take();
1076                CURRENT.with(|c| *c.borrow_mut() = prev);
1077            }
1078        }
1079        let _restore = Restore(prev);
1080        f()
1081    }
1082}
1083
1084pub(crate) fn with_graph<R>(f: impl FnOnce(&SceneGraph) -> R) -> R {
1085    CURRENT.with(|c| {
1086        let c = c.borrow();
1087        f(c.as_ref()
1088            .expect("oxframe: node handles can only be used while a scene is running"))
1089    })
1090}