kolibri_embedded_gui/
toggle_switch.rs

1//! # Toggle Switch
2//!
3//! A customizable toggle switch widget that provides a simple on/off control.
4//!
5//! The toggle switch provides a slider-style control similar to those found in mobile applications,
6//! with a background track and sliding knob that moves between on/off positions.
7//! The widget supports customizable dimensions, colors based on theme, and hover/interaction states.
8//!
9//! This widget is part of the Kolibri embedded GUI framework's core widget set and integrates
10//! with the framework's [Smartstate] system for efficient rendering.
11
12use crate::smartstate::{Container, Smartstate};
13use crate::ui::{GuiError, GuiResult, Interaction, Response, Ui, Widget};
14use core::cmp::max;
15use embedded_graphics::draw_target::DrawTarget;
16use embedded_graphics::geometry::{Point, Size};
17use embedded_graphics::prelude::*;
18use embedded_graphics::primitives::{
19    Circle, CornerRadii, PrimitiveStyleBuilder, Rectangle, RoundedRectangle,
20};
21
22/// A toggle switch widget that provides an animated on/off control with a sliding knob.
23///
24/// The [ToggleSwitch] widget creates a visual control that allows users to toggle between
25/// two states (on/off). It features a sliding knob that moves horizontally across a track
26/// to indicate the current state.
27///
28/// The widget supports:
29/// - Customizable width and height
30/// - Theme-based colors for active/inactive states
31/// - Interactive hover and click effects
32/// - Integration with Kolibri's smartstate system for efficient rendering
33///
34/// ## Examples
35///
36/// ```no_run
37/// # use embedded_graphics::pixelcolor::Rgb565;
38/// # use embedded_graphics_simulator::{SimulatorDisplay, OutputSettingsBuilder, Window};
39/// # use kolibri_embedded_gui::style::medsize_rgb565_style;
40/// # use kolibri_embedded_gui::ui::Ui;
41/// # use embedded_graphics::prelude::*;
42/// # use embedded_graphics::primitives::Rectangle;
43/// # use embedded_iconoir::prelude::*;
44/// # use kolibri_embedded_gui::ui::*;
45/// # use kolibri_embedded_gui::label::*;
46/// # use kolibri_embedded_gui::smartstate::*;
47/// # let mut display = SimulatorDisplay::<Rgb565>::new(Size::new(320, 240));
48/// # let output_settings = OutputSettingsBuilder::new().build();
49/// # let mut window = Window::new("Kolibri Example", &output_settings);
50/// # let mut ui = Ui::new_fullscreen(&mut display, medsize_rgb565_style());
51/// # use kolibri_embedded_gui::toggle_switch::ToggleSwitch;
52/// let mut state = false;
53///
54/// // Create a basic toggle switch
55/// ui.add(ToggleSwitch::new(&mut state));
56///
57/// // Create a custom-sized toggle switch
58/// ui.add(ToggleSwitch::new(&mut state)
59///     .width(60)
60///     .height(30));
61/// ```
62pub struct ToggleSwitch<'a> {
63    active: &'a mut bool,
64    smartstate: Container<'a, Smartstate>,
65    width: u32,
66    height: u32,
67}
68
69impl<'a> ToggleSwitch<'a> {
70    /// Creates a new [ToggleSwitch] instance with the provided mutable reference to the active state.
71    ///
72    /// The new [ToggleSwitch] will have a default width of 50 pixels and a height of 25 pixels.
73    pub fn new(active: &'a mut bool) -> ToggleSwitch<'a> {
74        ToggleSwitch {
75            active,
76            smartstate: Container::empty(),
77            width: 50,
78            height: 25,
79        }
80    }
81
82    /// Adds a [Smartstate] to the toggle switch for incremental redrawing.
83    ///
84    /// The smartstate is used to efficiently manage the rendering of the toggle switch.
85    /// Through this [Smartstate], the toggle switch can leverage
86    /// the smartstate system to avoid unnecessary redraws and improve performance.
87    pub fn smartstate(mut self, smartstate: &'a mut Smartstate) -> Self {
88        self.smartstate.set(smartstate);
89        self
90    }
91
92    /// Sets the width of the toggle switch.
93    ///
94    /// The width determines the horizontal size of the switch's track. A minimum
95    /// width of 30 pixels is enforced to ensure proper rendering and usability.
96    ///
97    /// ## Examples
98    ///
99    /// ```no_run
100    /// # use embedded_graphics::pixelcolor::Rgb565;
101    /// # use embedded_graphics_simulator::{SimulatorDisplay, OutputSettingsBuilder, Window};
102    /// # use kolibri_embedded_gui::style::medsize_rgb565_style;
103    /// # use kolibri_embedded_gui::ui::Ui;
104    /// # use embedded_graphics::prelude::*;
105    /// # use embedded_graphics::primitives::Rectangle;
106    /// # use embedded_iconoir::prelude::*;
107    /// # use kolibri_embedded_gui::ui::*;
108    /// # use kolibri_embedded_gui::label::*;
109    /// # use kolibri_embedded_gui::smartstate::*;
110    /// # let mut display = SimulatorDisplay::<Rgb565>::new(Size::new(320, 240));
111    /// # let output_settings = OutputSettingsBuilder::new().build();
112    /// # let mut window = Window::new("Kolibri Example", &output_settings);
113    /// # let mut ui = Ui::new_fullscreen(&mut display, medsize_rgb565_style());
114    /// # use kolibri_embedded_gui::toggle_switch::ToggleSwitch;
115    /// let mut state = false;
116    /// ui.add(ToggleSwitch::new(&mut state).width(60));
117    /// ```
118    pub fn width(mut self, width: u32) -> Self {
119        self.width = max(width, 30); // Enforce a minimum width
120        self
121    }
122
123    /// Sets the height of the toggle switch.
124    ///
125    /// The height determines the vertical size of the switch's track and knob.
126    /// A minimum height of 15 pixels is enforced to ensure proper rendering and usability.
127    ///
128    /// ## Examples
129    /// ```no_run
130    /// # use embedded_graphics::pixelcolor::Rgb565;
131    /// # use embedded_graphics_simulator::{SimulatorDisplay, OutputSettingsBuilder, Window};
132    /// # use kolibri_embedded_gui::style::medsize_rgb565_style;
133    /// # use kolibri_embedded_gui::ui::Ui;
134    /// # use embedded_graphics::prelude::*;
135    /// # use embedded_graphics::primitives::Rectangle;
136    /// # use embedded_iconoir::prelude::*;
137    /// # use kolibri_embedded_gui::ui::*;
138    /// # use kolibri_embedded_gui::label::*;
139    /// # use kolibri_embedded_gui::smartstate::*;
140    /// # let mut display = SimulatorDisplay::<Rgb565>::new(Size::new(320, 240));
141    /// # let output_settings = OutputSettingsBuilder::new().build();
142    /// # let mut window = Window::new("Kolibri Example", &output_settings);
143    /// # let mut ui = Ui::new_fullscreen(&mut display, medsize_rgb565_style());
144    /// # use kolibri_embedded_gui::toggle_switch::ToggleSwitch;
145    /// let mut state = false;
146    ///
147    /// loop {
148    ///     // [...]
149    ///     ui.add(ToggleSwitch::new(&mut state).height(30).width(60));
150    /// }
151    /// ```
152    pub fn height(mut self, height: u32) -> Self {
153        self.height = max(height, 15); // Enforce a minimum height
154        self
155    }
156}
157
158impl Widget for ToggleSwitch<'_> {
159    fn draw<DRAW: DrawTarget<Color = COL>, COL: PixelColor>(
160        &mut self,
161        ui: &mut Ui<DRAW, COL>,
162    ) -> GuiResult<Response> {
163        // Calculate total size including padding
164        let padding = ui.style().spacing.button_padding;
165        let total_size = Size::new(
166            self.width + 2 * padding.width,
167            self.height + 2 * padding.height,
168        );
169
170        // Allocate space in the UI
171        let iresponse = ui.allocate_space(total_size)?;
172
173        // Handle interaction
174        let mut changed = false;
175        if matches!(iresponse.interaction, Interaction::Release(_)) {
176            *self.active = !*self.active;
177            changed = true;
178        }
179
180        // Colors for active and inactive states
181        let switch_color = if *self.active {
182            ui.style().primary_color
183        } else {
184            ui.style().item_background_color
185        };
186
187        let knob_color = match iresponse.interaction {
188            Interaction::Click(_) | Interaction::Drag(_) => ui.style().primary_color,
189            Interaction::Hover(_) => ui.style().highlight_item_background_color,
190            _ => ui.style().item_background_color,
191        };
192
193        // Determine border color based on interaction
194        let border_color = match iresponse.interaction {
195            Interaction::Hover(_) => ui.style().highlight_border_color,
196            _ => ui.style().border_color,
197        };
198
199        // Inside the draw method, replace the current smartstate handling with:
200
201        let prevstate = self.smartstate.clone_inner();
202
203        // Determine state based on both toggle state and interaction
204        let state = match (iresponse.interaction, *self.active) {
205            (Interaction::Click(_) | Interaction::Drag(_), true) => 1,
206            (Interaction::Click(_) | Interaction::Drag(_), false) => 2,
207            (Interaction::Hover(_), true) => 3,
208            (Interaction::Hover(_), false) => 4,
209            (_, true) => 5,
210            (_, false) => 6,
211        };
212
213        self.smartstate.modify(|st| *st = Smartstate::state(state));
214
215        // Determine if redraw is needed based on state change or active state change
216        let redraw = !self.smartstate.eq_option(&prevstate) || changed;
217
218        if redraw {
219            ui.start_drawing(&iresponse.area);
220
221            // Define the switch background (rounded rectangle)
222            let switch_rect = RoundedRectangle::new(
223                Rectangle::new(
224                    iresponse.area.top_left
225                        + Point::new(padding.width as i32, padding.height as i32),
226                    Size::new(self.width, self.height),
227                ),
228                CornerRadii::new(Size::new(self.height / 2, self.height / 2)),
229            );
230
231            let switch_style = PrimitiveStyleBuilder::new()
232                .fill_color(switch_color)
233                .stroke_color(border_color)
234                .stroke_width(ui.style().border_width)
235                .build();
236
237            ui.draw(&switch_rect.into_styled(switch_style))
238                .map_err(|_| GuiError::DrawError(Some("Couldn't draw ToggleSwitch background")))?;
239
240            // Calculate knob position
241            let knob_radius = (self.height / 2) - ui.style().border_width;
242            let knob_x = if *self.active {
243                // Positioned on the right
244                iresponse.area.top_left.x + padding.width as i32 + self.width as i32
245                    - knob_radius as i32
246                    - ui.style().border_width as i32
247            } else {
248                // Positioned on the left
249                iresponse.area.top_left.x
250                    + padding.width as i32
251                    + knob_radius as i32
252                    + ui.style().border_width as i32
253            };
254
255            let knob_center = Point::new(
256                knob_x,
257                iresponse.area.top_left.y + padding.height as i32 + (self.height / 2) as i32,
258            );
259
260            let knob = Circle::with_center(knob_center, knob_radius * 2 - 3);
261
262            let knob_style = PrimitiveStyleBuilder::new()
263                .fill_color(knob_color)
264                .stroke_color(border_color)
265                .stroke_width(2)
266                .build();
267
268            ui.draw(&knob.into_styled(knob_style))
269                .map_err(|_| GuiError::DrawError(Some("Couldn't draw ToggleSwitch knob")))?;
270
271            ui.finalize()?;
272        }
273
274        let click = matches!(iresponse.interaction, Interaction::Release(_));
275        let down = matches!(
276            iresponse.interaction,
277            Interaction::Click(_) | Interaction::Drag(_)
278        );
279
280        Ok(Response::new(iresponse)
281            .set_clicked(click)
282            .set_down(down)
283            .set_changed(changed))
284    }
285}