Skip to main content

fui/node/
custom_drawable.rs

1use super::core::*;
2use super::*;
3
4#[derive(Clone)]
5/// A retained visual surface whose callback records immediate drawing commands.
6///
7/// The same callback API is supported by browser/WebAssembly and native desktop
8/// hosts. Keep expensive drawing resources outside the callback and invalidate
9/// the drawable after retained state changes.
10///
11/// ```no_run
12/// use fui::prelude::*;
13///
14/// let preview = custom_drawable(|context| {
15///     context.draw_circle(32.0, 32.0, 20.0, Paint::fill(rgb(0x38, 0xbd, 0xf8)));
16/// });
17/// preview
18///     .width(64.0, Unit::Pixel)
19///     .height(64.0, Unit::Pixel)
20///     .semantic_label("Drawing preview");
21/// preview.mark_dirty();
22/// ```
23pub struct CustomDrawable {
24    base: FlexBox,
25    draw_callback: DrawCallback,
26}
27
28#[derive(Clone)]
29/// A weak invalidation handle that does not keep its [`CustomDrawable`] alive.
30pub struct DrawableInvalidator {
31    base: WeakFlexBox,
32}
33
34impl DrawableInvalidator {
35    /// Schedules a redraw if the associated drawable is still mounted.
36    pub fn mark_dirty(&self) {
37        if let Some(base) = self.base.upgrade() {
38            mark_base_dirty(&base);
39        }
40    }
41}
42
43impl CustomDrawable {
44    /// Creates a retained custom-drawing surface.
45    ///
46    /// The runtime saves canvas state, clips to the drawable bounds, invokes
47    /// `handler`, restores state, and flushes the command batch.
48    pub fn new(handler: impl Fn(&mut DrawContext) + 'static) -> Self {
49        let base = FlexBox::default();
50        base.custom_drawable(true);
51        Self {
52            base,
53            draw_callback: Rc::new(handler),
54        }
55    }
56
57    /// Schedules a commit when visible drawing state has changed.
58    pub fn mark_dirty(&self) {
59        mark_base_dirty(&self.base);
60    }
61
62    /// Returns a weak invalidator suitable for timers and readiness callbacks.
63    pub fn invalidator(&self) -> DrawableInvalidator {
64        DrawableInvalidator {
65            base: self.base.downgrade(),
66        }
67    }
68}
69
70fn mark_base_dirty(base: &FlexBox) {
71    let handle = base.handle();
72    if handle != NodeHandle::INVALID {
73        let Some(bounds) = ui::get_visible_bounds(handle.raw()) else {
74            return;
75        };
76        if bounds[2] <= 0.0 || bounds[3] <= 0.0 {
77            return;
78        }
79    }
80    crate::frame_scheduler::mark_needs_commit();
81}
82
83impl Node for CustomDrawable {
84    fn retained_node_ref(&self) -> NodeRef {
85        NodeRef::from_node(self.base.core.clone(), self.clone())
86    }
87
88    fn build_self(&self) {
89        self.base.build_self();
90        let weak_base = self.base.downgrade();
91        let draw_callback = self.draw_callback.clone();
92        self.base.core.borrow_mut().draw_callback = Some(Rc::new(move |ctx| {
93            let Some(base) = weak_base.upgrade() else {
94                return;
95            };
96            let bounds = base.get_bounds();
97            let (tl, tr, br, bl) = base
98                .props
99                .borrow()
100                .box_style
101                .map(|style| {
102                    (
103                        style.radius_tl,
104                        style.radius_tr,
105                        style.radius_br,
106                        style.radius_bl,
107                    )
108                })
109                .unwrap_or((0.0, 0.0, 0.0, 0.0));
110
111            ctx.save();
112            if tl > 0.0 || tr > 0.0 || br > 0.0 || bl > 0.0 {
113                ctx.clip_round_rect(0.0, 0.0, bounds[2], bounds[3], tl, tr, br, bl);
114            } else {
115                ctx.clip_rect(0.0, 0.0, bounds[2], bounds[3]);
116            }
117            draw_callback(ctx);
118            ctx.restore();
119            ctx.flush();
120        }));
121    }
122}
123
124impl HasFlexBoxRoot for CustomDrawable {
125    fn flex_box_root(&self) -> &FlexBox {
126        &self.base
127    }
128}
129
130impl ThemeBindable for CustomDrawable {
131    fn theme_binding_node(&self) -> NodeRef {
132        self.base.retained_node_ref()
133    }
134
135    fn weak_theme_target(&self) -> Box<dyn Fn() -> Option<Self>> {
136        let weak_base = self.base.downgrade();
137        let draw_callback = self.draw_callback.clone();
138        Box::new(move || {
139            weak_base.upgrade().map(|base| Self {
140                base,
141                draw_callback: draw_callback.clone(),
142            })
143        })
144    }
145}
146
147#[cfg(test)]
148mod tests {
149    use super::*;
150    use crate::app::Application;
151    use crate::ffi::{self, Call};
152
153    fn assert_flex_box_surface<T: FlexBoxSurface>() {}
154    fn assert_theme_bindable<T: ThemeBindable>() {}
155
156    #[test]
157    fn custom_drawable_exposes_generic_retained_visual_surfaces() {
158        assert_flex_box_surface::<CustomDrawable>();
159        assert_theme_bindable::<CustomDrawable>();
160
161        let drawable = CustomDrawable::new(|_| {});
162        drawable
163            .width(300.0, Unit::Pixel)
164            .height(200.0, Unit::Pixel)
165            .min_width(120.0, Unit::Pixel)
166            .margin(1.0, 2.0, 3.0, 4.0)
167            .padding(5.0, 6.0, 7.0, 8.0)
168            .corner_radius(12.0)
169            .bg_color(0x112233FF)
170            .clip_to_bounds(true);
171
172        let invalidator = drawable.invalidator();
173        drop(drawable);
174        invalidator.mark_dirty();
175    }
176
177    #[test]
178    fn custom_drawable_requests_focus_through_node_trait() {
179        ffi::test::reset();
180        let drawable = CustomDrawable::new(|_| {});
181        drawable.focusable(true, 0);
182        Application::mount(drawable.clone());
183        let handle = drawable.handle().raw();
184        ffi::test::take_calls();
185
186        drawable.focus_now();
187
188        let calls = ffi::test::take_calls();
189        assert!(calls.iter().any(
190            |call| matches!(call, Call::RequestFocus { handle: requested } if *requested == handle)
191        ));
192        Application::unmount();
193    }
194}