kolibri_embedded_gui/toggle_button.rs
1//! # Toggle Button Widget
2//!
3//! A customizable toggle button widget that provides a clickable on/off control.
4//!
5//! The toggle button provides a traditional button-style control that maintains its state,
6//! featuring different visual styles for active and inactive states. It supports text labels
7//! and integrates with the framework's theming system for consistent appearance.
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::mono_font::MonoTextStyle;
18use embedded_graphics::pixelcolor::PixelColor;
19use embedded_graphics::prelude::*;
20use embedded_graphics::primitives::{PrimitiveStyleBuilder, Rectangle};
21use embedded_graphics::text::{Baseline, Text};
22
23/// A button widget that can be toggled on and off.
24///
25/// [ToggleButton] provides a clickable button that maintains an on/off state. When clicked,
26/// it toggles between these states and displays different visual styles accordingly.
27/// The button includes a text label and supports various interaction states like hover and click.
28///
29/// ## Examples
30///
31/// ```no_run
32/// # use embedded_graphics::pixelcolor::Rgb565;
33/// # use embedded_graphics_simulator::{SimulatorDisplay, OutputSettingsBuilder, Window};
34/// # use kolibri_embedded_gui::style::medsize_rgb565_style;
35/// # use kolibri_embedded_gui::ui::Ui;
36/// # use embedded_graphics::prelude::*;
37/// # use embedded_graphics::primitives::Rectangle;
38/// # use embedded_iconoir::prelude::*;
39/// # use kolibri_embedded_gui::ui::*;
40/// # use kolibri_embedded_gui::label::*;
41/// # use kolibri_embedded_gui::smartstate::*;
42/// # let mut display = SimulatorDisplay::<Rgb565>::new(Size::new(320, 240));
43/// # let output_settings = OutputSettingsBuilder::new().build();
44/// # let mut window = Window::new("Kolibri Example", &output_settings);
45/// # let mut ui = Ui::new_fullscreen(&mut display, medsize_rgb565_style());
46/// # use kolibri_embedded_gui::toggle_button::ToggleButton;
47/// let mut state = false;
48///
49/// loop {
50/// // [...]
51/// ui.add(ToggleButton::new("Toggle Me", &mut state));
52/// }
53/// ```
54pub struct ToggleButton<'a> {
55 label: &'a str,
56 active: &'a mut bool,
57 smartstate: Container<'a, Smartstate>,
58}
59
60impl<'a> ToggleButton<'a> {
61 /// Creates a new [ToggleButton] with the given label and active state.
62 ///
63 /// The `label` parameter is the text to display on the button, and the `active`
64 /// parameter is a mutable reference to a boolean that tracks the on/off state
65 /// of the button.
66 ///
67 /// ## Examples
68 ///
69 /// ```no_run
70 /// # use embedded_graphics::pixelcolor::Rgb565;
71 /// # use embedded_graphics_simulator::{SimulatorDisplay, OutputSettingsBuilder, Window};
72 /// # use kolibri_embedded_gui::style::medsize_rgb565_style;
73 /// # use kolibri_embedded_gui::ui::Ui;
74 /// # use embedded_graphics::prelude::*;
75 /// # use embedded_graphics::primitives::Rectangle;
76 /// # use embedded_iconoir::prelude::*;
77 /// # use kolibri_embedded_gui::ui::*;
78 /// # use kolibri_embedded_gui::label::*;
79 /// # use kolibri_embedded_gui::smartstate::*;
80 /// # let mut display = SimulatorDisplay::<Rgb565>::new(Size::new(320, 240));
81 /// # let output_settings = OutputSettingsBuilder::new().build();
82 /// # let mut window = Window::new("Kolibri Example", &output_settings);
83 /// # let mut ui = Ui::new_fullscreen(&mut display, medsize_rgb565_style());
84 /// # use kolibri_embedded_gui::toggle_button::ToggleButton;
85 /// let mut state = false;
86 /// let mut smartstateProvider = SmartstateProvider::<20>::new();
87 ///
88 /// loop {
89 /// // [...]
90 /// if ui.add(ToggleButton::new("Toggle Me", &mut state)).changed() {
91 /// // handle toggle
92 /// }
93 /// // or with smartstate:
94 /// if ui.add(ToggleButton::new("Toggle Me", &mut state).smartstate(smartstateProvider.nxt())).changed() {
95 /// // handle toggle
96 /// }
97 ///
98 /// }
99 pub fn new(label: &'a str, active: &'a mut bool) -> ToggleButton<'a> {
100 ToggleButton {
101 label,
102 active,
103 smartstate: Container::empty(),
104 }
105 }
106
107 /// Attaches a [Smartstate] to the toggle button for incremental redrawing.
108 ///
109 /// Smartstates enable efficient rendering by tracking the button's visual state
110 /// and only redrawing when necessary.
111 ///
112 /// Returns self for method chaining.
113 pub fn smartstate(mut self, smartstate: &'a mut Smartstate) -> Self {
114 self.smartstate.set(smartstate);
115 self
116 }
117}
118
119impl Widget for ToggleButton<'_> {
120 fn draw<DRAW: DrawTarget<Color = COL>, COL: PixelColor>(
121 &mut self,
122 ui: &mut Ui<DRAW, COL>,
123 ) -> GuiResult<Response> {
124 // Prepare text
125 let font = ui.style().default_font;
126 let mut text = Text::new(
127 self.label,
128 Point::zero(),
129 MonoTextStyle::new(&font, ui.style().text_color),
130 );
131
132 // Determine size
133 let text_bounds = text.bounding_box();
134 let padding = ui.style().spacing.button_padding;
135 let border = ui.style().border_width;
136 let height = ui.style().default_widget_height;
137
138 let size = Size::new(
139 text_bounds.size.width + 2 * padding.width + 2 * border,
140 max(
141 text_bounds.size.height + 2 * padding.height + 2 * border,
142 height,
143 ),
144 );
145
146 // Allocate space
147 let iresponse = ui.allocate_space(size)?;
148
149 // Position text
150 text.translate_mut(
151 iresponse.area.top_left
152 + Point::new(
153 (padding.width + border) as i32,
154 (padding.height + border) as i32,
155 ),
156 );
157 text.text_style.baseline = Baseline::Top;
158
159 // Handle interaction
160 let mut changed = false;
161 if let Interaction::Release(_) = iresponse.interaction {
162 *self.active = !*self.active;
163 changed = true;
164 }
165
166 // Determine styles based on state and interaction
167 let prevstate = self.smartstate.clone_inner();
168
169 // Determine widget style
170 let style = match (*self.active, iresponse.interaction) {
171 (true, Interaction::Click(_) | Interaction::Drag(_) | Interaction::Release(_)) => {
172 self.smartstate.modify(|st| *st = Smartstate::state(1));
173 PrimitiveStyleBuilder::new()
174 .stroke_color(ui.style().highlight_border_color)
175 .stroke_width(ui.style().highlight_border_width)
176 .fill_color(ui.style().primary_color)
177 .build()
178 }
179 (true, Interaction::Hover(_)) => {
180 self.smartstate.modify(|st| *st = Smartstate::state(2));
181 PrimitiveStyleBuilder::new()
182 .stroke_color(ui.style().highlight_border_color)
183 .stroke_width(ui.style().highlight_border_width)
184 .fill_color(ui.style().primary_color)
185 .build()
186 }
187 (true, _) => {
188 self.smartstate.modify(|st| *st = Smartstate::state(3));
189 PrimitiveStyleBuilder::new()
190 .stroke_color(ui.style().border_color)
191 .stroke_width(ui.style().border_width)
192 .fill_color(ui.style().primary_color)
193 .build()
194 }
195 (false, Interaction::Click(_) | Interaction::Drag(_) | Interaction::Release(_)) => {
196 self.smartstate.modify(|st| *st = Smartstate::state(4));
197 PrimitiveStyleBuilder::new()
198 .stroke_color(ui.style().highlight_border_color)
199 .stroke_width(ui.style().highlight_border_width)
200 .fill_color(ui.style().primary_color)
201 .build()
202 }
203 (false, Interaction::Hover(_)) => {
204 self.smartstate.modify(|st| *st = Smartstate::state(5));
205 PrimitiveStyleBuilder::new()
206 .stroke_color(ui.style().highlight_border_color)
207 .stroke_width(ui.style().highlight_border_width)
208 .fill_color(ui.style().highlight_item_background_color)
209 .build()
210 }
211 (false, _) => {
212 self.smartstate.modify(|st| *st = Smartstate::state(6));
213 PrimitiveStyleBuilder::new()
214 .stroke_color(ui.style().border_color)
215 .stroke_width(ui.style().border_width)
216 .fill_color(ui.style().item_background_color)
217 .build()
218 }
219 };
220
221 let redraw = !self.smartstate.eq_option(&prevstate) || changed;
222
223 if redraw {
224 ui.start_drawing(&iresponse.area);
225
226 let rect = Rectangle::new(iresponse.area.top_left, iresponse.area.size);
227 ui.draw(&rect.into_styled(style))
228 .map_err(|_| GuiError::DrawError(Some("Couldn't draw ToggleButton")))?;
229 ui.draw(&text)
230 .map_err(|_| GuiError::DrawError(Some("Couldn't draw ToggleButton label")))?;
231
232 ui.finalize()?;
233 }
234
235 let click = matches!(iresponse.interaction, Interaction::Release(_));
236 let down = matches!(
237 iresponse.interaction,
238 Interaction::Click(_) | Interaction::Drag(_)
239 );
240
241 Ok(Response::new(iresponse)
242 .set_clicked(click)
243 .set_down(down)
244 .set_changed(changed))
245 }
246}