Skip to main content

fui/node/
custom_drawable.rs

1use super::core::*;
2use super::*;
3
4#[derive(Clone)]
5pub struct CustomDrawable {
6    base: FlexBox,
7    draw_callback: DrawCallback,
8}
9
10#[derive(Clone)]
11pub struct DrawableInvalidator {
12    base: WeakFlexBox,
13}
14
15impl DrawableInvalidator {
16    pub fn mark_dirty(&self) {
17        if let Some(base) = self.base.upgrade() {
18            mark_base_dirty(&base);
19        }
20    }
21}
22
23impl CustomDrawable {
24    pub fn new(handler: impl Fn(&mut DrawContext) + 'static) -> Self {
25        let base = FlexBox::default();
26        base.custom_drawable(true);
27        Self {
28            base,
29            draw_callback: Rc::new(handler),
30        }
31    }
32
33    pub fn mark_dirty(&self) {
34        mark_base_dirty(&self.base);
35    }
36
37    pub fn invalidator(&self) -> DrawableInvalidator {
38        DrawableInvalidator {
39            base: self.base.downgrade(),
40        }
41    }
42}
43
44fn mark_base_dirty(base: &FlexBox) {
45    let handle = base.handle();
46    if handle != NodeHandle::INVALID {
47        let Some(bounds) = ui::get_visible_bounds(handle.raw()) else {
48            return;
49        };
50        if bounds[2] <= 0.0 || bounds[3] <= 0.0 {
51            return;
52        }
53    }
54    crate::frame_scheduler::mark_needs_commit();
55}
56
57impl Node for CustomDrawable {
58    fn retained_node_ref(&self) -> NodeRef {
59        NodeRef::from_node(self.base.core.clone(), self.clone())
60    }
61
62    fn build_self(&self) {
63        self.base.build_self();
64        let weak_base = self.base.downgrade();
65        let draw_callback = self.draw_callback.clone();
66        self.base.core.borrow_mut().draw_callback = Some(Rc::new(move |ctx| {
67            let Some(base) = weak_base.upgrade() else {
68                return;
69            };
70            let bounds = base.get_bounds();
71            let (tl, tr, br, bl) = base
72                .props
73                .borrow()
74                .box_style
75                .map(|style| {
76                    (
77                        style.radius_tl,
78                        style.radius_tr,
79                        style.radius_br,
80                        style.radius_bl,
81                    )
82                })
83                .unwrap_or((0.0, 0.0, 0.0, 0.0));
84
85            ctx.save();
86            if tl > 0.0 || tr > 0.0 || br > 0.0 || bl > 0.0 {
87                ctx.clip_round_rect(0.0, 0.0, bounds[2], bounds[3], tl, tr, br, bl);
88            } else {
89                ctx.clip_rect(0.0, 0.0, bounds[2], bounds[3]);
90            }
91            draw_callback(ctx);
92            ctx.restore();
93            ctx.flush();
94        }));
95    }
96}
97
98impl HasFlexBoxRoot for CustomDrawable {
99    fn flex_box_root(&self) -> &FlexBox {
100        &self.base
101    }
102}
103
104impl ThemeBindable for CustomDrawable {
105    fn theme_binding_node(&self) -> NodeRef {
106        self.base.retained_node_ref()
107    }
108
109    fn weak_theme_target(&self) -> Box<dyn Fn() -> Option<Self>> {
110        let weak_base = self.base.downgrade();
111        let draw_callback = self.draw_callback.clone();
112        Box::new(move || {
113            weak_base.upgrade().map(|base| Self {
114                base,
115                draw_callback: draw_callback.clone(),
116            })
117        })
118    }
119}
120
121#[cfg(test)]
122mod tests {
123    use super::*;
124    use crate::app::Application;
125    use crate::ffi::{self, Call};
126
127    fn assert_flex_box_surface<T: FlexBoxSurface>() {}
128    fn assert_theme_bindable<T: ThemeBindable>() {}
129
130    #[test]
131    fn custom_drawable_exposes_generic_retained_visual_surfaces() {
132        assert_flex_box_surface::<CustomDrawable>();
133        assert_theme_bindable::<CustomDrawable>();
134
135        let drawable = CustomDrawable::new(|_| {});
136        drawable
137            .width(300.0, Unit::Pixel)
138            .height(200.0, Unit::Pixel)
139            .min_width(120.0, Unit::Pixel)
140            .margin(1.0, 2.0, 3.0, 4.0)
141            .padding(5.0, 6.0, 7.0, 8.0)
142            .corner_radius(12.0)
143            .bg_color(0x112233FF)
144            .clip_to_bounds(true);
145
146        let invalidator = drawable.invalidator();
147        drop(drawable);
148        invalidator.mark_dirty();
149    }
150
151    #[test]
152    fn custom_drawable_requests_focus_through_node_trait() {
153        ffi::test::reset();
154        let drawable = CustomDrawable::new(|_| {});
155        drawable.focusable(true, 0);
156        Application::mount(drawable.clone());
157        let handle = drawable.handle().raw();
158        ffi::test::take_calls();
159
160        drawable.focus_now();
161
162        let calls = ffi::test::take_calls();
163        assert!(calls.iter().any(
164            |call| matches!(call, Call::RequestFocus { handle: requested } if *requested == handle)
165        ));
166        Application::unmount();
167    }
168}