Skip to main content

oxframe_2d/
handles.rs

1use std::ops::Deref;
2
3use kurbo::{BezPath, Point};
4use oxframe_core::{BlendMode, Color, Gradient, Signal, Vec2};
5
6use crate::builder::{
7    self, CircleBuilder, CodeBuilder, GridBuilder, GroupBuilder, ImgBuilder, LayoutBuilder,
8    LineBuilder, NodeBuilder, PathBuilder, PolygonBuilder, RectBuilder, StarBuilder, TxtBuilder,
9};
10use crate::draw::{LineCap, LineJoin};
11use crate::graph::{NodeId, NodeKind, SceneGraph, with_graph};
12use crate::text::RichSpan;
13
14/// Untyped node handle with the properties every node has.
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
16pub struct Node(NodeId);
17
18macro_rules! base_signal {
19    ($($(#[$doc:meta])* $field:ident: $ty:ty),* $(,)?) => {
20        $(
21            $(#[$doc])*
22            #[doc = ""]
23            #[doc = concat!("The `", stringify!($field), "` property as an animatable signal.")]
24            pub fn $field(self) -> Signal<$ty> {
25                with_graph(|g| g.node(self.0).$field.clone())
26            }
27        )*
28    };
29}
30
31impl Node {
32    /// Index of the node in the scene graph.
33    pub fn id(self) -> NodeId {
34        self.0
35    }
36
37    base_signal! {
38        x: f64,
39        y: f64,
40        /// Degrees, clockwise.
41        rotation: f64,
42        scale: Vec2,
43        opacity: f64,
44        fill: Color,
45        stroke: Color,
46        line_width: f64,
47        /// Start of the drawn part of the outline (fraction of its length).
48        start: f64,
49        /// End of the drawn part of the outline.
50        end: f64,
51        fill_gradient: Option<Gradient>,
52        stroke_gradient: Option<Gradient>,
53        dash: Vec<f64>,
54        dash_offset: f64,
55        line_cap: LineCap,
56        line_join: LineJoin,
57        blur: f64,
58        shadow_color: Color,
59        shadow_blur: f64,
60        shadow_offset: Vec2,
61        clip: bool,
62        blend: BlendMode,
63        z_index: f64,
64        anchor: Vec2,
65        grow: f64,
66        shrink: f64,
67        basis: Option<f64>,
68        margin: f64,
69        align_self: Option<crate::LayoutAlign>,
70    }
71
72    /// Adds a child node; children are positioned relative to the parent.
73    #[allow(clippy::should_implement_trait)] // `view.add(..)` mirrors Motion Canvas
74    pub fn add<B: NodeBuilder>(self, child: B) -> B::Handle {
75        builder::add(self.0, child)
76    }
77
78    /// Detaches the node from its parent; it stops being drawn.
79    pub fn remove(self) {
80        with_graph(|g| g.detach(self.0));
81    }
82
83    /// Moves the node under another parent.
84    pub fn reparent(self, parent: impl Into<Node>) {
85        let parent = parent.into();
86        with_graph(|g| g.reparent(self.0, parent.0));
87    }
88
89    /// Direct children, in creation order.
90    pub fn children(self) -> Vec<Node> {
91        with_graph(|g| g.node(self.0).children.iter().map(|&c| Node(c)).collect())
92    }
93
94    /// Size of the node's box in its own coordinates.
95    pub fn size(self) -> (f64, f64) {
96        with_graph(|g| g.measure(self.0))
97    }
98
99    /// Position of the node's origin in scene coordinates.
100    pub fn absolute_position(self) -> Vec2 {
101        with_graph(|g| (g.world_transform(self.0).unwrap_or_default() * Point::ZERO).to_vec2())
102    }
103}
104
105macro_rules! typed_handle {
106    ($name:ident, $builder:ident) => {
107        #[doc = concat!("Handle to a `", stringify!($name), "` node in the scene: cheap to copy, its")]
108        #[doc = "methods return the node's properties as [`Signal`]s. Derefs to [`Node`]."]
109        #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
110        pub struct $name(Node);
111
112        impl $name {
113            /// Starts a builder; add it with [`Node::add`] (e.g. `view.add(..)`).
114            #[allow(clippy::new_ret_no_self)]
115            pub fn new() -> $builder {
116                $builder::default()
117            }
118
119            pub(crate) fn from_id(id: NodeId) -> Self {
120                Self(Node(id))
121            }
122
123            /// The untyped node handle (same as `*self`).
124            pub fn node(self) -> Node {
125                self.0
126            }
127        }
128
129        impl Deref for $name {
130            type Target = Node;
131            fn deref(&self) -> &Node {
132                &self.0
133            }
134        }
135
136        impl From<$name> for Node {
137            fn from(h: $name) -> Node {
138                h.0
139            }
140        }
141    };
142}
143
144typed_handle!(Group, GroupBuilder);
145typed_handle!(Rect, RectBuilder);
146typed_handle!(Circle, CircleBuilder);
147typed_handle!(Path, PathBuilder);
148typed_handle!(Line, LineBuilder);
149typed_handle!(Txt, TxtBuilder);
150typed_handle!(Img, ImgBuilder);
151typed_handle!(Layout, LayoutBuilder);
152typed_handle!(Grid, GridBuilder);
153typed_handle!(Polygon, PolygonBuilder);
154typed_handle!(Star, StarBuilder);
155typed_handle!(Code, CodeBuilder);
156
157macro_rules! kind_signal {
158    ($handle:ident :: $kind:ident { $($(#[$doc:meta])* $field:ident: $ty:ty),* $(,)? }) => {
159        impl $handle {
160            $(
161                $(#[$doc])*
162                #[doc = ""]
163                #[doc = concat!("The `", stringify!($field), "` property as an animatable signal.")]
164                pub fn $field(self) -> Signal<$ty> {
165                    with_graph(|g| match &g.node(self.id()).kind {
166                        NodeKind::$kind { $field, .. } => $field.clone(),
167                        _ => unreachable!(concat!("handle does not point to a ", stringify!($kind))),
168                    })
169                }
170            )*
171        }
172    };
173}
174
175kind_signal!(Rect::Rect {
176    width: f64,
177    height: f64,
178    radius: f64
179});
180kind_signal!(Circle::Circle {
181    /// Diameter.
182    size: f64,
183    start_angle: f64,
184    end_angle: f64,
185});
186kind_signal!(Polygon::Polygon {
187    sides: f64,
188    size: f64
189});
190kind_signal!(Star::Star {
191    points: f64,
192    size: f64,
193    inner: f64
194});
195kind_signal!(Path::Path { data: BezPath });
196kind_signal!(Line::Line { points: Vec<Vec2>, arrow_size: f64 });
197kind_signal!(Txt::Txt { text: String, spans: Vec<RichSpan>, font_size: f64, weight: f64 });
198kind_signal!(Code::Code {
199    code: String,
200    font_size: f64,
201    /// Focused lines as (first, last), 1-based; (0, 0) = all.
202    focus: Vec2,
203    /// Alpha of unfocused lines.
204    dim: f64,
205});
206
207impl Txt {
208    /// Morphs to `text` glyph by glyph: letters present in both strings
209    /// travel to their new places, the others fade.
210    pub fn morph_to(self, text: impl Into<String>, duration: f64) -> oxframe_core::BoxFuture {
211        let to = text.into();
212        let (txt, morph, t) = with_graph(|g| match &g.node(self.id()).kind {
213            NodeKind::Txt {
214                text,
215                morph,
216                morph_t,
217                ..
218            } => (text.clone(), morph.clone(), morph_t.clone()),
219            _ => unreachable!("handle does not point to a Txt"),
220        });
221        Box::pin(async move {
222            morph.set(Some((txt.get(), to.clone())));
223            t.set(0.0);
224            t.to(1.0, duration).await;
225            txt.set(to);
226            morph.set(None);
227        })
228    }
229}
230
231impl Code {
232    /// Moves the focus to lines `first..=last` over `duration`.
233    pub fn focus_lines(
234        self,
235        first: usize,
236        last: usize,
237        duration: f64,
238    ) -> oxframe_core::Tween<Vec2> {
239        self.focus()
240            .to(Vec2::new(first as f64, last as f64), duration)
241    }
242}
243kind_signal!(Img::Img {
244    width: f64,
245    height: f64
246});
247kind_signal!(Layout::Layout {
248    gap: f64,
249    padding: f64,
250    radius: f64,
251    /// Fixed width; `None` sizes to content.
252    width: Option<f64>,
253    /// Fixed height; `None` sizes to content.
254    height: Option<f64>,
255});
256kind_signal!(Grid::Grid {
257    width: f64,
258    height: f64,
259    spacing: f64
260});
261
262/// Root of a scene. Origin is the center of the frame, y points down.
263#[derive(Debug, Clone, Copy, PartialEq, Eq)]
264pub struct View(Node);
265
266impl View {
267    /// The view of the scene being executed.
268    pub fn current() -> Self {
269        Self(Node(SceneGraph::ROOT))
270    }
271
272    /// Frame size in pixels.
273    pub fn size(self) -> (u32, u32) {
274        with_graph(|g| g.size())
275    }
276}
277
278impl Deref for View {
279    type Target = Node;
280    fn deref(&self) -> &Node {
281        &self.0
282    }
283}
284
285impl From<View> for Node {
286    fn from(v: View) -> Node {
287        v.0
288    }
289}
290
291#[cfg(test)]
292mod tests {
293    use super::*;
294    use crate::Primitive;
295
296    fn scene(f: impl FnOnce(View)) -> std::rc::Rc<SceneGraph> {
297        let graph = SceneGraph::new(400, 400);
298        graph.enter(|| f(View::current()));
299        graph
300    }
301
302    #[test]
303    fn builders_set_initial_values_and_handles_return_signals() {
304        scene(|view| {
305            let r = view.add(
306                Rect::new()
307                    .size(50.0)
308                    .radius(5.0)
309                    .x(-10.0)
310                    .fill(Color::BLACK),
311            );
312            assert_eq!(r.width().get(), 50.0);
313            assert_eq!(r.height().get(), 50.0);
314            assert_eq!(r.radius().get(), 5.0);
315            assert_eq!(r.x().get(), -10.0);
316            assert_eq!(r.fill().get(), Color::BLACK);
317            r.x().set(3.0);
318            assert_eq!(r.x().get(), 3.0);
319            let c = r.add(Circle::new().size(10.0));
320            assert_eq!(r.children(), vec![c.node()]);
321            assert_eq!(view.children(), vec![r.node()]);
322            assert_eq!(c.size().name().as_deref(), Some("Circle#2.size"));
323        });
324    }
325
326    #[test]
327    fn draw_list_composes_transforms() {
328        let g = scene(|view| {
329            let grp = view.add(Group::new().x(10.0).scale(2.0));
330            grp.add(Circle::new().x(5.0));
331        });
332        let items = g.draw_list();
333        assert_eq!(items.len(), 1);
334        assert_eq!(items[0].transform * Point::ZERO, Point::new(20.0, 0.0));
335    }
336
337    #[test]
338    fn layout_places_children_in_a_row() {
339        let g = scene(|view| {
340            view.add(
341                Layout::new()
342                    .gap(10.0)
343                    .padding(5.0)
344                    .child(Rect::new().size(20.0))
345                    .child(Rect::new().width(40.0).height(10.0)),
346            );
347        });
348        let layout = NodeId(1);
349        assert_eq!(g.measure(layout), (5.0 + 20.0 + 10.0 + 40.0 + 5.0, 30.0));
350        let centers: Vec<_> = g
351            .draw_list()
352            .iter()
353            .map(|i| i.transform * Point::ZERO)
354            .collect();
355        // Total 80 wide: first box spans -35..-15, second -5..35.
356        assert_eq!(centers, vec![Point::new(-25.0, 0.0), Point::new(15.0, 0.0)]);
357    }
358
359    #[test]
360    fn column_with_fixed_size_and_alignment() {
361        let g = scene(|view| {
362            view.add(
363                Layout::new()
364                    .column()
365                    .height(100.0)
366                    .width(100.0)
367                    .justify(crate::Justify::Start)
368                    .align(crate::LayoutAlign::Start)
369                    .child(Rect::new().size(20.0)),
370            );
371        });
372        let c = g.draw_list()[0].transform * Point::ZERO;
373        assert_eq!(c, Point::new(-40.0, -40.0));
374    }
375
376    #[test]
377    fn text_line_path_and_img_draw() {
378        let png = {
379            let mut v = Vec::new();
380            image::RgbaImage::from_pixel(4, 2, image::Rgba([255, 0, 0, 255]))
381                .write_to(&mut std::io::Cursor::new(&mut v), image::ImageFormat::Png)
382                .unwrap();
383            v
384        };
385        let g = scene(|view| {
386            let t = view.add(Txt::new().text("Hi").font_size(40.0));
387            assert!(t.size().0 > 20.0);
388            view.add(
389                Line::new()
390                    .points([(0.0, 0.0), (100.0, 0.0)])
391                    .end_arrow(true),
392            );
393            view.add(Path::new().svg("M0 0 L10 0 L10 10 Z"));
394            let img = view.add(Img::new().src(&png).width(40.0));
395            assert_eq!(img.height().get(), 20.0);
396        });
397        let kinds: Vec<_> = g
398            .draw_list()
399            .iter()
400            .map(|i| match i.prim {
401                Primitive::Fill { .. } => "fill",
402                Primitive::Stroke { .. } => "stroke",
403                Primitive::Image { .. } => "image",
404                Primitive::PushLayer(_) => "push",
405                Primitive::PopLayer => "pop",
406            })
407            .collect();
408        assert_eq!(kinds, vec!["fill", "stroke", "fill", "fill", "image"]);
409    }
410
411    #[test]
412    fn trimming_and_pick() {
413        let g = scene(|view| {
414            view.add(Rect::new().size(100.0).name("back"));
415            view.add(Circle::new().size(40.0).x(30.0).end(0.5));
416        });
417        assert_eq!(g.pick(Point::new(-40.0, 0.0)), Some(NodeId(1)));
418        assert_eq!(g.pick(Point::new(300.0, 0.0)), None);
419        // The half circle covers the right half of its disc.
420        assert_eq!(g.pick(Point::new(45.0, 0.0)), Some(NodeId(2)));
421    }
422
423    fn kinds(g: &SceneGraph) -> Vec<&'static str> {
424        g.draw_list()
425            .iter()
426            .map(|i| match i.prim {
427                Primitive::Fill { .. } => "fill",
428                Primitive::Stroke { .. } => "stroke",
429                Primitive::Image { .. } => "image",
430                Primitive::PushLayer(_) => "push",
431                Primitive::PopLayer => "pop",
432            })
433            .collect()
434    }
435
436    #[test]
437    fn layers_only_when_needed_and_balanced() {
438        // Translucent leaf: opacity folds into the paint, no layer.
439        let g = scene(|v| {
440            v.add(Rect::new().opacity(0.5));
441        });
442        assert_eq!(kinds(&g), ["fill"]);
443        // Translucent group with children: one layer around them.
444        let g = scene(|v| {
445            v.add(
446                Group::new()
447                    .opacity(0.5)
448                    .child(Rect::new())
449                    .child(Circle::new()),
450            );
451        });
452        assert_eq!(kinds(&g), ["push", "fill", "fill", "pop"]);
453        // Clip wraps only the children; shadow wraps node + children.
454        let g = scene(|v| {
455            v.add(
456                Circle::new()
457                    .clip(true)
458                    .shadow(Color::BLACK, 4.0, (2.0, 2.0))
459                    .child(Rect::new()),
460            );
461        });
462        assert_eq!(kinds(&g), ["push", "fill", "push", "fill", "pop", "pop"]);
463    }
464
465    #[test]
466    fn z_index_orders_siblings_and_anchor_moves_box() {
467        let g = scene(|v| {
468            v.add(Rect::new().name("a").z_index(2.0));
469            v.add(Rect::new().name("b"));
470            v.add(Rect::new().name("c").size(20.0).anchor((-1.0, -1.0)).x(5.0));
471        });
472        let items = g.draw_list();
473        let order: Vec<u32> = items.iter().map(|i| i.node.0).collect();
474        assert_eq!(order, vec![2, 3, 1]);
475        // Anchored at the top-left: the box center sits at (5+10, 0+10).
476        assert_eq!(items[1].transform * Point::ZERO, Point::new(15.0, 10.0));
477    }
478
479    fn boxes(g: &SceneGraph) -> Vec<(Point, f64)> {
480        g.draw_list()
481            .iter()
482            .filter_map(|i| match &i.prim {
483                Primitive::Fill { path, .. } => {
484                    let bb = kurbo::Shape::bounding_box(path);
485                    Some((i.transform * Point::ZERO, bb.width()))
486                }
487                _ => None,
488            })
489            .collect()
490    }
491
492    #[test]
493    fn flex_grow_stretch_wrap_and_grid() {
494        use crate::{Justify, LayoutAlign};
495        // grow: a 200-wide row; the second child takes the free space.
496        let g = scene(|v| {
497            v.add(
498                Layout::new()
499                    .width(200.0)
500                    .justify(Justify::Start)
501                    .child(Rect::new().size(40.0))
502                    .child(Rect::new().size(40.0).grow(1.0)),
503            );
504        });
505        let b = boxes(&g);
506        assert_eq!(b[0], (Point::new(-80.0, 0.0), 40.0));
507        assert_eq!(b[1], (Point::new(20.0, 0.0), 160.0));
508
509        // stretch: children fill the cross axis.
510        let g = scene(|v| {
511            v.add(
512                Layout::new()
513                    .column()
514                    .width(120.0)
515                    .align(LayoutAlign::Stretch)
516                    .child(Rect::new().size(30.0)),
517            );
518        });
519        assert_eq!(boxes(&g)[0].1, 120.0);
520
521        // wrap: three 60-wide boxes in 130 → two lines.
522        let g = scene(|v| {
523            v.add(
524                Layout::new()
525                    .width(130.0)
526                    .wrap(true)
527                    .child(Rect::new().size(60.0))
528                    .child(Rect::new().size(60.0))
529                    .child(Rect::new().size(60.0)),
530            );
531        });
532        let ys: Vec<f64> = boxes(&g).iter().map(|b| b.0.y).collect();
533        assert_eq!(ys[0], ys[1]);
534        assert!(ys[2] > ys[0]);
535
536        // grid: 2 columns of equal width.
537        let g = scene(|v| {
538            v.add(
539                Layout::new()
540                    .grid(2)
541                    .width(200.0)
542                    .align(LayoutAlign::Stretch)
543                    .child(Rect::new().size(10.0))
544                    .child(Rect::new().size(10.0))
545                    .child(Rect::new().size(10.0)),
546            );
547        });
548        let b = boxes(&g);
549        assert_eq!(
550            b.iter().map(|b| b.1).collect::<Vec<_>>(),
551            vec![100.0, 100.0, 100.0]
552        );
553        assert_eq!(b[2].0.x, b[0].0.x);
554        assert!(b[2].0.y > b[0].0.y);
555    }
556
557    #[test]
558    fn describe_lists_signals() {
559        let g = scene(|view| {
560            view.add(Txt::new().text("abc"));
561        });
562        let d = g.describe(NodeId(1));
563        assert!(d.iter().any(|(k, v)| k == "text" && v == "abc"));
564    }
565}