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    Shader, 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.
331pub(crate) fn insert(
332    graph: &SceneGraph,
333    parent: NodeId,
334    kind: NodeKind,
335    mut base: BaseProps,
336) -> NodeId {
337    let children = std::mem::take(&mut base.children);
338    let id = graph.insert(Some(parent), kind, base);
339    for c in children {
340        (c.0)(graph, id);
341    }
342    id
343}
344
345fn sig<T: Clone + 'static>(graph: &SceneGraph, kind: &str, field: &str, v: T) -> Signal<T> {
346    Signal::new(v).with_name(format!("{kind}#{}.{field}", graph.next_id()))
347}
348
349macro_rules! builder {
350    (
351        $(#[$doc:meta])*
352        $builder:ident => $handle:ident, kind $kind:literal, defaults { $($bf:ident: $bv:expr),* $(,)? },
353        fields { $($field:ident: $ty:ty = $default:expr),* $(,)? }
354    ) => {
355        $(#[$doc])*
356        #[doc = ""]
357        #[doc = concat!("Builder for a [`", stringify!($handle), "`] node: set the initial values, then")]
358        #[doc = "pass it to `add`."]
359        #[derive(Debug)]
360        #[must_use = "a builder does nothing until it is passed to `add`"]
361        pub struct $builder {
362            base: BaseProps,
363            $(pub(crate) $field: $ty,)*
364        }
365
366        impl Default for $builder {
367            fn default() -> Self {
368                Self {
369                    base: BaseProps { $($bf: $bv,)* ..Default::default() },
370                    $($field: $default,)*
371                }
372            }
373        }
374
375        impl $builder {
376            #[allow(dead_code)]
377            const KIND: &'static str = $kind;
378        }
379    };
380}
381
382macro_rules! setters {
383    ($builder:ident { $($(#[$doc:meta])* $field:ident: $ty:ty),* $(,)? }) => {
384        impl $builder {
385            $(
386                $(#[$doc])*
387                #[doc = ""]
388                #[doc = concat!("Sets the initial `", stringify!($field), "`.")]
389                pub fn $field(mut self, v: impl Into<$ty>) -> Self {
390                    self.$field = v.into();
391                    self
392                }
393            )*
394        }
395    };
396}
397
398macro_rules! base_accessor {
399    () => {
400        fn base(&mut self) -> &mut BaseProps {
401            &mut self.base
402        }
403    };
404}
405
406// ------------------------------------------------------------------ Group
407
408builder!(GroupBuilder => Group, kind "Group", defaults {}, fields {});
409
410impl NodeBuilder for GroupBuilder {
411    type Handle = Group;
412    base_accessor!();
413    fn build(self, graph: &SceneGraph, parent: NodeId) -> Group {
414        Group::from_id(insert(graph, parent, NodeKind::Group, self.base))
415    }
416}
417
418// ----------------------------------------------------------------- Camera
419
420builder!(
421    /// A camera: everything added to it is seen through it, so animating its
422    /// `x`/`y` (the point it looks at), `zoom` and `rotation` pans, zooms
423    /// and turns the view. Nodes added to the view itself stay on screen
424    /// (titles, captions, a HUD).
425    CameraBuilder => Camera, kind "Camera", defaults {}, fields { zoom: f64 = 1.0 });
426setters!(CameraBuilder {
427    /// 1 shows the world at its size, 2 twice as big.
428    zoom: f64
429});
430
431impl NodeBuilder for CameraBuilder {
432    type Handle = Camera;
433    base_accessor!();
434    fn build(self, g: &SceneGraph, parent: NodeId) -> Camera {
435        let kind = NodeKind::Camera {
436            zoom: sig(g, Self::KIND, "zoom", self.zoom),
437        };
438        Camera::from_id(insert(g, parent, kind, self.base))
439    }
440}
441
442// ------------------------------------------------------------------- Rect
443
444builder!(RectBuilder => Rect, kind "Rect", defaults { fill: Color::WHITE, fill_explicit: true },
445    fields { width: Span = Span::Px(100.0), height: Span = Span::Px(100.0), radius: f64 = 0.0 });
446
447impl RectBuilder {
448    /// Width in pixels, or [`pct`](crate::pct)`(80)` for a fraction of the frame.
449    pub fn width(mut self, v: impl Into<Span>) -> Self {
450        self.width = v.into();
451        self
452    }
453    /// Height in pixels, or a percentage of the frame.
454    pub fn height(mut self, v: impl Into<Span>) -> Self {
455        self.height = v.into();
456        self
457    }
458    /// Corner radius, in pixels or a theme step (`.radius(Md)`).
459    /// Clamped to half of the shorter side when drawn.
460    pub fn radius(mut self, v: impl Corner) -> Self {
461        self.radius = v.radius();
462        self
463    }
464    /// Sets width and height to the same pixel size.
465    pub fn size(mut self, v: f64) -> Self {
466        (self.width, self.height) = (Span::Px(v), Span::Px(v));
467        self
468    }
469}
470
471impl NodeBuilder for RectBuilder {
472    type Handle = Rect;
473    base_accessor!();
474    fn build(self, g: &SceneGraph, parent: NodeId) -> Rect {
475        let k = Self::KIND;
476        let n = g.next_id();
477        let kind = NodeKind::Rect {
478            width: place::span_signal(g, parent, Axis::X, self.width, format!("{k}#{n}.width")),
479            height: place::span_signal(g, parent, Axis::Y, self.height, format!("{k}#{n}.height")),
480            radius: sig(g, k, "radius", self.radius),
481        };
482        Rect::from_id(insert(g, parent, kind, self.base))
483    }
484}
485
486// ----------------------------------------------------------------- Circle
487
488builder!(CircleBuilder => Circle, kind "Circle", defaults { fill: Color::WHITE, fill_explicit: true },
489    fields { size: f64 = 100.0, start_angle: f64 = 0.0, end_angle: f64 = 360.0, pie: bool = false });
490setters!(CircleBuilder {
491    /// Diameter.
492    size: f64,
493    /// Degrees clockwise from 12 o'clock.
494    start_angle: f64,
495    end_angle: f64,
496    /// Close the arc through the center (pie slice).
497    pie: bool
498});
499
500impl NodeBuilder for CircleBuilder {
501    type Handle = Circle;
502    base_accessor!();
503    fn build(self, g: &SceneGraph, parent: NodeId) -> Circle {
504        let k = Self::KIND;
505        let kind = NodeKind::Circle {
506            size: sig(g, k, "size", self.size),
507            start_angle: sig(g, k, "start_angle", self.start_angle),
508            end_angle: sig(g, k, "end_angle", self.end_angle),
509            pie: self.pie,
510        };
511        Circle::from_id(insert(g, parent, kind, self.base))
512    }
513}
514
515// ---------------------------------------------------------------- Polygon
516
517builder!(PolygonBuilder => Polygon, kind "Polygon", defaults { fill: Color::WHITE, fill_explicit: true },
518    fields { sides: f64 = 6.0, size: f64 = 100.0 });
519setters!(PolygonBuilder {
520    /// Number of sides (rounded; animatable as a number).
521    sides: f64,
522    /// Diameter of the circumscribed circle.
523    size: f64
524});
525
526impl NodeBuilder for PolygonBuilder {
527    type Handle = Polygon;
528    base_accessor!();
529    fn build(self, g: &SceneGraph, parent: NodeId) -> Polygon {
530        let k = Self::KIND;
531        let kind = NodeKind::Polygon {
532            sides: sig(g, k, "sides", self.sides),
533            size: sig(g, k, "size", self.size),
534        };
535        Polygon::from_id(insert(g, parent, kind, self.base))
536    }
537}
538
539// ------------------------------------------------------------------- Star
540
541builder!(StarBuilder => Star, kind "Star", defaults { fill: Color::WHITE, fill_explicit: true },
542    fields { points: f64 = 5.0, size: f64 = 100.0, inner: f64 = 0.45 });
543setters!(StarBuilder {
544    points: f64,
545    /// Outer diameter.
546    size: f64,
547    /// Inner radius as a fraction of the outer one.
548    inner: f64
549});
550
551impl NodeBuilder for StarBuilder {
552    type Handle = Star;
553    base_accessor!();
554    fn build(self, g: &SceneGraph, parent: NodeId) -> Star {
555        let k = Self::KIND;
556        let kind = NodeKind::Star {
557            points: sig(g, k, "points", self.points),
558            size: sig(g, k, "size", self.size),
559            inner: sig(g, k, "inner", self.inner),
560        };
561        Star::from_id(insert(g, parent, kind, self.base))
562    }
563}
564
565// ------------------------------------------------------------------- Path
566
567builder!(PathBuilder => Path, kind "Path", defaults { fill: Color::WHITE, fill_explicit: true },
568    fields { data: BezPath = BezPath::new() });
569setters!(PathBuilder { data: BezPath });
570
571impl PathBuilder {
572    /// Path from SVG path syntax (`"M0 0 L10 0 Z"`).
573    ///
574    /// # Panics
575    ///
576    /// If `d` is not valid SVG path data.
577    pub fn svg(mut self, d: &str) -> Self {
578        self.data = BezPath::from_svg(d).unwrap_or_else(|e| panic!("invalid SVG path {d:?}: {e}"));
579        self
580    }
581}
582
583impl NodeBuilder for PathBuilder {
584    type Handle = Path;
585    base_accessor!();
586    fn build(self, g: &SceneGraph, parent: NodeId) -> Path {
587        let kind = NodeKind::Path {
588            data: sig(g, Self::KIND, "data", self.data),
589        };
590        Path::from_id(insert(g, parent, kind, self.base))
591    }
592}
593
594// ------------------------------------------------------------------- Line
595
596builder!(LineBuilder => Line, kind "Line",
597    defaults { stroke: Color::WHITE, line_width: 4.0 },
598    fields { points: Vec<Vec2> = Vec::new(), closed: bool = false, smooth: bool = false,
599             start_arrow: bool = false, end_arrow: bool = false, arrow_size: f64 = 24.0 });
600setters!(LineBuilder {
601    closed: bool,
602    /// Smooth curve through the points instead of straight segments.
603    smooth: bool,
604    start_arrow: bool,
605    end_arrow: bool,
606    arrow_size: f64
607});
608
609impl LineBuilder {
610    /// Points the line passes through, in local coordinates.
611    pub fn points<P: Into<Vec2>>(mut self, pts: impl IntoIterator<Item = P>) -> Self {
612        self.points = pts.into_iter().map(Into::into).collect();
613        self
614    }
615}
616
617impl NodeBuilder for LineBuilder {
618    type Handle = Line;
619    base_accessor!();
620    fn build(self, g: &SceneGraph, parent: NodeId) -> Line {
621        let kind = NodeKind::Line {
622            points: sig(g, Self::KIND, "points", self.points),
623            closed: self.closed,
624            smooth: self.smooth,
625            start_arrow: self.start_arrow,
626            end_arrow: self.end_arrow,
627            arrow_size: sig(g, Self::KIND, "arrow_size", self.arrow_size),
628        };
629        Line::from_id(insert(g, parent, kind, self.base))
630    }
631}
632
633// -------------------------------------------------------------------- Txt
634
635builder!(TxtBuilder => Txt, kind "Txt", defaults { fill: Color::WHITE },
636    fields { text: String = String::new(), spans: Vec<RichSpan> = Vec::new(),
637             style: TextStyle = TextStyle::default() });
638
639impl TxtBuilder {
640    /// The text to show.
641    pub fn text(mut self, v: impl Into<String>) -> Self {
642        self.text = v.into();
643        self
644    }
645    /// Styled runs instead of plain text: `[span("Hello "), span("world").bold()]`.
646    pub fn rich(mut self, spans: impl IntoIterator<Item = RichSpan>) -> Self {
647        self.spans = spans.into_iter().collect();
648        self
649    }
650    /// Font size in pixels. Default: 48.
651    pub fn font_size(mut self, v: f64) -> Self {
652        self.style.size = v;
653        self
654    }
655    /// 100..=900.
656    pub fn weight(mut self, v: f64) -> Self {
657        self.style.weight = v;
658        self
659    }
660    /// [`SANS`](crate::SANS), [`MONO`](crate::MONO) or a family registered
661    /// with `oxframe_text::register_font`.
662    pub fn family(mut self, v: impl Into<String>) -> Self {
663        self.style.family = v.into();
664        self
665    }
666    /// Italic style.
667    pub fn italic(mut self, v: bool) -> Self {
668        self.style.italic = v;
669        self
670    }
671    /// Multiple of the font size.
672    pub fn line_height(mut self, v: f64) -> Self {
673        self.style.line_height = v;
674        self
675    }
676    /// Extra space between letters, in pixels.
677    pub fn letter_spacing(mut self, v: f64) -> Self {
678        self.style.letter_spacing = v;
679        self
680    }
681    /// Alignment of lines within the text block.
682    pub fn align(mut self, v: crate::TextAlign) -> Self {
683        self.style.align = v;
684        self
685    }
686    /// Wrap width.
687    pub fn max_width(mut self, v: f64) -> Self {
688        self.style.max_width = Some(v);
689        self
690    }
691    /// Largest title from the theme.
692    pub fn display(self) -> Self {
693        self.role(TypeRole::Display)
694    }
695    /// Page title from the theme.
696    pub fn h1(self) -> Self {
697        self.role(TypeRole::H1)
698    }
699    /// Section title from the theme.
700    pub fn h2(self) -> Self {
701        self.role(TypeRole::H2)
702    }
703    /// Running text from the theme.
704    pub fn body(self) -> Self {
705        self.role(TypeRole::Body)
706    }
707    /// Secondary text from the theme.
708    pub fn caption(self) -> Self {
709        self.role(TypeRole::Caption)
710    }
711    /// Monospace text from the theme.
712    pub fn code(self) -> Self {
713        self.family(crate::MONO).role(TypeRole::Code)
714    }
715
716    fn role(mut self, role: TypeRole) -> Self {
717        let r = theme::current().role(role);
718        self.style.size = r.size;
719        self.style.weight = r.weight;
720        self.style.line_height = r.line_height;
721        self
722    }
723}
724
725impl NodeBuilder for TxtBuilder {
726    type Handle = Txt;
727    base_accessor!();
728    fn build(mut self, g: &SceneGraph, parent: NodeId) -> Txt {
729        if !self.base.fill_explicit {
730            self.base.fill = theme::current().text;
731        }
732        let k = Self::KIND;
733        let kind = NodeKind::Txt {
734            text: sig(g, k, "text", self.text),
735            spans: sig(g, k, "spans", self.spans),
736            font_size: sig(g, k, "font_size", self.style.size),
737            weight: sig(g, k, "weight", self.style.weight),
738            style: self.style,
739            morph: sig(g, k, "morph", None),
740            morph_t: sig(g, k, "morph_t", 0.0),
741        };
742        Txt::from_id(insert(g, parent, kind, self.base))
743    }
744}
745
746// ------------------------------------------------------------------- Code
747
748builder!(CodeBuilder => Code, kind "Code", defaults {},
749    fields { code: String = String::new(), lang: oxframe_text::Lang = oxframe_text::Lang::Rust,
750             theme: CodeTheme = CodeTheme::dark(), theme_set: bool = false, style: TextStyle = TextStyle {
751                 family: oxframe_text::MONO.to_owned(), size: 32.0, line_height: 1.45, ..TextStyle::default()
752             }, focus: Vec2 = Vec2::ZERO, dim: f64 = 0.3 });
753
754impl CodeBuilder {
755    /// The source code to show.
756    pub fn code(mut self, v: impl Into<String>) -> Self {
757        self.code = v.into();
758        self
759    }
760    /// "rust", "js", "ts", "python", "json", "toml", "shell"; anything else
761    /// is plain text.
762    pub fn language(mut self, name: &str) -> Self {
763        self.lang = oxframe_text::Lang::parse(name);
764        self
765    }
766    /// Syntax highlighting colors. Without this, a light project theme uses
767    /// [`CodeTheme::light`] and a dark one uses [`CodeTheme::dark`].
768    pub fn theme(mut self, t: CodeTheme) -> Self {
769        self.theme = t;
770        self.theme_set = true;
771        self
772    }
773    /// Font size in pixels.
774    pub fn font_size(mut self, v: f64) -> Self {
775        self.style.size = v;
776        self
777    }
778    /// Line height as a multiple of the font size.
779    pub fn line_height(mut self, v: f64) -> Self {
780        self.style.line_height = v;
781        self
782    }
783    /// Highlight lines `first..=last` (1-based); others are dimmed.
784    pub fn focus(mut self, first: usize, last: usize) -> Self {
785        self.focus = Vec2::new(first as f64, last as f64);
786        self
787    }
788    /// Alpha of unfocused lines (default 0.3).
789    pub fn dim(mut self, v: f64) -> Self {
790        self.dim = v;
791        self
792    }
793}
794
795impl NodeBuilder for CodeBuilder {
796    type Handle = Code;
797    base_accessor!();
798    fn build(self, g: &SceneGraph, parent: NodeId) -> Code {
799        let k = Self::KIND;
800        let theme = if self.theme_set {
801            self.theme
802        } else if theme::current().is_light() {
803            CodeTheme::light()
804        } else {
805            CodeTheme::dark()
806        };
807        let kind = NodeKind::Code {
808            code: sig(g, k, "code", self.code),
809            edit_from: sig(g, k, "edit_from", String::new()),
810            edit_to: sig(g, k, "edit_to", String::new()),
811            editing: sig(g, k, "editing", false),
812            edit_t: sig(g, k, "edit_t", 0.0),
813            edit_flash: sig(g, k, "edit_flash", false),
814            lang: self.lang,
815            theme,
816            font_size: sig(g, k, "font_size", self.style.size),
817            style: self.style,
818            focus: sig(g, k, "focus", self.focus),
819            dim: sig(g, k, "dim", self.dim),
820        };
821        Code::from_id(insert(g, parent, kind, self.base))
822    }
823}
824
825// -------------------------------------------------------------------- Img
826
827builder!(ImgBuilder => Img, kind "Img", defaults {},
828    fields { source: Option<ImageSource> = None, width: Option<f64> = None, height: Option<f64> = None });
829
830impl ImgBuilder {
831    /// PNG, JPEG or SVG bytes, typically `include_bytes!("../assets/x.png")`
832    /// — the only option under wasm.
833    ///
834    /// # Panics
835    ///
836    /// If the bytes are not a PNG, JPEG or SVG image.
837    pub fn src(mut self, bytes: &[u8]) -> Self {
838        self.source =
839            Some(ImageSource::decode(bytes).unwrap_or_else(|e| panic!("cannot decode image: {e}")));
840        self
841    }
842
843    /// Reads an image file (native only).
844    ///
845    /// # Panics
846    ///
847    /// If the file cannot be read or decoded.
848    #[cfg(not(target_arch = "wasm32"))]
849    pub fn file(self, path: impl AsRef<std::path::Path>) -> Self {
850        let path = path.as_ref();
851        let bytes =
852            std::fs::read(path).unwrap_or_else(|e| panic!("cannot read {}: {e}", path.display()));
853        self.src(&bytes)
854    }
855
856    /// Width; height follows the aspect ratio unless also set.
857    pub fn width(mut self, v: f64) -> Self {
858        self.width = Some(v);
859        self
860    }
861
862    /// Height; width follows the aspect ratio unless also set.
863    pub fn height(mut self, v: f64) -> Self {
864        self.height = Some(v);
865        self
866    }
867}
868
869impl NodeBuilder for ImgBuilder {
870    type Handle = Img;
871    base_accessor!();
872    fn build(self, g: &SceneGraph, parent: NodeId) -> Img {
873        let source = self
874            .source
875            .expect("Img needs a source: call .src(bytes) or .file(path)");
876        let (nw, nh) = source.natural_size();
877        let aspect = if nh > 0.0 { nw / nh } else { 1.0 };
878        let (w, h) = match (self.width, self.height) {
879            (Some(w), Some(h)) => (w, h),
880            (Some(w), None) => (w, w / aspect),
881            (None, Some(h)) => (h * aspect, h),
882            (None, None) => (nw, nh),
883        };
884        let k = Self::KIND;
885        let kind = NodeKind::Img {
886            source,
887            width: sig(g, k, "width", w),
888            height: sig(g, k, "height", h),
889        };
890        Img::from_id(insert(g, parent, kind, self.base))
891    }
892}
893
894// -------------------------------------------------------------------- Svg
895
896builder!(
897    /// An SVG file, expanded into groups, paths and images.
898    SvgBuilder => Svg, kind "Svg", defaults {},
899    fields { bytes: Vec<u8> = Vec::new(), width: Option<Span> = None, height: Option<Span> = None });
900
901impl SvgBuilder {
902    /// SVG bytes, typically `include_bytes!("../assets/logo.svg")` — the only
903    /// option under wasm.
904    ///
905    /// # Panics
906    ///
907    /// If the bytes are not an SVG `usvg` can parse.
908    pub fn src(mut self, bytes: &[u8]) -> Self {
909        self.bytes = bytes.to_vec();
910        self
911    }
912
913    /// Reads an SVG file (native only).
914    ///
915    /// # Panics
916    ///
917    /// If the file cannot be read or parsed.
918    #[cfg(not(target_arch = "wasm32"))]
919    pub fn file(self, path: impl AsRef<std::path::Path>) -> Self {
920        let path = path.as_ref();
921        let bytes =
922            std::fs::read(path).unwrap_or_else(|e| panic!("cannot read {}: {e}", path.display()));
923        self.src(&bytes)
924    }
925
926    /// Width in pixels, or [`pct`](crate::pct) of the parent's width when
927    /// the node is added. Height keeps the view box aspect unless also set.
928    pub fn width(mut self, v: impl Into<Span>) -> Self {
929        self.width = Some(v.into());
930        self
931    }
932
933    /// Height in pixels, or a percentage of the parent's height when the
934    /// node is added. Width keeps the view box aspect unless also set.
935    pub fn height(mut self, v: impl Into<Span>) -> Self {
936        self.height = Some(v.into());
937        self
938    }
939}
940
941impl NodeBuilder for SvgBuilder {
942    type Handle = crate::handles::Svg;
943    base_accessor!();
944    fn build(mut self, g: &SceneGraph, parent: NodeId) -> crate::handles::Svg {
945        if self.bytes.is_empty() {
946            panic!("Svg needs a source: call .src(bytes) or .file(path)");
947        }
948        let tree = usvg::Tree::from_data(&self.bytes, &usvg::Options::default())
949            .unwrap_or_else(|e| panic!("cannot parse svg: {e}"));
950        let size = tree.size();
951        let (nw, nh) = (f64::from(size.width()), f64::from(size.height()));
952        let aspect = if nh > 0.0 { nw / nh } else { 1.0 };
953        // The paths are scaled once, so a percentage is read now.
954        let px = |span: Option<Span>, axis: Axis| {
955            span.map(|s| place::span_signal(g, parent, axis, s, String::new()).get())
956        };
957        let (w, h) = match (px(self.width, Axis::X), px(self.height, Axis::Y)) {
958            (Some(w), Some(h)) => (w, h),
959            (Some(w), None) => (w, if aspect > 0.0 { w / aspect } else { w }),
960            (None, Some(h)) => (h * aspect, h),
961            (None, None) => (nw, nh),
962        };
963        if self.base.name.is_none()
964            && let Some(id) = (!tree.root().id().is_empty()).then(|| tree.root().id())
965        {
966            self.base.name = Some(id.to_string());
967        }
968        self.base.opacity *= f64::from(tree.root().opacity().get());
969        self.base.blend = crate::svg::blend(tree.root().blend_mode());
970        let k = Self::KIND;
971        let kind = NodeKind::Svg {
972            width: sig(g, k, "width", w),
973            height: sig(g, k, "height", h),
974        };
975        let id = insert(g, parent, kind, self.base);
976        let fit = crate::svg::Fit {
977            nw,
978            nh,
979            sx: if nw > 0.0 { w / nw } else { 1.0 },
980            sy: if nh > 0.0 { h / nh } else { 1.0 },
981        };
982        crate::svg::inflate(g, id, tree.root(), &fit);
983        crate::handles::Svg::from_id(id)
984    }
985}
986
987// ----------------------------------------------------------------- Layout
988
989builder!(
990    /// Places children in a row or column. Children's `x`/`y` become offsets
991    /// from their layout position.
992    LayoutBuilder => Layout, kind "Layout", defaults {},
993    fields { direction: Direction = Direction::Row, align: LayoutAlign = LayoutAlign::Center,
994             justify: Justify = Justify::Center, wrap: bool = false, columns: Option<u16> = None,
995             gap: f64 = 0.0, padding: f64 = 0.0,
996             width: Option<Span> = None, height: Option<Span> = None, radius: f64 = 0.0 });
997setters!(LayoutBuilder {
998    direction: Direction,
999    align: LayoutAlign,
1000    justify: Justify
1001});
1002
1003impl LayoutBuilder {
1004    /// Space between children, in pixels or a theme step (`.gap(Sm)`).
1005    pub fn gap(mut self, v: impl Len) -> Self {
1006        self.gap = v.px();
1007        self
1008    }
1009    /// Space inside the edges, in pixels or a theme step (`.padding(Md)`).
1010    pub fn padding(mut self, v: impl Len) -> Self {
1011        self.padding = v.px();
1012        self
1013    }
1014    /// Corner radius, in pixels or a theme step.
1015    pub fn radius(mut self, v: impl Corner) -> Self {
1016        self.radius = v.radius();
1017        self
1018    }
1019}
1020
1021impl LayoutBuilder {
1022    /// Wrap children onto several lines when they do not fit.
1023    pub fn wrap(mut self, v: bool) -> Self {
1024        self.wrap = v;
1025        self
1026    }
1027    /// CSS grid with `n` equal columns instead of a row/column.
1028    pub fn grid(mut self, columns: u16) -> Self {
1029        self.columns = Some(columns.max(1));
1030        self
1031    }
1032    /// Children side by side (the default).
1033    pub fn row(self) -> Self {
1034        self.direction(Direction::Row)
1035    }
1036    /// Children stacked top to bottom.
1037    pub fn column(self) -> Self {
1038        self.direction(Direction::Column)
1039    }
1040    /// Fixed width in pixels or [`pct`](crate::pct)`(80)` (otherwise sized to content).
1041    pub fn width(mut self, v: impl Into<Span>) -> Self {
1042        self.width = Some(v.into());
1043        self
1044    }
1045    /// Fixed height in pixels or a percentage (otherwise sized to content).
1046    pub fn height(mut self, v: impl Into<Span>) -> Self {
1047        self.height = Some(v.into());
1048        self
1049    }
1050}
1051
1052impl NodeBuilder for LayoutBuilder {
1053    type Handle = Layout;
1054    base_accessor!();
1055    fn build(self, g: &SceneGraph, parent: NodeId) -> Layout {
1056        let k = Self::KIND;
1057        let kind = NodeKind::Layout {
1058            direction: self.direction,
1059            align: self.align,
1060            justify: self.justify,
1061            wrap: self.wrap,
1062            columns: self.columns,
1063            gap: sig(g, k, "gap", self.gap),
1064            padding: sig(g, k, "padding", self.padding),
1065            width: place::span_option(
1066                g,
1067                parent,
1068                Axis::X,
1069                self.width,
1070                format!("{k}#{}.width", g.next_id()),
1071            ),
1072            height: place::span_option(
1073                g,
1074                parent,
1075                Axis::Y,
1076                self.height,
1077                format!("{k}#{}.height", g.next_id()),
1078            ),
1079            radius: sig(g, k, "radius", self.radius),
1080        };
1081        Layout::from_id(insert(g, parent, kind, self.base))
1082    }
1083}
1084
1085// ------------------------------------------------------------------- Grid
1086
1087builder!(GridBuilder => Grid, kind "Grid",
1088    defaults { stroke: Color::rgba(1.0, 1.0, 1.0, 0.15), line_width: 1.0 },
1089    fields { width: f64 = 1920.0, height: f64 = 1080.0, spacing: f64 = 80.0 });
1090setters!(GridBuilder {
1091    width: f64,
1092    height: f64,
1093    spacing: f64
1094});
1095
1096impl NodeBuilder for GridBuilder {
1097    type Handle = Grid;
1098    base_accessor!();
1099    fn build(self, g: &SceneGraph, parent: NodeId) -> Grid {
1100        let k = Self::KIND;
1101        let kind = NodeKind::Grid {
1102            width: sig(g, k, "width", self.width),
1103            height: sig(g, k, "height", self.height),
1104            spacing: sig(g, k, "spacing", self.spacing),
1105        };
1106        Grid::from_id(insert(g, parent, kind, self.base))
1107    }
1108}
1109
1110// ------------------------------------------------------------------ Custom
1111
1112/// Builder for a [`Custom`] node.
1113#[must_use = "a builder does nothing until it is passed to `add`"]
1114pub struct CustomBuilder {
1115    base: BaseProps,
1116    draw: crate::draw::DrawFn,
1117    width: f64,
1118    height: f64,
1119    fullscreen: bool,
1120}
1121
1122impl CustomBuilder {
1123    pub(crate) fn from_draw(draw: impl Fn(&mut crate::draw::DrawCtx, f64) + 'static) -> Self {
1124        Self {
1125            base: BaseProps::default(),
1126            draw: std::rc::Rc::new(draw),
1127            width: 100.0,
1128            height: 100.0,
1129            fullscreen: false,
1130        }
1131    }
1132
1133    /// Layout box. The closure's shapes are not measured into this.
1134    pub fn size(mut self, width: f64, height: f64) -> Self {
1135        self.width = width;
1136        self.height = height;
1137        self.fullscreen = false;
1138        self
1139    }
1140
1141    /// Uses the frame as the box, centered on the parent. Put the node on
1142    /// the view when it should ignore a camera.
1143    pub fn fullscreen(mut self) -> Self {
1144        self.fullscreen = true;
1145        self
1146    }
1147}
1148
1149impl NodeBuilder for CustomBuilder {
1150    type Handle = Custom;
1151    base_accessor!();
1152    fn build(self, graph: &SceneGraph, parent: NodeId) -> Custom {
1153        let (width, height) = if self.fullscreen {
1154            graph.frame_size()
1155        } else {
1156            (self.width, self.height)
1157        };
1158        let kind = NodeKind::Custom {
1159            draw: self.draw,
1160            width,
1161            height,
1162            measure: None,
1163            state: None,
1164            type_name: "Custom",
1165        };
1166        Custom::from_id(insert(graph, parent, kind, self.base))
1167    }
1168}
1169
1170// -------------------------------------------------------------- Particles
1171
1172/// Builder for a [`Particles`] node.
1173#[must_use = "a builder does nothing until it is passed to `add`"]
1174pub struct ParticlesBuilder {
1175    base: BaseProps,
1176    seed: u64,
1177    gravity: f64,
1178    toward_camera: bool,
1179    life: f64,
1180    speed: f64,
1181    radius: f64,
1182}
1183
1184impl Default for ParticlesBuilder {
1185    fn default() -> Self {
1186        Self {
1187            base: BaseProps::default(),
1188            seed: 1,
1189            gravity: 980.0,
1190            toward_camera: false,
1191            life: 1.4,
1192            speed: 360.0,
1193            radius: 5.0,
1194        }
1195    }
1196}
1197
1198impl ParticlesBuilder {
1199    /// Seed of the burst generator. The same seed is the same spray.
1200    pub fn seed(mut self, seed: u64) -> Self {
1201        self.seed = seed;
1202        self
1203    }
1204
1205    /// Downward acceleration, in pixels per second squared.
1206    pub fn gravity(mut self, gravity: f64) -> Self {
1207        self.gravity = gravity;
1208        self
1209    }
1210
1211    /// Particles grow as they come toward the viewer. Their motion stays flat.
1212    pub fn toward_camera(mut self) -> Self {
1213        self.toward_camera = true;
1214        self
1215    }
1216
1217    /// How long each particle lives, in seconds.
1218    pub fn life(mut self, seconds: f64) -> Self {
1219        self.life = seconds;
1220        self
1221    }
1222
1223    /// Launch speed, in pixels per second.
1224    pub fn speed(mut self, speed: f64) -> Self {
1225        self.speed = speed;
1226        self
1227    }
1228
1229    /// Dot radius, in pixels, before the depth scale.
1230    pub fn radius(mut self, radius: f64) -> Self {
1231        self.radius = radius;
1232        self
1233    }
1234}
1235
1236impl NodeBuilder for ParticlesBuilder {
1237    type Handle = Particles;
1238    base_accessor!();
1239    fn build(self, graph: &SceneGraph, parent: NodeId) -> Particles {
1240        let kind = NodeKind::Particles {
1241            seed: self.seed,
1242            gravity: sig(graph, "Particles", "gravity", self.gravity),
1243            toward_camera: self.toward_camera,
1244            life: self.life,
1245            speed: self.speed,
1246            radius: self.radius,
1247            state: crate::sim::ParticlesState::new(),
1248        };
1249        Particles::from_id(insert(graph, parent, kind, self.base))
1250    }
1251}
1252
1253// ------------------------------------------------------------------ Shader
1254
1255enum Seed {
1256    Num(f64),
1257    Vec2(f64, f64),
1258    Color(Color),
1259    Bool(bool),
1260}
1261
1262/// Builder for a [`Shader`] node.
1263#[must_use = "a builder does nothing until it is passed to `add`"]
1264pub struct ShaderBuilder {
1265    base: BaseProps,
1266    code: crate::shader::ShaderCode,
1267    preset: crate::shader::Preset,
1268    width: Span,
1269    height: Span,
1270    fullscreen: bool,
1271    uniforms: Vec<(String, Seed)>,
1272    inputs: Vec<(String, crate::post::Input)>,
1273}
1274
1275impl ShaderBuilder {
1276    pub(crate) fn new(code: crate::shader::ShaderCode) -> Self {
1277        Self {
1278            base: BaseProps::default(),
1279            code,
1280            preset: crate::shader::Preset::Custom,
1281            width: Span::Px(100.0),
1282            height: Span::Px(100.0),
1283            fullscreen: false,
1284            uniforms: Vec::new(),
1285            inputs: Vec::new(),
1286        }
1287    }
1288
1289    pub(crate) fn preset() -> Self {
1290        let mut s = Self::new(crate::shader::ShaderCode::Wgsl(
1291            crate::shader::waves_wgsl().into(),
1292        ));
1293        s.preset = crate::shader::Preset::Waves;
1294        s.uniform("amp", 0.05)
1295            .uniform("speed", 1.0)
1296            .uniform_color("deep", Color::rgba(0.102_f32, 0.322_f32, 0.549_f32, 1.0))
1297            .uniform_color("foam", Color::rgba(0.910_f32, 0.953_f32, 0.980_f32, 1.0))
1298    }
1299
1300    /// Width in pixels, or [`pct`](crate::pct)`(100)` for the whole frame.
1301    pub fn width(mut self, v: impl Into<Span>) -> Self {
1302        self.width = v.into();
1303        self.fullscreen = false;
1304        self
1305    }
1306
1307    /// Height in pixels, or a percentage of the frame.
1308    pub fn height(mut self, v: impl Into<Span>) -> Self {
1309        self.height = v.into();
1310        self.fullscreen = false;
1311        self
1312    }
1313
1314    /// Uses the frame as the box. Put the node on the view so a camera
1315    /// does not move it.
1316    pub fn fullscreen(mut self) -> Self {
1317        self.fullscreen = true;
1318        self
1319    }
1320
1321    /// Sets a numeric `Params` field. A name that is not in the shader
1322    /// fails `check` (`param_unknown`).
1323    pub fn uniform(mut self, name: impl Into<String>, value: f64) -> Self {
1324        self.set_uniform(name.into(), Seed::Num(value));
1325        self
1326    }
1327
1328    /// Sets a `vec2<f32>` field from a point.
1329    pub fn uniform_vec2(mut self, name: impl Into<String>, value: impl Into<Vec2>) -> Self {
1330        let v = value.into();
1331        self.set_uniform(name.into(), Seed::Vec2(v.x, v.y));
1332        self
1333    }
1334
1335    /// Sets a `vec4<f32>` field from a color or a theme role such as `Accent`.
1336    pub fn uniform_color(mut self, name: impl Into<String>, value: impl IntoPaint) -> Self {
1337        self.set_uniform(name.into(), Seed::Color(value.into_paint()));
1338        self
1339    }
1340
1341    /// Sets a `bool` field.
1342    pub fn uniform_bool(mut self, name: impl Into<String>, value: bool) -> Self {
1343        self.set_uniform(name.into(), Seed::Bool(value));
1344        self
1345    }
1346
1347    /// Wave height, as a fraction of the box. The preset default is 0.05.
1348    pub fn amp(self, value: f64) -> Self {
1349        self.uniform("amp", value)
1350    }
1351
1352    /// How fast the crest travels. The preset default is 1.
1353    pub fn speed(self, value: f64) -> Self {
1354        self.uniform("speed", value)
1355    }
1356
1357    /// Color below the crest. A theme role such as `Accent` follows the theme.
1358    pub fn deep(self, value: impl IntoPaint) -> Self {
1359        self.uniform_color("deep", value)
1360    }
1361
1362    /// Color of the crest.
1363    pub fn foam(self, value: impl IntoPaint) -> Self {
1364        self.uniform_color("foam", value)
1365    }
1366
1367    /// Names what the shader reads. `lens_drops` reads `"scene"`, which is
1368    /// the frame drawn before it whether or not this is called. A name the
1369    /// preset does not read fails `check` (`param_unknown`).
1370    pub fn input(mut self, name: impl Into<String>, input: crate::post::Input) -> Self {
1371        let name = name.into();
1372        if let Some(slot) = self.inputs.iter_mut().find(|(n, _)| *n == name) {
1373            slot.1 = input;
1374        } else {
1375            self.inputs.push((name, input));
1376        }
1377        self
1378    }
1379
1380    /// How many drops cover the lens of `lens_drops`, 0 to 1. The preset
1381    /// default is 0.6; start from 0 to keep the lens dry until a splash.
1382    pub fn wetness(self, value: f64) -> Self {
1383        self.uniform("wetness", value)
1384    }
1385
1386    /// Drop layout of `lens_drops`. The same seed places the same drops.
1387    pub fn seed(self, value: u64) -> Self {
1388        self.uniform("seed", value as f64)
1389    }
1390
1391    /// Drop size of `lens_drops`, as a multiple of the default.
1392    pub fn drop_size(self, value: f64) -> Self {
1393        self.uniform("drop_size", value)
1394    }
1395
1396    fn set_uniform(&mut self, name: String, value: Seed) {
1397        if let Some(slot) = self.uniforms.iter_mut().find(|(n, _)| *n == name) {
1398            slot.1 = value;
1399        } else {
1400            self.uniforms.push((name, value));
1401        }
1402    }
1403}
1404
1405impl NodeBuilder for ShaderBuilder {
1406    type Handle = Shader;
1407    base_accessor!();
1408    fn build(self, graph: &SceneGraph, parent: NodeId) -> Shader {
1409        let (width, height) = if self.fullscreen {
1410            let (w, h) = graph.frame_size();
1411            (Span::Px(w), Span::Px(h))
1412        } else {
1413            (self.width, self.height)
1414        };
1415        let n = graph.next_id();
1416        let uniforms = self
1417            .uniforms
1418            .into_iter()
1419            .map(|(name, seed)| {
1420                let value = match seed {
1421                    Seed::Num(v) => crate::shader::UniformValue::Num(
1422                        Signal::new(v).with_name(format!("Shader#{n}.{name}")),
1423                    ),
1424                    Seed::Vec2(x, y) => crate::shader::UniformValue::Vec2(
1425                        Signal::new(Vec2::new(x, y)).with_name(format!("Shader#{n}.{name}")),
1426                    ),
1427                    Seed::Color(c) => crate::shader::UniformValue::Color(
1428                        Signal::new(c).with_name(format!("Shader#{n}.{name}")),
1429                    ),
1430                    Seed::Bool(v) => crate::shader::UniformValue::Bool(
1431                        Signal::new(if v { 1.0 } else { 0.0 })
1432                            .with_name(format!("Shader#{n}.{name}")),
1433                    ),
1434                };
1435                crate::shader::Uniform { name, value }
1436            })
1437            .collect();
1438        let kind = NodeKind::Shader {
1439            code: self.code,
1440            preset: self.preset,
1441            width: place::span_signal(graph, parent, Axis::X, width, format!("Shader#{n}.width")),
1442            height: place::span_signal(
1443                graph,
1444                parent,
1445                Axis::Y,
1446                height,
1447                format!("Shader#{n}.height"),
1448            ),
1449            uniforms,
1450            inputs: self.inputs,
1451        };
1452        Shader::from_id(insert(graph, parent, kind, self.base))
1453    }
1454}
1455
1456/// The waves preset: a crest of `foam` over `deep` water.
1457pub fn waves() -> ShaderBuilder {
1458    ShaderBuilder::preset()
1459}
1460
1461/// The lens_drops preset: water drops on the lens that magnify and flip
1462/// what was drawn before this node, inside its box.
1463///
1464/// Add it last and make it [`fullscreen`](ShaderBuilder::fullscreen) to
1465/// wet the whole frame. Animate [`Shader::wetness`](crate::Shader::wetness)
1466/// for a splash.
1467pub fn lens_drops() -> ShaderBuilder {
1468    let mut s = ShaderBuilder::new(crate::shader::ShaderCode::Wgsl(String::new()));
1469    s.preset = crate::shader::Preset::LensDrops;
1470    s.uniform("wetness", 0.6)
1471        .uniform("seed", 0.0)
1472        .uniform("drop_size", 1.0)
1473}
1474
1475// ------------------------------------------------------------------ Effect
1476
1477/// A shader effect for [`Node::effect`](crate::Node::effect).
1478#[must_use = "an effect does nothing until it is passed to `effect`"]
1479pub struct EffectBuilder {
1480    kind: crate::post::EffectKind,
1481    strength: f64,
1482    speed: f64,
1483}
1484
1485impl EffectBuilder {
1486    /// 0 leaves the node alone, 1 is the full effect. Default 1.
1487    pub fn strength(mut self, value: f64) -> Self {
1488        self.strength = value;
1489        self
1490    }
1491
1492    /// How fast the rings travel. Default 6.
1493    pub fn speed(mut self, value: f64) -> Self {
1494        self.speed = value;
1495        self
1496    }
1497
1498    pub(crate) fn build(self, id: NodeId) -> crate::post::EffectData {
1499        crate::post::EffectData {
1500            kind: self.kind,
1501            strength: Signal::new(self.strength).with_name(format!("Effect#{}.strength", id.0)),
1502            speed: Signal::new(self.speed).with_name(format!("Effect#{}.speed", id.0)),
1503        }
1504    }
1505}
1506
1507/// Rings that push a node's colors out from its center, like a drop on
1508/// water. The node's outline stays, so a flat fill shows no rings: use it
1509/// on text, a gradient, an image or water. Put it on a node with
1510/// [`Node::effect`](crate::Node::effect).
1511pub fn ripple() -> EffectBuilder {
1512    EffectBuilder {
1513        kind: crate::post::EffectKind::Ripple,
1514        strength: 1.0,
1515        speed: 6.0,
1516    }
1517}
1518
1519// ------------------------------------------------------------------ Video
1520
1521/// Builder for a [`Video`](crate::Video) node.
1522#[must_use = "a builder does nothing until it is passed to `add`"]
1523pub struct VideoBuilder {
1524    base: BaseProps,
1525    path: Option<std::path::PathBuf>,
1526    width: Option<Span>,
1527    height: Option<Span>,
1528    radius: f64,
1529    muted: bool,
1530    rate: f64,
1531    start: f64,
1532}
1533
1534impl VideoBuilder {
1535    pub(crate) fn new() -> Self {
1536        Self {
1537            base: BaseProps::default(),
1538            path: None,
1539            width: None,
1540            height: None,
1541            radius: 0.0,
1542            muted: false,
1543            rate: 1.0,
1544            start: 0.0,
1545        }
1546    }
1547
1548    /// The clip, relative to the project root: MP4 (H.264), WebM (VP9, with
1549    /// alpha) or ProRes 4444 (with alpha).
1550    pub fn file(mut self, path: impl AsRef<std::path::Path>) -> Self {
1551        self.path = Some(path.as_ref().to_path_buf());
1552        self
1553    }
1554
1555    /// Width in pixels or `pct()` of the parent. Height follows the clip's
1556    /// aspect unless also set.
1557    pub fn width(mut self, v: impl Into<Span>) -> Self {
1558        self.width = Some(v.into());
1559        self
1560    }
1561
1562    /// Height in pixels or `pct()` of the parent.
1563    pub fn height(mut self, v: impl Into<Span>) -> Self {
1564        self.height = Some(v.into());
1565        self
1566    }
1567
1568    /// Corner radius of the frame.
1569    pub fn radius(mut self, v: f64) -> Self {
1570        self.radius = v;
1571        self
1572    }
1573
1574    /// Leaves the clip's sound out of the mix.
1575    pub fn muted(mut self, v: bool) -> Self {
1576        self.muted = v;
1577        self
1578    }
1579
1580    /// Starting playback rate; `playback_rate()` animates it.
1581    pub fn playback_rate(mut self, v: f64) -> Self {
1582        self.rate = v;
1583        self
1584    }
1585
1586    /// Starts the clip at `seconds` into the file.
1587    pub fn start_at(mut self, seconds: f64) -> Self {
1588        self.start = seconds.max(0.0);
1589        self
1590    }
1591}
1592
1593impl NodeBuilder for VideoBuilder {
1594    type Handle = crate::Video;
1595    base_accessor!();
1596    fn build(self, g: &SceneGraph, parent: NodeId) -> crate::Video {
1597        let path = self.path.unwrap_or_default();
1598        let (source, problem) = crate::video::open(&path);
1599        let info = source.info();
1600        let aspect = f64::from(info.width.max(1)) / f64::from(info.height.max(1));
1601        let px = |span: Option<Span>, axis: Axis| {
1602            span.map(|s| place::span_signal(g, parent, axis, s, String::new()).get())
1603        };
1604        let (w, h) = match (px(self.width, Axis::X), px(self.height, Axis::Y)) {
1605            (Some(w), Some(h)) => (w, h),
1606            (Some(w), None) => (w, w / aspect),
1607            (None, Some(h)) => (h * aspect, h),
1608            (None, None) => (f64::from(info.width), f64::from(info.height)),
1609        };
1610        let n = g.next_id();
1611        let kind = NodeKind::Video {
1612            data: std::rc::Rc::new(crate::video::VideoData {
1613                source,
1614                path,
1615                problem,
1616                muted: self.muted,
1617                playback: std::rc::Rc::new(crate::video::Playback::default()),
1618            }),
1619            width: Signal::new(w).with_name(format!("Video#{n}.width")),
1620            height: Signal::new(h).with_name(format!("Video#{n}.height")),
1621            time: Signal::new(self.start).with_name(format!("Video#{n}.time")),
1622            rate: Signal::new(self.rate).with_name(format!("Video#{n}.playback_rate")),
1623            radius: Signal::new(self.radius).with_name(format!("Video#{n}.radius")),
1624        };
1625        crate::Video::from_id(insert(g, parent, kind, self.base))
1626    }
1627}