Skip to main content

oxframe_2d/
builder.rs

1use kurbo::{BezPath, Vec2};
2use oxframe_core::{BlendMode, Color, Gradient, Signal};
3use oxframe_text::TextStyle;
4
5use crate::draw::{LineCap, LineJoin};
6use crate::graph::{Direction, Justify, LayoutAlign, NodeId, NodeKind, SceneGraph, with_graph};
7use crate::handles::{
8    Camera, Circle, Code, Custom, Grid, Group, Img, Layout, Line, Particles, Path, Polygon, Rect,
9    Star, Txt,
10};
11use crate::image::ImageSource;
12use crate::place::{self, Axis, Edge, Place, Side, Span};
13use crate::text::{CodeTheme, RichSpan};
14use crate::theme::{self, Corner, IntoPaint, Len, Step, TypeRole};
15
16/// Initial values shared by every node; set through [`NodeBuilder`] methods.
17#[doc(hidden)]
18#[derive(Debug)]
19pub struct BaseProps {
20    pub name: Option<String>,
21    pub x: f64,
22    pub y: f64,
23    pub rotation: f64,
24    pub scale: Vec2,
25    pub opacity: f64,
26    pub fill: Color,
27    /// Set when `fill` was chosen explicitly. Text leaves this false and
28    /// takes the theme's text color at build time.
29    pub fill_explicit: bool,
30    pub stroke: Color,
31    pub line_width: f64,
32    pub start: f64,
33    pub end: f64,
34    pub fill_gradient: Option<Gradient>,
35    pub stroke_gradient: Option<Gradient>,
36    pub dash: Vec<f64>,
37    pub dash_offset: f64,
38    pub line_cap: LineCap,
39    pub line_join: LineJoin,
40    pub blur: f64,
41    pub shadow_color: Color,
42    pub shadow_blur: f64,
43    pub shadow_offset: Vec2,
44    pub clip: bool,
45    pub blend: BlendMode,
46    pub z_index: f64,
47    pub anchor: Vec2,
48    pub grow: f64,
49    pub shrink: f64,
50    pub basis: Option<f64>,
51    pub margin: f64,
52    pub align_self: Option<LayoutAlign>,
53    pub children: Vec<ChildBuilder>,
54    /// Resolved every frame. Set by `next_to`, `align_to`, `to_edge`, `center_in`.
55    pub place: Option<Place>,
56}
57
58impl Default for BaseProps {
59    fn default() -> Self {
60        Self {
61            name: None,
62            x: 0.0,
63            y: 0.0,
64            rotation: 0.0,
65            scale: Vec2::new(1.0, 1.0),
66            opacity: 1.0,
67            fill: Color::TRANSPARENT,
68            fill_explicit: false,
69            stroke: Color::TRANSPARENT,
70            line_width: 0.0,
71            start: 0.0,
72            end: 1.0,
73            fill_gradient: None,
74            stroke_gradient: None,
75            dash: Vec::new(),
76            dash_offset: 0.0,
77            line_cap: LineCap::Round,
78            line_join: LineJoin::Round,
79            blur: 0.0,
80            shadow_color: Color::TRANSPARENT,
81            shadow_blur: 0.0,
82            shadow_offset: Vec2::ZERO,
83            clip: false,
84            blend: BlendMode::Normal,
85            z_index: 0.0,
86            anchor: Vec2::ZERO,
87            grow: 0.0,
88            shrink: 0.0,
89            basis: None,
90            margin: 0.0,
91            align_self: None,
92            children: Vec::new(),
93            place: None,
94        }
95    }
96}
97
98/// A type-erased child added with [`NodeBuilder::child`].
99type BuildFn = Box<dyn FnOnce(&SceneGraph, NodeId)>;
100
101/// A child declared with [`NodeBuilder::child`], built together with its parent.
102pub struct ChildBuilder(BuildFn);
103
104impl std::fmt::Debug for ChildBuilder {
105    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
106        f.write_str("ChildBuilder")
107    }
108}
109
110/// Describes a node before it is added to the scene.
111pub trait NodeBuilder: Sized + 'static {
112    /// Handle returned by [`Node::add`](crate::Node::add).
113    type Handle;
114
115    #[doc(hidden)]
116    fn base(&mut self) -> &mut BaseProps;
117
118    #[doc(hidden)]
119    fn build(self, graph: &SceneGraph, parent: NodeId) -> Self::Handle;
120
121    /// Name shown in the editor and in `oxframe check` / `oxframe inspect`.
122    /// Without it, the node is labeled by the `add` call site (`src/main.rs:42`).
123    fn name(mut self, v: impl Into<String>) -> Self {
124        self.base().name = Some(v.into());
125        self
126    }
127    /// Horizontal position; 0 is the parent's center, positive is right.
128    fn x(mut self, v: f64) -> Self {
129        self.base().x = v;
130        self
131    }
132    /// Vertical position; 0 is the parent's center, positive is down.
133    fn y(mut self, v: f64) -> Self {
134        self.base().y = v;
135        self
136    }
137    /// Sets `x` and `y` together, e.g. `.position((100.0, -50.0))`.
138    fn position(mut self, v: impl Into<Vec2>) -> Self {
139        let v = v.into();
140        let b = self.base();
141        (b.x, b.y) = (v.x, v.y);
142        self
143    }
144    /// Degrees, clockwise.
145    fn rotation(mut self, v: f64) -> Self {
146        self.base().rotation = v;
147        self
148    }
149    /// Uniform scale; 1 is the natural size.
150    fn scale(mut self, v: f64) -> Self {
151        self.base().scale = Vec2::new(v, v);
152        self
153    }
154    /// Separate horizontal and vertical scale.
155    fn scale_xy(mut self, v: impl Into<Vec2>) -> Self {
156        self.base().scale = v.into();
157        self
158    }
159    /// Opacity of the node and its children, 0 (invisible) to 1.
160    fn opacity(mut self, v: f64) -> Self {
161        self.base().opacity = v;
162        self
163    }
164    /// Fill color: a [`Color`](oxframe_core::Color) or a theme role such as `Accent`.
165    /// Default: transparent, except text, which uses the theme's text color.
166    fn fill(mut self, v: impl IntoPaint) -> Self {
167        let b = self.base();
168        b.fill = v.into_paint();
169        b.fill_explicit = true;
170        self
171    }
172    /// Outline color; also set `line_width`. A theme role works the same as a color.
173    /// Default: transparent.
174    fn stroke(mut self, v: impl IntoPaint) -> Self {
175        self.base().stroke = v.into_paint();
176        self
177    }
178    /// Outline width in pixels. Default: 0 (no outline).
179    fn line_width(mut self, v: f64) -> Self {
180        self.base().line_width = v;
181        self
182    }
183    /// Start of the drawn part of the outline, fraction of its length.
184    fn start(mut self, v: f64) -> Self {
185        self.base().start = v;
186        self
187    }
188    /// End of the drawn part of the outline; animate 0 → 1 to draw it on.
189    fn end(mut self, v: f64) -> Self {
190        self.base().end = v;
191        self
192    }
193    /// Gradient fill (local coordinates, origin at the node center);
194    /// overrides `fill`.
195    fn fill_gradient(mut self, g: Gradient) -> Self {
196        self.base().fill_gradient = Some(g);
197        self
198    }
199    /// Gradient outline; overrides `stroke`.
200    fn stroke_gradient(mut self, g: Gradient) -> Self {
201        self.base().stroke_gradient = Some(g);
202        self
203    }
204    /// Dash pattern: on, off, on, … lengths. Animate `dash_offset` for
205    /// marching ants.
206    fn dash(mut self, pattern: impl Into<Vec<f64>>) -> Self {
207        self.base().dash = pattern.into();
208        self
209    }
210    /// Shifts the dash pattern along the outline.
211    fn dash_offset(mut self, v: f64) -> Self {
212        self.base().dash_offset = v;
213        self
214    }
215    /// Shape of outline ends. Default: round.
216    fn line_cap(mut self, v: LineCap) -> Self {
217        self.base().line_cap = v;
218        self
219    }
220    /// Shape of outline corners. Default: round.
221    fn line_join(mut self, v: LineJoin) -> Self {
222        self.base().line_join = v;
223        self
224    }
225    /// Gaussian blur of the node and its children (standard deviation, in
226    /// the node's units).
227    fn blur(mut self, v: f64) -> Self {
228        self.base().blur = v;
229        self
230    }
231    /// Drop shadow under the node and its children.
232    fn shadow(mut self, color: impl IntoPaint, blur: f64, offset: impl Into<Vec2>) -> Self {
233        let b = self.base();
234        (b.shadow_color, b.shadow_blur, b.shadow_offset) =
235            (color.into_paint(), blur, offset.into());
236        self
237    }
238    /// The theme's shadow for this step (`Xs` and `Sm` are the small one, `Xl` the large).
239    fn elevation(self, step: Step) -> Self {
240        let s = theme::current().shadow(step);
241        self.shadow(s.color, s.blur, s.offset)
242    }
243    /// A shadow without offset.
244    fn glow(self, color: impl IntoPaint, blur: f64) -> Self {
245        self.shadow(color, blur, Vec2::ZERO)
246    }
247    /// Clip children to this node's outline (or box).
248    fn clip(mut self, v: bool) -> Self {
249        self.base().clip = v;
250        self
251    }
252    /// How the node is composited onto what is below it.
253    fn blend(mut self, v: BlendMode) -> Self {
254        self.base().blend = v;
255        self
256    }
257    /// Paint order among siblings; higher is on top.
258    fn z_index(mut self, v: f64) -> Self {
259        self.base().z_index = v;
260        self
261    }
262    /// Pivot and placement point: (-1, -1) top-left … (1, 1) bottom-right.
263    fn anchor(mut self, v: impl Into<Vec2>) -> Self {
264        self.base().anchor = v.into();
265        self
266    }
267    /// Inside a `Layout`: share of the free space this node takes.
268    fn grow(mut self, v: f64) -> Self {
269        self.base().grow = v;
270        self
271    }
272    /// Inside a `Layout`: how much this node gives up when space is short.
273    fn shrink(mut self, v: f64) -> Self {
274        self.base().shrink = v;
275        self
276    }
277    /// Inside a `Layout`: main-axis size before growing/shrinking.
278    fn basis(mut self, v: f64) -> Self {
279        self.base().basis = Some(v);
280        self
281    }
282    /// Inside a `Layout`: space around this node, in pixels or a theme step (`.margin(Md)`).
283    fn margin(mut self, v: impl Len) -> Self {
284        self.base().margin = v.px();
285        self
286    }
287    /// Inside a `Layout`: overrides the layout's cross-axis alignment.
288    fn align_self(mut self, v: LayoutAlign) -> Self {
289        self.base().align_self = Some(v);
290        self
291    }
292    /// Sits just outside `other`, on `side`. The gap is the theme's `Md`
293    /// step. Recomputed every frame, so this node follows `other`.
294    fn next_to(mut self, other: impl Into<crate::Node>, side: Side) -> Self {
295        self.base().place = Some(Place::next_to(other.into().id(), side));
296        self
297    }
298    /// Lines this node's `edge` up with the same edge of `other`.
299    fn align_to(mut self, other: impl Into<crate::Node>, edge: Edge) -> Self {
300        self.base().place = Some(Place::align_to(other.into().id(), edge));
301        self
302    }
303    /// Pins one edge to the safe area (not the raw frame edge). The other
304    /// axis stays at `x` or `y`.
305    fn to_edge(mut self, edge: Edge) -> Self {
306        self.base().place = Some(Place::to_edge(edge));
307        self
308    }
309    /// Same center as `other`. Pass the view to center in the frame.
310    fn center_in(mut self, other: impl Into<crate::Node>) -> Self {
311        self.base().place = Some(Place::center_in(other.into().id()));
312        self
313    }
314    /// Adds a child created together with this node. Use `Node::add` when
315    /// you need the child's handle.
316    fn child<C: NodeBuilder>(mut self, child: C) -> Self {
317        self.base()
318            .children
319            .push(ChildBuilder(Box::new(move |g, parent| {
320                child.build(g, parent);
321            })));
322        self
323    }
324}
325
326pub(crate) fn add<B: NodeBuilder>(parent: NodeId, builder: B) -> B::Handle {
327    with_graph(|g| builder.build(g, parent))
328}
329
330/// Inserts the node and then its declared children.
331fn insert(graph: &SceneGraph, parent: NodeId, kind: NodeKind, mut base: BaseProps) -> NodeId {
332    let children = std::mem::take(&mut base.children);
333    let id = graph.insert(Some(parent), kind, base);
334    for c in children {
335        (c.0)(graph, id);
336    }
337    id
338}
339
340fn sig<T: Clone + 'static>(graph: &SceneGraph, kind: &str, field: &str, v: T) -> Signal<T> {
341    Signal::new(v).with_name(format!("{kind}#{}.{field}", graph.next_id()))
342}
343
344macro_rules! builder {
345    (
346        $(#[$doc:meta])*
347        $builder:ident => $handle:ident, kind $kind:literal, defaults { $($bf:ident: $bv:expr),* $(,)? },
348        fields { $($field:ident: $ty:ty = $default:expr),* $(,)? }
349    ) => {
350        $(#[$doc])*
351        #[doc = ""]
352        #[doc = concat!("Builder for a [`", stringify!($handle), "`] node: set the initial values, then")]
353        #[doc = "pass it to `add`."]
354        #[derive(Debug)]
355        #[must_use = "a builder does nothing until it is passed to `add`"]
356        pub struct $builder {
357            base: BaseProps,
358            $(pub(crate) $field: $ty,)*
359        }
360
361        impl Default for $builder {
362            fn default() -> Self {
363                Self {
364                    base: BaseProps { $($bf: $bv,)* ..Default::default() },
365                    $($field: $default,)*
366                }
367            }
368        }
369
370        impl $builder {
371            #[allow(dead_code)]
372            const KIND: &'static str = $kind;
373        }
374    };
375}
376
377macro_rules! setters {
378    ($builder:ident { $($(#[$doc:meta])* $field:ident: $ty:ty),* $(,)? }) => {
379        impl $builder {
380            $(
381                $(#[$doc])*
382                #[doc = ""]
383                #[doc = concat!("Sets the initial `", stringify!($field), "`.")]
384                pub fn $field(mut self, v: impl Into<$ty>) -> Self {
385                    self.$field = v.into();
386                    self
387                }
388            )*
389        }
390    };
391}
392
393macro_rules! base_accessor {
394    () => {
395        fn base(&mut self) -> &mut BaseProps {
396            &mut self.base
397        }
398    };
399}
400
401// ------------------------------------------------------------------ Group
402
403builder!(GroupBuilder => Group, kind "Group", defaults {}, fields {});
404
405impl NodeBuilder for GroupBuilder {
406    type Handle = Group;
407    base_accessor!();
408    fn build(self, graph: &SceneGraph, parent: NodeId) -> Group {
409        Group::from_id(insert(graph, parent, NodeKind::Group, self.base))
410    }
411}
412
413// ----------------------------------------------------------------- Camera
414
415builder!(
416    /// A camera: everything added to it is seen through it, so animating its
417    /// `x`/`y` (the point it looks at), `zoom` and `rotation` pans, zooms
418    /// and turns the view. Nodes added to the view itself stay on screen
419    /// (titles, captions, a HUD).
420    CameraBuilder => Camera, kind "Camera", defaults {}, fields { zoom: f64 = 1.0 });
421setters!(CameraBuilder {
422    /// 1 shows the world at its size, 2 twice as big.
423    zoom: f64
424});
425
426impl NodeBuilder for CameraBuilder {
427    type Handle = Camera;
428    base_accessor!();
429    fn build(self, g: &SceneGraph, parent: NodeId) -> Camera {
430        let kind = NodeKind::Camera {
431            zoom: sig(g, Self::KIND, "zoom", self.zoom),
432        };
433        Camera::from_id(insert(g, parent, kind, self.base))
434    }
435}
436
437// ------------------------------------------------------------------- Rect
438
439builder!(RectBuilder => Rect, kind "Rect", defaults { fill: Color::WHITE, fill_explicit: true },
440    fields { width: Span = Span::Px(100.0), height: Span = Span::Px(100.0), radius: f64 = 0.0 });
441
442impl RectBuilder {
443    /// Width in pixels, or [`pct`](crate::pct)`(80)` for a fraction of the frame.
444    pub fn width(mut self, v: impl Into<Span>) -> Self {
445        self.width = v.into();
446        self
447    }
448    /// Height in pixels, or a percentage of the frame.
449    pub fn height(mut self, v: impl Into<Span>) -> Self {
450        self.height = v.into();
451        self
452    }
453    /// Corner radius, in pixels or a theme step (`.radius(Md)`).
454    /// Clamped to half of the shorter side when drawn.
455    pub fn radius(mut self, v: impl Corner) -> Self {
456        self.radius = v.radius();
457        self
458    }
459    /// Sets width and height to the same pixel size.
460    pub fn size(mut self, v: f64) -> Self {
461        (self.width, self.height) = (Span::Px(v), Span::Px(v));
462        self
463    }
464}
465
466impl NodeBuilder for RectBuilder {
467    type Handle = Rect;
468    base_accessor!();
469    fn build(self, g: &SceneGraph, parent: NodeId) -> Rect {
470        let k = Self::KIND;
471        let n = g.next_id();
472        let kind = NodeKind::Rect {
473            width: place::span_signal(g, parent, Axis::X, self.width, format!("{k}#{n}.width")),
474            height: place::span_signal(g, parent, Axis::Y, self.height, format!("{k}#{n}.height")),
475            radius: sig(g, k, "radius", self.radius),
476        };
477        Rect::from_id(insert(g, parent, kind, self.base))
478    }
479}
480
481// ----------------------------------------------------------------- Circle
482
483builder!(CircleBuilder => Circle, kind "Circle", defaults { fill: Color::WHITE, fill_explicit: true },
484    fields { size: f64 = 100.0, start_angle: f64 = 0.0, end_angle: f64 = 360.0, pie: bool = false });
485setters!(CircleBuilder {
486    /// Diameter.
487    size: f64,
488    /// Degrees clockwise from 12 o'clock.
489    start_angle: f64,
490    end_angle: f64,
491    /// Close the arc through the center (pie slice).
492    pie: bool
493});
494
495impl NodeBuilder for CircleBuilder {
496    type Handle = Circle;
497    base_accessor!();
498    fn build(self, g: &SceneGraph, parent: NodeId) -> Circle {
499        let k = Self::KIND;
500        let kind = NodeKind::Circle {
501            size: sig(g, k, "size", self.size),
502            start_angle: sig(g, k, "start_angle", self.start_angle),
503            end_angle: sig(g, k, "end_angle", self.end_angle),
504            pie: self.pie,
505        };
506        Circle::from_id(insert(g, parent, kind, self.base))
507    }
508}
509
510// ---------------------------------------------------------------- Polygon
511
512builder!(PolygonBuilder => Polygon, kind "Polygon", defaults { fill: Color::WHITE, fill_explicit: true },
513    fields { sides: f64 = 6.0, size: f64 = 100.0 });
514setters!(PolygonBuilder {
515    /// Number of sides (rounded; animatable as a number).
516    sides: f64,
517    /// Diameter of the circumscribed circle.
518    size: f64
519});
520
521impl NodeBuilder for PolygonBuilder {
522    type Handle = Polygon;
523    base_accessor!();
524    fn build(self, g: &SceneGraph, parent: NodeId) -> Polygon {
525        let k = Self::KIND;
526        let kind = NodeKind::Polygon {
527            sides: sig(g, k, "sides", self.sides),
528            size: sig(g, k, "size", self.size),
529        };
530        Polygon::from_id(insert(g, parent, kind, self.base))
531    }
532}
533
534// ------------------------------------------------------------------- Star
535
536builder!(StarBuilder => Star, kind "Star", defaults { fill: Color::WHITE, fill_explicit: true },
537    fields { points: f64 = 5.0, size: f64 = 100.0, inner: f64 = 0.45 });
538setters!(StarBuilder {
539    points: f64,
540    /// Outer diameter.
541    size: f64,
542    /// Inner radius as a fraction of the outer one.
543    inner: f64
544});
545
546impl NodeBuilder for StarBuilder {
547    type Handle = Star;
548    base_accessor!();
549    fn build(self, g: &SceneGraph, parent: NodeId) -> Star {
550        let k = Self::KIND;
551        let kind = NodeKind::Star {
552            points: sig(g, k, "points", self.points),
553            size: sig(g, k, "size", self.size),
554            inner: sig(g, k, "inner", self.inner),
555        };
556        Star::from_id(insert(g, parent, kind, self.base))
557    }
558}
559
560// ------------------------------------------------------------------- Path
561
562builder!(PathBuilder => Path, kind "Path", defaults { fill: Color::WHITE, fill_explicit: true },
563    fields { data: BezPath = BezPath::new() });
564setters!(PathBuilder { data: BezPath });
565
566impl PathBuilder {
567    /// Path from SVG path syntax (`"M0 0 L10 0 Z"`).
568    ///
569    /// # Panics
570    ///
571    /// If `d` is not valid SVG path data.
572    pub fn svg(mut self, d: &str) -> Self {
573        self.data = BezPath::from_svg(d).unwrap_or_else(|e| panic!("invalid SVG path {d:?}: {e}"));
574        self
575    }
576}
577
578impl NodeBuilder for PathBuilder {
579    type Handle = Path;
580    base_accessor!();
581    fn build(self, g: &SceneGraph, parent: NodeId) -> Path {
582        let kind = NodeKind::Path {
583            data: sig(g, Self::KIND, "data", self.data),
584        };
585        Path::from_id(insert(g, parent, kind, self.base))
586    }
587}
588
589// ------------------------------------------------------------------- Line
590
591builder!(LineBuilder => Line, kind "Line",
592    defaults { stroke: Color::WHITE, line_width: 4.0 },
593    fields { points: Vec<Vec2> = Vec::new(), closed: bool = false, smooth: bool = false,
594             start_arrow: bool = false, end_arrow: bool = false, arrow_size: f64 = 24.0 });
595setters!(LineBuilder {
596    closed: bool,
597    /// Smooth curve through the points instead of straight segments.
598    smooth: bool,
599    start_arrow: bool,
600    end_arrow: bool,
601    arrow_size: f64
602});
603
604impl LineBuilder {
605    /// Points the line passes through, in local coordinates.
606    pub fn points<P: Into<Vec2>>(mut self, pts: impl IntoIterator<Item = P>) -> Self {
607        self.points = pts.into_iter().map(Into::into).collect();
608        self
609    }
610}
611
612impl NodeBuilder for LineBuilder {
613    type Handle = Line;
614    base_accessor!();
615    fn build(self, g: &SceneGraph, parent: NodeId) -> Line {
616        let kind = NodeKind::Line {
617            points: sig(g, Self::KIND, "points", self.points),
618            closed: self.closed,
619            smooth: self.smooth,
620            start_arrow: self.start_arrow,
621            end_arrow: self.end_arrow,
622            arrow_size: sig(g, Self::KIND, "arrow_size", self.arrow_size),
623        };
624        Line::from_id(insert(g, parent, kind, self.base))
625    }
626}
627
628// -------------------------------------------------------------------- Txt
629
630builder!(TxtBuilder => Txt, kind "Txt", defaults { fill: Color::WHITE },
631    fields { text: String = String::new(), spans: Vec<RichSpan> = Vec::new(),
632             style: TextStyle = TextStyle::default() });
633
634impl TxtBuilder {
635    /// The text to show.
636    pub fn text(mut self, v: impl Into<String>) -> Self {
637        self.text = v.into();
638        self
639    }
640    /// Styled runs instead of plain text: `[span("Hello "), span("world").bold()]`.
641    pub fn rich(mut self, spans: impl IntoIterator<Item = RichSpan>) -> Self {
642        self.spans = spans.into_iter().collect();
643        self
644    }
645    /// Font size in pixels. Default: 48.
646    pub fn font_size(mut self, v: f64) -> Self {
647        self.style.size = v;
648        self
649    }
650    /// 100..=900.
651    pub fn weight(mut self, v: f64) -> Self {
652        self.style.weight = v;
653        self
654    }
655    /// [`SANS`](crate::SANS), [`MONO`](crate::MONO) or a family registered
656    /// with `oxframe_text::register_font`.
657    pub fn family(mut self, v: impl Into<String>) -> Self {
658        self.style.family = v.into();
659        self
660    }
661    /// Italic style.
662    pub fn italic(mut self, v: bool) -> Self {
663        self.style.italic = v;
664        self
665    }
666    /// Multiple of the font size.
667    pub fn line_height(mut self, v: f64) -> Self {
668        self.style.line_height = v;
669        self
670    }
671    /// Extra space between letters, in pixels.
672    pub fn letter_spacing(mut self, v: f64) -> Self {
673        self.style.letter_spacing = v;
674        self
675    }
676    /// Alignment of lines within the text block.
677    pub fn align(mut self, v: crate::TextAlign) -> Self {
678        self.style.align = v;
679        self
680    }
681    /// Wrap width.
682    pub fn max_width(mut self, v: f64) -> Self {
683        self.style.max_width = Some(v);
684        self
685    }
686    /// Largest title from the theme.
687    pub fn display(self) -> Self {
688        self.role(TypeRole::Display)
689    }
690    /// Page title from the theme.
691    pub fn h1(self) -> Self {
692        self.role(TypeRole::H1)
693    }
694    /// Section title from the theme.
695    pub fn h2(self) -> Self {
696        self.role(TypeRole::H2)
697    }
698    /// Running text from the theme.
699    pub fn body(self) -> Self {
700        self.role(TypeRole::Body)
701    }
702    /// Secondary text from the theme.
703    pub fn caption(self) -> Self {
704        self.role(TypeRole::Caption)
705    }
706    /// Monospace text from the theme.
707    pub fn code(self) -> Self {
708        self.family(crate::MONO).role(TypeRole::Code)
709    }
710
711    fn role(mut self, role: TypeRole) -> Self {
712        let r = theme::current().role(role);
713        self.style.size = r.size;
714        self.style.weight = r.weight;
715        self.style.line_height = r.line_height;
716        self
717    }
718}
719
720impl NodeBuilder for TxtBuilder {
721    type Handle = Txt;
722    base_accessor!();
723    fn build(mut self, g: &SceneGraph, parent: NodeId) -> Txt {
724        if !self.base.fill_explicit {
725            self.base.fill = theme::current().text;
726        }
727        let k = Self::KIND;
728        let kind = NodeKind::Txt {
729            text: sig(g, k, "text", self.text),
730            spans: sig(g, k, "spans", self.spans),
731            font_size: sig(g, k, "font_size", self.style.size),
732            weight: sig(g, k, "weight", self.style.weight),
733            style: self.style,
734            morph: sig(g, k, "morph", None),
735            morph_t: sig(g, k, "morph_t", 0.0),
736        };
737        Txt::from_id(insert(g, parent, kind, self.base))
738    }
739}
740
741// ------------------------------------------------------------------- Code
742
743builder!(CodeBuilder => Code, kind "Code", defaults {},
744    fields { code: String = String::new(), lang: oxframe_text::Lang = oxframe_text::Lang::Rust,
745             theme: CodeTheme = CodeTheme::dark(), theme_set: bool = false, style: TextStyle = TextStyle {
746                 family: oxframe_text::MONO.to_owned(), size: 32.0, line_height: 1.45, ..TextStyle::default()
747             }, focus: Vec2 = Vec2::ZERO, dim: f64 = 0.3 });
748
749impl CodeBuilder {
750    /// The source code to show.
751    pub fn code(mut self, v: impl Into<String>) -> Self {
752        self.code = v.into();
753        self
754    }
755    /// "rust", "js", "ts", "python", "json", "toml", "shell"; anything else
756    /// is plain text.
757    pub fn language(mut self, name: &str) -> Self {
758        self.lang = oxframe_text::Lang::parse(name);
759        self
760    }
761    /// Syntax highlighting colors. Without this, a light project theme uses
762    /// [`CodeTheme::light`] and a dark one uses [`CodeTheme::dark`].
763    pub fn theme(mut self, t: CodeTheme) -> Self {
764        self.theme = t;
765        self.theme_set = true;
766        self
767    }
768    /// Font size in pixels.
769    pub fn font_size(mut self, v: f64) -> Self {
770        self.style.size = v;
771        self
772    }
773    /// Line height as a multiple of the font size.
774    pub fn line_height(mut self, v: f64) -> Self {
775        self.style.line_height = v;
776        self
777    }
778    /// Highlight lines `first..=last` (1-based); others are dimmed.
779    pub fn focus(mut self, first: usize, last: usize) -> Self {
780        self.focus = Vec2::new(first as f64, last as f64);
781        self
782    }
783    /// Alpha of unfocused lines (default 0.3).
784    pub fn dim(mut self, v: f64) -> Self {
785        self.dim = v;
786        self
787    }
788}
789
790impl NodeBuilder for CodeBuilder {
791    type Handle = Code;
792    base_accessor!();
793    fn build(self, g: &SceneGraph, parent: NodeId) -> Code {
794        let k = Self::KIND;
795        let theme = if self.theme_set {
796            self.theme
797        } else if theme::current().is_light() {
798            CodeTheme::light()
799        } else {
800            CodeTheme::dark()
801        };
802        let kind = NodeKind::Code {
803            code: sig(g, k, "code", self.code),
804            edit_from: sig(g, k, "edit_from", String::new()),
805            edit_to: sig(g, k, "edit_to", String::new()),
806            editing: sig(g, k, "editing", false),
807            edit_t: sig(g, k, "edit_t", 0.0),
808            edit_flash: sig(g, k, "edit_flash", false),
809            lang: self.lang,
810            theme,
811            font_size: sig(g, k, "font_size", self.style.size),
812            style: self.style,
813            focus: sig(g, k, "focus", self.focus),
814            dim: sig(g, k, "dim", self.dim),
815        };
816        Code::from_id(insert(g, parent, kind, self.base))
817    }
818}
819
820// -------------------------------------------------------------------- Img
821
822builder!(ImgBuilder => Img, kind "Img", defaults {},
823    fields { source: Option<ImageSource> = None, width: Option<f64> = None, height: Option<f64> = None });
824
825impl ImgBuilder {
826    /// PNG, JPEG or SVG bytes, typically `include_bytes!("../assets/x.png")`
827    /// — the only option under wasm.
828    ///
829    /// # Panics
830    ///
831    /// If the bytes are not a PNG, JPEG or SVG image.
832    pub fn src(mut self, bytes: &[u8]) -> Self {
833        self.source =
834            Some(ImageSource::decode(bytes).unwrap_or_else(|e| panic!("cannot decode image: {e}")));
835        self
836    }
837
838    /// Reads an image file (native only).
839    ///
840    /// # Panics
841    ///
842    /// If the file cannot be read or decoded.
843    #[cfg(not(target_arch = "wasm32"))]
844    pub fn file(self, path: impl AsRef<std::path::Path>) -> Self {
845        let path = path.as_ref();
846        let bytes =
847            std::fs::read(path).unwrap_or_else(|e| panic!("cannot read {}: {e}", path.display()));
848        self.src(&bytes)
849    }
850
851    /// Width; height follows the aspect ratio unless also set.
852    pub fn width(mut self, v: f64) -> Self {
853        self.width = Some(v);
854        self
855    }
856
857    /// Height; width follows the aspect ratio unless also set.
858    pub fn height(mut self, v: f64) -> Self {
859        self.height = Some(v);
860        self
861    }
862}
863
864impl NodeBuilder for ImgBuilder {
865    type Handle = Img;
866    base_accessor!();
867    fn build(self, g: &SceneGraph, parent: NodeId) -> Img {
868        let source = self
869            .source
870            .expect("Img needs a source: call .src(bytes) or .file(path)");
871        let (nw, nh) = source.natural_size();
872        let aspect = if nh > 0.0 { nw / nh } else { 1.0 };
873        let (w, h) = match (self.width, self.height) {
874            (Some(w), Some(h)) => (w, h),
875            (Some(w), None) => (w, w / aspect),
876            (None, Some(h)) => (h * aspect, h),
877            (None, None) => (nw, nh),
878        };
879        let k = Self::KIND;
880        let kind = NodeKind::Img {
881            source,
882            width: sig(g, k, "width", w),
883            height: sig(g, k, "height", h),
884        };
885        Img::from_id(insert(g, parent, kind, self.base))
886    }
887}
888
889// -------------------------------------------------------------------- Svg
890
891builder!(
892    /// An SVG file, expanded into groups, paths and images.
893    SvgBuilder => Svg, kind "Svg", defaults {},
894    fields { bytes: Vec<u8> = Vec::new(), width: Option<f64> = None, height: Option<f64> = None });
895
896impl SvgBuilder {
897    /// SVG bytes, typically `include_bytes!("../assets/logo.svg")` — the only
898    /// option under wasm.
899    ///
900    /// # Panics
901    ///
902    /// If the bytes are not an SVG `usvg` can parse.
903    pub fn src(mut self, bytes: &[u8]) -> Self {
904        self.bytes = bytes.to_vec();
905        self
906    }
907
908    /// Reads an SVG file (native only).
909    ///
910    /// # Panics
911    ///
912    /// If the file cannot be read or parsed.
913    #[cfg(not(target_arch = "wasm32"))]
914    pub fn file(self, path: impl AsRef<std::path::Path>) -> Self {
915        let path = path.as_ref();
916        let bytes =
917            std::fs::read(path).unwrap_or_else(|e| panic!("cannot read {}: {e}", path.display()));
918        self.src(&bytes)
919    }
920
921    /// Width in pixels. Height keeps the view box aspect unless also set.
922    pub fn width(mut self, v: f64) -> Self {
923        self.width = Some(v);
924        self
925    }
926
927    /// Height in pixels. Width keeps the view box aspect unless also set.
928    pub fn height(mut self, v: f64) -> Self {
929        self.height = Some(v);
930        self
931    }
932}
933
934impl NodeBuilder for SvgBuilder {
935    type Handle = crate::handles::Svg;
936    base_accessor!();
937    fn build(mut self, g: &SceneGraph, parent: NodeId) -> crate::handles::Svg {
938        if self.bytes.is_empty() {
939            panic!("Svg needs a source: call .src(bytes) or .file(path)");
940        }
941        let tree = usvg::Tree::from_data(&self.bytes, &usvg::Options::default())
942            .unwrap_or_else(|e| panic!("cannot parse svg: {e}"));
943        let size = tree.size();
944        let (nw, nh) = (f64::from(size.width()), f64::from(size.height()));
945        let aspect = if nh > 0.0 { nw / nh } else { 1.0 };
946        let (w, h) = match (self.width, self.height) {
947            (Some(w), Some(h)) => (w, h),
948            (Some(w), None) => (w, if aspect > 0.0 { w / aspect } else { w }),
949            (None, Some(h)) => (h * aspect, h),
950            (None, None) => (nw, nh),
951        };
952        if self.base.name.is_none()
953            && let Some(id) = (!tree.root().id().is_empty()).then(|| tree.root().id())
954        {
955            self.base.name = Some(id.to_string());
956        }
957        self.base.opacity *= f64::from(tree.root().opacity().get());
958        self.base.blend = crate::svg::blend(tree.root().blend_mode());
959        let k = Self::KIND;
960        let kind = NodeKind::Svg {
961            width: sig(g, k, "width", w),
962            height: sig(g, k, "height", h),
963        };
964        let id = insert(g, parent, kind, self.base);
965        let fit = crate::svg::Fit {
966            nw,
967            nh,
968            sx: if nw > 0.0 { w / nw } else { 1.0 },
969            sy: if nh > 0.0 { h / nh } else { 1.0 },
970        };
971        crate::svg::inflate(g, id, tree.root(), &fit);
972        crate::handles::Svg::from_id(id)
973    }
974}
975
976// ----------------------------------------------------------------- Layout
977
978builder!(
979    /// Places children in a row or column. Children's `x`/`y` become offsets
980    /// from their layout position.
981    LayoutBuilder => Layout, kind "Layout", defaults {},
982    fields { direction: Direction = Direction::Row, align: LayoutAlign = LayoutAlign::Center,
983             justify: Justify = Justify::Center, wrap: bool = false, columns: Option<u16> = None,
984             gap: f64 = 0.0, padding: f64 = 0.0,
985             width: Option<Span> = None, height: Option<Span> = None, radius: f64 = 0.0 });
986setters!(LayoutBuilder {
987    direction: Direction,
988    align: LayoutAlign,
989    justify: Justify
990});
991
992impl LayoutBuilder {
993    /// Space between children, in pixels or a theme step (`.gap(Sm)`).
994    pub fn gap(mut self, v: impl Len) -> Self {
995        self.gap = v.px();
996        self
997    }
998    /// Space inside the edges, in pixels or a theme step (`.padding(Md)`).
999    pub fn padding(mut self, v: impl Len) -> Self {
1000        self.padding = v.px();
1001        self
1002    }
1003    /// Corner radius, in pixels or a theme step.
1004    pub fn radius(mut self, v: impl Corner) -> Self {
1005        self.radius = v.radius();
1006        self
1007    }
1008}
1009
1010impl LayoutBuilder {
1011    /// Wrap children onto several lines when they do not fit.
1012    pub fn wrap(mut self, v: bool) -> Self {
1013        self.wrap = v;
1014        self
1015    }
1016    /// CSS grid with `n` equal columns instead of a row/column.
1017    pub fn grid(mut self, columns: u16) -> Self {
1018        self.columns = Some(columns.max(1));
1019        self
1020    }
1021    /// Children side by side (the default).
1022    pub fn row(self) -> Self {
1023        self.direction(Direction::Row)
1024    }
1025    /// Children stacked top to bottom.
1026    pub fn column(self) -> Self {
1027        self.direction(Direction::Column)
1028    }
1029    /// Fixed width in pixels or [`pct`](crate::pct)`(80)` (otherwise sized to content).
1030    pub fn width(mut self, v: impl Into<Span>) -> Self {
1031        self.width = Some(v.into());
1032        self
1033    }
1034    /// Fixed height in pixels or a percentage (otherwise sized to content).
1035    pub fn height(mut self, v: impl Into<Span>) -> Self {
1036        self.height = Some(v.into());
1037        self
1038    }
1039}
1040
1041impl NodeBuilder for LayoutBuilder {
1042    type Handle = Layout;
1043    base_accessor!();
1044    fn build(self, g: &SceneGraph, parent: NodeId) -> Layout {
1045        let k = Self::KIND;
1046        let kind = NodeKind::Layout {
1047            direction: self.direction,
1048            align: self.align,
1049            justify: self.justify,
1050            wrap: self.wrap,
1051            columns: self.columns,
1052            gap: sig(g, k, "gap", self.gap),
1053            padding: sig(g, k, "padding", self.padding),
1054            width: place::span_option(
1055                g,
1056                parent,
1057                Axis::X,
1058                self.width,
1059                format!("{k}#{}.width", g.next_id()),
1060            ),
1061            height: place::span_option(
1062                g,
1063                parent,
1064                Axis::Y,
1065                self.height,
1066                format!("{k}#{}.height", g.next_id()),
1067            ),
1068            radius: sig(g, k, "radius", self.radius),
1069        };
1070        Layout::from_id(insert(g, parent, kind, self.base))
1071    }
1072}
1073
1074// ------------------------------------------------------------------- Grid
1075
1076builder!(GridBuilder => Grid, kind "Grid",
1077    defaults { stroke: Color::rgba(1.0, 1.0, 1.0, 0.15), line_width: 1.0 },
1078    fields { width: f64 = 1920.0, height: f64 = 1080.0, spacing: f64 = 80.0 });
1079setters!(GridBuilder {
1080    width: f64,
1081    height: f64,
1082    spacing: f64
1083});
1084
1085impl NodeBuilder for GridBuilder {
1086    type Handle = Grid;
1087    base_accessor!();
1088    fn build(self, g: &SceneGraph, parent: NodeId) -> Grid {
1089        let k = Self::KIND;
1090        let kind = NodeKind::Grid {
1091            width: sig(g, k, "width", self.width),
1092            height: sig(g, k, "height", self.height),
1093            spacing: sig(g, k, "spacing", self.spacing),
1094        };
1095        Grid::from_id(insert(g, parent, kind, self.base))
1096    }
1097}
1098
1099// ------------------------------------------------------------------ Custom
1100
1101/// Builder for a [`Custom`] node.
1102#[must_use = "a builder does nothing until it is passed to `add`"]
1103pub struct CustomBuilder {
1104    base: BaseProps,
1105    draw: crate::draw::DrawFn,
1106    width: f64,
1107    height: f64,
1108    fullscreen: bool,
1109}
1110
1111impl CustomBuilder {
1112    pub(crate) fn from_draw(draw: impl Fn(&mut crate::draw::DrawCtx, f64) + 'static) -> Self {
1113        Self {
1114            base: BaseProps::default(),
1115            draw: std::rc::Rc::new(draw),
1116            width: 100.0,
1117            height: 100.0,
1118            fullscreen: false,
1119        }
1120    }
1121
1122    /// Layout box. The closure's shapes are not measured into this.
1123    pub fn size(mut self, width: f64, height: f64) -> Self {
1124        self.width = width;
1125        self.height = height;
1126        self.fullscreen = false;
1127        self
1128    }
1129
1130    /// Uses the frame as the box, centered on the parent. Put the node on
1131    /// the view when it should ignore a camera.
1132    pub fn fullscreen(mut self) -> Self {
1133        self.fullscreen = true;
1134        self
1135    }
1136}
1137
1138impl NodeBuilder for CustomBuilder {
1139    type Handle = Custom;
1140    base_accessor!();
1141    fn build(self, graph: &SceneGraph, parent: NodeId) -> Custom {
1142        let (width, height) = if self.fullscreen {
1143            graph.frame_size()
1144        } else {
1145            (self.width, self.height)
1146        };
1147        let kind = NodeKind::Custom {
1148            draw: self.draw,
1149            width,
1150            height,
1151        };
1152        Custom::from_id(insert(graph, parent, kind, self.base))
1153    }
1154}
1155
1156// -------------------------------------------------------------- Particles
1157
1158/// Builder for a [`Particles`] node.
1159#[must_use = "a builder does nothing until it is passed to `add`"]
1160pub struct ParticlesBuilder {
1161    base: BaseProps,
1162    seed: u64,
1163    gravity: f64,
1164    toward_camera: bool,
1165    life: f64,
1166    speed: f64,
1167    radius: f64,
1168}
1169
1170impl Default for ParticlesBuilder {
1171    fn default() -> Self {
1172        Self {
1173            base: BaseProps::default(),
1174            seed: 1,
1175            gravity: 980.0,
1176            toward_camera: false,
1177            life: 1.4,
1178            speed: 360.0,
1179            radius: 5.0,
1180        }
1181    }
1182}
1183
1184impl ParticlesBuilder {
1185    /// Seed of the burst generator. The same seed is the same spray.
1186    pub fn seed(mut self, seed: u64) -> Self {
1187        self.seed = seed;
1188        self
1189    }
1190
1191    /// Downward acceleration, in pixels per second squared.
1192    pub fn gravity(mut self, gravity: f64) -> Self {
1193        self.gravity = gravity;
1194        self
1195    }
1196
1197    /// Particles grow as they come toward the viewer. Their motion stays flat.
1198    pub fn toward_camera(mut self) -> Self {
1199        self.toward_camera = true;
1200        self
1201    }
1202
1203    /// How long each particle lives, in seconds.
1204    pub fn life(mut self, seconds: f64) -> Self {
1205        self.life = seconds;
1206        self
1207    }
1208
1209    /// Launch speed, in pixels per second.
1210    pub fn speed(mut self, speed: f64) -> Self {
1211        self.speed = speed;
1212        self
1213    }
1214
1215    /// Dot radius, in pixels, before the depth scale.
1216    pub fn radius(mut self, radius: f64) -> Self {
1217        self.radius = radius;
1218        self
1219    }
1220}
1221
1222impl NodeBuilder for ParticlesBuilder {
1223    type Handle = Particles;
1224    base_accessor!();
1225    fn build(self, graph: &SceneGraph, parent: NodeId) -> Particles {
1226        let kind = NodeKind::Particles {
1227            seed: self.seed,
1228            gravity: sig(graph, "Particles", "gravity", self.gravity),
1229            toward_camera: self.toward_camera,
1230            life: self.life,
1231            speed: self.speed,
1232            radius: self.radius,
1233            state: crate::sim::ParticlesState::new(),
1234        };
1235        Particles::from_id(insert(graph, parent, kind, self.base))
1236    }
1237}