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}