frus_widgets/
colorpicker.rs1use frus_core::{Color, Rect, Scene};
5use frus_layout::{Dimension, Style};
6
7use crate::grid::GridView;
8use crate::interaction::Status;
9use crate::theme::Theme;
10use crate::widget::Widget;
11
12const SWATCH: f32 = 30.0;
13
14struct Swatch<Msg> {
16 color: Color,
17 selected: bool,
18 message: Msg,
19}
20
21impl<Msg: Clone> Widget<Msg> for Swatch<Msg> {
22 fn style(&self) -> Style {
23 Style {
24 width: Dimension::Length(SWATCH),
25 height: Dimension::Length(SWATCH),
26 ..Default::default()
27 }
28 }
29
30 fn children(&self) -> &[Box<dyn Widget<Msg>>] {
31 &[]
32 }
33
34 fn paint(&self, bounds: Rect, status: Status, theme: &Theme, scene: &mut Scene) {
35 let o = status.opacity;
36 scene.draw_rect(bounds, self.color.fade(o), 8.0, 0.0, Color::TRANSPARENT);
37 if self.selected {
38 let ring = Rect::new(
40 bounds.x - 2.0,
41 bounds.y - 2.0,
42 bounds.width + 4.0,
43 bounds.height + 4.0,
44 );
45 scene.draw_rect(ring, Color::TRANSPARENT, 10.0, 2.0, theme.focus.fade(o));
46 }
47 }
48
49 fn on_click(&self) -> Option<Msg> {
50 Some(self.message.clone())
51 }
52
53 fn focusable(&self) -> bool {
54 true
55 }
56}
57
58pub struct ColorPicker<Msg = crate::callback::Callback> {
60 selected: Option<Color>,
61 on_pick: Box<dyn Fn(Color) -> Msg>,
62 grid: GridView<Msg>,
63}
64
65impl<Msg: Clone + 'static> ColorPicker<Msg> {
66 pub fn new(
69 selected: Option<Color>,
70 columns: usize,
71 on_pick: impl Fn(Color) -> Msg + 'static,
72 ) -> Self {
73 Self {
74 selected,
75 on_pick: Box::new(on_pick),
76 grid: GridView::new(columns.max(1)).gap(8.0),
77 }
78 }
79
80 pub fn swatch(mut self, color: Color) -> Self {
82 let selected = self.selected == Some(color);
83 let message = (self.on_pick)(color);
84 self.grid = self.grid.cell(Swatch {
85 color,
86 selected,
87 message,
88 });
89 self
90 }
91}
92
93impl<Msg: Clone + 'static> Widget<Msg> for ColorPicker<Msg> {
97 fn style(&self) -> Style {
98 Widget::<Msg>::style(&self.grid)
99 }
100
101 fn children(&self) -> &[Box<dyn Widget<Msg>>] {
102 Widget::<Msg>::children(&self.grid)
103 }
104
105 fn tile_shape(&self) -> Option<f32> {
108 Widget::<Msg>::tile_shape(&self.grid)
109 }
110
111 fn paint(&self, _bounds: Rect, _status: Status, _theme: &Theme, _scene: &mut Scene) {}
112
113 fn on_click(&self) -> Option<Msg> {
114 None
115 }
116}
117
118#[cfg(test)]
119mod tests {
120 use super::*;
121 use crate::{build_ui, Runtime, Size};
122 use frus_core::Primitive;
123
124 #[derive(Clone, Debug, PartialEq)]
125 enum Msg {
126 Pick(Color),
127 }
128
129 #[test]
130 fn swatches_pick_and_selected_has_ring() {
131 let red = Color::rgb(1.0, 0.0, 0.0);
132 let blue = Color::rgb(0.0, 0.0, 1.0);
133 let picker = ColorPicker::new(Some(red), 4, Msg::Pick)
134 .swatch(red)
135 .swatch(blue);
136 assert_eq!(Widget::<Msg>::children(&picker).len(), 2);
137
138 let ui = build_ui(
139 &picker,
140 Size::new(200.0, 60.0),
141 &Runtime::default(),
142 &Theme::default(),
143 );
144 let focus = Theme::default().focus;
146 let has_ring = ui.scene().primitives().iter().any(|p| {
147 matches!(p, Primitive::Rect { border_color, border_width, .. } if *border_width > 0.0 && *border_color == focus)
148 });
149 assert!(has_ring, "the selected swatch has a ring");
150 }
151}