danceinterpreter_rs/ui/widget/
canvas_toggle.rs1use crate::Message;
4use iced::advanced::graphics::core::event::Event;
5use iced::mouse::Cursor;
6use iced::widget::canvas::{Cache, Frame, Geometry, Path};
7use iced::widget::{Action, Canvas, canvas};
8use iced::{Element, Length, Point, Rectangle, Renderer, Theme, mouse, window};
9use std::rc::Rc;
10use std::time::Instant;
11
12type DrawFunction<'a> = Rc<dyn Fn(&Theme, &mut Frame, Rectangle, Cursor, bool) + 'a>;
13type ToggleFunction<'a> = Rc<dyn Fn(bool) -> Message + 'a>;
14
15const ANIM_SECS: f32 = 0.28;
16
17#[derive(Clone)]
18pub struct CanvasToggle<'a> {
19 is_checked: bool,
20 on_toggle: Option<ToggleFunction<'a>>,
21 on_draw: Option<DrawFunction<'a>>,
22 cache: &'a Cache,
23 width: Length,
24 height: Length,
25}
26
27impl<'a> CanvasToggle<'a> {
28 const DEFAULT_SIZE: f32 = 75.0;
29
30 pub fn new(is_checked: bool, cache: &'a Cache) -> Self {
31 Self {
32 is_checked,
33 on_toggle: None,
34 on_draw: None,
35 cache,
36 width: Length::Fixed(Self::DEFAULT_SIZE),
37 height: Length::Fixed(Self::DEFAULT_SIZE),
38 }
39 }
40
41 pub fn on_toggle<F>(mut self, f: F) -> Self
42 where
43 F: 'a + Fn(bool) -> Message,
44 {
45 self.on_toggle = Some(Rc::new(f));
46 self
47 }
48
49 pub fn on_draw<F>(mut self, f: F) -> Self
50 where
51 F: 'a + Fn(&Theme, &mut Frame, Rectangle, Cursor, bool) + 'a,
52 {
53 self.on_draw = Some(Rc::new(f));
54 self
55 }
56
57 pub fn width(mut self, width: f32) -> Self {
58 self.width = Length::Fixed(width);
59 self
60 }
61
62 pub fn height(mut self, height: f32) -> Self {
63 self.height = Length::Fixed(height);
64 self
65 }
66}
67
68impl<'a> From<CanvasToggle<'a>> for Canvas<CanvasToggle<'a>, Message, Theme, Renderer> {
69 fn from(value: CanvasToggle<'a>) -> Self {
70 let w = value.width;
71 let h = value.height;
72 Canvas::new(value).width(w).height(h)
73 }
74}
75
76impl<'a> From<CanvasToggle<'a>> for Element<'a, Message, Theme, Renderer> {
77 fn from(value: CanvasToggle<'a>) -> Self {
78 <CanvasToggle<'a> as Into<Canvas<CanvasToggle, Message>>>::into(value).into()
79 }
80}
81
82impl<'a> canvas::Program<Message> for CanvasToggle<'a> {
83 type State = Option<Instant>;
85
86 fn update(
87 &self,
88 state: &mut Self::State,
89 event: &Event,
90 bounds: Rectangle,
91 cursor: mouse::Cursor,
92 ) -> Option<Action<Message>> {
93 if let Some(started) = *state {
94 if started.elapsed().as_secs_f32() < ANIM_SECS {
95 if matches!(
98 event,
99 Event::Mouse(_) | Event::Window(window::Event::RedrawRequested(_))
100 ) {
101 return Some(Action::request_redraw());
102 }
103 } else {
104 *state = None;
105 }
106 }
107
108 if let Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) = event
109 && cursor.is_over(bounds)
110 && let Some(on_toggle) = &self.on_toggle
111 {
112 *state = Some(Instant::now());
113 return Some(Action::publish(on_toggle(!self.is_checked)));
114 }
115
116 None
117 }
118
119 fn draw(
120 &self,
121 state: &Self::State,
122 renderer: &Renderer,
123 theme: &Theme,
124 bounds: Rectangle,
125 cursor: mouse::Cursor,
126 ) -> Vec<Geometry<Renderer>> {
127 let Some(on_draw) = &self.on_draw else {
128 return Vec::new();
129 };
130
131 if state.is_none() {
134 let geo = self.cache.draw(renderer, bounds.size(), |frame| {
135 on_draw(theme, frame, bounds, cursor, self.is_checked);
136 });
137 return vec![geo];
138 }
139
140 let mut frame = Frame::new(renderer, bounds.size());
142 on_draw(theme, &mut frame, bounds, cursor, self.is_checked);
143
144 if let Some(started) = state {
145 let progress = (started.elapsed().as_secs_f32() / ANIM_SECS).clamp(0.0, 1.0);
146
147 let size = frame.size();
148 let cx = size.width / 2.0;
149 let cy = size.height / 2.0;
150 let dim = size.width.min(size.height);
151
152 let max_r = dim * 0.46;
154 let r = max_r * progress;
155 let alpha = (1.0 - progress) * 0.45;
156
157 let mut ripple_color = theme.extended_palette().secondary.base.color;
159 ripple_color.a = alpha;
160
161 let ripple = Path::new(|b| b.circle(Point::new(cx, cy), r));
162 frame.fill(
163 &ripple,
164 canvas::Fill {
165 style: canvas::Style::Solid(ripple_color),
166 rule: canvas::fill::Rule::NonZero,
167 },
168 );
169 }
170
171 vec![frame.into_geometry()]
172 }
173
174 fn mouse_interaction(
175 &self,
176 _state: &Self::State,
177 bounds: Rectangle,
178 cursor: mouse::Cursor,
179 ) -> mouse::Interaction {
180 if cursor.is_over(bounds) {
181 mouse::Interaction::Pointer
182 } else {
183 mouse::Interaction::default()
184 }
185 }
186}