Skip to main content

danceinterpreter_rs/ui/widget/
canvas_toggle.rs

1// canvas_toggle.rs
2
3use 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    // State now holds the instant of the last click for the ripple animation.
84    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                // Mouse events and the redraw-requested window event both
96                // creates loop, until animation is finished
97                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        // When there is no active animation we can use the shared cache so
132        // the canvas is only redrawn when the parent invalidates it.
133        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        // --- animated frame: draw base + ripple overlay on a fresh Frame ---
141        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            // Ripple expands from 0 → bg_r and fades from 0.45 → 0
153            let max_r = dim * 0.46;
154            let r = max_r * progress;
155            let alpha = (1.0 - progress) * 0.45;
156
157            // Use the secondary colour so the ripple feels intentional
158            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}