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