Skip to main content

rlvgl_widgets/
button_matrix.rs

1//! Grid of labeled buttons arranged in rows (LPAR-12a).
2//!
3//! [`ButtonMatrix`] lays buttons from a flat string map (rows separated by
4//! `"\n"` entries) and dispatches pointer and key events to individual cells.
5//!
6//! # Map format
7//!
8//! ```text
9//! let map = &["One", "Two", "\n", "Three", "Four", "Five"];
10//! ```
11//!
12//! `"\n"` entries act as row breaks; leading / trailing / adjacent `"\n"` entries
13//! create empty rows.  Buttons are assigned sequential [`ButtonId`] values in
14//! row-major order, skipping `"\n"`.
15
16use alloc::string::String;
17use alloc::vec::Vec;
18use rlvgl_core::draw::draw_widget_bg;
19use rlvgl_core::event::Event;
20use rlvgl_core::font::{FontMetrics, WidgetFont, shape_text_ltr};
21use rlvgl_core::renderer::{ClipRenderer, Renderer};
22use rlvgl_core::style::Style;
23use rlvgl_core::widget::{Color, Rect, Widget};
24
25/// Opaque button identifier within a [`ButtonMatrix`].
26///
27/// IDs are assigned sequentially in row-major order when [`ButtonMatrix::set_map`]
28/// is called.  [`BUTTON_NONE`] is the sentinel for "no button".
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30pub struct ButtonId(pub u16);
31
32/// Sentinel [`ButtonId`] meaning "no button is selected / pressed".
33pub const BUTTON_NONE: ButtonId = ButtonId(u16::MAX);
34
35/// Bit-flags controlling individual button appearance and behavior.
36///
37/// Construct by OR-ing the associated constants:
38///
39/// ```ignore
40/// let ctrl = ButtonMatrixControl::CHECKABLE | ButtonMatrixControl::CHECKED;
41/// ```
42#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
43pub struct ButtonMatrixControl(pub u32);
44
45impl ButtonMatrixControl {
46    /// Button is hidden (not drawn, not hit-tested).
47    pub const HIDDEN: Self = Self(1 << 0);
48    /// Button is disabled (drawn dimmed, not activatable).
49    pub const DISABLED: Self = Self(1 << 1);
50    /// Button is inactive (drawn dimmed, pointer events ignored).
51    pub const INACTIVE: Self = Self(1 << 2);
52    /// Button can be toggled on/off when clicked.
53    pub const CHECKABLE: Self = Self(1 << 3);
54    /// Button is currently in the checked state.
55    pub const CHECKED: Self = Self(1 << 4);
56    /// Activate on pointer-down instead of pointer-up.
57    pub const CLICK_TRIGGER: Self = Self(1 << 5);
58    /// Do not repeat activation while the pointer is held.
59    pub const NO_REPEAT: Self = Self(1 << 6);
60    /// Show as a popover (layout hint for the renderer).
61    pub const POPOVER: Self = Self(1 << 7);
62    /// Parse inline color codes (`#RRGGBB`) in the label.
63    pub const RECOLOR: Self = Self(1 << 8);
64    /// App-defined custom flag 1.
65    pub const CUSTOM1: Self = Self(1 << 9);
66    /// App-defined custom flag 2.
67    pub const CUSTOM2: Self = Self(1 << 10);
68
69    /// Return `true` when `flag` is set.
70    pub fn contains(self, flag: Self) -> bool {
71        self.0 & flag.0 != 0
72    }
73
74    /// OR in `flag`.
75    pub fn insert(&mut self, flag: Self) {
76        self.0 |= flag.0;
77    }
78
79    /// Remove `flag`.
80    pub fn remove(&mut self, flag: Self) {
81        self.0 &= !flag.0;
82    }
83}
84
85impl core::ops::BitOr for ButtonMatrixControl {
86    type Output = Self;
87    fn bitor(self, rhs: Self) -> Self {
88        Self(self.0 | rhs.0)
89    }
90}
91
92impl core::ops::BitOrAssign for ButtonMatrixControl {
93    fn bitor_assign(&mut self, rhs: Self) {
94        self.0 |= rhs.0;
95    }
96}
97
98/// A single button cell within a [`ButtonMatrix`].
99pub struct ButtonMatrixButton {
100    /// Display label for the button.
101    pub label: String,
102    /// Appearance and behavior flags.
103    pub control: ButtonMatrixControl,
104    /// Relative width weight (1–15); buttons in a row share its total width
105    /// proportionally.
106    pub width: u8,
107}
108
109impl ButtonMatrixButton {
110    fn new(label: impl Into<String>) -> Self {
111        Self {
112            label: label.into(),
113            control: ButtonMatrixControl::default(),
114            width: 1,
115        }
116    }
117
118    fn is_hidden(&self) -> bool {
119        self.control.contains(ButtonMatrixControl::HIDDEN)
120    }
121
122    fn is_disabled(&self) -> bool {
123        self.control.contains(ButtonMatrixControl::DISABLED)
124    }
125
126    fn is_inactive(&self) -> bool {
127        self.control.contains(ButtonMatrixControl::INACTIVE)
128    }
129
130    fn is_navigable(&self) -> bool {
131        !self.is_hidden() && !self.is_disabled() && !self.is_inactive()
132    }
133}
134
135/// Grid of labeled buttons arranged in rows.
136///
137/// Populate with [`set_map`](Self::set_map), adjust per-button appearance via
138/// the `set_button_*` family, and react to [`Event::PressRelease`] or key
139/// navigation events.
140///
141/// # Layout
142///
143/// Row height is `bounds.height / row_count`.  Within each row, button widths
144/// are computed proportionally from their [`ButtonMatrixButton::width`] field
145/// relative to the row total.
146pub struct ButtonMatrix {
147    bounds: Rect,
148    rows: Vec<Vec<ButtonMatrixButton>>,
149    selected: ButtonId,
150    pressed_id: ButtonId,
151    one_checked: bool,
152    /// Background style for the matrix container.
153    pub style: Style,
154    /// Color used for button labels.
155    pub text_color: Color,
156    /// Color used for the normal button background.
157    pub button_color: Color,
158    /// Color used when a button is pressed.
159    pub pressed_color: Color,
160    /// Color used when a button is checked.
161    pub checked_color: Color,
162    /// Color used when a button is disabled or inactive.
163    pub disabled_color: Color,
164    /// Font assignment for this widget (FONT-00 §5); resolves to `FONT_6X10`
165    /// when unset.
166    font: WidgetFont,
167}
168
169impl ButtonMatrix {
170    /// Create an empty button matrix occupying `bounds`.
171    ///
172    /// Call [`set_map`](Self::set_map) to populate buttons before drawing.
173    pub fn new(bounds: Rect) -> Self {
174        Self {
175            bounds,
176            rows: Vec::new(),
177            selected: BUTTON_NONE,
178            pressed_id: BUTTON_NONE,
179            one_checked: false,
180            style: Style::default(),
181            text_color: Color(50, 50, 50, 255),
182            button_color: Color(200, 200, 200, 255),
183            pressed_color: Color(150, 150, 220, 255),
184            checked_color: Color(100, 160, 230, 255),
185            disabled_color: Color(180, 180, 180, 128),
186            font: WidgetFont::new(),
187        }
188    }
189
190    /// Assign the font used to render this widget (FONT-00 §5); resolves to
191    /// `FONT_6X10` when unset.
192    pub fn set_font(&mut self, font: &'static dyn FontMetrics) {
193        self.font.set(font);
194    }
195
196    /// Populate the button grid from a flat string slice.
197    ///
198    /// `"\n"` entries are row separators; all other entries become button
199    /// labels.  Existing buttons and their controls are replaced.
200    pub fn set_map(&mut self, map: &[&str]) {
201        self.rows.clear();
202        self.selected = BUTTON_NONE;
203        self.pressed_id = BUTTON_NONE;
204
205        let mut current_row: Vec<ButtonMatrixButton> = Vec::new();
206        for &entry in map {
207            if entry == "\n" {
208                self.rows.push(current_row);
209                current_row = Vec::new();
210            } else {
211                current_row.push(ButtonMatrixButton::new(entry));
212            }
213        }
214        // Push the last (potentially empty) row if it has buttons, or if map
215        // ended with a non-"\n" sequence.
216        if !current_row.is_empty() || (!map.is_empty() && map.last() != Some(&"\n")) {
217            self.rows.push(current_row);
218        }
219    }
220
221    // ── Flat button access ────────────────────────────────────────────────
222
223    fn flat_button(&self, id: ButtonId) -> Option<&ButtonMatrixButton> {
224        let mut idx = id.0 as usize;
225        for row in &self.rows {
226            if idx < row.len() {
227                return Some(&row[idx]);
228            }
229            idx -= row.len();
230        }
231        None
232    }
233
234    fn flat_button_mut(&mut self, id: ButtonId) -> Option<&mut ButtonMatrixButton> {
235        let mut idx = id.0 as usize;
236        for row in &mut self.rows {
237            if idx < row.len() {
238                return Some(&mut row[idx]);
239            }
240            idx -= row.len();
241        }
242        None
243    }
244
245    /// Return the label of button `id`, or `None` if `id` is out of range.
246    pub fn button_text(&self, id: ButtonId) -> Option<&str> {
247        self.flat_button(id).map(|b| b.label.as_str())
248    }
249
250    /// Return the total number of buttons (excluding row separators).
251    pub fn button_count(&self) -> usize {
252        self.rows.iter().map(|r| r.len()).sum()
253    }
254
255    /// Return a flat list of all buttons in row-major order.
256    pub fn buttons(&self) -> Vec<&ButtonMatrixButton> {
257        self.rows.iter().flat_map(|r| r.iter()).collect()
258    }
259
260    // ── Control methods ───────────────────────────────────────────────────
261
262    /// OR `control` flags into button `id`.
263    pub fn set_button_control(&mut self, id: ButtonId, control: ButtonMatrixControl) {
264        if let Some(b) = self.flat_button_mut(id) {
265            b.control.insert(control);
266        }
267    }
268
269    /// Remove `control` flags from button `id`.
270    pub fn clear_button_control(&mut self, id: ButtonId, control: ButtonMatrixControl) {
271        if let Some(b) = self.flat_button_mut(id) {
272            b.control.remove(control);
273        }
274    }
275
276    /// Set `control` flags on all buttons.
277    pub fn set_all_controls(&mut self, control: ButtonMatrixControl) {
278        for row in &mut self.rows {
279            for b in row {
280                b.control.insert(control);
281            }
282        }
283    }
284
285    /// Clear `control` flags on all buttons.
286    pub fn clear_all_controls(&mut self, control: ButtonMatrixControl) {
287        for row in &mut self.rows {
288            for b in row {
289                b.control.remove(control);
290            }
291        }
292    }
293
294    /// Assign controls to buttons by flat index.
295    ///
296    /// Extra entries are ignored; buttons without a corresponding entry are
297    /// left unchanged.
298    pub fn set_control_map(&mut self, map: &[ButtonMatrixControl]) {
299        let mut idx = 0usize;
300        for row in &mut self.rows {
301            for b in row {
302                if idx >= map.len() {
303                    return;
304                }
305                b.control = map[idx];
306                idx += 1;
307            }
308        }
309    }
310
311    /// Return the control flags of button `id`.
312    pub fn control(&self, id: ButtonId) -> ButtonMatrixControl {
313        self.flat_button(id).map(|b| b.control).unwrap_or_default()
314    }
315
316    /// Set the relative width of button `id` (clamped to `1..=15`).
317    pub fn set_button_width(&mut self, id: ButtonId, width: u8) {
318        if let Some(b) = self.flat_button_mut(id) {
319            b.width = width.clamp(1, 15);
320        }
321    }
322
323    /// Return the relative width of button `id`, or `1` if not found.
324    pub fn button_width(&self, id: ButtonId) -> u8 {
325        self.flat_button(id).map(|b| b.width).unwrap_or(1)
326    }
327
328    // ── Selection ─────────────────────────────────────────────────────────
329
330    /// Set the selected (keyboard-focused) button.
331    ///
332    /// Out-of-range IDs are silently ignored.
333    pub fn set_selected_button(&mut self, id: ButtonId) {
334        if id == BUTTON_NONE || (id.0 as usize) < self.button_count() {
335            self.selected = id;
336        }
337    }
338
339    /// Return the currently selected button.
340    pub fn selected_button(&self) -> ButtonId {
341        self.selected
342    }
343
344    // ── Checked state ─────────────────────────────────────────────────────
345
346    /// Enable or disable the one-checked constraint.
347    ///
348    /// When enabling, all checked buttons except the first (lowest ID) are
349    /// unchecked.
350    pub fn set_one_checked(&mut self, enabled: bool) {
351        self.one_checked = enabled;
352        if enabled {
353            let mut found_first = false;
354            for row in &mut self.rows {
355                for b in row {
356                    if b.control.contains(ButtonMatrixControl::CHECKED) {
357                        if found_first {
358                            b.control.remove(ButtonMatrixControl::CHECKED);
359                        } else {
360                            found_first = true;
361                        }
362                    }
363                }
364            }
365        }
366    }
367
368    /// Return `true` when the one-checked constraint is enabled.
369    pub fn one_checked(&self) -> bool {
370        self.one_checked
371    }
372
373    /// Set the checked state of button `id`.
374    ///
375    /// When `one_checked` is enabled and `checked` is `true`, all other
376    /// buttons are unchecked first.
377    pub fn set_button_checked(&mut self, id: ButtonId, checked: bool) {
378        if id == BUTTON_NONE || (id.0 as usize) >= self.button_count() {
379            return;
380        }
381        if checked && self.one_checked {
382            self.clear_all_controls(ButtonMatrixControl::CHECKED);
383        }
384        if let Some(b) = self.flat_button_mut(id) {
385            if checked {
386                b.control.insert(ButtonMatrixControl::CHECKED);
387            } else {
388                b.control.remove(ButtonMatrixControl::CHECKED);
389            }
390        }
391    }
392
393    /// Return `true` when button `id` is in the checked state.
394    pub fn button_checked(&self, id: ButtonId) -> bool {
395        self.flat_button(id)
396            .map(|b| b.control.contains(ButtonMatrixControl::CHECKED))
397            .unwrap_or(false)
398    }
399
400    // ── Key navigation ────────────────────────────────────────────────────
401
402    /// Select the next navigable button (wraps around).
403    pub fn navigate_next(&mut self) {
404        let count = self.button_count();
405        if count == 0 {
406            return;
407        }
408        let start = if self.selected == BUTTON_NONE {
409            0
410        } else {
411            (self.selected.0 as usize + 1) % count
412        };
413        for i in 0..count {
414            let id = ButtonId(((start + i) % count) as u16);
415            if self
416                .flat_button(id)
417                .map(|b| b.is_navigable())
418                .unwrap_or(false)
419            {
420                self.selected = id;
421                return;
422            }
423        }
424    }
425
426    /// Select the previous navigable button (wraps around).
427    pub fn navigate_prev(&mut self) {
428        let count = self.button_count();
429        if count == 0 {
430            return;
431        }
432        let start = if self.selected == BUTTON_NONE {
433            count - 1
434        } else {
435            (self.selected.0 as usize + count - 1) % count
436        };
437        for i in 0..count {
438            let id = ButtonId(((start + count - i) % count) as u16);
439            if self
440                .flat_button(id)
441                .map(|b| b.is_navigable())
442                .unwrap_or(false)
443            {
444                self.selected = id;
445                return;
446            }
447        }
448    }
449
450    /// Activate the currently selected button.
451    ///
452    /// No-ops when nothing is selected or the selected button is not
453    /// navigable.
454    pub fn activate_selected(&mut self) {
455        let id = self.selected;
456        if id == BUTTON_NONE {
457            return;
458        }
459        let navigable = self
460            .flat_button(id)
461            .map(|b| b.is_navigable())
462            .unwrap_or(false);
463        if navigable {
464            self.activate(id);
465        }
466    }
467
468    // ── Internal helpers ──────────────────────────────────────────────────
469
470    /// Apply toggle / one-checked logic and update selection.
471    fn activate(&mut self, id: ButtonId) {
472        let checkable = self
473            .flat_button(id)
474            .map(|b| b.control.contains(ButtonMatrixControl::CHECKABLE))
475            .unwrap_or(false);
476        if checkable {
477            let currently_checked = self.button_checked(id);
478            self.set_button_checked(id, !currently_checked);
479        }
480        self.selected = id;
481    }
482
483    /// Row height in pixels (clamped to 0 when there are no rows).
484    fn row_height(&self) -> i32 {
485        let n = self.rows.len() as i32;
486        if n == 0 { 0 } else { self.bounds.height / n }
487    }
488
489    /// Return the row rect for row index `r`.
490    fn row_rect(&self, r: usize) -> Rect {
491        let rh = self.row_height();
492        // Last row claims remainder to avoid rounding gaps
493        let h = if r + 1 == self.rows.len() {
494            self.bounds.height - rh * r as i32
495        } else {
496            rh
497        };
498        Rect {
499            x: self.bounds.x,
500            y: self.bounds.y + rh * r as i32,
501            width: self.bounds.width,
502            height: h.max(0),
503        }
504    }
505
506    /// Hit-test a point against all visible buttons; return the first match.
507    fn button_at(&self, px: i32, py: i32) -> ButtonId {
508        let mut global_idx: u16 = 0;
509        for (r, row) in self.rows.iter().enumerate() {
510            let rr = self.row_rect(r);
511            let total_weight: u32 = row.iter().map(|b| b.width as u32).sum();
512            let total_weight = total_weight.max(1);
513            let mut x_off = 0i32;
514            for (col, btn) in row.iter().enumerate() {
515                let w = if col + 1 == row.len() {
516                    rr.width - x_off
517                } else {
518                    ((btn.width as i32) * rr.width) / total_weight as i32
519                };
520                let cell = Rect {
521                    x: rr.x + x_off,
522                    y: rr.y,
523                    width: w.max(0),
524                    height: rr.height,
525                };
526                if !btn.is_hidden()
527                    && px >= cell.x
528                    && px < cell.x + cell.width
529                    && py >= cell.y
530                    && py < cell.y + cell.height
531                {
532                    return ButtonId(global_idx);
533                }
534                x_off += w;
535                global_idx += 1;
536            }
537        }
538        BUTTON_NONE
539    }
540}
541
542impl Widget for ButtonMatrix {
543    fn bounds(&self) -> Rect {
544        self.bounds
545    }
546
547    fn widget_font_mut(&mut self) -> Option<&mut WidgetFont> {
548        Some(&mut self.font)
549    }
550
551    fn set_bounds(&mut self, bounds: Rect) {
552        self.bounds = bounds;
553    }
554
555    fn draw(&self, renderer: &mut dyn Renderer) {
556        draw_widget_bg(renderer, self.bounds, &self.style);
557
558        let font = self.font.resolve();
559        let metrics = font.line_metrics();
560
561        let mut global_idx: u16 = 0;
562        for (r, row) in self.rows.iter().enumerate() {
563            let rr = self.row_rect(r);
564            let total_weight: u32 = row.iter().map(|b| b.width as u32).sum();
565            let total_weight = total_weight.max(1);
566            let mut x_off = 0i32;
567            for (col, btn) in row.iter().enumerate() {
568                let id = ButtonId(global_idx);
569                let w = if col + 1 == row.len() {
570                    rr.width - x_off
571                } else {
572                    ((btn.width as i32) * rr.width) / total_weight as i32
573                };
574                let cell = Rect {
575                    x: rr.x + x_off,
576                    y: rr.y,
577                    width: w.max(0),
578                    height: rr.height,
579                };
580                x_off += w;
581
582                if btn.is_hidden() {
583                    global_idx += 1;
584                    continue;
585                }
586
587                // Choose background color
588                let bg = if btn.is_disabled() || btn.is_inactive() {
589                    self.disabled_color
590                } else if self.pressed_id == id {
591                    self.pressed_color
592                } else if btn.control.contains(ButtonMatrixControl::CHECKED) {
593                    self.checked_color
594                } else {
595                    self.button_color
596                };
597                renderer.fill_rect(cell, bg.with_alpha(self.style.alpha));
598
599                // Draw label centered in the cell
600                if cell.width > 0 && cell.height > 0 {
601                    let baseline = cell.y
602                        + metrics.ascent as i32
603                        + (cell.height - metrics.line_height as i32) / 2;
604                    let shaped = shape_text_ltr(font, &btn.label, (cell.x, baseline), 0);
605                    let label_color = self.text_color.with_alpha(self.style.alpha);
606                    let mut clipped = ClipRenderer::new(renderer, cell);
607                    clipped.draw_text_shaped(&shaped, (0, 0), label_color);
608                }
609
610                global_idx += 1;
611            }
612        }
613    }
614
615    fn handle_event(&mut self, event: &Event) -> bool {
616        match event {
617            Event::PressDown { x, y } => {
618                let id = self.button_at(*x, *y);
619                if id == BUTTON_NONE {
620                    return false;
621                }
622                let activatable = self
623                    .flat_button(id)
624                    .map(|b| !b.is_disabled() && !b.is_inactive())
625                    .unwrap_or(false);
626                if activatable {
627                    self.pressed_id = id;
628                    return true;
629                }
630                false
631            }
632            Event::PressRelease { x, y } => {
633                let id = self.button_at(*x, *y);
634                if self.pressed_id == BUTTON_NONE {
635                    return false;
636                }
637                let was_pressed = self.pressed_id;
638                self.pressed_id = BUTTON_NONE;
639                if id == was_pressed {
640                    self.activate(id);
641                    return true;
642                }
643                false
644            }
645            Event::PointerUp { .. } => {
646                self.pressed_id = BUTTON_NONE;
647                false
648            }
649            _ => false,
650        }
651    }
652}
653
654#[cfg(test)]
655mod tests {
656    use super::*;
657
658    fn rect(x: i32, y: i32, w: i32, h: i32) -> Rect {
659        Rect {
660            x,
661            y,
662            width: w,
663            height: h,
664        }
665    }
666
667    struct NullRenderer;
668    impl rlvgl_core::renderer::Renderer for NullRenderer {
669        fn fill_rect(&mut self, _rect: Rect, _color: Color) {}
670        fn draw_text(&mut self, _pos: (i32, i32), _text: &str, _color: Color) {}
671    }
672
673    #[test]
674    fn new_empty_matrix() {
675        let m = ButtonMatrix::new(rect(0, 0, 300, 100));
676        assert_eq!(m.button_count(), 0);
677        assert_eq!(m.selected_button(), BUTTON_NONE);
678    }
679
680    #[test]
681    fn set_map_parses_rows_and_counts() {
682        let mut m = ButtonMatrix::new(rect(0, 0, 300, 100));
683        m.set_map(&["A", "B", "\n", "C", "D", "E"]);
684        assert_eq!(m.button_count(), 5);
685        assert_eq!(m.button_text(ButtonId(0)), Some("A"));
686        assert_eq!(m.button_text(ButtonId(1)), Some("B"));
687        assert_eq!(m.button_text(ButtonId(2)), Some("C"));
688        assert_eq!(m.button_text(ButtonId(3)), Some("D"));
689        assert_eq!(m.button_text(ButtonId(4)), Some("E"));
690    }
691
692    #[test]
693    fn button_text_oob_returns_none() {
694        let mut m = ButtonMatrix::new(rect(0, 0, 300, 100));
695        m.set_map(&["A"]);
696        assert!(m.button_text(ButtonId(99)).is_none());
697    }
698
699    #[test]
700    fn set_button_width_clamps() {
701        let mut m = ButtonMatrix::new(rect(0, 0, 300, 100));
702        m.set_map(&["A", "B"]);
703        m.set_button_width(ButtonId(0), 0); // clamped to 1
704        assert_eq!(m.button_width(ButtonId(0)), 1);
705        m.set_button_width(ButtonId(0), 20); // clamped to 15
706        assert_eq!(m.button_width(ButtonId(0)), 15);
707    }
708
709    #[test]
710    fn set_button_control_and_clear() {
711        let mut m = ButtonMatrix::new(rect(0, 0, 300, 100));
712        m.set_map(&["A"]);
713        m.set_button_control(ButtonId(0), ButtonMatrixControl::DISABLED);
714        assert!(
715            m.control(ButtonId(0))
716                .contains(ButtonMatrixControl::DISABLED)
717        );
718        m.clear_button_control(ButtonId(0), ButtonMatrixControl::DISABLED);
719        assert!(
720            !m.control(ButtonId(0))
721                .contains(ButtonMatrixControl::DISABLED)
722        );
723    }
724
725    #[test]
726    fn set_all_controls_and_clear_all() {
727        let mut m = ButtonMatrix::new(rect(0, 0, 300, 100));
728        m.set_map(&["A", "B", "\n", "C"]);
729        m.set_all_controls(ButtonMatrixControl::CHECKABLE);
730        for id in 0..3 {
731            assert!(
732                m.control(ButtonId(id))
733                    .contains(ButtonMatrixControl::CHECKABLE)
734            );
735        }
736        m.clear_all_controls(ButtonMatrixControl::CHECKABLE);
737        for id in 0..3 {
738            assert!(
739                !m.control(ButtonId(id))
740                    .contains(ButtonMatrixControl::CHECKABLE)
741            );
742        }
743    }
744
745    #[test]
746    fn set_control_map_assigns_by_index() {
747        let mut m = ButtonMatrix::new(rect(0, 0, 300, 100));
748        m.set_map(&["A", "B", "C"]);
749        m.set_control_map(&[
750            ButtonMatrixControl::CHECKABLE,
751            ButtonMatrixControl::DISABLED,
752            ButtonMatrixControl::HIDDEN,
753        ]);
754        assert!(
755            m.control(ButtonId(0))
756                .contains(ButtonMatrixControl::CHECKABLE)
757        );
758        assert!(
759            m.control(ButtonId(1))
760                .contains(ButtonMatrixControl::DISABLED)
761        );
762        assert!(m.control(ButtonId(2)).contains(ButtonMatrixControl::HIDDEN));
763    }
764
765    #[test]
766    fn one_checked_enforced_on_enable() {
767        let mut m = ButtonMatrix::new(rect(0, 0, 300, 100));
768        m.set_map(&["A", "B", "C"]);
769        // Pre-check multiple buttons
770        m.set_button_control(ButtonId(0), ButtonMatrixControl::CHECKED);
771        m.set_button_control(ButtonId(1), ButtonMatrixControl::CHECKED);
772        m.set_button_control(ButtonId(2), ButtonMatrixControl::CHECKED);
773        m.set_one_checked(true);
774        let checked_count = (0..3).filter(|&i| m.button_checked(ButtonId(i))).count();
775        assert_eq!(checked_count, 1);
776    }
777
778    #[test]
779    fn set_button_checked_respects_one_checked() {
780        let mut m = ButtonMatrix::new(rect(0, 0, 300, 100));
781        m.set_map(&["A", "B", "C"]);
782        m.set_all_controls(ButtonMatrixControl::CHECKABLE);
783        m.set_one_checked(true);
784        m.set_button_checked(ButtonId(0), true);
785        m.set_button_checked(ButtonId(1), true);
786        // Only button 1 should be checked now
787        assert!(!m.button_checked(ButtonId(0)));
788        assert!(m.button_checked(ButtonId(1)));
789    }
790
791    #[test]
792    fn navigate_next_skips_disabled() {
793        let mut m = ButtonMatrix::new(rect(0, 0, 300, 100));
794        m.set_map(&["A", "B", "C"]);
795        m.set_button_control(ButtonId(1), ButtonMatrixControl::DISABLED);
796        m.navigate_next(); // from NONE → 0 (A)
797        assert_eq!(m.selected_button(), ButtonId(0));
798        m.navigate_next(); // skip 1 (disabled) → 2 (C)
799        assert_eq!(m.selected_button(), ButtonId(2));
800    }
801
802    #[test]
803    fn navigate_prev_wraps() {
804        let mut m = ButtonMatrix::new(rect(0, 0, 300, 100));
805        m.set_map(&["A", "B", "C"]);
806        m.set_selected_button(ButtonId(0));
807        m.navigate_prev();
808        assert_eq!(m.selected_button(), ButtonId(2));
809    }
810
811    #[test]
812    fn activate_selected_toggles_checkable() {
813        let mut m = ButtonMatrix::new(rect(0, 0, 300, 100));
814        m.set_map(&["A"]);
815        m.set_button_control(ButtonId(0), ButtonMatrixControl::CHECKABLE);
816        m.set_selected_button(ButtonId(0));
817        m.activate_selected();
818        assert!(m.button_checked(ButtonId(0)));
819        m.activate_selected();
820        assert!(!m.button_checked(ButtonId(0)));
821    }
822
823    #[test]
824    fn press_release_inside_activates_button() {
825        let mut m = ButtonMatrix::new(rect(0, 0, 300, 60));
826        m.set_map(&["A", "B"]);
827        // Button A occupies x: 0..150, y: 0..60
828        assert!(m.handle_event(&Event::PressDown { x: 50, y: 30 }));
829        assert!(m.handle_event(&Event::PressRelease { x: 50, y: 30 }));
830        assert_eq!(m.selected_button(), ButtonId(0));
831    }
832
833    #[test]
834    fn press_outside_not_consumed() {
835        let mut m = ButtonMatrix::new(rect(0, 0, 300, 60));
836        m.set_map(&["A"]);
837        assert!(!m.handle_event(&Event::PressDown { x: 400, y: 30 }));
838    }
839
840    #[test]
841    fn disabled_button_not_pressed() {
842        let mut m = ButtonMatrix::new(rect(0, 0, 300, 60));
843        m.set_map(&["A"]);
844        m.set_button_control(ButtonId(0), ButtonMatrixControl::DISABLED);
845        assert!(!m.handle_event(&Event::PressDown { x: 10, y: 10 }));
846    }
847
848    #[test]
849    fn pointer_up_clears_pressed_id() {
850        let mut m = ButtonMatrix::new(rect(0, 0, 300, 60));
851        m.set_map(&["A"]);
852        m.handle_event(&Event::PressDown { x: 10, y: 10 });
853        assert_eq!(m.pressed_id, ButtonId(0));
854        m.handle_event(&Event::PointerUp { x: 10, y: 10 });
855        assert_eq!(m.pressed_id, BUTTON_NONE);
856    }
857
858    #[test]
859    fn set_bounds_adopted() {
860        let mut m = ButtonMatrix::new(rect(0, 0, 300, 60));
861        m.set_bounds(rect(10, 20, 400, 80));
862        assert_eq!(m.bounds(), rect(10, 20, 400, 80));
863    }
864
865    #[test]
866    fn draw_does_not_panic() {
867        let mut m = ButtonMatrix::new(rect(0, 0, 300, 60));
868        m.set_map(&["One", "Two", "\n", "Three"]);
869        let mut r = NullRenderer;
870        m.draw(&mut r);
871    }
872}