Skip to main content

rlvgl_ui/
button_matrix.rs

1// SPDX-License-Identifier: MIT
2//! Button-matrix component with a UI-layer activation callback.
3//!
4//! This wrapper keeps the LVGL-parity `ButtonMatrix` name while adding fluent
5//! setup and `on_activate`.
6
7use alloc::{boxed::Box, string::String};
8use rlvgl_core::{
9    event::Event,
10    font::{FontMetrics, WidgetFont},
11    renderer::Renderer,
12    widget::{Color, Rect, Widget},
13};
14use rlvgl_widgets::button_matrix::ButtonMatrix as BaseButtonMatrix;
15
16use crate::theme::{ColorScheme, ComponentSize, Theme, Variant};
17
18pub use rlvgl_widgets::button_matrix::{BUTTON_NONE, ButtonId, ButtonMatrixControl};
19
20/// Callback invoked when a button activates, receiving `(id, label)`.
21type ActivateHandler = dyn FnMut(ButtonId, &str);
22
23/// Grid of labeled buttons arranged in rows.
24pub struct ButtonMatrix {
25    inner: BaseButtonMatrix,
26    on_activate: Option<Box<ActivateHandler>>,
27}
28
29impl ButtonMatrix {
30    /// Create an empty button matrix.
31    pub fn new(bounds: Rect) -> Self {
32        Self {
33            inner: BaseButtonMatrix::new(bounds),
34            on_activate: None,
35        }
36    }
37
38    /// Populate buttons from a flat map and return the widget.
39    pub fn with_map(mut self, map: &[&str]) -> Self {
40        self.inner.set_map(map);
41        self
42    }
43
44    /// Populate buttons from a flat map.
45    pub fn set_map(&mut self, map: &[&str]) {
46        self.inner.set_map(map);
47    }
48
49    /// Return the total number of buttons.
50    pub fn button_count(&self) -> usize {
51        self.inner.button_count()
52    }
53
54    /// Return button text by id.
55    pub fn button_text(&self, id: ButtonId) -> Option<&str> {
56        self.inner.button_text(id)
57    }
58
59    /// Register a callback fired when a button is activated.
60    pub fn on_activate<F: FnMut(ButtonId, &str) + 'static>(mut self, handler: F) -> Self {
61        self.on_activate = Some(Box::new(handler));
62        self
63    }
64
65    /// Set the selected button.
66    pub fn set_selected_button(&mut self, id: ButtonId) {
67        self.inner.set_selected_button(id);
68    }
69
70    /// Return the selected button.
71    pub fn selected_button(&self) -> ButtonId {
72        self.inner.selected_button()
73    }
74
75    /// Select the next navigable button.
76    pub fn navigate_next(&mut self) {
77        self.inner.navigate_next();
78    }
79
80    /// Select the previous navigable button.
81    pub fn navigate_prev(&mut self) {
82        self.inner.navigate_prev();
83    }
84
85    /// Activate the currently selected button.
86    pub fn activate_selected(&mut self) {
87        self.inner.activate_selected();
88        self.emit_activate(self.inner.selected_button());
89    }
90
91    /// OR control flags into one button.
92    pub fn set_button_control(&mut self, id: ButtonId, control: ButtonMatrixControl) {
93        self.inner.set_button_control(id, control);
94    }
95
96    /// Remove control flags from one button.
97    pub fn clear_button_control(&mut self, id: ButtonId, control: ButtonMatrixControl) {
98        self.inner.clear_button_control(id, control);
99    }
100
101    /// Set control flags on all buttons.
102    pub fn set_all_controls(&mut self, control: ButtonMatrixControl) {
103        self.inner.set_all_controls(control);
104    }
105
106    /// Clear control flags on all buttons.
107    pub fn clear_all_controls(&mut self, control: ButtonMatrixControl) {
108        self.inner.clear_all_controls(control);
109    }
110
111    /// Enable or disable the one-checked constraint.
112    pub fn set_one_checked(&mut self, enabled: bool) {
113        self.inner.set_one_checked(enabled);
114    }
115
116    /// Return whether the one-checked constraint is enabled.
117    pub fn one_checked(&self) -> bool {
118        self.inner.one_checked()
119    }
120
121    /// Set a button's checked state.
122    pub fn set_button_checked(&mut self, id: ButtonId, checked: bool) {
123        self.inner.set_button_checked(id, checked);
124    }
125
126    /// Return whether a button is checked.
127    pub fn button_checked(&self, id: ButtonId) -> bool {
128        self.inner.button_checked(id)
129    }
130
131    /// Set a button's relative width.
132    pub fn set_button_width(&mut self, id: ButtonId, width: u8) {
133        self.inner.set_button_width(id, width);
134    }
135
136    /// Set button label color and return the widget.
137    pub fn text_color(mut self, color: Color) -> Self {
138        self.inner.text_color = color;
139        self
140    }
141
142    /// Set normal button background color and return the widget.
143    pub fn button_color(mut self, color: Color) -> Self {
144        self.inner.button_color = color;
145        self
146    }
147
148    /// Set pressed button background color and return the widget.
149    pub fn pressed_color(mut self, color: Color) -> Self {
150        self.inner.pressed_color = color;
151        self
152    }
153
154    /// Set checked button background color and return the widget.
155    pub fn checked_color(mut self, color: Color) -> Self {
156        self.inner.checked_color = color;
157        self
158    }
159
160    /// Assign the font used to render this matrix.
161    pub fn set_font(&mut self, font: &'static dyn FontMetrics) {
162        self.inner.set_font(font);
163    }
164
165    /// Apply themed container, button-state, and text colors.
166    pub fn themed(
167        mut self,
168        theme: &Theme,
169        scheme: ColorScheme,
170        variant: Variant,
171        size: ComponentSize,
172    ) -> Self {
173        let resolved = theme.component_style(scheme, variant, size);
174        let colors = theme.scheme(scheme);
175        self.inner.style = resolved.style;
176        self.inner.text_color = resolved.text_color;
177        self.inner.button_color = resolved.style.bg_color;
178        self.inner.checked_color = resolved.accent_color;
179        self.inner.pressed_color = colors.muted;
180        self.inner.disabled_color = colors.border.with_alpha(128);
181        self
182    }
183
184    /// Immutable access to the matrix style.
185    pub fn style(&self) -> &rlvgl_core::style::Style {
186        &self.inner.style
187    }
188
189    /// Mutable access to the matrix style.
190    pub fn style_mut(&mut self) -> &mut rlvgl_core::style::Style {
191        &mut self.inner.style
192    }
193
194    fn emit_activate(&mut self, id: ButtonId) {
195        if id == BUTTON_NONE {
196            return;
197        }
198        let Some(text) = self.inner.button_text(id).map(String::from) else {
199            return;
200        };
201        if let Some(cb) = self.on_activate.as_mut() {
202            cb(id, &text);
203        }
204    }
205}
206
207impl Widget for ButtonMatrix {
208    fn bounds(&self) -> Rect {
209        self.inner.bounds()
210    }
211
212    fn widget_font_mut(&mut self) -> Option<&mut WidgetFont> {
213        self.inner.widget_font_mut()
214    }
215
216    fn set_bounds(&mut self, bounds: Rect) {
217        self.inner.set_bounds(bounds);
218    }
219
220    fn draw(&self, renderer: &mut dyn Renderer) {
221        self.inner.draw(renderer);
222    }
223
224    fn handle_event(&mut self, event: &Event) -> bool {
225        let is_release = matches!(event, Event::PressRelease { .. });
226        let handled = self.inner.handle_event(event);
227        if handled && is_release {
228            self.emit_activate(self.inner.selected_button());
229        }
230        handled
231    }
232}
233
234#[cfg(test)]
235mod tests {
236    use super::*;
237    use alloc::rc::Rc;
238    use core::cell::Cell;
239
240    #[test]
241    fn button_matrix_activation_callback_reports_id() {
242        let activated = Rc::new(Cell::new(BUTTON_NONE));
243        let flag = activated.clone();
244        let mut matrix = ButtonMatrix::new(rect(0, 0, 100, 40))
245            .with_map(&["A", "B"])
246            .on_activate(move |id, _| flag.set(id));
247
248        assert!(matrix.handle_event(&Event::PressDown { x: 5, y: 5 }));
249        assert!(matrix.handle_event(&Event::PressRelease { x: 5, y: 5 }));
250
251        assert_eq!(activated.get(), ButtonId(0));
252    }
253
254    #[test]
255    fn button_matrix_themed_sets_state_colors() {
256        let theme = Theme::material_light();
257        let matrix = ButtonMatrix::new(rect(0, 0, 100, 40)).themed(
258            &theme,
259            ColorScheme::Warning,
260            Variant::Outline,
261            ComponentSize::Lg,
262        );
263
264        assert_eq!(matrix.style().border_width, 2);
265        assert_eq!(
266            matrix.inner.checked_color,
267            theme.scheme(ColorScheme::Warning).solid
268        );
269    }
270
271    fn rect(x: i32, y: i32, width: i32, height: i32) -> Rect {
272        Rect {
273            x,
274            y,
275            width,
276            height,
277        }
278    }
279}