1use alloc::{string::String, vec::Vec};
3use rlvgl_core::draw::draw_widget_bg;
4use rlvgl_core::event::Event;
5use rlvgl_core::font::{FontMetrics, WidgetFont, shape_text_ltr};
6use rlvgl_core::renderer::Renderer;
7use rlvgl_core::style::Style;
8use rlvgl_core::widget::{Color, Rect, Widget};
9
10pub struct List {
12 bounds: Rect,
13 pub style: Style,
15 pub text_color: Color,
17 items: Vec<String>,
18 selected: Option<usize>,
19 font: WidgetFont,
22}
23
24impl List {
25 pub fn new(bounds: Rect) -> Self {
27 Self {
28 bounds,
29 style: Style::default(),
30 text_color: Color(0, 0, 0, 255),
31 items: Vec::new(),
32 selected: None,
33 font: WidgetFont::new(),
34 }
35 }
36
37 pub fn add_item(&mut self, text: impl Into<String>) {
39 self.items.push(text.into());
40 }
41
42 pub fn set_items(&mut self, items: &[impl AsRef<str>]) {
44 self.items.clear();
45 self.items
46 .extend(items.iter().map(|item| String::from(item.as_ref())));
47 self.selected = None;
48 }
49
50 pub fn items(&self) -> &[String] {
52 &self.items
53 }
54
55 pub fn selected(&self) -> Option<usize> {
57 self.selected
58 }
59
60 pub fn set_font(&mut self, font: &'static dyn FontMetrics) {
63 self.font.set(font);
64 }
65
66 fn index_at(&self, y: i32) -> Option<usize> {
68 let row_height = 16;
69 if y < self.bounds.y || y >= self.bounds.y + self.bounds.height {
70 return None;
71 }
72 let idx = (y - self.bounds.y) / row_height;
73 if idx < 0 {
74 return None;
75 }
76 let idx = idx as usize;
77 if idx < self.items.len() {
78 Some(idx)
79 } else {
80 None
81 }
82 }
83}
84
85impl Widget for List {
86 fn bounds(&self) -> Rect {
87 self.bounds
88 }
89
90 fn widget_font_mut(&mut self) -> Option<&mut WidgetFont> {
91 Some(&mut self.font)
92 }
93
94 fn draw(&self, renderer: &mut dyn Renderer) {
95 let a = self.style.alpha;
96 draw_widget_bg(renderer, self.bounds, &self.style);
97 let font = self.font.resolve();
98 let row_height = 16;
99 for (i, item) in self.items.iter().enumerate() {
100 let y = self.bounds.y + (i as i32 * row_height);
101 let pos = (self.bounds.x + 2, y + row_height);
102 let color = if self.selected == Some(i) {
103 self.style.border_color
104 } else {
105 self.text_color
106 };
107 let shaped = shape_text_ltr(font, item, pos, 0);
108 renderer.draw_text_shaped(&shaped, (0, 0), color.with_alpha(a));
109 }
110 }
111
112 fn handle_event(&mut self, event: &Event) -> bool {
114 let Event::PressRelease { x, y } = event else {
115 return false;
116 };
117
118 if *x < self.bounds.x || *x >= self.bounds.x + self.bounds.width {
119 return false;
120 }
121
122 let Some(idx) = self.index_at(*y) else {
123 return false;
124 };
125
126 self.selected = Some(idx);
127 true
128 }
129}
130
131#[cfg(test)]
132mod tests {
133 use super::*;
134
135 #[test]
136 fn set_items_replaces_values_and_clears_selection() {
137 let mut list = List::new(Rect {
138 x: 0,
139 y: 0,
140 width: 100,
141 height: 48,
142 });
143 list.add_item("A");
144 list.add_item("B");
145 assert!(list.handle_event(&Event::PressRelease { x: 5, y: 20 }));
146 assert_eq!(list.selected(), Some(1));
147
148 list.set_items(&["C"]);
149
150 assert_eq!(list.items(), &["C"]);
151 assert_eq!(list.selected(), None);
152 }
153}