Skip to main content

oxframe_2d/
handles.rs

1use std::ops::Deref;
2
3use kurbo::{BezPath, Point};
4use std::future::Future;
5use std::pin::Pin;
6use std::task::{Context, Poll};
7
8use oxframe_core::{BlendMode, Color, Ease, Gradient, Seconds, Signal, Tween, Vec2, all};
9
10use crate::builder::{
11    self, CameraBuilder, CircleBuilder, CodeBuilder, GridBuilder, GroupBuilder, ImgBuilder,
12    LayoutBuilder, LineBuilder, NodeBuilder, ParticlesBuilder, PathBuilder, PolygonBuilder,
13    RectBuilder, ShaderBuilder, StarBuilder, SvgBuilder, TxtBuilder,
14};
15use crate::draw::{LineCap, LineJoin};
16use crate::graph::{NodeId, NodeKind, SceneGraph, with_graph};
17use crate::place::{Edge, Place, SafeArea, Side};
18use crate::text::RichSpan;
19
20/// How far [`Node::slide_in`] starts from the node's place, in pixels.
21pub const SLIDE_DISTANCE: f64 = 160.0;
22
23/// Untyped node handle with the properties every node has.
24#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
25pub struct Node(NodeId);
26
27macro_rules! base_signal {
28    ($($(#[$doc:meta])* $field:ident: $ty:ty),* $(,)?) => {
29        $(
30            $(#[$doc])*
31            #[doc = ""]
32            #[doc = concat!("The `", stringify!($field), "` property as an animatable signal.")]
33            pub fn $field(self) -> Signal<$ty> {
34                with_graph(|g| g.node(self.0).$field.clone())
35            }
36        )*
37    };
38}
39
40impl Node {
41    /// Index of the node in the scene graph.
42    pub fn id(self) -> NodeId {
43        self.0
44    }
45
46    /// Wraps an id from [`NodeBuilder::build`](crate::NodeBuilder::build).
47    ///
48    /// Other crates implement [`NodeBuilder`](crate::NodeBuilder) with this:
49    /// the `parent` argument is only an id, and handles are what `add` needs.
50    pub fn from_id(id: NodeId) -> Self {
51        Self(id)
52    }
53
54    base_signal! {
55        x: f64,
56        y: f64,
57        /// Degrees, clockwise.
58        rotation: f64,
59        scale: Vec2,
60        opacity: f64,
61        fill: Color,
62        stroke: Color,
63        line_width: f64,
64        /// Start of the drawn part of the outline (fraction of its length).
65        start: f64,
66        /// End of the drawn part of the outline.
67        end: f64,
68        fill_gradient: Option<Gradient>,
69        stroke_gradient: Option<Gradient>,
70        dash: Vec<f64>,
71        dash_offset: f64,
72        line_cap: LineCap,
73        line_join: LineJoin,
74        blur: f64,
75        shadow_color: Color,
76        shadow_blur: f64,
77        shadow_offset: Vec2,
78        clip: bool,
79        blend: BlendMode,
80        z_index: f64,
81        anchor: Vec2,
82        grow: f64,
83        shrink: f64,
84        basis: Option<f64>,
85        margin: f64,
86        align_self: Option<crate::LayoutAlign>,
87    }
88
89    /// Adds a child node; children are positioned relative to the parent.
90    ///
91    /// The call site is stored on the node (`oxframe check` and `oxframe
92    /// inspect` print it). `.name("title")` overrides the automatic label.
93    #[allow(clippy::should_implement_trait)] // `view.add(..)` mirrors Motion Canvas
94    #[track_caller]
95    pub fn add<B: NodeBuilder>(self, child: B) -> B::Handle {
96        let loc = std::panic::Location::caller();
97        let prev = crate::graph::push_add_at(loc.file(), loc.line());
98        let handle = builder::add(self.0, child);
99        crate::graph::pop_add_at(prev);
100        handle
101    }
102
103    /// Detaches the node from its parent; it stops being drawn.
104    pub fn remove(self) {
105        with_graph(|g| g.detach(self.0));
106    }
107
108    /// Fades the node in from transparent, over the theme's normal duration
109    /// with its enter easing.
110    pub fn fade_in(self) -> Tween<f64> {
111        let motion = crate::theme::current().motion;
112        self.opacity().set(0.0);
113        self.opacity().to(1.0, motion.normal).ease(motion.enter)
114    }
115
116    /// Grows the node from nothing to its current scale with a small
117    /// overshoot (the theme's emphasis easing, `OUT_BACK` by default).
118    pub fn pop_in(self) -> Tween<Vec2> {
119        let motion = crate::theme::current().motion;
120        let full = self.scale().peek();
121        self.scale().set(Vec2::ZERO);
122        self.scale().to(full, motion.normal).ease(motion.emphasis)
123    }
124
125    /// Draws the outline from start to end (`end` from 0 to 1), as if with a
126    /// pen. Works on every shape with an outline: paths, lines, rectangles,
127    /// circles, polygons.
128    pub fn draw_on(self) -> Tween<f64> {
129        let motion = crate::theme::current().motion;
130        self.end().set(0.0);
131        self.end().to(1.0, motion.slow).ease(motion.enter)
132    }
133
134    /// Slides the node into its place from `SLIDE_DISTANCE` pixels towards
135    /// `from`, fading in on the way.
136    pub fn slide_in(self, from: Side) -> impl std::future::Future<Output = ()> + 'static {
137        let motion = crate::theme::current().motion;
138        let (axis, offset) = match from {
139            Side::Left => (self.x(), -SLIDE_DISTANCE),
140            Side::Right => (self.x(), SLIDE_DISTANCE),
141            Side::Top => (self.y(), -SLIDE_DISTANCE),
142            Side::Bottom => (self.y(), SLIDE_DISTANCE),
143        };
144        let home = axis.peek();
145        axis.set(home + offset);
146        let moves = all((
147            axis.to(home, motion.normal).ease(motion.enter),
148            self.fade_in(),
149        ));
150        async move {
151            moves.await;
152        }
153    }
154
155    /// Sits just outside `other`. The gap is the theme's `Md` step, and the
156    /// position is recomputed every frame.
157    pub fn next_to(self, other: impl Into<Node>, side: Side) -> Self {
158        let other = other.into();
159        with_graph(|g| g.set_place(self.0, Place::next_to(other.id(), side)));
160        self
161    }
162
163    /// Lines this node's `edge` up with the same edge of `other`.
164    pub fn align_to(self, other: impl Into<Node>, edge: Edge) -> Self {
165        let other = other.into();
166        with_graph(|g| g.set_place(self.0, Place::align_to(other.id(), edge)));
167        self
168    }
169
170    /// Pins one edge to the safe area. The other axis is left as it is.
171    pub fn to_edge(self, edge: Edge) -> Self {
172        with_graph(|g| g.set_place(self.0, Place::to_edge(edge)));
173        self
174    }
175
176    /// Same center as `other`.
177    pub fn center_in(self, other: impl Into<Node>) -> Self {
178        let other = other.into();
179        with_graph(|g| g.set_place(self.0, Place::center_in(other.id())));
180        self
181    }
182
183    /// Fades the node out, over the theme's normal duration with its exit easing.
184    pub fn fade_out(self) -> Tween<f64> {
185        let motion = crate::theme::current().motion;
186        self.opacity().to(0.0, motion.normal).ease(motion.exit)
187    }
188
189    /// Moves the node under another parent.
190    pub fn reparent(self, parent: impl Into<Node>) {
191        let parent = parent.into();
192        with_graph(|g| g.reparent(self.0, parent.0));
193    }
194
195    /// Direct children, in creation order.
196    pub fn children(self) -> Vec<Node> {
197        with_graph(|g| g.node(self.0).children.iter().map(|&c| Node(c)).collect())
198    }
199
200    /// Size of the node's box in its own coordinates.
201    pub fn size(self) -> (f64, f64) {
202        with_graph(|g| g.measure(self.0))
203    }
204
205    /// Position of the node's origin in scene coordinates.
206    pub fn absolute_position(self) -> Vec2 {
207        with_graph(|g| (g.world_transform(self.0).unwrap_or_default() * Point::ZERO).to_vec2())
208    }
209}
210
211macro_rules! typed_handle {
212    ($name:ident, $builder:ident) => {
213        #[doc = concat!("Handle to a `", stringify!($name), "` node in the scene: cheap to copy, its")]
214        #[doc = "methods return the node's properties as [`Signal`]s. Derefs to [`Node`]."]
215        #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
216        pub struct $name(Node);
217
218        impl $name {
219            /// Starts a builder; add it with [`Node::add`] (e.g. `view.add(..)`).
220            #[allow(clippy::new_ret_no_self)]
221            pub fn new() -> $builder {
222                $builder::default()
223            }
224
225            pub(crate) fn from_id(id: NodeId) -> Self {
226                Self(Node(id))
227            }
228
229            /// The untyped node handle (same as `*self`).
230            pub fn node(self) -> Node {
231                self.0
232            }
233        }
234
235        impl Deref for $name {
236            type Target = Node;
237            fn deref(&self) -> &Node {
238                &self.0
239            }
240        }
241
242        impl From<$name> for Node {
243            fn from(h: $name) -> Node {
244                h.0
245            }
246        }
247    };
248}
249
250typed_handle!(Group, GroupBuilder);
251typed_handle!(Camera, CameraBuilder);
252typed_handle!(Rect, RectBuilder);
253typed_handle!(Circle, CircleBuilder);
254typed_handle!(Path, PathBuilder);
255typed_handle!(Line, LineBuilder);
256typed_handle!(Txt, TxtBuilder);
257typed_handle!(Img, ImgBuilder);
258typed_handle!(Svg, SvgBuilder);
259typed_handle!(Layout, LayoutBuilder);
260typed_handle!(Grid, GridBuilder);
261typed_handle!(Polygon, PolygonBuilder);
262typed_handle!(Star, StarBuilder);
263typed_handle!(Code, CodeBuilder);
264
265/// Handle to geometry drawn by a closure. Derefs to [`Node`].
266#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
267pub struct Custom(Node);
268
269impl Custom {
270    /// Starts a builder. `draw` is called every time the frame is painted,
271    /// with the frame time in seconds. It must depend only on that time, on
272    /// signals it captured, and on `ctx.rng(seed)`.
273    #[allow(clippy::new_ret_no_self)]
274    pub fn new(draw: impl Fn(&mut crate::draw::DrawCtx, f64) + 'static) -> crate::CustomBuilder {
275        crate::CustomBuilder::from_draw(draw)
276    }
277
278    pub(crate) fn from_id(id: NodeId) -> Self {
279        Self(Node(id))
280    }
281
282    /// The untyped node handle.
283    pub fn node(self) -> Node {
284        self.0
285    }
286}
287
288impl Deref for Custom {
289    type Target = Node;
290    fn deref(&self) -> &Node {
291        &self.0
292    }
293}
294
295/// Handle to a WGSL shader. Derefs to [`Node`].
296#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
297pub struct Shader(Node);
298
299impl Shader {
300    /// A shader from WGSL text. The text defines `struct Params`, the
301    /// uniform `p`, and `fn shade`. oxframe adds `ox` and `ShadeIn`.
302    ///
303    /// The waves preset is what gets painted. A custom `shade` is
304    /// type-checked on every draw.
305    #[allow(clippy::new_ret_no_self)]
306    pub fn source(wgsl: impl Into<String>) -> ShaderBuilder {
307        ShaderBuilder::new(crate::shader::ShaderCode::Wgsl(wgsl.into()))
308    }
309
310    /// A shader read from `path` on every draw, so saving the file updates
311    /// the running scene without a rebuild.
312    pub fn file(path: impl AsRef<std::path::Path>) -> ShaderBuilder {
313        ShaderBuilder::new(crate::shader::ShaderCode::File(path.as_ref().into()))
314    }
315
316    pub(crate) fn from_id(id: NodeId) -> Self {
317        Self(Node(id))
318    }
319
320    /// The untyped node handle.
321    pub fn node(self) -> Node {
322        self.0
323    }
324
325    /// The numeric uniform `name`.
326    ///
327    /// # Panics
328    ///
329    /// If the shader has no numeric uniform with that name. The message
330    /// lists the names it does have.
331    pub fn uniform(self, name: &str) -> Signal<f64> {
332        self.number(name)
333    }
334
335    /// Wave height of the waves preset.
336    pub fn amp(self) -> Signal<f64> {
337        self.number("amp")
338    }
339
340    /// Crest speed of the waves preset.
341    pub fn speed(self) -> Signal<f64> {
342        self.number("speed")
343    }
344
345    /// Water color of the waves preset.
346    pub fn deep(self) -> Signal<Color> {
347        self.color("deep")
348    }
349
350    /// Crest color of the waves preset.
351    pub fn foam(self) -> Signal<Color> {
352        self.color("foam")
353    }
354
355    fn number(self, name: &str) -> Signal<f64> {
356        with_graph(|g| {
357            let NodeKind::Shader { uniforms, .. } = &g.node(self.id()).kind else {
358                unreachable!("handle does not point to a Shader");
359            };
360            for u in uniforms {
361                if u.name == name
362                    && let crate::shader::UniformValue::Num(v) = &u.value
363                {
364                    return v.clone();
365                }
366            }
367            panic!("no numeric uniform {name}. {}", uniform_names(uniforms));
368        })
369    }
370
371    fn color(self, name: &str) -> Signal<Color> {
372        with_graph(|g| {
373            let NodeKind::Shader { uniforms, .. } = &g.node(self.id()).kind else {
374                unreachable!("handle does not point to a Shader");
375            };
376            for u in uniforms {
377                if u.name == name
378                    && let crate::shader::UniformValue::Color(v) = &u.value
379                {
380                    return v.clone();
381                }
382            }
383            panic!("no color uniform {name}. {}", uniform_names(uniforms));
384        })
385    }
386}
387
388fn uniform_names(uniforms: &[crate::shader::Uniform]) -> String {
389    let names: Vec<&str> = uniforms.iter().map(|u| u.name.as_str()).collect();
390    format!("Available: {}.", names.join(", "))
391}
392
393impl Deref for Shader {
394    type Target = Node;
395    fn deref(&self) -> &Node {
396        &self.0
397    }
398}
399
400impl From<Custom> for Node {
401    fn from(h: Custom) -> Node {
402        h.0
403    }
404}
405
406typed_handle!(Particles, ParticlesBuilder);
407
408macro_rules! kind_signal {
409    ($handle:ident :: $kind:ident { $($(#[$doc:meta])* $field:ident: $ty:ty),* $(,)? }) => {
410        impl $handle {
411            $(
412                $(#[$doc])*
413                #[doc = ""]
414                #[doc = concat!("The `", stringify!($field), "` property as an animatable signal.")]
415                pub fn $field(self) -> Signal<$ty> {
416                    with_graph(|g| match &g.node(self.id()).kind {
417                        NodeKind::$kind { $field, .. } => $field.clone(),
418                        _ => unreachable!(concat!("handle does not point to a ", stringify!($kind))),
419                    })
420                }
421            )*
422        }
423    };
424}
425
426kind_signal!(Particles::Particles { gravity: f64 });
427
428impl Particles {
429    /// Emits `count` particles from `origin` at the current script time.
430    ///
431    /// The future is ready immediately: the burst is an event, and the
432    /// particles then move on their own. Seeking back before this time
433    /// removes them; seeking forward restores the same dots.
434    pub fn burst(self, origin: impl Into<Vec2>, count: u32) -> oxframe_core::WaitFor {
435        let origin = origin.into();
436        let time = oxframe_core::time();
437        with_graph(|g| {
438            if let NodeKind::Particles { state, .. } = &g.node(self.id()).kind {
439                state.bursts.borrow_mut().push(crate::sim::Burst {
440                    time,
441                    origin,
442                    count,
443                });
444            }
445        });
446        oxframe_core::wait_for(0.0)
447    }
448}
449
450kind_signal!(Rect::Rect {
451    width: f64,
452    height: f64,
453    radius: f64
454});
455kind_signal!(Circle::Circle {
456    /// Diameter.
457    size: f64,
458    start_angle: f64,
459    end_angle: f64,
460});
461kind_signal!(Polygon::Polygon {
462    sides: f64,
463    size: f64
464});
465kind_signal!(Star::Star {
466    points: f64,
467    size: f64,
468    inner: f64
469});
470kind_signal!(Path::Path { data: BezPath });
471kind_signal!(Camera::Camera {
472    /// 1 shows the world at its size, 2 twice as big.
473    zoom: f64
474});
475
476impl Camera {
477    /// Moves the camera so `node`'s center is in the middle of the frame.
478    pub fn center_on(
479        self,
480        node: impl Into<Node>,
481        duration: impl Seconds,
482    ) -> impl std::future::Future<Output = ()> + 'static {
483        let node = node.into();
484        let d = duration.seconds();
485        let target = with_graph(|g| g.box_in(node.id(), self.id())).map(|r| r.center());
486        let moves = target.map(|c| all((self.x().to(c.x, d), self.y().to(c.y, d))));
487        async move {
488            if let Some(m) = moves {
489                m.await;
490            }
491        }
492    }
493
494    /// Centers on `node` and zooms so all of it fits in the frame with
495    /// `padding` pixels to spare. Measures the node when called, so a layout
496    /// that is still growing fits at its current size.
497    pub fn fit(
498        self,
499        node: impl Into<Node>,
500        padding: f64,
501        duration: impl Seconds,
502    ) -> impl std::future::Future<Output = ()> + 'static {
503        let node = node.into();
504        let d = duration.seconds();
505        let (bounds, (fw, fh)) = with_graph(|g| (g.box_in(node.id(), self.id()), g.frame_size()));
506        let moves = bounds.map(|r| {
507            let zoom = ((fw - 2.0 * padding) / r.width().max(1e-6))
508                .min((fh - 2.0 * padding) / r.height().max(1e-6))
509                .max(1e-3);
510            let c = r.center();
511            all((
512                self.x().to(c.x, d),
513                self.y().to(c.y, d),
514                self.zoom().to(zoom, d),
515            ))
516        });
517        async move {
518            if let Some(m) = moves {
519                m.await;
520            }
521        }
522    }
523
524    /// Back to the start: looking at the origin, no zoom, no rotation.
525    pub fn reset(self, duration: impl Seconds) -> impl std::future::Future<Output = ()> + 'static {
526        let d = duration.seconds();
527        let moves = all((
528            self.x().to(0.0, d),
529            self.y().to(0.0, d),
530            self.zoom().to(1.0, d),
531            self.rotation().to(0.0, d),
532        ));
533        async move {
534            moves.await;
535        }
536    }
537}
538kind_signal!(Line::Line { points: Vec<Vec2>, arrow_size: f64 });
539kind_signal!(Txt::Txt { text: String, spans: Vec<RichSpan>, font_size: f64, weight: f64 });
540kind_signal!(Code::Code {
541    font_size: f64,
542    /// Focused lines as (first, last), 1-based; (0, 0) = all.
543    focus: Vec2,
544    /// Alpha of unfocused lines.
545    dim: f64,
546});
547
548impl Txt {
549    /// Morphs to `text` glyph by glyph: letters present in both strings
550    /// travel to their new places, the others fade.
551    pub fn morph_to(self, text: impl Into<String>, duration: f64) -> oxframe_core::BoxFuture {
552        let to = text.into();
553        let (txt, morph, t) = with_graph(|g| match &g.node(self.id()).kind {
554            NodeKind::Txt {
555                text,
556                morph,
557                morph_t,
558                ..
559            } => (text.clone(), morph.clone(), morph_t.clone()),
560            _ => unreachable!("handle does not point to a Txt"),
561        });
562        Box::pin(async move {
563            morph.set(Some((txt.get(), to.clone())));
564            t.set(0.0);
565            t.to(1.0, duration).await;
566            txt.set(to);
567            morph.set(None);
568        })
569    }
570}
571
572impl Code {
573    /// The source.
574    ///
575    /// `.to(next, seconds)` diffs the two versions: tokens that stay slide
576    /// to their new places, deleted tokens fade and collapse, new tokens
577    /// fade in. `Txt` types its text out; code does not. `.get()`, `.set()`
578    /// and `.bind()` still replace the source with no animation, and `.set`
579    /// cancels a diff that is already running.
580    pub fn code(self) -> CodeText {
581        with_graph(|g| match &g.node(self.id()).kind {
582            NodeKind::Code {
583                code,
584                edit_from,
585                edit_to,
586                editing,
587                edit_t,
588                edit_flash,
589                focus,
590                lang,
591                ..
592            } => CodeText {
593                code: code.clone(),
594                edit_from: edit_from.clone(),
595                edit_to: edit_to.clone(),
596                editing: editing.clone(),
597                edit_t: edit_t.clone(),
598                edit_flash: edit_flash.clone(),
599                focus: focus.clone(),
600                lang: *lang,
601            },
602            _ => unreachable!("handle does not point to a Code"),
603        })
604    }
605
606    /// Moves the focus to lines `first..=last` over `duration`.
607    ///
608    /// Lines are 1-based. While a diff is running, a focus that sits on
609    /// lines which shift rides with those lines, and lands on their new
610    /// numbers when the diff ends.
611    pub fn focus_lines(
612        self,
613        first: usize,
614        last: usize,
615        duration: f64,
616    ) -> oxframe_core::Tween<Vec2> {
617        self.focus()
618            .to(Vec2::new(first as f64, last as f64), duration)
619    }
620
621    /// Inserts `text` at `line` (1-based) and `col` (0-based characters).
622    ///
623    /// # Panics
624    ///
625    /// If the line or column is outside the current source. The line just
626    /// after the last one, column 0, is the end of the source.
627    pub fn insert(
628        self,
629        line: usize,
630        col: usize,
631        text: impl Into<String>,
632        duration: impl Seconds,
633    ) -> CodeTween {
634        let text = text.into();
635        let src = self.code().get();
636        let next = oxframe_text::insert_at(&src, line, col, &text)
637            .unwrap_or_else(|e| panic!("code.insert: {e}"));
638        self.code().to(next, duration)
639    }
640
641    /// Removes the half-open range `((start_line, start_col), (end_line, end_col))`.
642    ///
643    /// Lines are 1-based, columns are 0-based characters. `((5, 0), (6, 0))`
644    /// removes line 5.
645    ///
646    /// # Panics
647    ///
648    /// If a point is outside the source, or the end is before the start.
649    pub fn remove(
650        self,
651        range: ((usize, usize), (usize, usize)),
652        duration: impl Seconds,
653    ) -> CodeTween {
654        self.rewrite(range, String::new(), duration, "code.remove")
655    }
656
657    /// Replaces the half-open range with `text`. See [`remove`](Self::remove)
658    /// for how the range is counted.
659    ///
660    /// # Panics
661    ///
662    /// If a point is outside the source, or the end is before the start.
663    pub fn replace(
664        self,
665        range: ((usize, usize), (usize, usize)),
666        text: impl Into<String>,
667        duration: impl Seconds,
668    ) -> CodeTween {
669        self.rewrite(range, text.into(), duration, "code.replace")
670    }
671
672    fn rewrite(
673        self,
674        ((l0, c0), (l1, c1)): ((usize, usize), (usize, usize)),
675        text: String,
676        duration: impl Seconds,
677        what: &str,
678    ) -> CodeTween {
679        let src = self.code().get();
680        let next = oxframe_text::replace_range(
681            &src,
682            oxframe_text::Loc { line: l0, col: c0 },
683            oxframe_text::Loc { line: l1, col: c1 },
684            &text,
685        )
686        .unwrap_or_else(|e| panic!("{what}: {e}"));
687        self.code().to(next, duration)
688    }
689}
690
691/// The source of a [`Code`] node.
692///
693/// Dereferences to the underlying [`Signal`], so `.get()` reads it.
694/// [`.to()`](CodeText::to) runs a diff tween instead of the typewriter
695/// [`Signal::to`](Signal::to) uses for plain strings.
696#[derive(Clone)]
697pub struct CodeText {
698    code: Signal<String>,
699    edit_from: Signal<String>,
700    edit_to: Signal<String>,
701    editing: Signal<bool>,
702    edit_t: Signal<f64>,
703    edit_flash: Signal<bool>,
704    focus: Signal<Vec2>,
705    lang: oxframe_text::Lang,
706}
707
708impl std::ops::Deref for CodeText {
709    type Target = Signal<String>;
710    fn deref(&self) -> &Signal<String> {
711        &self.code
712    }
713}
714
715impl CodeText {
716    /// Replaces the source immediately and cancels a running diff.
717    pub fn set(&self, value: impl Into<String>) {
718        self.editing.set(false);
719        self.code.set(value.into());
720    }
721
722    /// Makes the source follow `f`, and cancels a running diff.
723    pub fn bind(&self, f: impl Fn() -> String + 'static) {
724        self.editing.set(false);
725        self.code.bind(f);
726    }
727
728    /// Diffs the current source into `target` over `duration`.
729    pub fn to(&self, target: impl Into<String>, duration: impl Seconds) -> CodeTween {
730        let target = target.into();
731        let from = self.code.get();
732        self.edit_from.set(from.clone());
733        self.edit_to.set(target.clone());
734        self.edit_flash.set(false);
735        self.editing.set(true);
736        self.edit_t.set(0.0);
737        CodeTween {
738            tween: self.edit_t.to(1.0, duration),
739            code: self.code.clone(),
740            editing: self.editing.clone(),
741            edit_flash: self.edit_flash.clone(),
742            focus: self.focus.clone(),
743            lang: self.lang,
744            from,
745            target,
746            finished: false,
747        }
748    }
749}
750
751/// Diff tween returned by [`CodeText::to`], [`Code::insert`], [`Code::remove`]
752/// and [`Code::replace`].
753#[must_use = "a tween does nothing unless awaited"]
754pub struct CodeTween {
755    tween: Tween<f64>,
756    code: Signal<String>,
757    editing: Signal<bool>,
758    edit_flash: Signal<bool>,
759    focus: Signal<Vec2>,
760    lang: oxframe_text::Lang,
761    from: String,
762    target: String,
763    finished: bool,
764}
765
766impl CodeTween {
767    /// Easing curve. The default is [`presets::DEFAULT`](oxframe_core::presets::DEFAULT).
768    pub fn ease(mut self, ease: impl Ease + 'static) -> Self {
769        self.tween = self.tween.ease(ease);
770        self
771    }
772
773    /// Changes the length. `f64` is seconds; a theme pace is a duration too.
774    pub fn duration(mut self, duration: impl Seconds) -> Self {
775        self.tween = self.tween.duration(duration);
776        self
777    }
778
779    /// Flashes a background behind text that this edit inserts.
780    pub fn flash(self) -> Self {
781        self.edit_flash.set(true);
782        self
783    }
784}
785
786impl Future for CodeTween {
787    type Output = ();
788
789    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> {
790        let this = self.get_mut();
791        if Pin::new(&mut this.tween).poll(cx).is_pending() {
792            return Poll::Pending;
793        }
794        if !this.finished {
795            this.finished = true;
796            let focus = this.focus.get();
797            let (a, b) = (focus.x.round() as i64, focus.y.round() as i64);
798            if a > 0 || b > 0 {
799                let diff = oxframe_text::diff_code(&this.from, &this.target, this.lang);
800                if let Some((n0, n1)) =
801                    oxframe_text::map_focus(&diff, a.max(0) as usize, b.max(0) as usize)
802                {
803                    this.focus.set(Vec2::new(n0 as f64, n1 as f64));
804                }
805            }
806            this.code.set(this.target.clone());
807            this.editing.set(false);
808        }
809        Poll::Ready(())
810    }
811}
812kind_signal!(Svg::Svg {
813    width: f64,
814    height: f64
815});
816
817impl Svg {
818    /// The node imported from the SVG element with this `id`.
819    ///
820    /// # Panics
821    ///
822    /// If the file has no element with that id.
823    pub fn find(self, id: &str) -> Node {
824        let id = with_graph(|g| g.descendant_named(self.id(), id))
825            .unwrap_or_else(|| panic!("svg has no element with id {id:?}"));
826        Node::from_id(id)
827    }
828
829    /// Every path from the file, in paint order.
830    pub fn paths(self) -> Vec<Path> {
831        with_graph(|g| {
832            g.descendant_paths(self.id())
833                .into_iter()
834                .map(Path::from_id)
835                .collect()
836        })
837    }
838}
839
840kind_signal!(Img::Img {
841    width: f64,
842    height: f64
843});
844kind_signal!(Layout::Layout {
845    gap: f64,
846    padding: f64,
847    radius: f64,
848    /// Fixed width; `None` sizes to content.
849    width: Option<f64>,
850    /// Fixed height; `None` sizes to content.
851    height: Option<f64>,
852});
853kind_signal!(Grid::Grid {
854    width: f64,
855    height: f64,
856    spacing: f64
857});
858
859/// Root of a scene. Origin is the center of the frame, y points down.
860#[derive(Debug, Clone, Copy, PartialEq, Eq)]
861pub struct View(Node);
862
863impl View {
864    /// The view of the scene being executed.
865    pub fn current() -> Self {
866        Self(Node(SceneGraph::ROOT))
867    }
868
869    /// Frame size in pixels.
870    pub fn size(self) -> (u32, u32) {
871        with_graph(|g| g.size())
872    }
873
874    /// True when the frame is taller than it is wide.
875    pub fn is_portrait(self) -> bool {
876        let (w, h) = self.size();
877        h > w
878    }
879
880    /// The rectangle [`Node::to_edge`](crate::Node::to_edge) pins to, in scene
881    /// coordinates. A portrait frame leaves room for a vertical video's
882    /// status bar and buttons.
883    pub fn safe_area(self) -> SafeArea {
884        let (w, h) = self.size();
885        SafeArea::for_frame(f64::from(w), f64::from(h))
886    }
887}
888
889impl Deref for View {
890    type Target = Node;
891    fn deref(&self) -> &Node {
892        &self.0
893    }
894}
895
896impl From<View> for Node {
897    fn from(v: View) -> Node {
898        v.0
899    }
900}
901
902#[cfg(test)]
903mod tests {
904    use super::*;
905    use crate::Primitive;
906
907    fn scene(f: impl FnOnce(View)) -> std::rc::Rc<SceneGraph> {
908        let graph = SceneGraph::new(400, 400);
909        graph.enter(|| f(View::current()));
910        graph
911    }
912
913    /// Runs `scene` to the end inside a graph; returns the frames it took.
914    fn play(
915        scene_fn: impl FnOnce(View) -> std::pin::Pin<Box<dyn std::future::Future<Output = ()>>>,
916    ) -> u32 {
917        let graph = SceneGraph::new(400, 400);
918        graph.enter(|| {
919            let fut = scene_fn(View::current());
920            let mut ex =
921                oxframe_core::Executor::new(60.0, oxframe_protocol::Meta::default(), 0, fut);
922            let mut frames = 0;
923            while !ex.step() {
924                frames += 1;
925                assert!(frames < 10_000, "scene did not finish");
926            }
927            frames
928        })
929    }
930
931    /// Screen position of a node's origin.
932    fn screen(n: impl Into<Node>) -> (f64, f64) {
933        let p = n.into().absolute_position();
934        ((p.x * 1e6).round() / 1e6, (p.y * 1e6).round() / 1e6)
935    }
936
937    #[test]
938    fn camera_pans_zooms_and_turns_its_children_only() {
939        scene(|view| {
940            let cam = view.add(Camera::new());
941            let dot = cam.add(Circle::new().size(10.0).x(100.0).y(50.0));
942            let hud = view.add(Txt::new().text("HUD").x(-300.0).y(-200.0));
943            // A fresh camera changes nothing.
944            assert_eq!(screen(dot), (100.0, 50.0));
945            // Looking at the dot brings it to the center.
946            cam.x().set(100.0);
947            cam.y().set(50.0);
948            assert_eq!(screen(dot), (0.0, 0.0));
949            // Zoom scales distances from the point looked at.
950            cam.zoom().set(2.0);
951            let other = cam.add(Circle::new().size(10.0).x(110.0).y(50.0));
952            assert_eq!(screen(other), (20.0, 0.0));
953            // Turning the camera clockwise turns the world the other way.
954            cam.rotation().set(90.0);
955            assert_eq!(screen(other), (0.0, -20.0));
956            // Nodes outside the camera stay put.
957            assert_eq!(screen(hud), (-300.0, -200.0));
958        });
959    }
960
961    #[test]
962    fn camera_center_on_fit_and_reset() {
963        play(|view| {
964            let cam = view.add(Camera::new());
965            let card = cam.add(Rect::new().width(200.0).height(100.0).x(300.0).y(-100.0));
966            let row = cam.add(
967                Layout::new()
968                    .x(-500.0)
969                    .gap(10.0)
970                    .child(Rect::new().size(50.0))
971                    .child(Rect::new().size(50.0)),
972            );
973            Box::pin(async move {
974                cam.center_on(card, 1.0).await;
975                assert_eq!((cam.x().get(), cam.y().get()), (300.0, -100.0));
976                assert_eq!(screen(card), (0.0, 0.0));
977                // 400×400 frame, 20 px padding: limited by width (360 / 200).
978                cam.fit(card, 20.0, 0.0).await;
979                assert!(
980                    (cam.zoom().get() - 1.8).abs() < 1e-9,
981                    "{}",
982                    cam.zoom().get()
983                );
984                // A layout that grew is fitted at its current size.
985                row.gap().to(110.0, 0.5).await;
986                cam.fit(row, 0.0, 0.0).await;
987                let (w, _) = row.size();
988                assert!((w - 210.0).abs() < 1e-6, "layout width {w}");
989                assert!((cam.zoom().get() - 400.0 / 210.0).abs() < 1e-9);
990                assert!((cam.x().get() + 500.0).abs() < 1e-9);
991                cam.reset(0.5).await;
992                assert_eq!(
993                    (
994                        cam.x().get(),
995                        cam.y().get(),
996                        cam.zoom().get(),
997                        cam.rotation().get()
998                    ),
999                    (0.0, 0.0, 1.0, 0.0)
1000                );
1001            })
1002        });
1003    }
1004
1005    #[test]
1006    fn entrance_helpers_start_at_once_and_end_in_place() {
1007        let normal = crate::theme::current().motion.normal;
1008        let expected = (normal * 60.0).round() as u32;
1009        let frames = play(|view| {
1010            let r = view.add(Rect::new().size(100.0).x(50.0));
1011            let pop = r.pop_in();
1012            // The start value is set when the helper is called, not awaited:
1013            // nothing flashes in its final state for a frame.
1014            assert_eq!(r.scale().get(), Vec2::ZERO);
1015            let slide = r.slide_in(Side::Left);
1016            assert_eq!(r.x().get(), 50.0 - SLIDE_DISTANCE);
1017            assert_eq!(r.opacity().get(), 0.0);
1018            let draw = r.draw_on();
1019            assert_eq!(r.end().get(), 0.0);
1020            Box::pin(async move {
1021                all((pop, slide)).await;
1022                assert_eq!(r.scale().get(), Vec2::new(1.0, 1.0));
1023                assert_eq!((r.x().get(), r.opacity().get()), (50.0, 1.0));
1024                draw.await;
1025                assert_eq!(r.end().get(), 1.0);
1026                r.fade_out().duration(0.5).await;
1027                assert_eq!(r.opacity().get(), 0.0);
1028            })
1029        });
1030        let slow = crate::theme::current().motion.slow;
1031        assert_eq!(frames, expected + (slow * 60.0).round() as u32 + 30);
1032    }
1033
1034    #[test]
1035    fn a_diff_lands_on_the_new_source_and_moves_the_focus() {
1036        play(|view| {
1037            let code = view.add(Code::new().code("a\nkeep\n").focus(2, 2));
1038            Box::pin(async move {
1039                code.insert(1, 0, "NEW\n", 0.2).flash().await;
1040                assert_eq!(code.code().get(), "NEW\na\nkeep\n");
1041                let f = code.focus().get();
1042                assert_eq!((f.x.round() as i32, f.y.round() as i32), (3, 3));
1043                code.replace(((3, 0), (3, 4)), "stay", 0.0).await;
1044                assert_eq!(code.code().get(), "NEW\na\nstay\n");
1045            })
1046        });
1047    }
1048
1049    #[test]
1050    fn add_records_its_call_site_and_scene_bounds() {
1051        let g = scene(|view| {
1052            view.add(Rect::new().size(100.0).name("box"));
1053            view.add(Rect::new().size(10.0).x(100.0));
1054        });
1055        let nodes = g.inspect_nodes();
1056        let named = nodes.iter().find(|n| n.name == "box").unwrap();
1057        assert_eq!(named.kind, "Rect");
1058        assert!(named.source.as_ref().unwrap().contains("handles.rs:"));
1059        assert!(
1060            (named.bounds[0] + 50.0).abs() < 1e-6 && (named.bounds[2] - 50.0).abs() < 1e-6,
1061            "{:?}",
1062            named.bounds
1063        );
1064        let auto = nodes
1065            .iter()
1066            .find(|n| n.name != "box" && n.name != "view")
1067            .unwrap();
1068        assert_eq!(auto.name, auto.source.clone().unwrap());
1069    }
1070
1071    #[test]
1072    fn builders_set_initial_values_and_handles_return_signals() {
1073        scene(|view| {
1074            let r = view.add(
1075                Rect::new()
1076                    .size(50.0)
1077                    .radius(5.0)
1078                    .x(-10.0)
1079                    .fill(Color::BLACK),
1080            );
1081            assert_eq!(r.width().get(), 50.0);
1082            assert_eq!(r.height().get(), 50.0);
1083            assert_eq!(r.radius().get(), 5.0);
1084            assert_eq!(r.x().get(), -10.0);
1085            assert_eq!(r.fill().get(), Color::BLACK);
1086            r.x().set(3.0);
1087            assert_eq!(r.x().get(), 3.0);
1088            let c = r.add(Circle::new().size(10.0));
1089            assert_eq!(r.children(), vec![c.node()]);
1090            assert_eq!(view.children(), vec![r.node()]);
1091            assert_eq!(c.size().name().as_deref(), Some("Circle#2.size"));
1092        });
1093    }
1094
1095    #[test]
1096    fn draw_list_composes_transforms() {
1097        let g = scene(|view| {
1098            let grp = view.add(Group::new().x(10.0).scale(2.0));
1099            grp.add(Circle::new().x(5.0));
1100        });
1101        let items = g.draw_list();
1102        assert_eq!(items.len(), 1);
1103        assert_eq!(items[0].transform * Point::ZERO, Point::new(20.0, 0.0));
1104    }
1105
1106    #[test]
1107    fn layout_places_children_in_a_row() {
1108        let g = scene(|view| {
1109            view.add(
1110                Layout::new()
1111                    .gap(10.0)
1112                    .padding(5.0)
1113                    .child(Rect::new().size(20.0))
1114                    .child(Rect::new().width(40.0).height(10.0)),
1115            );
1116        });
1117        let layout = NodeId(1);
1118        assert_eq!(g.measure(layout), (5.0 + 20.0 + 10.0 + 40.0 + 5.0, 30.0));
1119        let centers: Vec<_> = g
1120            .draw_list()
1121            .iter()
1122            .map(|i| i.transform * Point::ZERO)
1123            .collect();
1124        // Total 80 wide: first box spans -35..-15, second -5..35.
1125        assert_eq!(centers, vec![Point::new(-25.0, 0.0), Point::new(15.0, 0.0)]);
1126    }
1127
1128    #[test]
1129    fn column_with_fixed_size_and_alignment() {
1130        let g = scene(|view| {
1131            view.add(
1132                Layout::new()
1133                    .column()
1134                    .height(100.0)
1135                    .width(100.0)
1136                    .justify(crate::Justify::Start)
1137                    .align(crate::LayoutAlign::Start)
1138                    .child(Rect::new().size(20.0)),
1139            );
1140        });
1141        let c = g.draw_list()[0].transform * Point::ZERO;
1142        assert_eq!(c, Point::new(-40.0, -40.0));
1143    }
1144
1145    #[test]
1146    fn text_line_path_and_img_draw() {
1147        let png = {
1148            let mut v = Vec::new();
1149            image::RgbaImage::from_pixel(4, 2, image::Rgba([255, 0, 0, 255]))
1150                .write_to(&mut std::io::Cursor::new(&mut v), image::ImageFormat::Png)
1151                .unwrap();
1152            v
1153        };
1154        let g = scene(|view| {
1155            let t = view.add(Txt::new().text("Hi").font_size(40.0));
1156            assert!(t.size().0 > 20.0);
1157            view.add(
1158                Line::new()
1159                    .points([(0.0, 0.0), (100.0, 0.0)])
1160                    .end_arrow(true),
1161            );
1162            view.add(Path::new().svg("M0 0 L10 0 L10 10 Z"));
1163            let img = view.add(Img::new().src(&png).width(40.0));
1164            assert_eq!(img.height().get(), 20.0);
1165        });
1166        let kinds: Vec<_> = g
1167            .draw_list()
1168            .iter()
1169            .map(|i| match i.prim {
1170                Primitive::Fill { .. } => "fill",
1171                Primitive::Stroke { .. } => "stroke",
1172                Primitive::Image { .. } => "image",
1173                Primitive::PushLayer(_) => "push",
1174                Primitive::PopLayer => "pop",
1175            })
1176            .collect();
1177        assert_eq!(kinds, vec!["fill", "stroke", "fill", "fill", "image"]);
1178    }
1179
1180    #[test]
1181    fn trimming_and_pick() {
1182        let g = scene(|view| {
1183            view.add(Rect::new().size(100.0).name("back"));
1184            view.add(Circle::new().size(40.0).x(30.0).end(0.5));
1185        });
1186        assert_eq!(g.pick(Point::new(-40.0, 0.0)), Some(NodeId(1)));
1187        assert_eq!(g.pick(Point::new(300.0, 0.0)), None);
1188        // The half circle covers the right half of its disc.
1189        assert_eq!(g.pick(Point::new(45.0, 0.0)), Some(NodeId(2)));
1190    }
1191
1192    fn kinds(g: &SceneGraph) -> Vec<&'static str> {
1193        g.draw_list()
1194            .iter()
1195            .map(|i| match i.prim {
1196                Primitive::Fill { .. } => "fill",
1197                Primitive::Stroke { .. } => "stroke",
1198                Primitive::Image { .. } => "image",
1199                Primitive::PushLayer(_) => "push",
1200                Primitive::PopLayer => "pop",
1201            })
1202            .collect()
1203    }
1204
1205    #[test]
1206    fn layers_only_when_needed_and_balanced() {
1207        // Translucent leaf: opacity folds into the paint, no layer.
1208        let g = scene(|v| {
1209            v.add(Rect::new().opacity(0.5));
1210        });
1211        assert_eq!(kinds(&g), ["fill"]);
1212        // Translucent group with children: one layer around them.
1213        let g = scene(|v| {
1214            v.add(
1215                Group::new()
1216                    .opacity(0.5)
1217                    .child(Rect::new())
1218                    .child(Circle::new()),
1219            );
1220        });
1221        assert_eq!(kinds(&g), ["push", "fill", "fill", "pop"]);
1222        // Clip wraps only the children; shadow wraps node + children.
1223        let g = scene(|v| {
1224            v.add(
1225                Circle::new()
1226                    .clip(true)
1227                    .shadow(Color::BLACK, 4.0, (2.0, 2.0))
1228                    .child(Rect::new()),
1229            );
1230        });
1231        assert_eq!(kinds(&g), ["push", "fill", "push", "fill", "pop", "pop"]);
1232    }
1233
1234    #[test]
1235    fn z_index_orders_siblings_and_anchor_moves_box() {
1236        let g = scene(|v| {
1237            v.add(Rect::new().name("a").z_index(2.0));
1238            v.add(Rect::new().name("b"));
1239            v.add(Rect::new().name("c").size(20.0).anchor((-1.0, -1.0)).x(5.0));
1240        });
1241        let items = g.draw_list();
1242        let order: Vec<u32> = items.iter().map(|i| i.node.0).collect();
1243        assert_eq!(order, vec![2, 3, 1]);
1244        // Anchored at the top-left: the box center sits at (5+10, 0+10).
1245        assert_eq!(items[1].transform * Point::ZERO, Point::new(15.0, 10.0));
1246    }
1247
1248    fn boxes(g: &SceneGraph) -> Vec<(Point, f64)> {
1249        g.draw_list()
1250            .iter()
1251            .filter_map(|i| match &i.prim {
1252                Primitive::Fill { path, .. } => {
1253                    let bb = kurbo::Shape::bounding_box(path);
1254                    Some((i.transform * Point::ZERO, bb.width()))
1255                }
1256                _ => None,
1257            })
1258            .collect()
1259    }
1260
1261    #[test]
1262    fn flex_grow_stretch_wrap_and_grid() {
1263        use crate::{Justify, LayoutAlign};
1264        // grow: a 200-wide row; the second child takes the free space.
1265        let g = scene(|v| {
1266            v.add(
1267                Layout::new()
1268                    .width(200.0)
1269                    .justify(Justify::Start)
1270                    .child(Rect::new().size(40.0))
1271                    .child(Rect::new().size(40.0).grow(1.0)),
1272            );
1273        });
1274        let b = boxes(&g);
1275        assert_eq!(b[0], (Point::new(-80.0, 0.0), 40.0));
1276        assert_eq!(b[1], (Point::new(20.0, 0.0), 160.0));
1277
1278        // stretch: children fill the cross axis.
1279        let g = scene(|v| {
1280            v.add(
1281                Layout::new()
1282                    .column()
1283                    .width(120.0)
1284                    .align(LayoutAlign::Stretch)
1285                    .child(Rect::new().size(30.0)),
1286            );
1287        });
1288        assert_eq!(boxes(&g)[0].1, 120.0);
1289
1290        // wrap: three 60-wide boxes in 130 → two lines.
1291        let g = scene(|v| {
1292            v.add(
1293                Layout::new()
1294                    .width(130.0)
1295                    .wrap(true)
1296                    .child(Rect::new().size(60.0))
1297                    .child(Rect::new().size(60.0))
1298                    .child(Rect::new().size(60.0)),
1299            );
1300        });
1301        let ys: Vec<f64> = boxes(&g).iter().map(|b| b.0.y).collect();
1302        assert_eq!(ys[0], ys[1]);
1303        assert!(ys[2] > ys[0]);
1304
1305        // grid: 2 columns of equal width.
1306        let g = scene(|v| {
1307            v.add(
1308                Layout::new()
1309                    .grid(2)
1310                    .width(200.0)
1311                    .align(LayoutAlign::Stretch)
1312                    .child(Rect::new().size(10.0))
1313                    .child(Rect::new().size(10.0))
1314                    .child(Rect::new().size(10.0)),
1315            );
1316        });
1317        let b = boxes(&g);
1318        assert_eq!(
1319            b.iter().map(|b| b.1).collect::<Vec<_>>(),
1320            vec![100.0, 100.0, 100.0]
1321        );
1322        assert_eq!(b[2].0.x, b[0].0.x);
1323        assert!(b[2].0.y > b[0].0.y);
1324    }
1325
1326    #[test]
1327    fn describe_lists_signals() {
1328        let g = scene(|view| {
1329            view.add(Txt::new().text("abc"));
1330        });
1331        let d = g.describe(NodeId(1));
1332        assert!(d.iter().any(|(k, v)| k == "text" && v == "abc"));
1333    }
1334}