Skip to main content

oxframe_2d/
handles.rs

1use std::ops::Deref;
2
3use kurbo::{BezPath, Point};
4use oxframe_core::{BlendMode, Color, Gradient, Seconds, Signal, Tween, Vec2, all};
5
6use crate::builder::{
7    self, CameraBuilder, CircleBuilder, CodeBuilder, GridBuilder, GroupBuilder, ImgBuilder,
8    LayoutBuilder, LineBuilder, NodeBuilder, PathBuilder, PolygonBuilder, RectBuilder, StarBuilder,
9    TxtBuilder,
10};
11use crate::draw::{LineCap, LineJoin};
12use crate::graph::{NodeId, NodeKind, SceneGraph, with_graph};
13use crate::place::{Edge, Place, SafeArea, Side};
14use crate::text::RichSpan;
15
16/// How far [`Node::slide_in`] starts from the node's place, in pixels.
17pub const SLIDE_DISTANCE: f64 = 160.0;
18
19/// Untyped node handle with the properties every node has.
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
21pub struct Node(NodeId);
22
23macro_rules! base_signal {
24    ($($(#[$doc:meta])* $field:ident: $ty:ty),* $(,)?) => {
25        $(
26            $(#[$doc])*
27            #[doc = ""]
28            #[doc = concat!("The `", stringify!($field), "` property as an animatable signal.")]
29            pub fn $field(self) -> Signal<$ty> {
30                with_graph(|g| g.node(self.0).$field.clone())
31            }
32        )*
33    };
34}
35
36impl Node {
37    /// Index of the node in the scene graph.
38    pub fn id(self) -> NodeId {
39        self.0
40    }
41
42    /// Wraps an id from [`NodeBuilder::build`](crate::NodeBuilder::build).
43    ///
44    /// Other crates implement [`NodeBuilder`](crate::NodeBuilder) with this:
45    /// the `parent` argument is only an id, and handles are what `add` needs.
46    pub fn from_id(id: NodeId) -> Self {
47        Self(id)
48    }
49
50    base_signal! {
51        x: f64,
52        y: f64,
53        /// Degrees, clockwise.
54        rotation: f64,
55        scale: Vec2,
56        opacity: f64,
57        fill: Color,
58        stroke: Color,
59        line_width: f64,
60        /// Start of the drawn part of the outline (fraction of its length).
61        start: f64,
62        /// End of the drawn part of the outline.
63        end: f64,
64        fill_gradient: Option<Gradient>,
65        stroke_gradient: Option<Gradient>,
66        dash: Vec<f64>,
67        dash_offset: f64,
68        line_cap: LineCap,
69        line_join: LineJoin,
70        blur: f64,
71        shadow_color: Color,
72        shadow_blur: f64,
73        shadow_offset: Vec2,
74        clip: bool,
75        blend: BlendMode,
76        z_index: f64,
77        anchor: Vec2,
78        grow: f64,
79        shrink: f64,
80        basis: Option<f64>,
81        margin: f64,
82        align_self: Option<crate::LayoutAlign>,
83    }
84
85    /// Adds a child node; children are positioned relative to the parent.
86    ///
87    /// The call site is stored on the node (`oxframe check` and `oxframe
88    /// inspect` print it). `.name("title")` overrides the automatic label.
89    #[allow(clippy::should_implement_trait)] // `view.add(..)` mirrors Motion Canvas
90    #[track_caller]
91    pub fn add<B: NodeBuilder>(self, child: B) -> B::Handle {
92        let loc = std::panic::Location::caller();
93        let prev = crate::graph::push_add_at(loc.file(), loc.line());
94        let handle = builder::add(self.0, child);
95        crate::graph::pop_add_at(prev);
96        handle
97    }
98
99    /// Detaches the node from its parent; it stops being drawn.
100    pub fn remove(self) {
101        with_graph(|g| g.detach(self.0));
102    }
103
104    /// Fades the node in from transparent, over the theme's normal duration
105    /// with its enter easing.
106    pub fn fade_in(self) -> Tween<f64> {
107        let motion = crate::theme::current().motion;
108        self.opacity().set(0.0);
109        self.opacity().to(1.0, motion.normal).ease(motion.enter)
110    }
111
112    /// Grows the node from nothing to its current scale with a small
113    /// overshoot (the theme's emphasis easing, `OUT_BACK` by default).
114    pub fn pop_in(self) -> Tween<Vec2> {
115        let motion = crate::theme::current().motion;
116        let full = self.scale().peek();
117        self.scale().set(Vec2::ZERO);
118        self.scale().to(full, motion.normal).ease(motion.emphasis)
119    }
120
121    /// Draws the outline from start to end (`end` from 0 to 1), as if with a
122    /// pen. Works on every shape with an outline: paths, lines, rectangles,
123    /// circles, polygons.
124    pub fn draw_on(self) -> Tween<f64> {
125        let motion = crate::theme::current().motion;
126        self.end().set(0.0);
127        self.end().to(1.0, motion.slow).ease(motion.enter)
128    }
129
130    /// Slides the node into its place from `SLIDE_DISTANCE` pixels towards
131    /// `from`, fading in on the way.
132    pub fn slide_in(self, from: Side) -> impl std::future::Future<Output = ()> + 'static {
133        let motion = crate::theme::current().motion;
134        let (axis, offset) = match from {
135            Side::Left => (self.x(), -SLIDE_DISTANCE),
136            Side::Right => (self.x(), SLIDE_DISTANCE),
137            Side::Top => (self.y(), -SLIDE_DISTANCE),
138            Side::Bottom => (self.y(), SLIDE_DISTANCE),
139        };
140        let home = axis.peek();
141        axis.set(home + offset);
142        let moves = all((
143            axis.to(home, motion.normal).ease(motion.enter),
144            self.fade_in(),
145        ));
146        async move {
147            moves.await;
148        }
149    }
150
151    /// Sits just outside `other`. The gap is the theme's `Md` step, and the
152    /// position is recomputed every frame.
153    pub fn next_to(self, other: impl Into<Node>, side: Side) -> Self {
154        let other = other.into();
155        with_graph(|g| g.set_place(self.0, Place::next_to(other.id(), side)));
156        self
157    }
158
159    /// Lines this node's `edge` up with the same edge of `other`.
160    pub fn align_to(self, other: impl Into<Node>, edge: Edge) -> Self {
161        let other = other.into();
162        with_graph(|g| g.set_place(self.0, Place::align_to(other.id(), edge)));
163        self
164    }
165
166    /// Pins one edge to the safe area. The other axis is left as it is.
167    pub fn to_edge(self, edge: Edge) -> Self {
168        with_graph(|g| g.set_place(self.0, Place::to_edge(edge)));
169        self
170    }
171
172    /// Same center as `other`.
173    pub fn center_in(self, other: impl Into<Node>) -> Self {
174        let other = other.into();
175        with_graph(|g| g.set_place(self.0, Place::center_in(other.id())));
176        self
177    }
178
179    /// Fades the node out, over the theme's normal duration with its exit easing.
180    pub fn fade_out(self) -> Tween<f64> {
181        let motion = crate::theme::current().motion;
182        self.opacity().to(0.0, motion.normal).ease(motion.exit)
183    }
184
185    /// Moves the node under another parent.
186    pub fn reparent(self, parent: impl Into<Node>) {
187        let parent = parent.into();
188        with_graph(|g| g.reparent(self.0, parent.0));
189    }
190
191    /// Direct children, in creation order.
192    pub fn children(self) -> Vec<Node> {
193        with_graph(|g| g.node(self.0).children.iter().map(|&c| Node(c)).collect())
194    }
195
196    /// Size of the node's box in its own coordinates.
197    pub fn size(self) -> (f64, f64) {
198        with_graph(|g| g.measure(self.0))
199    }
200
201    /// Position of the node's origin in scene coordinates.
202    pub fn absolute_position(self) -> Vec2 {
203        with_graph(|g| (g.world_transform(self.0).unwrap_or_default() * Point::ZERO).to_vec2())
204    }
205}
206
207macro_rules! typed_handle {
208    ($name:ident, $builder:ident) => {
209        #[doc = concat!("Handle to a `", stringify!($name), "` node in the scene: cheap to copy, its")]
210        #[doc = "methods return the node's properties as [`Signal`]s. Derefs to [`Node`]."]
211        #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
212        pub struct $name(Node);
213
214        impl $name {
215            /// Starts a builder; add it with [`Node::add`] (e.g. `view.add(..)`).
216            #[allow(clippy::new_ret_no_self)]
217            pub fn new() -> $builder {
218                $builder::default()
219            }
220
221            pub(crate) fn from_id(id: NodeId) -> Self {
222                Self(Node(id))
223            }
224
225            /// The untyped node handle (same as `*self`).
226            pub fn node(self) -> Node {
227                self.0
228            }
229        }
230
231        impl Deref for $name {
232            type Target = Node;
233            fn deref(&self) -> &Node {
234                &self.0
235            }
236        }
237
238        impl From<$name> for Node {
239            fn from(h: $name) -> Node {
240                h.0
241            }
242        }
243    };
244}
245
246typed_handle!(Group, GroupBuilder);
247typed_handle!(Camera, CameraBuilder);
248typed_handle!(Rect, RectBuilder);
249typed_handle!(Circle, CircleBuilder);
250typed_handle!(Path, PathBuilder);
251typed_handle!(Line, LineBuilder);
252typed_handle!(Txt, TxtBuilder);
253typed_handle!(Img, ImgBuilder);
254typed_handle!(Layout, LayoutBuilder);
255typed_handle!(Grid, GridBuilder);
256typed_handle!(Polygon, PolygonBuilder);
257typed_handle!(Star, StarBuilder);
258typed_handle!(Code, CodeBuilder);
259
260macro_rules! kind_signal {
261    ($handle:ident :: $kind:ident { $($(#[$doc:meta])* $field:ident: $ty:ty),* $(,)? }) => {
262        impl $handle {
263            $(
264                $(#[$doc])*
265                #[doc = ""]
266                #[doc = concat!("The `", stringify!($field), "` property as an animatable signal.")]
267                pub fn $field(self) -> Signal<$ty> {
268                    with_graph(|g| match &g.node(self.id()).kind {
269                        NodeKind::$kind { $field, .. } => $field.clone(),
270                        _ => unreachable!(concat!("handle does not point to a ", stringify!($kind))),
271                    })
272                }
273            )*
274        }
275    };
276}
277
278kind_signal!(Rect::Rect {
279    width: f64,
280    height: f64,
281    radius: f64
282});
283kind_signal!(Circle::Circle {
284    /// Diameter.
285    size: f64,
286    start_angle: f64,
287    end_angle: f64,
288});
289kind_signal!(Polygon::Polygon {
290    sides: f64,
291    size: f64
292});
293kind_signal!(Star::Star {
294    points: f64,
295    size: f64,
296    inner: f64
297});
298kind_signal!(Path::Path { data: BezPath });
299kind_signal!(Camera::Camera {
300    /// 1 shows the world at its size, 2 twice as big.
301    zoom: f64
302});
303
304impl Camera {
305    /// Moves the camera so `node`'s center is in the middle of the frame.
306    pub fn center_on(
307        self,
308        node: impl Into<Node>,
309        duration: impl Seconds,
310    ) -> impl std::future::Future<Output = ()> + 'static {
311        let node = node.into();
312        let d = duration.seconds();
313        let target = with_graph(|g| g.box_in(node.id(), self.id())).map(|r| r.center());
314        let moves = target.map(|c| all((self.x().to(c.x, d), self.y().to(c.y, d))));
315        async move {
316            if let Some(m) = moves {
317                m.await;
318            }
319        }
320    }
321
322    /// Centers on `node` and zooms so all of it fits in the frame with
323    /// `padding` pixels to spare. Measures the node when called, so a layout
324    /// that is still growing fits at its current size.
325    pub fn fit(
326        self,
327        node: impl Into<Node>,
328        padding: f64,
329        duration: impl Seconds,
330    ) -> impl std::future::Future<Output = ()> + 'static {
331        let node = node.into();
332        let d = duration.seconds();
333        let (bounds, (fw, fh)) = with_graph(|g| (g.box_in(node.id(), self.id()), g.frame_size()));
334        let moves = bounds.map(|r| {
335            let zoom = ((fw - 2.0 * padding) / r.width().max(1e-6))
336                .min((fh - 2.0 * padding) / r.height().max(1e-6))
337                .max(1e-3);
338            let c = r.center();
339            all((
340                self.x().to(c.x, d),
341                self.y().to(c.y, d),
342                self.zoom().to(zoom, d),
343            ))
344        });
345        async move {
346            if let Some(m) = moves {
347                m.await;
348            }
349        }
350    }
351
352    /// Back to the start: looking at the origin, no zoom, no rotation.
353    pub fn reset(self, duration: impl Seconds) -> impl std::future::Future<Output = ()> + 'static {
354        let d = duration.seconds();
355        let moves = all((
356            self.x().to(0.0, d),
357            self.y().to(0.0, d),
358            self.zoom().to(1.0, d),
359            self.rotation().to(0.0, d),
360        ));
361        async move {
362            moves.await;
363        }
364    }
365}
366kind_signal!(Line::Line { points: Vec<Vec2>, arrow_size: f64 });
367kind_signal!(Txt::Txt { text: String, spans: Vec<RichSpan>, font_size: f64, weight: f64 });
368kind_signal!(Code::Code {
369    code: String,
370    font_size: f64,
371    /// Focused lines as (first, last), 1-based; (0, 0) = all.
372    focus: Vec2,
373    /// Alpha of unfocused lines.
374    dim: f64,
375});
376
377impl Txt {
378    /// Morphs to `text` glyph by glyph: letters present in both strings
379    /// travel to their new places, the others fade.
380    pub fn morph_to(self, text: impl Into<String>, duration: f64) -> oxframe_core::BoxFuture {
381        let to = text.into();
382        let (txt, morph, t) = with_graph(|g| match &g.node(self.id()).kind {
383            NodeKind::Txt {
384                text,
385                morph,
386                morph_t,
387                ..
388            } => (text.clone(), morph.clone(), morph_t.clone()),
389            _ => unreachable!("handle does not point to a Txt"),
390        });
391        Box::pin(async move {
392            morph.set(Some((txt.get(), to.clone())));
393            t.set(0.0);
394            t.to(1.0, duration).await;
395            txt.set(to);
396            morph.set(None);
397        })
398    }
399}
400
401impl Code {
402    /// Moves the focus to lines `first..=last` over `duration`.
403    pub fn focus_lines(
404        self,
405        first: usize,
406        last: usize,
407        duration: f64,
408    ) -> oxframe_core::Tween<Vec2> {
409        self.focus()
410            .to(Vec2::new(first as f64, last as f64), duration)
411    }
412}
413kind_signal!(Img::Img {
414    width: f64,
415    height: f64
416});
417kind_signal!(Layout::Layout {
418    gap: f64,
419    padding: f64,
420    radius: f64,
421    /// Fixed width; `None` sizes to content.
422    width: Option<f64>,
423    /// Fixed height; `None` sizes to content.
424    height: Option<f64>,
425});
426kind_signal!(Grid::Grid {
427    width: f64,
428    height: f64,
429    spacing: f64
430});
431
432/// Root of a scene. Origin is the center of the frame, y points down.
433#[derive(Debug, Clone, Copy, PartialEq, Eq)]
434pub struct View(Node);
435
436impl View {
437    /// The view of the scene being executed.
438    pub fn current() -> Self {
439        Self(Node(SceneGraph::ROOT))
440    }
441
442    /// Frame size in pixels.
443    pub fn size(self) -> (u32, u32) {
444        with_graph(|g| g.size())
445    }
446
447    /// True when the frame is taller than it is wide.
448    pub fn is_portrait(self) -> bool {
449        let (w, h) = self.size();
450        h > w
451    }
452
453    /// The rectangle [`Node::to_edge`](crate::Node::to_edge) pins to, in scene
454    /// coordinates. A portrait frame leaves room for a vertical video's
455    /// status bar and buttons.
456    pub fn safe_area(self) -> SafeArea {
457        let (w, h) = self.size();
458        SafeArea::for_frame(f64::from(w), f64::from(h))
459    }
460}
461
462impl Deref for View {
463    type Target = Node;
464    fn deref(&self) -> &Node {
465        &self.0
466    }
467}
468
469impl From<View> for Node {
470    fn from(v: View) -> Node {
471        v.0
472    }
473}
474
475#[cfg(test)]
476mod tests {
477    use super::*;
478    use crate::Primitive;
479
480    fn scene(f: impl FnOnce(View)) -> std::rc::Rc<SceneGraph> {
481        let graph = SceneGraph::new(400, 400);
482        graph.enter(|| f(View::current()));
483        graph
484    }
485
486    /// Runs `scene` to the end inside a graph; returns the frames it took.
487    fn play(
488        scene_fn: impl FnOnce(View) -> std::pin::Pin<Box<dyn std::future::Future<Output = ()>>>,
489    ) -> u32 {
490        let graph = SceneGraph::new(400, 400);
491        graph.enter(|| {
492            let fut = scene_fn(View::current());
493            let mut ex =
494                oxframe_core::Executor::new(60.0, oxframe_protocol::Meta::default(), 0, fut);
495            let mut frames = 0;
496            while !ex.step() {
497                frames += 1;
498                assert!(frames < 10_000, "scene did not finish");
499            }
500            frames
501        })
502    }
503
504    /// Screen position of a node's origin.
505    fn screen(n: impl Into<Node>) -> (f64, f64) {
506        let p = n.into().absolute_position();
507        ((p.x * 1e6).round() / 1e6, (p.y * 1e6).round() / 1e6)
508    }
509
510    #[test]
511    fn camera_pans_zooms_and_turns_its_children_only() {
512        scene(|view| {
513            let cam = view.add(Camera::new());
514            let dot = cam.add(Circle::new().size(10.0).x(100.0).y(50.0));
515            let hud = view.add(Txt::new().text("HUD").x(-300.0).y(-200.0));
516            // A fresh camera changes nothing.
517            assert_eq!(screen(dot), (100.0, 50.0));
518            // Looking at the dot brings it to the center.
519            cam.x().set(100.0);
520            cam.y().set(50.0);
521            assert_eq!(screen(dot), (0.0, 0.0));
522            // Zoom scales distances from the point looked at.
523            cam.zoom().set(2.0);
524            let other = cam.add(Circle::new().size(10.0).x(110.0).y(50.0));
525            assert_eq!(screen(other), (20.0, 0.0));
526            // Turning the camera clockwise turns the world the other way.
527            cam.rotation().set(90.0);
528            assert_eq!(screen(other), (0.0, -20.0));
529            // Nodes outside the camera stay put.
530            assert_eq!(screen(hud), (-300.0, -200.0));
531        });
532    }
533
534    #[test]
535    fn camera_center_on_fit_and_reset() {
536        play(|view| {
537            let cam = view.add(Camera::new());
538            let card = cam.add(Rect::new().width(200.0).height(100.0).x(300.0).y(-100.0));
539            let row = cam.add(
540                Layout::new()
541                    .x(-500.0)
542                    .gap(10.0)
543                    .child(Rect::new().size(50.0))
544                    .child(Rect::new().size(50.0)),
545            );
546            Box::pin(async move {
547                cam.center_on(card, 1.0).await;
548                assert_eq!((cam.x().get(), cam.y().get()), (300.0, -100.0));
549                assert_eq!(screen(card), (0.0, 0.0));
550                // 400×400 frame, 20 px padding: limited by width (360 / 200).
551                cam.fit(card, 20.0, 0.0).await;
552                assert!(
553                    (cam.zoom().get() - 1.8).abs() < 1e-9,
554                    "{}",
555                    cam.zoom().get()
556                );
557                // A layout that grew is fitted at its current size.
558                row.gap().to(110.0, 0.5).await;
559                cam.fit(row, 0.0, 0.0).await;
560                let (w, _) = row.size();
561                assert!((w - 210.0).abs() < 1e-6, "layout width {w}");
562                assert!((cam.zoom().get() - 400.0 / 210.0).abs() < 1e-9);
563                assert!((cam.x().get() + 500.0).abs() < 1e-9);
564                cam.reset(0.5).await;
565                assert_eq!(
566                    (
567                        cam.x().get(),
568                        cam.y().get(),
569                        cam.zoom().get(),
570                        cam.rotation().get()
571                    ),
572                    (0.0, 0.0, 1.0, 0.0)
573                );
574            })
575        });
576    }
577
578    #[test]
579    fn entrance_helpers_start_at_once_and_end_in_place() {
580        let normal = crate::theme::current().motion.normal;
581        let expected = (normal * 60.0).round() as u32;
582        let frames = play(|view| {
583            let r = view.add(Rect::new().size(100.0).x(50.0));
584            let pop = r.pop_in();
585            // The start value is set when the helper is called, not awaited:
586            // nothing flashes in its final state for a frame.
587            assert_eq!(r.scale().get(), Vec2::ZERO);
588            let slide = r.slide_in(Side::Left);
589            assert_eq!(r.x().get(), 50.0 - SLIDE_DISTANCE);
590            assert_eq!(r.opacity().get(), 0.0);
591            let draw = r.draw_on();
592            assert_eq!(r.end().get(), 0.0);
593            Box::pin(async move {
594                all((pop, slide)).await;
595                assert_eq!(r.scale().get(), Vec2::new(1.0, 1.0));
596                assert_eq!((r.x().get(), r.opacity().get()), (50.0, 1.0));
597                draw.await;
598                assert_eq!(r.end().get(), 1.0);
599                r.fade_out().duration(0.5).await;
600                assert_eq!(r.opacity().get(), 0.0);
601            })
602        });
603        let slow = crate::theme::current().motion.slow;
604        assert_eq!(frames, expected + (slow * 60.0).round() as u32 + 30);
605    }
606
607    #[test]
608    fn add_records_its_call_site_and_scene_bounds() {
609        let g = scene(|view| {
610            view.add(Rect::new().size(100.0).name("box"));
611            view.add(Rect::new().size(10.0).x(100.0));
612        });
613        let nodes = g.inspect_nodes();
614        let named = nodes.iter().find(|n| n.name == "box").unwrap();
615        assert_eq!(named.kind, "Rect");
616        assert!(named.source.as_ref().unwrap().contains("handles.rs:"));
617        assert!(
618            (named.bounds[0] + 50.0).abs() < 1e-6 && (named.bounds[2] - 50.0).abs() < 1e-6,
619            "{:?}",
620            named.bounds
621        );
622        let auto = nodes
623            .iter()
624            .find(|n| n.name != "box" && n.name != "view")
625            .unwrap();
626        assert_eq!(auto.name, auto.source.clone().unwrap());
627    }
628
629    #[test]
630    fn builders_set_initial_values_and_handles_return_signals() {
631        scene(|view| {
632            let r = view.add(
633                Rect::new()
634                    .size(50.0)
635                    .radius(5.0)
636                    .x(-10.0)
637                    .fill(Color::BLACK),
638            );
639            assert_eq!(r.width().get(), 50.0);
640            assert_eq!(r.height().get(), 50.0);
641            assert_eq!(r.radius().get(), 5.0);
642            assert_eq!(r.x().get(), -10.0);
643            assert_eq!(r.fill().get(), Color::BLACK);
644            r.x().set(3.0);
645            assert_eq!(r.x().get(), 3.0);
646            let c = r.add(Circle::new().size(10.0));
647            assert_eq!(r.children(), vec![c.node()]);
648            assert_eq!(view.children(), vec![r.node()]);
649            assert_eq!(c.size().name().as_deref(), Some("Circle#2.size"));
650        });
651    }
652
653    #[test]
654    fn draw_list_composes_transforms() {
655        let g = scene(|view| {
656            let grp = view.add(Group::new().x(10.0).scale(2.0));
657            grp.add(Circle::new().x(5.0));
658        });
659        let items = g.draw_list();
660        assert_eq!(items.len(), 1);
661        assert_eq!(items[0].transform * Point::ZERO, Point::new(20.0, 0.0));
662    }
663
664    #[test]
665    fn layout_places_children_in_a_row() {
666        let g = scene(|view| {
667            view.add(
668                Layout::new()
669                    .gap(10.0)
670                    .padding(5.0)
671                    .child(Rect::new().size(20.0))
672                    .child(Rect::new().width(40.0).height(10.0)),
673            );
674        });
675        let layout = NodeId(1);
676        assert_eq!(g.measure(layout), (5.0 + 20.0 + 10.0 + 40.0 + 5.0, 30.0));
677        let centers: Vec<_> = g
678            .draw_list()
679            .iter()
680            .map(|i| i.transform * Point::ZERO)
681            .collect();
682        // Total 80 wide: first box spans -35..-15, second -5..35.
683        assert_eq!(centers, vec![Point::new(-25.0, 0.0), Point::new(15.0, 0.0)]);
684    }
685
686    #[test]
687    fn column_with_fixed_size_and_alignment() {
688        let g = scene(|view| {
689            view.add(
690                Layout::new()
691                    .column()
692                    .height(100.0)
693                    .width(100.0)
694                    .justify(crate::Justify::Start)
695                    .align(crate::LayoutAlign::Start)
696                    .child(Rect::new().size(20.0)),
697            );
698        });
699        let c = g.draw_list()[0].transform * Point::ZERO;
700        assert_eq!(c, Point::new(-40.0, -40.0));
701    }
702
703    #[test]
704    fn text_line_path_and_img_draw() {
705        let png = {
706            let mut v = Vec::new();
707            image::RgbaImage::from_pixel(4, 2, image::Rgba([255, 0, 0, 255]))
708                .write_to(&mut std::io::Cursor::new(&mut v), image::ImageFormat::Png)
709                .unwrap();
710            v
711        };
712        let g = scene(|view| {
713            let t = view.add(Txt::new().text("Hi").font_size(40.0));
714            assert!(t.size().0 > 20.0);
715            view.add(
716                Line::new()
717                    .points([(0.0, 0.0), (100.0, 0.0)])
718                    .end_arrow(true),
719            );
720            view.add(Path::new().svg("M0 0 L10 0 L10 10 Z"));
721            let img = view.add(Img::new().src(&png).width(40.0));
722            assert_eq!(img.height().get(), 20.0);
723        });
724        let kinds: Vec<_> = g
725            .draw_list()
726            .iter()
727            .map(|i| match i.prim {
728                Primitive::Fill { .. } => "fill",
729                Primitive::Stroke { .. } => "stroke",
730                Primitive::Image { .. } => "image",
731                Primitive::PushLayer(_) => "push",
732                Primitive::PopLayer => "pop",
733            })
734            .collect();
735        assert_eq!(kinds, vec!["fill", "stroke", "fill", "fill", "image"]);
736    }
737
738    #[test]
739    fn trimming_and_pick() {
740        let g = scene(|view| {
741            view.add(Rect::new().size(100.0).name("back"));
742            view.add(Circle::new().size(40.0).x(30.0).end(0.5));
743        });
744        assert_eq!(g.pick(Point::new(-40.0, 0.0)), Some(NodeId(1)));
745        assert_eq!(g.pick(Point::new(300.0, 0.0)), None);
746        // The half circle covers the right half of its disc.
747        assert_eq!(g.pick(Point::new(45.0, 0.0)), Some(NodeId(2)));
748    }
749
750    fn kinds(g: &SceneGraph) -> Vec<&'static str> {
751        g.draw_list()
752            .iter()
753            .map(|i| match i.prim {
754                Primitive::Fill { .. } => "fill",
755                Primitive::Stroke { .. } => "stroke",
756                Primitive::Image { .. } => "image",
757                Primitive::PushLayer(_) => "push",
758                Primitive::PopLayer => "pop",
759            })
760            .collect()
761    }
762
763    #[test]
764    fn layers_only_when_needed_and_balanced() {
765        // Translucent leaf: opacity folds into the paint, no layer.
766        let g = scene(|v| {
767            v.add(Rect::new().opacity(0.5));
768        });
769        assert_eq!(kinds(&g), ["fill"]);
770        // Translucent group with children: one layer around them.
771        let g = scene(|v| {
772            v.add(
773                Group::new()
774                    .opacity(0.5)
775                    .child(Rect::new())
776                    .child(Circle::new()),
777            );
778        });
779        assert_eq!(kinds(&g), ["push", "fill", "fill", "pop"]);
780        // Clip wraps only the children; shadow wraps node + children.
781        let g = scene(|v| {
782            v.add(
783                Circle::new()
784                    .clip(true)
785                    .shadow(Color::BLACK, 4.0, (2.0, 2.0))
786                    .child(Rect::new()),
787            );
788        });
789        assert_eq!(kinds(&g), ["push", "fill", "push", "fill", "pop", "pop"]);
790    }
791
792    #[test]
793    fn z_index_orders_siblings_and_anchor_moves_box() {
794        let g = scene(|v| {
795            v.add(Rect::new().name("a").z_index(2.0));
796            v.add(Rect::new().name("b"));
797            v.add(Rect::new().name("c").size(20.0).anchor((-1.0, -1.0)).x(5.0));
798        });
799        let items = g.draw_list();
800        let order: Vec<u32> = items.iter().map(|i| i.node.0).collect();
801        assert_eq!(order, vec![2, 3, 1]);
802        // Anchored at the top-left: the box center sits at (5+10, 0+10).
803        assert_eq!(items[1].transform * Point::ZERO, Point::new(15.0, 10.0));
804    }
805
806    fn boxes(g: &SceneGraph) -> Vec<(Point, f64)> {
807        g.draw_list()
808            .iter()
809            .filter_map(|i| match &i.prim {
810                Primitive::Fill { path, .. } => {
811                    let bb = kurbo::Shape::bounding_box(path);
812                    Some((i.transform * Point::ZERO, bb.width()))
813                }
814                _ => None,
815            })
816            .collect()
817    }
818
819    #[test]
820    fn flex_grow_stretch_wrap_and_grid() {
821        use crate::{Justify, LayoutAlign};
822        // grow: a 200-wide row; the second child takes the free space.
823        let g = scene(|v| {
824            v.add(
825                Layout::new()
826                    .width(200.0)
827                    .justify(Justify::Start)
828                    .child(Rect::new().size(40.0))
829                    .child(Rect::new().size(40.0).grow(1.0)),
830            );
831        });
832        let b = boxes(&g);
833        assert_eq!(b[0], (Point::new(-80.0, 0.0), 40.0));
834        assert_eq!(b[1], (Point::new(20.0, 0.0), 160.0));
835
836        // stretch: children fill the cross axis.
837        let g = scene(|v| {
838            v.add(
839                Layout::new()
840                    .column()
841                    .width(120.0)
842                    .align(LayoutAlign::Stretch)
843                    .child(Rect::new().size(30.0)),
844            );
845        });
846        assert_eq!(boxes(&g)[0].1, 120.0);
847
848        // wrap: three 60-wide boxes in 130 → two lines.
849        let g = scene(|v| {
850            v.add(
851                Layout::new()
852                    .width(130.0)
853                    .wrap(true)
854                    .child(Rect::new().size(60.0))
855                    .child(Rect::new().size(60.0))
856                    .child(Rect::new().size(60.0)),
857            );
858        });
859        let ys: Vec<f64> = boxes(&g).iter().map(|b| b.0.y).collect();
860        assert_eq!(ys[0], ys[1]);
861        assert!(ys[2] > ys[0]);
862
863        // grid: 2 columns of equal width.
864        let g = scene(|v| {
865            v.add(
866                Layout::new()
867                    .grid(2)
868                    .width(200.0)
869                    .align(LayoutAlign::Stretch)
870                    .child(Rect::new().size(10.0))
871                    .child(Rect::new().size(10.0))
872                    .child(Rect::new().size(10.0)),
873            );
874        });
875        let b = boxes(&g);
876        assert_eq!(
877            b.iter().map(|b| b.1).collect::<Vec<_>>(),
878            vec![100.0, 100.0, 100.0]
879        );
880        assert_eq!(b[2].0.x, b[0].0.x);
881        assert!(b[2].0.y > b[0].0.y);
882    }
883
884    #[test]
885    fn describe_lists_signals() {
886        let g = scene(|view| {
887            view.add(Txt::new().text("abc"));
888        });
889        let d = g.describe(NodeId(1));
890        assert!(d.iter().any(|(k, v)| k == "text" && v == "abc"));
891    }
892}