kolibri_embedded_gui/button.rs
1//! # Button Widget
2//!
3//! See [Button] for more info.
4
5use crate::smartstate::{Container, Smartstate};
6use crate::ui::{GuiResult, Interaction, Response, Ui, Widget};
7use core::cmp::max;
8use core::ops::Add;
9use embedded_graphics::draw_target::DrawTarget;
10use embedded_graphics::geometry::{Point, Size};
11use embedded_graphics::mono_font::MonoTextStyle;
12use embedded_graphics::pixelcolor::PixelColor;
13use embedded_graphics::prelude::*;
14use embedded_graphics::primitives::{PrimitiveStyleBuilder, Rectangle};
15use embedded_graphics::text::{Baseline, Text};
16
17/// # Button Widget
18///
19/// A clickable button widget that displays text and responds to user interaction.
20///
21/// Buttons are one of the most fundamental widgets in Kolibri. They provide a simple way to trigger
22/// actions in response to user input. Buttons can be created with just a text label and optionally
23/// support smartstate-based incremental redrawing for better performance.
24///
25/// # Features
26/// - Text label with customizable font and colors
27/// - Visual feedback for different interaction states (normal, hover, pressed)
28/// - Optional smartstate support for incremental redrawing
29/// - Automatic sizing based on text content and style settings
30///
31/// # Example
32/// ```no_run
33/// # use embedded_graphics::pixelcolor::Rgb565;
34/// # use embedded_graphics_simulator::{SimulatorDisplay, OutputSettingsBuilder, Window};
35/// # use kolibri_embedded_gui::style::medsize_rgb565_style;
36/// # use kolibri_embedded_gui::ui::Ui;
37/// # use embedded_graphics::prelude::*;
38/// # use embedded_graphics::primitives::Rectangle;
39/// # use embedded_iconoir::prelude::*;
40/// # use embedded_iconoir::size12px;
41/// # use kolibri_embedded_gui::ui::*;
42/// # use kolibri_embedded_gui::label::*;
43/// # use kolibri_embedded_gui::smartstate::*;
44/// # let mut display = SimulatorDisplay::<Rgb565>::new(Size::new(320, 240));
45/// # let output_settings = OutputSettingsBuilder::new().build();
46/// # let mut window = Window::new("Kolibri Example", &output_settings);
47/// # let mut ui = Ui::new_fullscreen(&mut display, medsize_rgb565_style());
48/// # use kolibri_embedded_gui::button::Button;
49///
50/// // Create a basic button
51/// if ui.add(Button::new("Click me!")).clicked() {
52/// // Handle click
53/// }
54///
55/// // Create a button with smartstate for incremental redrawing
56/// let mut smartstateProvider = SmartstateProvider::<20>::new();
57/// if ui.add(Button::new("Efficient!").smartstate(smartstateProvider.nxt())).clicked() {
58/// // Handle click with improved performance
59/// }
60///
61/// // Create a button in a horizontal layout
62/// if ui.add_horizontal(Button::new("-")).clicked() {
63/// // Handle click in horizontal layout
64/// }
65/// ```
66///
67/// # Visual States
68/// Buttons have three visual states that provide user feedback:
69/// 1. Normal - Default appearance with standard border and background
70/// 2. Hover - Enhanced appearance when mouse/pointer is over the button
71/// 3. Pressed - Highlighted appearance when clicked/pressed
72///
73/// # Styling
74/// Buttons follow the [UI]'s current style settings including:
75/// - Border colors and widths (normal and highlighted)
76/// - Background colors (normal, highlighted, and pressed)
77/// - Text color and font
78/// - Padding and spacing
79pub struct Button<'a> {
80 label: &'a str,
81 smartstate: Container<'a, Smartstate>,
82}
83
84impl<'a> Button<'a> {
85 /// Creates a new button with the given text label.
86 ///
87 /// # Arguments
88 /// * `label` - The text to display on the button
89 ///
90 /// # Returns
91 /// A new Button instance with the specified label and no smartstate
92 pub fn new(label: &'a str) -> Button<'a> {
93 Button {
94 label,
95 smartstate: Container::empty(),
96 }
97 }
98
99 /// Adds smartstate support to the button for incremental redrawing.
100 ///
101 /// When a smartstate is provided, the button will only redraw when its visual state changes,
102 /// significantly improving performance especially on slower displays.
103 ///
104 /// # Arguments
105 /// * `smartstate` - The smartstate to use for tracking the button's state
106 ///
107 /// # Returns
108 /// Self with smartstate configured
109 pub fn smartstate(mut self, smartstate: &'a mut Smartstate) -> Self {
110 self.smartstate.set(smartstate);
111 self
112 }
113}
114
115impl Widget for Button<'_> {
116 fn draw<DRAW: DrawTarget<Color = COL>, COL: PixelColor>(
117 &mut self,
118 ui: &mut Ui<DRAW, COL>,
119 ) -> GuiResult<Response> {
120 // get size
121 let font = ui.style().default_font;
122
123 let mut text = Text::new(
124 self.label,
125 Point::new(0, 0),
126 MonoTextStyle::new(&font, ui.style().text_color),
127 );
128
129 let height = ui.style().default_widget_height;
130 let size = text.bounding_box();
131 let padding = ui.style().spacing.button_padding;
132 let border = ui.style().border_width;
133
134 // allocate space
135 let iresponse = ui.allocate_space(Size::new(
136 size.size.width + 2 * padding.width + 2 * border,
137 max(size.size.height + 2 * padding.height + 2 * border, height),
138 ))?;
139
140 // move text
141 text.translate_mut(iresponse.area.top_left.add(Point::new(
142 (padding.width + border) as i32,
143 (padding.height + border) as i32,
144 )));
145
146 text.text_style.baseline = Baseline::Top;
147
148 // check for click
149 let click = matches!(iresponse.interaction, Interaction::Release(_));
150 let down = matches!(
151 iresponse.interaction,
152 Interaction::Click(_) | Interaction::Drag(_)
153 );
154
155 // styles and smartstate
156 let prevstate = self.smartstate.clone_inner();
157
158 let rect_style = match iresponse.interaction {
159 Interaction::None => {
160 self.smartstate.modify(|st| *st = Smartstate::state(1));
161
162 PrimitiveStyleBuilder::new()
163 .stroke_color(ui.style().border_color)
164 .stroke_width(ui.style().border_width)
165 .fill_color(ui.style().item_background_color)
166 .build()
167 }
168 Interaction::Hover(_) => {
169 self.smartstate.modify(|st| *st = Smartstate::state(2));
170
171 PrimitiveStyleBuilder::new()
172 .stroke_color(ui.style().highlight_border_color)
173 .stroke_width(ui.style().highlight_border_width)
174 .fill_color(ui.style().highlight_item_background_color)
175 .build()
176 }
177
178 _ => {
179 self.smartstate.modify(|st| *st = Smartstate::state(3));
180
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 };
188
189 if !self.smartstate.eq_option(&prevstate) {
190 ui.start_drawing(&iresponse.area);
191
192 ui.draw(
193 &Rectangle::new(iresponse.area.top_left, iresponse.area.size)
194 .into_styled(rect_style),
195 )
196 .ok();
197 ui.draw(&text).ok();
198
199 ui.finalize()?;
200 }
201
202 Ok(Response::new(iresponse).set_clicked(click).set_down(down))
203 }
204}