Skip to main content

matrix_gui/widgets/
listbox.rs

1//! List box widget for displaying scrollable text lists.
2//!
3//! This module provides a list box widget that displays a list of
4//! text items with support for scrolling, selection, and customizable
5//! styling.
6
7use embedded_graphics::{draw_target::DrawTarget, geometry::Point, prelude::PixelColor};
8
9use crate::prelude::*;
10
11/// List box widget for displaying scrollable text lists.
12///
13/// This widget displays a scrollable list of text items with support
14/// for selection highlighting. The list can have customizable font,
15/// alignment, colors, line spacing, padding, and border.
16///
17/// # Type Parameters
18///
19/// * `'a` - The lifetime of the items array reference
20/// * `ID` - The widget ID type implementing [`WidgetId`]
21/// * `COL` - The pixel color type implementing [`PixelColor`]
22pub struct ListBox<'a, ID, COL: PixelColor> {
23    /// The region defining the list box's position and size.
24    region: &'a Region<ID>,
25    /// Array of text items to display.
26    items: &'a [&'a str],
27    /// The index of the first item to display (for scrolling).
28    start_item: u16,
29    /// Optional font for rendering the text.
30    font: OptionFont<'a>,
31    /// Horizontal alignment for the text.
32    align: HorizontalAlign,
33    /// Optional color for the text.
34    color: OptionColor<COL>,
35    /// Spacing between lines (in pixels).
36    line_spacing: u8,
37    /// Inner padding around the text content (in pixels).
38    padding_inner: u8,
39    /// Outer padding around the entire widget (in pixels).
40    padding_outer: u8,
41    /// Border width (in pixels, 0 for no border).
42    border_width: u8,
43    /// Index of the selected item (0-based, -1 for no selection).
44    selected_item: i32,
45}
46
47impl<'a, ID: WidgetId, COL: PixelColor> ListBox<'a, ID, COL> {
48    pub const fn new(region: &'a Region<ID>, items: &'a [&'a str]) -> ListBox<'a, ID, COL> {
49        ListBox {
50            region,
51            items,
52            start_item: 0,
53            font: OptionFont::none(),
54            align: HorizontalAlign::Left,
55            color: OptionColor::none(),
56            line_spacing: 2,
57            padding_inner: 1,
58            padding_outer: 0,
59            border_width: 1,
60            selected_item: -1,
61        }
62    }
63
64    pub const fn with_start(mut self, start_item: u16) -> Self {
65        self.start_item = start_item;
66        self
67    }
68
69    pub const fn with_font(mut self, font: UiFont<'a>) -> Self {
70        self.font.set_font(font);
71        self
72    }
73
74    pub const fn with_align(mut self, align: HorizontalAlign) -> Self {
75        self.align = align;
76        self
77    }
78
79    pub const fn with_color(mut self, color: COL) -> Self {
80        self.color.set_color(color);
81        self
82    }
83
84    pub const fn with_line_spacing(mut self, spacing: u8) -> Self {
85        self.line_spacing = spacing;
86        self
87    }
88
89    pub const fn with_padding(mut self, inner: u8, outer: u8) -> Self {
90        self.padding_inner = inner;
91        self.padding_outer = outer;
92        self
93    }
94
95    pub const fn with_border(mut self, border_width: u8) -> Self {
96        self.border_width = border_width;
97        self
98    }
99
100    pub const fn with_selected_item(mut self, selected_item: i32) -> Self {
101        self.selected_item = selected_item;
102        self
103    }
104}
105
106impl<DRAW: DrawTarget<Color = COL>, ID: WidgetId, COL: PixelColor> Widget<DRAW, COL>
107    for ListBox<'_, ID, COL>
108{
109    fn draw(&mut self, ui: &mut Ui<DRAW, COL>) -> GuiResult<Response> {
110        let widget_state = ui.get_widget_state(self.region.id())?;
111        if widget_state.compare_set(RenderStatus::Rendered) {
112            return Ok(Response::Idle);
113        }
114
115        let mut area = self.region.rectangle();
116        if self.padding_outer > 0 {
117            area = area.offset(-(self.padding_outer as i32));
118        }
119
120        let font = self.font.font(ui.style());
121        let color = self.color.text_color(ui.style());
122        let mut rect_style = PrimitiveStyleBuilder::new()
123            .stroke_color(ui.style().border_color)
124            .stroke_width(self.border_width as u32)
125            .build();
126
127        ui.clear_area(&area)?;
128        if self.border_width > 0 {
129            ui.draw(&area.into_styled(rect_style)).ok();
130        }
131        if self.padding_inner > 0 {
132            area = area.offset(-(self.padding_inner as i32));
133        }
134
135        let start_point = area.top_left;
136        let mut y_offset = start_point.y;
137
138        for (index, item) in self.items.iter().enumerate() {
139            if index + 1 < self.start_item as usize {
140                continue;
141            }
142            let mut text = matrix_utils::make_text(*item, font, color);
143            let character_line_height = text.character_style.line_height;
144            let line_height = (character_line_height + self.line_spacing) as i32;
145            text.position = Point::new(start_point.x, y_offset);
146
147            ui.draw(&text)?;
148
149            if self.selected_item > 0 && self.selected_item as usize == index + 1 {
150                rect_style.stroke_width = 1;
151                let rect = Rectangle::new(
152                    text.position,
153                    Size::new(area.size.width as u32, character_line_height as u32),
154                );
155                ui.draw(&rect.into_styled(rect_style)).ok();
156            }
157
158            y_offset += line_height;
159            if y_offset - start_point.y > area.size.height as i32 {
160                break;
161            }
162        }
163
164        Ok(Response::Idle)
165    }
166}