Skip to main content

rlvgl_widgets/
list.rs

1//! Vertical scrolling list of selectable strings.
2use 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
10/// Scrollable list of selectable text items.
11pub struct List {
12    bounds: Rect,
13    /// Style used for list items.
14    pub style: Style,
15    /// Color for item text.
16    pub text_color: Color,
17    items: Vec<String>,
18    selected: Option<usize>,
19    /// Font assignment for this widget (FONT-00 §5); resolves to `FONT_6X10`
20    /// when unset.
21    font: WidgetFont,
22}
23
24impl List {
25    /// Create an empty list widget.
26    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    /// Append an item to the end of the list.
38    pub fn add_item(&mut self, text: impl Into<String>) {
39        self.items.push(text.into());
40    }
41
42    /// Replace all list items and clear the current selection.
43    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    /// Return a slice of all list items.
51    pub fn items(&self) -> &[String] {
52        &self.items
53    }
54
55    /// Index of the currently selected item, if any.
56    pub fn selected(&self) -> Option<usize> {
57        self.selected
58    }
59
60    /// Assign the font used to render this widget (FONT-00 §5); resolves to
61    /// `FONT_6X10` when unset.
62    pub fn set_font(&mut self, font: &'static dyn FontMetrics) {
63        self.font.set(font);
64    }
65
66    /// Translate a y coordinate into a list index.
67    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    /// Select an item when the pointer is released over it.
113    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}