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    Circle, Code, Grid, Group, Img, Layout, Line, Path, Polygon, Rect, Star, Txt,
9};
10use crate::image::ImageSource;
11use crate::text::{CodeTheme, RichSpan};
12
13/// Initial values shared by every node; set through [`NodeBuilder`] methods.
14#[doc(hidden)]
15#[derive(Debug)]
16pub struct BaseProps {
17    pub name: Option<String>,
18    pub x: f64,
19    pub y: f64,
20    pub rotation: f64,
21    pub scale: Vec2,
22    pub opacity: f64,
23    pub fill: Color,
24    pub stroke: Color,
25    pub line_width: f64,
26    pub start: f64,
27    pub end: f64,
28    pub fill_gradient: Option<Gradient>,
29    pub stroke_gradient: Option<Gradient>,
30    pub dash: Vec<f64>,
31    pub dash_offset: f64,
32    pub line_cap: LineCap,
33    pub line_join: LineJoin,
34    pub blur: f64,
35    pub shadow_color: Color,
36    pub shadow_blur: f64,
37    pub shadow_offset: Vec2,
38    pub clip: bool,
39    pub blend: BlendMode,
40    pub z_index: f64,
41    pub anchor: Vec2,
42    pub grow: f64,
43    pub shrink: f64,
44    pub basis: Option<f64>,
45    pub margin: f64,
46    pub align_self: Option<LayoutAlign>,
47    pub children: Vec<ChildBuilder>,
48}
49
50impl Default for BaseProps {
51    fn default() -> Self {
52        Self {
53            name: None,
54            x: 0.0,
55            y: 0.0,
56            rotation: 0.0,
57            scale: Vec2::new(1.0, 1.0),
58            opacity: 1.0,
59            fill: Color::TRANSPARENT,
60            stroke: Color::TRANSPARENT,
61            line_width: 0.0,
62            start: 0.0,
63            end: 1.0,
64            fill_gradient: None,
65            stroke_gradient: None,
66            dash: Vec::new(),
67            dash_offset: 0.0,
68            line_cap: LineCap::Round,
69            line_join: LineJoin::Round,
70            blur: 0.0,
71            shadow_color: Color::TRANSPARENT,
72            shadow_blur: 0.0,
73            shadow_offset: Vec2::ZERO,
74            clip: false,
75            blend: BlendMode::Normal,
76            z_index: 0.0,
77            anchor: Vec2::ZERO,
78            grow: 0.0,
79            shrink: 0.0,
80            basis: None,
81            margin: 0.0,
82            align_self: None,
83            children: Vec::new(),
84        }
85    }
86}
87
88/// A type-erased child added with [`NodeBuilder::child`].
89type BuildFn = Box<dyn FnOnce(&SceneGraph, NodeId)>;
90
91/// A child declared with [`NodeBuilder::child`], built together with its parent.
92pub struct ChildBuilder(BuildFn);
93
94impl std::fmt::Debug for ChildBuilder {
95    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
96        f.write_str("ChildBuilder")
97    }
98}
99
100/// Describes a node before it is added to the scene.
101pub trait NodeBuilder: Sized + 'static {
102    /// Handle returned by [`Node::add`](crate::Node::add).
103    type Handle;
104
105    #[doc(hidden)]
106    fn base(&mut self) -> &mut BaseProps;
107
108    #[doc(hidden)]
109    fn build(self, graph: &SceneGraph, parent: NodeId) -> Self::Handle;
110
111    /// Name shown in the editor's node tree.
112    fn name(mut self, v: impl Into<String>) -> Self {
113        self.base().name = Some(v.into());
114        self
115    }
116    /// Horizontal position; 0 is the parent's center, positive is right.
117    fn x(mut self, v: f64) -> Self {
118        self.base().x = v;
119        self
120    }
121    /// Vertical position; 0 is the parent's center, positive is down.
122    fn y(mut self, v: f64) -> Self {
123        self.base().y = v;
124        self
125    }
126    /// Sets `x` and `y` together, e.g. `.position((100.0, -50.0))`.
127    fn position(mut self, v: impl Into<Vec2>) -> Self {
128        let v = v.into();
129        let b = self.base();
130        (b.x, b.y) = (v.x, v.y);
131        self
132    }
133    /// Degrees, clockwise.
134    fn rotation(mut self, v: f64) -> Self {
135        self.base().rotation = v;
136        self
137    }
138    /// Uniform scale; 1 is the natural size.
139    fn scale(mut self, v: f64) -> Self {
140        self.base().scale = Vec2::new(v, v);
141        self
142    }
143    /// Separate horizontal and vertical scale.
144    fn scale_xy(mut self, v: impl Into<Vec2>) -> Self {
145        self.base().scale = v.into();
146        self
147    }
148    /// Opacity of the node and its children, 0 (invisible) to 1.
149    fn opacity(mut self, v: f64) -> Self {
150        self.base().opacity = v;
151        self
152    }
153    /// Fill color. Default: transparent.
154    fn fill(mut self, v: Color) -> Self {
155        self.base().fill = v;
156        self
157    }
158    /// Outline color; also set `line_width`. Default: transparent.
159    fn stroke(mut self, v: Color) -> Self {
160        self.base().stroke = v;
161        self
162    }
163    /// Outline width in pixels. Default: 0 (no outline).
164    fn line_width(mut self, v: f64) -> Self {
165        self.base().line_width = v;
166        self
167    }
168    /// Start of the drawn part of the outline, fraction of its length.
169    fn start(mut self, v: f64) -> Self {
170        self.base().start = v;
171        self
172    }
173    /// End of the drawn part of the outline; animate 0 → 1 to draw it on.
174    fn end(mut self, v: f64) -> Self {
175        self.base().end = v;
176        self
177    }
178    /// Gradient fill (local coordinates, origin at the node center);
179    /// overrides `fill`.
180    fn fill_gradient(mut self, g: Gradient) -> Self {
181        self.base().fill_gradient = Some(g);
182        self
183    }
184    /// Gradient outline; overrides `stroke`.
185    fn stroke_gradient(mut self, g: Gradient) -> Self {
186        self.base().stroke_gradient = Some(g);
187        self
188    }
189    /// Dash pattern: on, off, on, … lengths. Animate `dash_offset` for
190    /// marching ants.
191    fn dash(mut self, pattern: impl Into<Vec<f64>>) -> Self {
192        self.base().dash = pattern.into();
193        self
194    }
195    /// Shifts the dash pattern along the outline.
196    fn dash_offset(mut self, v: f64) -> Self {
197        self.base().dash_offset = v;
198        self
199    }
200    /// Shape of outline ends. Default: round.
201    fn line_cap(mut self, v: LineCap) -> Self {
202        self.base().line_cap = v;
203        self
204    }
205    /// Shape of outline corners. Default: round.
206    fn line_join(mut self, v: LineJoin) -> Self {
207        self.base().line_join = v;
208        self
209    }
210    /// Gaussian blur of the node and its children (standard deviation, in
211    /// the node's units).
212    fn blur(mut self, v: f64) -> Self {
213        self.base().blur = v;
214        self
215    }
216    /// Drop shadow under the node and its children.
217    fn shadow(mut self, color: Color, blur: f64, offset: impl Into<Vec2>) -> Self {
218        let b = self.base();
219        (b.shadow_color, b.shadow_blur, b.shadow_offset) = (color, blur, offset.into());
220        self
221    }
222    /// A shadow without offset.
223    fn glow(self, color: Color, blur: f64) -> Self {
224        self.shadow(color, blur, Vec2::ZERO)
225    }
226    /// Clip children to this node's outline (or box).
227    fn clip(mut self, v: bool) -> Self {
228        self.base().clip = v;
229        self
230    }
231    /// How the node is composited onto what is below it.
232    fn blend(mut self, v: BlendMode) -> Self {
233        self.base().blend = v;
234        self
235    }
236    /// Paint order among siblings; higher is on top.
237    fn z_index(mut self, v: f64) -> Self {
238        self.base().z_index = v;
239        self
240    }
241    /// Pivot and placement point: (-1, -1) top-left … (1, 1) bottom-right.
242    fn anchor(mut self, v: impl Into<Vec2>) -> Self {
243        self.base().anchor = v.into();
244        self
245    }
246    /// Inside a `Layout`: share of the free space this node takes.
247    fn grow(mut self, v: f64) -> Self {
248        self.base().grow = v;
249        self
250    }
251    /// Inside a `Layout`: how much this node gives up when space is short.
252    fn shrink(mut self, v: f64) -> Self {
253        self.base().shrink = v;
254        self
255    }
256    /// Inside a `Layout`: main-axis size before growing/shrinking.
257    fn basis(mut self, v: f64) -> Self {
258        self.base().basis = Some(v);
259        self
260    }
261    /// Inside a `Layout`: space around this node.
262    fn margin(mut self, v: f64) -> Self {
263        self.base().margin = v;
264        self
265    }
266    /// Inside a `Layout`: overrides the layout's cross-axis alignment.
267    fn align_self(mut self, v: LayoutAlign) -> Self {
268        self.base().align_self = Some(v);
269        self
270    }
271    /// Adds a child created together with this node. Use `Node::add` when
272    /// you need the child's handle.
273    fn child<C: NodeBuilder>(mut self, child: C) -> Self {
274        self.base()
275            .children
276            .push(ChildBuilder(Box::new(move |g, parent| {
277                child.build(g, parent);
278            })));
279        self
280    }
281}
282
283pub(crate) fn add<B: NodeBuilder>(parent: NodeId, builder: B) -> B::Handle {
284    with_graph(|g| builder.build(g, parent))
285}
286
287/// Inserts the node and then its declared children.
288fn insert(graph: &SceneGraph, parent: NodeId, kind: NodeKind, mut base: BaseProps) -> NodeId {
289    let children = std::mem::take(&mut base.children);
290    let id = graph.insert(Some(parent), kind, base);
291    for c in children {
292        (c.0)(graph, id);
293    }
294    id
295}
296
297fn sig<T: Clone + 'static>(graph: &SceneGraph, kind: &str, field: &str, v: T) -> Signal<T> {
298    Signal::new(v).with_name(format!("{kind}#{}.{field}", graph.next_id()))
299}
300
301macro_rules! builder {
302    (
303        $(#[$doc:meta])*
304        $builder:ident => $handle:ident, kind $kind:literal, defaults { $($bf:ident: $bv:expr),* $(,)? },
305        fields { $($field:ident: $ty:ty = $default:expr),* $(,)? }
306    ) => {
307        $(#[$doc])*
308        #[doc = ""]
309        #[doc = concat!("Builder for a [`", stringify!($handle), "`] node: set the initial values, then")]
310        #[doc = "pass it to `add`."]
311        #[derive(Debug)]
312        #[must_use = "a builder does nothing until it is passed to `add`"]
313        pub struct $builder {
314            base: BaseProps,
315            $(pub(crate) $field: $ty,)*
316        }
317
318        impl Default for $builder {
319            fn default() -> Self {
320                Self {
321                    base: BaseProps { $($bf: $bv,)* ..Default::default() },
322                    $($field: $default,)*
323                }
324            }
325        }
326
327        impl $builder {
328            #[allow(dead_code)]
329            const KIND: &'static str = $kind;
330        }
331    };
332}
333
334macro_rules! setters {
335    ($builder:ident { $($(#[$doc:meta])* $field:ident: $ty:ty),* $(,)? }) => {
336        impl $builder {
337            $(
338                $(#[$doc])*
339                #[doc = ""]
340                #[doc = concat!("Sets the initial `", stringify!($field), "`.")]
341                pub fn $field(mut self, v: impl Into<$ty>) -> Self {
342                    self.$field = v.into();
343                    self
344                }
345            )*
346        }
347    };
348}
349
350macro_rules! base_accessor {
351    () => {
352        fn base(&mut self) -> &mut BaseProps {
353            &mut self.base
354        }
355    };
356}
357
358// ------------------------------------------------------------------ Group
359
360builder!(GroupBuilder => Group, kind "Group", defaults {}, fields {});
361
362impl NodeBuilder for GroupBuilder {
363    type Handle = Group;
364    base_accessor!();
365    fn build(self, graph: &SceneGraph, parent: NodeId) -> Group {
366        Group::from_id(insert(graph, parent, NodeKind::Group, self.base))
367    }
368}
369
370// ------------------------------------------------------------------- Rect
371
372builder!(RectBuilder => Rect, kind "Rect", defaults { fill: Color::WHITE },
373    fields { width: f64 = 100.0, height: f64 = 100.0, radius: f64 = 0.0 });
374setters!(RectBuilder {
375    width: f64,
376    height: f64,
377    /// Corner radius; clamped to half of the shorter side when drawn.
378    radius: f64
379});
380
381impl RectBuilder {
382    /// Sets width and height.
383    pub fn size(mut self, v: f64) -> Self {
384        (self.width, self.height) = (v, v);
385        self
386    }
387}
388
389impl NodeBuilder for RectBuilder {
390    type Handle = Rect;
391    base_accessor!();
392    fn build(self, g: &SceneGraph, parent: NodeId) -> Rect {
393        let k = Self::KIND;
394        let kind = NodeKind::Rect {
395            width: sig(g, k, "width", self.width),
396            height: sig(g, k, "height", self.height),
397            radius: sig(g, k, "radius", self.radius),
398        };
399        Rect::from_id(insert(g, parent, kind, self.base))
400    }
401}
402
403// ----------------------------------------------------------------- Circle
404
405builder!(CircleBuilder => Circle, kind "Circle", defaults { fill: Color::WHITE },
406    fields { size: f64 = 100.0, start_angle: f64 = 0.0, end_angle: f64 = 360.0, pie: bool = false });
407setters!(CircleBuilder {
408    /// Diameter.
409    size: f64,
410    /// Degrees clockwise from 12 o'clock.
411    start_angle: f64,
412    end_angle: f64,
413    /// Close the arc through the center (pie slice).
414    pie: bool
415});
416
417impl NodeBuilder for CircleBuilder {
418    type Handle = Circle;
419    base_accessor!();
420    fn build(self, g: &SceneGraph, parent: NodeId) -> Circle {
421        let k = Self::KIND;
422        let kind = NodeKind::Circle {
423            size: sig(g, k, "size", self.size),
424            start_angle: sig(g, k, "start_angle", self.start_angle),
425            end_angle: sig(g, k, "end_angle", self.end_angle),
426            pie: self.pie,
427        };
428        Circle::from_id(insert(g, parent, kind, self.base))
429    }
430}
431
432// ---------------------------------------------------------------- Polygon
433
434builder!(PolygonBuilder => Polygon, kind "Polygon", defaults { fill: Color::WHITE },
435    fields { sides: f64 = 6.0, size: f64 = 100.0 });
436setters!(PolygonBuilder {
437    /// Number of sides (rounded; animatable as a number).
438    sides: f64,
439    /// Diameter of the circumscribed circle.
440    size: f64
441});
442
443impl NodeBuilder for PolygonBuilder {
444    type Handle = Polygon;
445    base_accessor!();
446    fn build(self, g: &SceneGraph, parent: NodeId) -> Polygon {
447        let k = Self::KIND;
448        let kind = NodeKind::Polygon {
449            sides: sig(g, k, "sides", self.sides),
450            size: sig(g, k, "size", self.size),
451        };
452        Polygon::from_id(insert(g, parent, kind, self.base))
453    }
454}
455
456// ------------------------------------------------------------------- Star
457
458builder!(StarBuilder => Star, kind "Star", defaults { fill: Color::WHITE },
459    fields { points: f64 = 5.0, size: f64 = 100.0, inner: f64 = 0.45 });
460setters!(StarBuilder {
461    points: f64,
462    /// Outer diameter.
463    size: f64,
464    /// Inner radius as a fraction of the outer one.
465    inner: f64
466});
467
468impl NodeBuilder for StarBuilder {
469    type Handle = Star;
470    base_accessor!();
471    fn build(self, g: &SceneGraph, parent: NodeId) -> Star {
472        let k = Self::KIND;
473        let kind = NodeKind::Star {
474            points: sig(g, k, "points", self.points),
475            size: sig(g, k, "size", self.size),
476            inner: sig(g, k, "inner", self.inner),
477        };
478        Star::from_id(insert(g, parent, kind, self.base))
479    }
480}
481
482// ------------------------------------------------------------------- Path
483
484builder!(PathBuilder => Path, kind "Path", defaults { fill: Color::WHITE },
485    fields { data: BezPath = BezPath::new() });
486setters!(PathBuilder { data: BezPath });
487
488impl PathBuilder {
489    /// Path from SVG path syntax (`"M0 0 L10 0 Z"`).
490    ///
491    /// # Panics
492    ///
493    /// If `d` is not valid SVG path data.
494    pub fn svg(mut self, d: &str) -> Self {
495        self.data = BezPath::from_svg(d).unwrap_or_else(|e| panic!("invalid SVG path {d:?}: {e}"));
496        self
497    }
498}
499
500impl NodeBuilder for PathBuilder {
501    type Handle = Path;
502    base_accessor!();
503    fn build(self, g: &SceneGraph, parent: NodeId) -> Path {
504        let kind = NodeKind::Path {
505            data: sig(g, Self::KIND, "data", self.data),
506        };
507        Path::from_id(insert(g, parent, kind, self.base))
508    }
509}
510
511// ------------------------------------------------------------------- Line
512
513builder!(LineBuilder => Line, kind "Line",
514    defaults { stroke: Color::WHITE, line_width: 4.0 },
515    fields { points: Vec<Vec2> = Vec::new(), closed: bool = false, smooth: bool = false,
516             start_arrow: bool = false, end_arrow: bool = false, arrow_size: f64 = 24.0 });
517setters!(LineBuilder {
518    closed: bool,
519    /// Smooth curve through the points instead of straight segments.
520    smooth: bool,
521    start_arrow: bool,
522    end_arrow: bool,
523    arrow_size: f64
524});
525
526impl LineBuilder {
527    /// Points the line passes through, in local coordinates.
528    pub fn points<P: Into<Vec2>>(mut self, pts: impl IntoIterator<Item = P>) -> Self {
529        self.points = pts.into_iter().map(Into::into).collect();
530        self
531    }
532}
533
534impl NodeBuilder for LineBuilder {
535    type Handle = Line;
536    base_accessor!();
537    fn build(self, g: &SceneGraph, parent: NodeId) -> Line {
538        let kind = NodeKind::Line {
539            points: sig(g, Self::KIND, "points", self.points),
540            closed: self.closed,
541            smooth: self.smooth,
542            start_arrow: self.start_arrow,
543            end_arrow: self.end_arrow,
544            arrow_size: sig(g, Self::KIND, "arrow_size", self.arrow_size),
545        };
546        Line::from_id(insert(g, parent, kind, self.base))
547    }
548}
549
550// -------------------------------------------------------------------- Txt
551
552builder!(TxtBuilder => Txt, kind "Txt", defaults { fill: Color::WHITE },
553    fields { text: String = String::new(), spans: Vec<RichSpan> = Vec::new(),
554             style: TextStyle = TextStyle::default() });
555
556impl TxtBuilder {
557    /// The text to show.
558    pub fn text(mut self, v: impl Into<String>) -> Self {
559        self.text = v.into();
560        self
561    }
562    /// Styled runs instead of plain text: `[span("Hello "), span("world").bold()]`.
563    pub fn rich(mut self, spans: impl IntoIterator<Item = RichSpan>) -> Self {
564        self.spans = spans.into_iter().collect();
565        self
566    }
567    /// Font size in pixels. Default: 48.
568    pub fn font_size(mut self, v: f64) -> Self {
569        self.style.size = v;
570        self
571    }
572    /// 100..=900.
573    pub fn weight(mut self, v: f64) -> Self {
574        self.style.weight = v;
575        self
576    }
577    /// [`SANS`](crate::SANS), [`MONO`](crate::MONO) or a family registered
578    /// with `oxframe_text::register_font`.
579    pub fn family(mut self, v: impl Into<String>) -> Self {
580        self.style.family = v.into();
581        self
582    }
583    /// Italic style.
584    pub fn italic(mut self, v: bool) -> Self {
585        self.style.italic = v;
586        self
587    }
588    /// Multiple of the font size.
589    pub fn line_height(mut self, v: f64) -> Self {
590        self.style.line_height = v;
591        self
592    }
593    /// Extra space between letters, in pixels.
594    pub fn letter_spacing(mut self, v: f64) -> Self {
595        self.style.letter_spacing = v;
596        self
597    }
598    /// Alignment of lines within the text block.
599    pub fn align(mut self, v: crate::TextAlign) -> Self {
600        self.style.align = v;
601        self
602    }
603    /// Wrap width.
604    pub fn max_width(mut self, v: f64) -> Self {
605        self.style.max_width = Some(v);
606        self
607    }
608}
609
610impl NodeBuilder for TxtBuilder {
611    type Handle = Txt;
612    base_accessor!();
613    fn build(self, g: &SceneGraph, parent: NodeId) -> Txt {
614        let k = Self::KIND;
615        let kind = NodeKind::Txt {
616            text: sig(g, k, "text", self.text),
617            spans: sig(g, k, "spans", self.spans),
618            font_size: sig(g, k, "font_size", self.style.size),
619            weight: sig(g, k, "weight", self.style.weight),
620            style: self.style,
621            morph: sig(g, k, "morph", None),
622            morph_t: sig(g, k, "morph_t", 0.0),
623        };
624        Txt::from_id(insert(g, parent, kind, self.base))
625    }
626}
627
628// ------------------------------------------------------------------- Code
629
630builder!(CodeBuilder => Code, kind "Code", defaults {},
631    fields { code: String = String::new(), lang: oxframe_text::Lang = oxframe_text::Lang::Rust,
632             theme: CodeTheme = CodeTheme::dark(), style: TextStyle = TextStyle {
633                 family: oxframe_text::MONO.to_owned(), size: 32.0, line_height: 1.45, ..TextStyle::default()
634             }, focus: Vec2 = Vec2::ZERO, dim: f64 = 0.3 });
635
636impl CodeBuilder {
637    /// The source code to show.
638    pub fn code(mut self, v: impl Into<String>) -> Self {
639        self.code = v.into();
640        self
641    }
642    /// "rust", "js", "ts", "python", "json", "toml", "shell"; anything else
643    /// is plain text.
644    pub fn language(mut self, name: &str) -> Self {
645        self.lang = oxframe_text::Lang::parse(name);
646        self
647    }
648    /// Syntax highlighting colors.
649    pub fn theme(mut self, t: CodeTheme) -> Self {
650        self.theme = t;
651        self
652    }
653    /// Font size in pixels.
654    pub fn font_size(mut self, v: f64) -> Self {
655        self.style.size = v;
656        self
657    }
658    /// Line height as a multiple of the font size.
659    pub fn line_height(mut self, v: f64) -> Self {
660        self.style.line_height = v;
661        self
662    }
663    /// Highlight lines `first..=last` (1-based); others are dimmed.
664    pub fn focus(mut self, first: usize, last: usize) -> Self {
665        self.focus = Vec2::new(first as f64, last as f64);
666        self
667    }
668    /// Alpha of unfocused lines (default 0.3).
669    pub fn dim(mut self, v: f64) -> Self {
670        self.dim = v;
671        self
672    }
673}
674
675impl NodeBuilder for CodeBuilder {
676    type Handle = Code;
677    base_accessor!();
678    fn build(self, g: &SceneGraph, parent: NodeId) -> Code {
679        let k = Self::KIND;
680        let kind = NodeKind::Code {
681            code: sig(g, k, "code", self.code),
682            lang: self.lang,
683            theme: self.theme,
684            font_size: sig(g, k, "font_size", self.style.size),
685            style: self.style,
686            focus: sig(g, k, "focus", self.focus),
687            dim: sig(g, k, "dim", self.dim),
688        };
689        Code::from_id(insert(g, parent, kind, self.base))
690    }
691}
692
693// -------------------------------------------------------------------- Img
694
695builder!(ImgBuilder => Img, kind "Img", defaults {},
696    fields { source: Option<ImageSource> = None, width: Option<f64> = None, height: Option<f64> = None });
697
698impl ImgBuilder {
699    /// PNG, JPEG or SVG bytes, typically `include_bytes!("../assets/x.png")`
700    /// — the only option under wasm.
701    ///
702    /// # Panics
703    ///
704    /// If the bytes are not a PNG, JPEG or SVG image.
705    pub fn src(mut self, bytes: &[u8]) -> Self {
706        self.source =
707            Some(ImageSource::decode(bytes).unwrap_or_else(|e| panic!("cannot decode image: {e}")));
708        self
709    }
710
711    /// Reads an image file (native only).
712    ///
713    /// # Panics
714    ///
715    /// If the file cannot be read or decoded.
716    #[cfg(not(target_arch = "wasm32"))]
717    pub fn file(self, path: impl AsRef<std::path::Path>) -> Self {
718        let path = path.as_ref();
719        let bytes =
720            std::fs::read(path).unwrap_or_else(|e| panic!("cannot read {}: {e}", path.display()));
721        self.src(&bytes)
722    }
723
724    /// Width; height follows the aspect ratio unless also set.
725    pub fn width(mut self, v: f64) -> Self {
726        self.width = Some(v);
727        self
728    }
729
730    /// Height; width follows the aspect ratio unless also set.
731    pub fn height(mut self, v: f64) -> Self {
732        self.height = Some(v);
733        self
734    }
735}
736
737impl NodeBuilder for ImgBuilder {
738    type Handle = Img;
739    base_accessor!();
740    fn build(self, g: &SceneGraph, parent: NodeId) -> Img {
741        let source = self
742            .source
743            .expect("Img needs a source: call .src(bytes) or .file(path)");
744        let (nw, nh) = source.natural_size();
745        let aspect = if nh > 0.0 { nw / nh } else { 1.0 };
746        let (w, h) = match (self.width, self.height) {
747            (Some(w), Some(h)) => (w, h),
748            (Some(w), None) => (w, w / aspect),
749            (None, Some(h)) => (h * aspect, h),
750            (None, None) => (nw, nh),
751        };
752        let k = Self::KIND;
753        let kind = NodeKind::Img {
754            source,
755            width: sig(g, k, "width", w),
756            height: sig(g, k, "height", h),
757        };
758        Img::from_id(insert(g, parent, kind, self.base))
759    }
760}
761
762// ----------------------------------------------------------------- Layout
763
764builder!(
765    /// Places children in a row or column. Children's `x`/`y` become offsets
766    /// from their layout position.
767    LayoutBuilder => Layout, kind "Layout", defaults {},
768    fields { direction: Direction = Direction::Row, align: LayoutAlign = LayoutAlign::Center,
769             justify: Justify = Justify::Center, wrap: bool = false, columns: Option<u16> = None,
770             gap: f64 = 0.0, padding: f64 = 0.0,
771             width: Option<f64> = None, height: Option<f64> = None, radius: f64 = 0.0 });
772setters!(LayoutBuilder {
773    direction: Direction,
774    align: LayoutAlign,
775    justify: Justify,
776    gap: f64,
777    padding: f64,
778    radius: f64
779});
780
781impl LayoutBuilder {
782    /// Wrap children onto several lines when they do not fit.
783    pub fn wrap(mut self, v: bool) -> Self {
784        self.wrap = v;
785        self
786    }
787    /// CSS grid with `n` equal columns instead of a row/column.
788    pub fn grid(mut self, columns: u16) -> Self {
789        self.columns = Some(columns.max(1));
790        self
791    }
792    /// Children side by side (the default).
793    pub fn row(self) -> Self {
794        self.direction(Direction::Row)
795    }
796    /// Children stacked top to bottom.
797    pub fn column(self) -> Self {
798        self.direction(Direction::Column)
799    }
800    /// Fixed width (otherwise sized to content).
801    pub fn width(mut self, v: f64) -> Self {
802        self.width = Some(v);
803        self
804    }
805    /// Fixed height (otherwise sized to content).
806    pub fn height(mut self, v: f64) -> Self {
807        self.height = Some(v);
808        self
809    }
810}
811
812impl NodeBuilder for LayoutBuilder {
813    type Handle = Layout;
814    base_accessor!();
815    fn build(self, g: &SceneGraph, parent: NodeId) -> Layout {
816        let k = Self::KIND;
817        let kind = NodeKind::Layout {
818            direction: self.direction,
819            align: self.align,
820            justify: self.justify,
821            wrap: self.wrap,
822            columns: self.columns,
823            gap: sig(g, k, "gap", self.gap),
824            padding: sig(g, k, "padding", self.padding),
825            width: sig(g, k, "width", self.width),
826            height: sig(g, k, "height", self.height),
827            radius: sig(g, k, "radius", self.radius),
828        };
829        Layout::from_id(insert(g, parent, kind, self.base))
830    }
831}
832
833// ------------------------------------------------------------------- Grid
834
835builder!(GridBuilder => Grid, kind "Grid",
836    defaults { stroke: Color::rgba(1.0, 1.0, 1.0, 0.15), line_width: 1.0 },
837    fields { width: f64 = 1920.0, height: f64 = 1080.0, spacing: f64 = 80.0 });
838setters!(GridBuilder {
839    width: f64,
840    height: f64,
841    spacing: f64
842});
843
844impl NodeBuilder for GridBuilder {
845    type Handle = Grid;
846    base_accessor!();
847    fn build(self, g: &SceneGraph, parent: NodeId) -> Grid {
848        let k = Self::KIND;
849        let kind = NodeKind::Grid {
850            width: sig(g, k, "width", self.width),
851            height: sig(g, k, "height", self.height),
852            spacing: sig(g, k, "spacing", self.spacing),
853        };
854        Grid::from_id(insert(g, parent, kind, self.base))
855    }
856}