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}