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