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