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}