too/views/
toggle_switch.rs

1use std::time::Duration;
2
3use crate::{
4    animation::{easing, Animation},
5    layout::Axis,
6    math::{lerp, Pos2, Size, Space},
7    renderer::{Pixel, Rgba},
8    view::{
9        Builder, Elements, EventCtx, Handled, Interest, Layout, Palette, Render, StyleKind, Ui,
10        View, ViewEvent,
11    },
12};
13
14#[derive(Debug, Copy, Clone)]
15pub struct ToggleStyle {
16    pub track: char,
17    pub track_color: Rgba,
18    pub track_hovered: Option<Rgba>,
19
20    pub on_knob: char,
21    pub on_knob_color: Rgba,
22
23    pub off_knob: char,
24    pub off_knob_color: Rgba,
25
26    pub on_knob_hovered: Option<Rgba>,
27    pub off_knob_hovered: Option<Rgba>,
28}
29
30impl ToggleStyle {
31    pub fn default(palette: &Palette, axis: Axis, _toggled: bool) -> Self {
32        Self {
33            track: axis.main((Elements::MEDIUM_RECT, Elements::LARGE_RECT)),
34            track_color: palette.outline,
35            track_hovered: None,
36            on_knob: Elements::LARGE_RECT,
37            on_knob_color: palette.primary,
38            off_knob: Elements::LARGE_RECT,
39            off_knob_color: palette.secondary,
40            on_knob_hovered: None,
41            off_knob_hovered: None,
42        }
43    }
44
45    pub fn large(palette: &Palette, axis: Axis, toggled: bool) -> Self {
46        Self::default(palette, axis, toggled)
47    }
48
49    pub fn small_rounded(palette: &Palette, axis: Axis, toggled: bool) -> Self {
50        Self {
51            track: axis.main((
52                Elements::THICK_HORIZONTAL_LINE,
53                Elements::THICK_VERTICAL_LINE,
54            )),
55            on_knob: Elements::CIRCLE,
56            off_knob: Elements::CIRCLE,
57            ..Self::default(palette, axis, toggled)
58        }
59    }
60
61    pub fn small_diamond(palette: &Palette, axis: Axis, toggled: bool) -> Self {
62        Self {
63            track: axis.main((
64                Elements::THICK_HORIZONTAL_LINE,
65                Elements::THICK_VERTICAL_LINE,
66            )),
67            on_knob: Elements::DIAMOND,
68            off_knob: Elements::DIAMOND,
69            ..Self::default(palette, axis, toggled)
70        }
71    }
72
73    pub fn small_square(palette: &Palette, axis: Axis, toggled: bool) -> Self {
74        Self {
75            track: axis.main((
76                Elements::THICK_HORIZONTAL_LINE,
77                Elements::THICK_VERTICAL_LINE,
78            )),
79            on_knob: Elements::MEDIUM_RECT,
80            off_knob: Elements::MEDIUM_RECT,
81            ..Self::default(palette, axis, toggled)
82        }
83    }
84}
85
86pub type ToggleClass = fn(&Palette, Axis, bool) -> ToggleStyle;
87
88#[derive(Copy, Clone, Debug, PartialEq, Default)]
89pub struct ToggleResponse {
90    pub(super) changed: bool,
91}
92
93impl ToggleResponse {
94    pub const fn changed(&self) -> bool {
95        self.changed
96    }
97}
98
99#[must_use = "a view does nothing unless `show()` or `show_children()` is called"]
100pub struct ToggleSwitch<'a> {
101    value: &'a mut bool,
102    axis: Axis,
103    class: StyleKind<ToggleClass, ToggleStyle>,
104}
105
106impl<'a> ToggleSwitch<'a> {
107    pub fn new(value: &'a mut bool) -> Self {
108        Self {
109            value,
110            axis: Axis::Horizontal,
111            class: StyleKind::deferred(ToggleStyle::default),
112        }
113    }
114
115    pub fn horizontal(mut self) -> Self {
116        self.axis = Axis::Horizontal;
117        self
118    }
119
120    pub fn vertical(mut self) -> Self {
121        self.axis = Axis::Vertical;
122        self
123    }
124
125    pub fn axis(mut self, axis: Axis) -> Self {
126        self.axis = axis;
127        self
128    }
129
130    pub const fn class(mut self, class: ToggleClass) -> Self {
131        self.class = StyleKind::Deferred(class);
132        self
133    }
134
135    pub const fn style(mut self, style: ToggleStyle) -> Self {
136        self.class = StyleKind::Direct(style);
137        self
138    }
139}
140
141impl<'v> Builder<'v> for ToggleSwitch<'v> {
142    type View = ToggleSwitchView;
143}
144
145#[derive(Debug)]
146pub struct ToggleSwitchView {
147    value: bool,
148    changed: bool,
149    axis: Axis,
150    class: StyleKind<ToggleClass, ToggleStyle>,
151}
152
153impl View for ToggleSwitchView {
154    type Args<'v> = ToggleSwitch<'v>;
155    type Response = ToggleResponse;
156
157    fn create(args: Self::Args<'_>) -> Self {
158        Self {
159            value: *args.value,
160            changed: false,
161            axis: args.axis,
162            class: args.class,
163        }
164    }
165
166    fn update(&mut self, args: Self::Args<'_>, _: &Ui) -> Self::Response {
167        self.axis = args.axis;
168        self.class = args.class;
169
170        let changed = self.changed;
171        if std::mem::take(&mut self.changed) {
172            *args.value = self.value;
173        } else if self.value != *args.value {
174            self.value = *args.value;
175        };
176        ToggleResponse { changed }
177    }
178
179    fn interests(&self) -> Interest {
180        Interest::MOUSE_INSIDE
181    }
182
183    fn event(&mut self, event: ViewEvent, ctx: EventCtx) -> Handled {
184        match event {
185            ViewEvent::MouseClicked { .. } => {
186                self.value = !self.value;
187                self.changed = true;
188
189                ctx.animation.add_once(ctx.current, || {
190                    Animation::new()
191                        .oneshot(true)
192                        .with(easing::sine_in_out)
193                        .schedule(Duration::from_millis(150))
194                        .unwrap()
195                });
196            }
197
198            ViewEvent::MouseDrag { delta, .. }
199                if (self.value && self.axis.main::<i32>(delta).is_negative())
200                    || (!self.value && self.axis.main::<i32>(delta).is_positive()) =>
201            {
202                self.value = !self.value;
203                self.changed = true;
204
205                ctx.animation.add_once(ctx.current, || {
206                    Animation::new()
207                        .oneshot(true)
208                        .with(easing::sine_in_out)
209                        .schedule(Duration::from_millis(50))
210                        .unwrap()
211                });
212            }
213
214            _ => return Handled::Bubble,
215        };
216
217        Handled::Sink
218    }
219
220    fn layout(&mut self, _: Layout, space: Space) -> Size {
221        let main = self.axis.main((4.0, 2.0));
222        space.fit(self.axis.pack(main, 1.0))
223    }
224
225    fn draw(&mut self, mut render: Render) {
226        let rect = render.rect();
227
228        let selected = self.value;
229
230        let style = match self.class {
231            StyleKind::Deferred(style) => (style)(render.palette, self.axis, selected),
232            StyleKind::Direct(style) => style,
233        };
234
235        let color = if render.is_hovered() {
236            style.track_hovered.unwrap_or(style.track_color)
237        } else {
238            style.track_color
239        };
240
241        render.fill_with(Pixel::new(style.track).fg(color));
242
243        let extent = self.axis.main::<f32>(rect.size()) - 1.0;
244
245        let x = match render.animation.get_mut(render.current) {
246            Some(animation) if selected => lerp(0.0, extent, *animation.value),
247            Some(animation) if !selected => lerp(extent, 0.0, *animation.value),
248            _ if selected => extent,
249            _ => 0.0,
250        };
251
252        let color = match (render.is_hovered(), selected) {
253            (true, true) => style.on_knob_hovered.unwrap_or(style.on_knob_color),
254            (true, false) => style.off_knob_hovered.unwrap_or(style.off_knob_color),
255            (false, true) => style.on_knob_color,
256            (false, false) => style.off_knob_color,
257        };
258
259        let knob = if selected {
260            style.on_knob
261        } else {
262            style.off_knob
263        };
264
265        let pos: Pos2 = self.axis.pack(x, 0.0);
266        render.set(pos, Pixel::new(knob).fg(color));
267    }
268}
269
270pub fn toggle_switch(value: &mut bool) -> ToggleSwitch<'_> {
271    ToggleSwitch::new(value)
272}