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