kolibri_embedded_gui/
iconbutton.rs

1//! # IconButton Widget
2//!
3//! The [IconButton] widget combines an icon with button interaction capabilities.
4//! It provides a compact way to create clickable icons with optional subtitles,
5//! supporting all the interaction states of standard buttons.
6//!
7//! ## Core Features
8//!
9//! - Combines icon display with button interaction (click, hover, press states)
10//! - Optional subtitle/label text below the icon
11//! - Visual feedback via color changes for different interaction states
12//! - Integration with Kolibri's theming system
13//! - Support for the smartstate system for efficient redrawing
14//!
15//! ## Usage
16//!
17//! ```no_run
18//! # use embedded_graphics::pixelcolor::Rgb565;
19//! # use embedded_graphics_simulator::{SimulatorDisplay, OutputSettingsBuilder, Window};
20//! # use kolibri_embedded_gui::style::medsize_rgb565_style;
21//! # use kolibri_embedded_gui::ui::Ui;
22//! # use embedded_graphics::prelude::*;
23//! # use embedded_graphics::primitives::Rectangle;
24//! # use embedded_iconoir::prelude::*;
25//! # use embedded_iconoir::size12px;
26//! # use kolibri_embedded_gui::ui::*;
27//! # use kolibri_embedded_gui::label::*;
28//! # use kolibri_embedded_gui::smartstate::*;
29//! # let mut display = SimulatorDisplay::<Rgb565>::new(Size::new(320, 240));
30//! # let output_settings = OutputSettingsBuilder::new().build();
31//! # let mut window = Window::new("Kolibri Example", &output_settings);
32//! # let mut ui = Ui::new_fullscreen(&mut display, medsize_rgb565_style());
33//! # use kolibri_embedded_gui::iconbutton::IconButton;
34//! # use embedded_iconoir::size12px::actions::AddCircle;
35//! // Basic icon button
36//! ui.add(IconButton::new(size12px::actions::AddCircle));
37//!
38//! // Icon button with subtitle
39//! ui.add(IconButton::new(size12px::actions::AddCircle).label("Settings"));
40//!
41//! // Using with the type system instead of passing an icon instance
42//! ui.add(IconButton::<size12px::actions::AddCircle>::new_from_type());
43//!
44//! // Using smartstate for efficient redrawing
45//! let mut smartstateProvider = SmartstateProvider::<20>::new();
46//! ui.add(IconButton::new(size12px::actions::AddCircle).smartstate(smartstateProvider.nxt()));
47//!
48//! // Handling button clicks
49//! if ui.add(IconButton::new(size12px::actions::AddCircle)).clicked() {
50//!     // Handle the click action
51//! }
52//! ```
53//!
54//! ## Implementation Details
55//!
56//! The [IconButton] widget uses different visual styles based on interaction state:
57//! - Normal: Standard background and border colors
58//! - Hover: Highlighted background and border for visual feedback
59//! - Pressed/Active: Primary color background with highlighted border
60//!
61use crate::smartstate::{Container, Smartstate};
62use crate::ui::{GuiResult, Interaction, Response, Ui, Widget};
63use core::cmp::max;
64use core::marker::PhantomData;
65use embedded_graphics::draw_target::DrawTarget;
66use embedded_graphics::geometry::{Point, Size};
67use embedded_graphics::image::Image;
68use embedded_graphics::mono_font::MonoTextStyle;
69use embedded_graphics::pixelcolor::PixelColor;
70use embedded_graphics::prelude::*;
71use embedded_graphics::primitives::{PrimitiveStyleBuilder, Rectangle};
72use embedded_graphics::text::{Alignment, Baseline, Text};
73use embedded_iconoir::prelude::{IconoirIcon, IconoirNewIcon};
74
75/// A button widget that displays an icon with optional text label.
76///
77/// [IconButton] combines the visual display of an icon with interactive button
78/// behavior. It changes appearance based on user interaction (normal, hover, pressed)
79/// and can optionally display a text label underneath the icon.
80pub struct IconButton<'a, ICON: IconoirIcon> {
81    icon: PhantomData<ICON>,
82    label: Option<&'a str>,
83    smartstate: Container<'a, Smartstate>,
84}
85
86impl<'a, ICON: IconoirIcon> IconButton<'a, ICON> {
87    /// Creates a new [IconButton] from an [IconoirIcon] instance.
88    ///
89    /// The icon color from the icon instance will be ignored, as the widget
90    /// will use the icon color from the current UI style.
91    ///
92    /// To see all icons you can use, look at [embedded_iconoir::size12px].
93    /// All other icon resolutions (from [embedded_iconoir::size12px] to [embedded_iconoir::size144px]) are available.
94    ///
95    /// # Example
96    ///
97    /// ```no_run
98    /// # use embedded_graphics::pixelcolor::Rgb565;
99    /// # use embedded_graphics_simulator::{SimulatorDisplay, OutputSettingsBuilder, Window};
100    /// # use kolibri_embedded_gui::style::medsize_rgb565_style;
101    /// # use kolibri_embedded_gui::ui::Ui;
102    /// # use embedded_graphics::prelude::*;
103    /// # use embedded_graphics::primitives::Rectangle;
104    /// # use embedded_iconoir::prelude::*;
105    /// # use embedded_iconoir::size12px;
106    /// # use kolibri_embedded_gui::ui::*;
107    /// # use kolibri_embedded_gui::label::*;
108    /// # use kolibri_embedded_gui::smartstate::*;
109    /// # let mut display = SimulatorDisplay::<Rgb565>::new(Size::new(320, 240));
110    /// # let output_settings = OutputSettingsBuilder::new().build();
111    /// # let mut window = Window::new("Kolibri Example", &output_settings);
112    /// # let mut ui = Ui::new_fullscreen(&mut display, medsize_rgb565_style());
113    /// # use kolibri_embedded_gui::iconbutton::IconButton;
114    /// use embedded_iconoir::size24px;
115    /// ui.add(IconButton::new(size24px::actions::AddCircle));
116    /// ```
117    pub fn new(_icon: ICON) -> Self {
118        Self {
119            icon: PhantomData,
120            smartstate: Container::empty(),
121            label: None,
122        }
123    }
124
125    /// Adds a text label/subtitle below the icon.
126    ///
127    /// The label text will be centered below the icon and sized according
128    /// to the current UI style font settings.
129    ///
130    /// # Example
131    ///
132    /// ```no_run
133    /// # use embedded_graphics::pixelcolor::Rgb565;
134    /// # use embedded_graphics_simulator::{SimulatorDisplay, OutputSettingsBuilder, Window};
135    /// # use kolibri_embedded_gui::style::medsize_rgb565_style;
136    /// # use kolibri_embedded_gui::ui::Ui;
137    /// # use embedded_graphics::prelude::*;
138    /// # use embedded_graphics::primitives::Rectangle;
139    /// # use embedded_iconoir::prelude::*;
140    /// # use embedded_iconoir::size12px;
141    /// # use kolibri_embedded_gui::ui::*;
142    /// # use kolibri_embedded_gui::label::*;
143    /// # use kolibri_embedded_gui::smartstate::*;
144    /// # let mut display = SimulatorDisplay::<Rgb565>::new(Size::new(320, 240));
145    /// # let output_settings = OutputSettingsBuilder::new().build();
146    /// # let mut window = Window::new("Kolibri Example", &output_settings);
147    /// # let mut ui = Ui::new_fullscreen(&mut display, medsize_rgb565_style());
148    /// # use kolibri_embedded_gui::iconbutton::IconButton;
149    /// use embedded_iconoir::size24px;
150    /// ui.add(IconButton::new(size24px::actions::AddCircle).label("Add"));
151    /// ```
152    pub fn label(mut self, label: &'a str) -> Self {
153        self.label = Some(label);
154        self
155    }
156
157    /// Creates a new [IconButton] using just the icon's type.
158    ///
159    /// This is a convenience method that allows creating an icon button without
160    /// instantiating the icon object first.
161    ///
162    /// # Example
163    /// ```no_run
164    /// # use embedded_graphics::pixelcolor::Rgb565;
165    /// # use embedded_graphics_simulator::{SimulatorDisplay, OutputSettingsBuilder, Window};
166    /// # use kolibri_embedded_gui::style::medsize_rgb565_style;
167    /// # use kolibri_embedded_gui::ui::Ui;
168    /// # use embedded_graphics::prelude::*;
169    /// # use embedded_graphics::primitives::Rectangle;
170    /// # use embedded_iconoir::prelude::*;
171    /// # use embedded_iconoir::size12px;
172    /// # use kolibri_embedded_gui::ui::*;
173    /// # use kolibri_embedded_gui::label::*;
174    /// # use kolibri_embedded_gui::smartstate::*;
175    /// # let mut display = SimulatorDisplay::<Rgb565>::new(Size::new(320, 240));
176    /// # let output_settings = OutputSettingsBuilder::new().build();
177    /// # let mut window = Window::new("Kolibri Example", &output_settings);
178    /// # let mut ui = Ui::new_fullscreen(&mut display, medsize_rgb565_style());
179    /// # use kolibri_embedded_gui::iconbutton::IconButton;
180    /// use embedded_iconoir::size24px;
181    /// ui.add(IconButton::<size24px::actions::AddCircle>::new_from_type());
182    /// ```
183    pub fn new_from_type() -> Self {
184        Self {
185            icon: PhantomData,
186            smartstate: Container::empty(),
187            label: None,
188        }
189    }
190
191    /// Attaches a [Smartstate] to this widget for incremental redrawing.
192    ///
193    /// When a smartstate is attached, the widget will only redraw when its
194    /// state changes, improving performance for stationary UI elements.
195    ///
196    /// # Example
197    ///
198    /// ```no_run
199    /// # use embedded_graphics::pixelcolor::Rgb565;
200    /// # use embedded_graphics_simulator::{SimulatorDisplay, OutputSettingsBuilder, Window};
201    /// # use kolibri_embedded_gui::style::medsize_rgb565_style;
202    /// # use kolibri_embedded_gui::ui::Ui;
203    /// # use embedded_graphics::prelude::*;
204    /// # use embedded_graphics::primitives::Rectangle;
205    /// # use embedded_iconoir::prelude::*;
206    /// # use embedded_iconoir::size12px;
207    /// # use kolibri_embedded_gui::ui::*;
208    /// # use kolibri_embedded_gui::label::*;
209    /// # use kolibri_embedded_gui::smartstate::*;
210    /// # let mut display = SimulatorDisplay::<Rgb565>::new(Size::new(320, 240));
211    /// # let output_settings = OutputSettingsBuilder::new().build();
212    /// # let mut window = Window::new("Kolibri Example", &output_settings);
213    /// # let mut ui = Ui::new_fullscreen(&mut display, medsize_rgb565_style());
214    /// # use kolibri_embedded_gui::iconbutton::IconButton;
215    /// let mut my_smartstate = Smartstate::empty();
216    /// ui.add(IconButton::new(size12px::actions::AddCircle).smartstate(&mut my_smartstate));
217    /// ```
218    ///
219    /// Returns `self` for method chaining.
220    pub fn smartstate(mut self, smartstate: &'a mut Smartstate) -> Self {
221        self.smartstate.set(smartstate);
222        self
223    }
224}
225
226impl<ICON: IconoirIcon> Widget for IconButton<'_, ICON> {
227    /// Draws the icon button within the UI.
228    ///
229    /// This method:
230    /// 1. Calculates the size based on icon and optional label
231    /// 2. Allocates space for the widget
232    /// 3. Positions the icon and optional label
233    /// 4. Detects interactions (hover, click, press)
234    /// 5. Manages visual appearance based on interaction state
235    /// 6. Updates the smartstate and draws when necessary
236    /// 7. Returns a response that includes click information
237    fn draw<DRAW: DrawTarget<Color = COL>, COL: PixelColor>(
238        &mut self,
239        ui: &mut Ui<DRAW, COL>,
240    ) -> GuiResult<Response> {
241        // get size
242        let icon = ICON::new(ui.style().icon_color);
243
244        let padding = ui.style().spacing.button_padding;
245        let border = ui.style().border_width;
246
247        let mut min_height = icon.bounding_box().size.height + 2 * padding.height + 2 * border;
248
249        let mut width = min_height;
250
251        let font = ui.style().default_font;
252
253        let mut text = if let Some(label) = self.label {
254            let mut text = Text::new(
255                label,
256                Point::new(0, 0),
257                MonoTextStyle::new(&font, ui.style().text_color),
258            );
259            text.text_style.alignment = Alignment::Center;
260            text.text_style.baseline = Baseline::Top;
261            min_height += padding.height + text.bounding_box().size.height;
262            width = width.max(text.bounding_box().size.width + 2 * padding.width + 2 * border);
263            Some(text)
264        } else {
265            None
266        };
267        let height = max(
268            max(ui.style().default_widget_height, ui.get_row_height()),
269            min_height,
270        );
271
272        let size = Size::new(width, height);
273
274        /*
275        let icon = match size.width - 2 * padding.width {
276            0..=17 => 12,
277            18..=24 => 18,
278            24..=32 => 24,
279            _ => 32,
280        };
281         */
282
283        // allocate space
284        let iresponse = ui.allocate_space(Size::new(size.width, max(size.height, height)))?;
285
286        // translate icon
287        let size = icon.bounding_box();
288
289        // center icon
290        let center_offset = iresponse.area.top_left
291            + Point::new(
292                ((iresponse.area.size.width - size.size.width) / 2) as i32,
293                ((iresponse.area.size.height
294                    - size.size.height
295                    - text
296                        .map(|t| t.bounding_box().size.height + padding.height)
297                        .unwrap_or(0))
298                    / 2) as i32,
299            );
300
301        let icon_img = Image::new(&icon, center_offset);
302
303        // center text (if it exists)
304        if let Some(text) = text.as_mut() {
305            let center_offset = iresponse.area.top_left
306                + Point::new(
307                    (iresponse.area.size.width / 2) as i32,
308                    (iresponse.area.size.height
309                        - text.bounding_box().size.height
310                        - padding.height
311                        - border) as i32,
312                );
313            text.translate_mut(center_offset);
314        }
315
316        // check for click
317        let click = matches!(iresponse.interaction, Interaction::Release(_));
318        let down = matches!(
319            iresponse.interaction,
320            Interaction::Click(_) | Interaction::Drag(_)
321        );
322
323        // styles and smartstate
324        let prevstate = self.smartstate.clone_inner();
325
326        let rect_style = match iresponse.interaction {
327            Interaction::None => {
328                self.smartstate.modify(|st| *st = Smartstate::state(1));
329
330                PrimitiveStyleBuilder::new()
331                    .stroke_color(ui.style().border_color)
332                    .stroke_width(ui.style().border_width)
333                    .fill_color(ui.style().item_background_color)
334                    .build()
335            }
336            Interaction::Hover(_) => {
337                self.smartstate.modify(|st| *st = Smartstate::state(2));
338                PrimitiveStyleBuilder::new()
339                    .stroke_color(ui.style().highlight_border_color)
340                    .stroke_width(ui.style().highlight_border_width)
341                    .fill_color(ui.style().highlight_item_background_color)
342                    .build()
343            }
344
345            _ => {
346                self.smartstate.modify(|st| *st = Smartstate::state(3));
347
348                PrimitiveStyleBuilder::new()
349                    .stroke_color(ui.style().highlight_border_color)
350                    .stroke_width(ui.style().highlight_border_width)
351                    .fill_color(ui.style().primary_color)
352                    .build()
353            }
354        };
355
356        if !self.smartstate.eq_option(&prevstate) {
357            ui.start_drawing(&iresponse.area);
358
359            ui.draw(
360                &Rectangle::new(iresponse.area.top_left, iresponse.area.size)
361                    .into_styled(rect_style),
362            )
363            .ok();
364            ui.draw(&icon_img).ok();
365            if let Some(text) = text.as_mut() {
366                ui.draw(text).unwrap();
367            }
368
369            ui.finalize()?;
370        }
371
372        Ok(Response::new(iresponse).set_clicked(click).set_down(down))
373    }
374}
375
376// Implement common traits for IconButton
377impl<ICON: IconoirIcon> core::fmt::Debug for IconButton<'_, ICON> {
378    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
379        f.debug_struct("IconButton")
380            .field("type", &core::any::type_name::<ICON>())
381            .field("label", &self.label)
382            .field("smartstate", &"<smartstate>")
383            .finish()
384    }
385}