1use 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30pub struct ButtonId(pub u16);
31
32pub const BUTTON_NONE: ButtonId = ButtonId(u16::MAX);
34
35#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
43pub struct ButtonMatrixControl(pub u32);
44
45impl ButtonMatrixControl {
46 pub const HIDDEN: Self = Self(1 << 0);
48 pub const DISABLED: Self = Self(1 << 1);
50 pub const INACTIVE: Self = Self(1 << 2);
52 pub const CHECKABLE: Self = Self(1 << 3);
54 pub const CHECKED: Self = Self(1 << 4);
56 pub const CLICK_TRIGGER: Self = Self(1 << 5);
58 pub const NO_REPEAT: Self = Self(1 << 6);
60 pub const POPOVER: Self = Self(1 << 7);
62 pub const RECOLOR: Self = Self(1 << 8);
64 pub const CUSTOM1: Self = Self(1 << 9);
66 pub const CUSTOM2: Self = Self(1 << 10);
68
69 pub fn contains(self, flag: Self) -> bool {
71 self.0 & flag.0 != 0
72 }
73
74 pub fn insert(&mut self, flag: Self) {
76 self.0 |= flag.0;
77 }
78
79 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
98pub struct ButtonMatrixButton {
100 pub label: String,
102 pub control: ButtonMatrixControl,
104 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
135pub struct ButtonMatrix {
147 bounds: Rect,
148 rows: Vec<Vec<ButtonMatrixButton>>,
149 selected: ButtonId,
150 pressed_id: ButtonId,
151 one_checked: bool,
152 pub style: Style,
154 pub text_color: Color,
156 pub button_color: Color,
158 pub pressed_color: Color,
160 pub checked_color: Color,
162 pub disabled_color: Color,
164 font: WidgetFont,
167}
168
169impl ButtonMatrix {
170 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 pub fn set_font(&mut self, font: &'static dyn FontMetrics) {
193 self.font.set(font);
194 }
195
196 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 if !current_row.is_empty() || (!map.is_empty() && map.last() != Some(&"\n")) {
217 self.rows.push(current_row);
218 }
219 }
220
221 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 pub fn button_text(&self, id: ButtonId) -> Option<&str> {
247 self.flat_button(id).map(|b| b.label.as_str())
248 }
249
250 pub fn button_count(&self) -> usize {
252 self.rows.iter().map(|r| r.len()).sum()
253 }
254
255 pub fn buttons(&self) -> Vec<&ButtonMatrixButton> {
257 self.rows.iter().flat_map(|r| r.iter()).collect()
258 }
259
260 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 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 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 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 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 pub fn control(&self, id: ButtonId) -> ButtonMatrixControl {
313 self.flat_button(id).map(|b| b.control).unwrap_or_default()
314 }
315
316 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 pub fn button_width(&self, id: ButtonId) -> u8 {
325 self.flat_button(id).map(|b| b.width).unwrap_or(1)
326 }
327
328 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 pub fn selected_button(&self) -> ButtonId {
341 self.selected
342 }
343
344 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 pub fn one_checked(&self) -> bool {
370 self.one_checked
371 }
372
373 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 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 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 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 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 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 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 fn row_rect(&self, r: usize) -> Rect {
491 let rh = self.row_height();
492 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 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 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 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); assert_eq!(m.button_width(ButtonId(0)), 1);
705 m.set_button_width(ButtonId(0), 20); 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 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 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(); assert_eq!(m.selected_button(), ButtonId(0));
798 m.navigate_next(); 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 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}