Skip to main content

telar_ui_core/
canvas.rs

1use crate::impl_leaf_widget;
2use crate::layout_leaf::LayoutLeaf;
3use geometry_core::Rect;
4use layout_core::{LayoutError, LayoutStyle};
5use platform_core::Event;
6use ui_tree::{Component, EventResult, RenderNode};
7
8pub struct Canvas {
9    leaf: LayoutLeaf,
10    draw: Box<dyn Fn(Rect) -> RenderNode>,
11}
12
13impl Canvas {
14    pub fn new(
15        layout_style: LayoutStyle,
16        draw_fn: impl Fn(Rect) -> RenderNode + 'static,
17    ) -> Result<Self, LayoutError> {
18        let leaf = LayoutLeaf::register(layout_style)?;
19        Ok(Self {
20            leaf,
21            draw: Box::new(draw_fn),
22        })
23    }
24}
25
26impl Canvas {
27    pub fn with_intrinsic_height(
28        height: f32,
29        draw_fn: impl Fn(geometry_core::Rect) -> ui_tree::RenderNode + 'static,
30    ) -> Result<Self, layout_core::LayoutError> {
31        Self::new(layout_core::LayoutStyle::new().height(height), draw_fn)
32    }
33}
34
35impl_leaf_widget!(Canvas);
36
37impl Component for Canvas {
38    fn view(&self) -> RenderNode {
39        let r = self.leaf.rect.get();
40        // A Canvas closure draws at fixed coordinates that ignore the layout rect, so a collapsed rect
41        // (e.g. a section hidden via `display:none`) would still paint over other content. Draw nothing.
42        if r.width <= 0.0 || r.height <= 0.0 {
43            return RenderNode::Empty;
44        }
45        // The closure draws in local space (at_layout_position translates the output), so it gets a zero-origin rect — passing the absolute layout rect would double-offset anything derived from rect.x/y.
46        let local = Rect {
47            x: 0.0,
48            y: 0.0,
49            width: r.width,
50            height: r.height,
51        };
52        let inner = (self.draw)(local);
53        self.leaf.at_layout_position(inner)
54    }
55
56    fn on_event(&mut self, _event: &Event) -> EventResult {
57        EventResult::Ignored
58    }
59
60    fn debug_name(&self) -> &'static str {
61        "Canvas"
62    }
63}
64
65#[cfg(test)]
66mod tests {
67    use crate::context::reset_layout_runtime;
68    use std::cell::Cell;
69    use std::rc::Rc;
70
71    use layout_core::AvailableSpace;
72    use renderer_core::{Color, DrawCommand, Paint, RectStyle, ShapeStyle};
73
74    use super::*;
75    use crate::context::{compute_layout, new_container};
76    use crate::layout_item::LayoutItem;
77
78    // `draw` must be re-invoked on every `view()`, not cached from construction — a `$signal` colour
79    // read inside it (the reactive path the transpiler now clones into a canvas child's `fill`/`stroke`)
80    // would otherwise freeze at whatever value was current when the closure was built.
81    #[test]
82    fn draw_closure_is_re_read_each_view_and_recolors() {
83        let color = Rc::new(Cell::new(Color::RED));
84        let color_read = color.clone();
85        reset_layout_runtime();
86        let canvas = Canvas::new(LayoutStyle::new().width(40.0).height(40.0), move |r| {
87            RenderNode::rect(r, RectStyle::default().with_fill(color_read.get()))
88        })
89        .unwrap();
90        let root = new_container(
91            LayoutStyle::new().width(40.0).height(40.0),
92            &[canvas.layout_node()],
93        )
94        .unwrap();
95        compute_layout(
96            root,
97            AvailableSpace::Definite(40.0),
98            AvailableSpace::Definite(40.0),
99        )
100        .unwrap();
101
102        assert_eq!(fill_of(&canvas.view()), Paint::Solid(Color::RED));
103        color.set(Color::BLUE);
104        assert_eq!(
105            fill_of(&canvas.view()),
106            Paint::Solid(Color::BLUE),
107            "draw closure must be re-read on the second view(), not cached from construction"
108        );
109    }
110
111    fn fill_of(view: &RenderNode) -> Paint {
112        let RenderNode::Transform { children, .. } = view else {
113            panic!("expected Transform")
114        };
115        let RenderNode::Primitive(DrawCommand::Rect { style, .. }) = &children[0] else {
116            panic!("expected a Rect primitive")
117        };
118        style.fill.expect("expected a fill")
119    }
120}