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}