Skip to main content

frus_widgets/
custompaint.rs

1//! [`CustomPaint`]: a **free canvas**. The widget reserves a fixed-size box and hands
2//! its painting over to a closure the application supplies, which draws whatever it
3//! likes — vector paths, rectangles and so on — through the [`Scene`].
4//!
5//! The closure receives its **resolved box** and the current **theme**, so it can theme
6//! itself as it paints, as every widget does. It emits no message: this is a purely
7//! visual widget.
8
9use std::marker::PhantomData;
10
11use frus_core::{Rect, Scene};
12use frus_layout::{Dimension, Style};
13
14use crate::interaction::Status;
15use crate::theme::Theme;
16use crate::widget::Widget;
17
18/// The paint function's type: `(scene, box, theme)`.
19type PaintFn = dyn Fn(&mut Scene, Rect, &Theme);
20
21/// A fixed-size custom canvas, painted by a closure.
22pub struct CustomPaint<Msg = crate::callback::Callback> {
23    width: f32,
24    height: f32,
25    painter: Box<PaintFn>,
26    // `fn() -> Msg` keeps the parameter without forcing `Msg: Send` or covariance.
27    _msg: PhantomData<fn() -> Msg>,
28}
29
30impl<Msg> CustomPaint<Msg> {
31    /// A `width×height` canvas painted by `painter`.
32    pub fn new(
33        width: f32,
34        height: f32,
35        painter: impl Fn(&mut Scene, Rect, &Theme) + 'static,
36    ) -> Self {
37        Self {
38            width,
39            height,
40            painter: Box::new(painter),
41            _msg: PhantomData,
42        }
43    }
44}
45
46impl<Msg> Widget<Msg> for CustomPaint<Msg> {
47    fn style(&self) -> Style {
48        Style {
49            width: Dimension::Length(self.width),
50            height: Dimension::Length(self.height),
51            ..Default::default()
52        }
53    }
54
55    fn children(&self) -> &[Box<dyn Widget<Msg>>] {
56        &[]
57    }
58
59    fn paint(&self, bounds: Rect, _status: Status, theme: &Theme, scene: &mut Scene) {
60        (self.painter)(scene, bounds, theme);
61    }
62
63    fn on_click(&self) -> Option<Msg> {
64        None
65    }
66}
67
68#[cfg(test)]
69mod tests {
70    use super::*;
71    use frus_core::{Color, Path, Point, Primitive};
72
73    #[test]
74    fn invokes_the_closure_with_its_box() {
75        let widget: CustomPaint<()> = CustomPaint::new(50.0, 30.0, |scene, bounds, _theme| {
76            let tri = Path::new()
77                .move_to(Point::new(bounds.x, bounds.y))
78                .line_to(Point::new(bounds.x + bounds.width, bounds.y))
79                .line_to(Point::new(bounds.x, bounds.y + bounds.height))
80                .close();
81            scene.fill_path(&tri, Color::rgb(0.2, 0.4, 0.6));
82        });
83
84        let mut scene = Scene::new();
85        Widget::<()>::paint(
86            &widget,
87            Rect::new(10.0, 20.0, 50.0, 30.0),
88            Status::default(),
89            &Theme::default(),
90            &mut scene,
91        );
92
93        assert_eq!(scene.len(), 1);
94        match &scene.primitives()[0] {
95            Primitive::Path {
96                path,
97                fill: Some(_),
98                ..
99            } => {
100                // The first vertex follows the box that was passed (x=10, y=20).
101                assert_eq!(
102                    path.verbs().first(),
103                    Some(&frus_core::PathVerb::MoveTo(Point::new(10.0, 20.0)))
104                );
105            }
106            _ => panic!("expected a filled path"),
107        }
108    }
109}