Skip to main content

rlvgl_ui/
select.rs

1// SPDX-License-Identifier: MIT
2//! Select component backed by the LVGL-parity dropdown widget.
3//!
4//! [`Select`] gives [`rlvgl_widgets::dropdown::Dropdown`] a common app-facing
5//! name, fluent option props, and an `on_change` hook.
6
7use alloc::boxed::Box;
8use rlvgl_core::{
9    event::Event,
10    font::{FontMetrics, WidgetFont},
11    renderer::Renderer,
12    widget::{Color, Rect, Widget},
13};
14use rlvgl_widgets::dropdown::Dropdown;
15
16use crate::theme::{ColorScheme, ComponentSize, Theme, Variant};
17
18pub use rlvgl_widgets::dropdown::DropdownDir as SelectDirection;
19
20/// Callback invoked when the selection changes, receiving `(index, text)`.
21type ChangeHandler = dyn FnMut(usize, &str);
22
23/// Closed-trigger + open-list select control.
24pub struct Select {
25    inner: Dropdown,
26    on_change: Option<Box<ChangeHandler>>,
27}
28
29impl Select {
30    /// Create an empty select control.
31    pub fn new(bounds: Rect) -> Self {
32        Self {
33            inner: Dropdown::new(bounds),
34            on_change: None,
35        }
36    }
37
38    /// Replace the option list and return the widget.
39    pub fn with_options(mut self, options: &[impl AsRef<str>]) -> Self {
40        self.inner.set_options(options);
41        self
42    }
43
44    /// Replace the option list.
45    pub fn set_options(&mut self, options: &[impl AsRef<str>]) {
46        self.inner.set_options(options);
47    }
48
49    /// Return the number of options.
50    pub fn option_count(&self) -> usize {
51        self.inner.option_count()
52    }
53
54    /// Return the currently selected option index.
55    pub fn selected(&self) -> usize {
56        self.inner.selected()
57    }
58
59    /// Set the selected option and return the widget.
60    pub fn with_selected_index(mut self, index: usize) -> Self {
61        self.inner.set_selected(index);
62        self
63    }
64
65    /// Set the selected option.
66    pub fn set_selected(&mut self, index: usize) {
67        self.inner.set_selected(index);
68    }
69
70    /// Return the selected option text, or `""` when empty.
71    pub fn selected_text(&self) -> &str {
72        self.inner.selected_text()
73    }
74
75    /// Register a callback fired when user interaction changes selection.
76    pub fn on_change<F: FnMut(usize, &str) + 'static>(mut self, handler: F) -> Self {
77        self.on_change = Some(Box::new(handler));
78        self
79    }
80
81    /// Open the option list.
82    pub fn open(&mut self) {
83        self.inner.open();
84    }
85
86    /// Close the option list.
87    pub fn close(&mut self) {
88        self.inner.close();
89    }
90
91    /// Toggle the option list.
92    pub fn toggle(&mut self) {
93        self.inner.toggle();
94    }
95
96    /// Return whether the option list is visible.
97    pub fn is_open(&self) -> bool {
98        self.inner.is_open()
99    }
100
101    /// Set the opening direction and return the widget.
102    pub fn direction(mut self, direction: SelectDirection) -> Self {
103        self.inner.set_dir(direction);
104        self
105    }
106
107    /// Return the opening direction.
108    pub fn direction_value(&self) -> SelectDirection {
109        self.inner.dir()
110    }
111
112    /// Set the opening direction.
113    pub fn set_direction(&mut self, direction: SelectDirection) {
114        self.inner.set_dir(direction);
115    }
116
117    /// Set an optional trigger symbol and return the widget.
118    pub fn symbol(mut self, symbol: Option<&str>) -> Self {
119        self.inner.set_symbol(symbol);
120        self
121    }
122
123    /// Return the trigger symbol, if any.
124    pub fn symbol_value(&self) -> Option<&str> {
125        self.inner.symbol()
126    }
127
128    /// Set an optional trigger symbol.
129    pub fn set_symbol(&mut self, symbol: Option<&str>) {
130        self.inner.set_symbol(symbol);
131    }
132
133    /// Enable or disable selected-item highlighting and return the widget.
134    pub fn selected_highlight(mut self, enable: bool) -> Self {
135        self.inner.set_selected_highlight(enable);
136        self
137    }
138
139    /// Return whether selected-item highlighting is enabled.
140    pub fn selected_highlight_value(&self) -> bool {
141        self.inner.selected_highlight()
142    }
143
144    /// Enable or disable selected-item highlighting.
145    pub fn set_selected_highlight(&mut self, enable: bool) {
146        self.inner.set_selected_highlight(enable);
147    }
148
149    /// Set the option list highlight color and return the widget.
150    pub fn selected_color(mut self, color: Color) -> Self {
151        self.inner.selected_color = color;
152        self
153    }
154
155    /// Set the option list highlight color.
156    pub fn set_selected_color(&mut self, color: Color) {
157        self.inner.selected_color = color;
158    }
159
160    /// Set select text color and return the widget.
161    pub fn text_color(mut self, color: Color) -> Self {
162        self.inner.text_color = color;
163        self
164    }
165
166    /// Set select text color.
167    pub fn set_text_color(&mut self, color: Color) {
168        self.inner.text_color = color;
169    }
170
171    /// Apply a themed trigger, item, text, and selected-row style.
172    pub fn themed(
173        mut self,
174        theme: &Theme,
175        scheme: ColorScheme,
176        variant: Variant,
177        size: ComponentSize,
178    ) -> Self {
179        let resolved = theme.component_style(scheme, variant, size);
180        let colors = theme.scheme(scheme);
181        self.inner.style = resolved.style;
182        self.inner.item_style = resolved.style;
183        self.inner.item_style.bg_color = match variant {
184            Variant::Solid => colors.muted,
185            Variant::Subtle => colors.subtle,
186            Variant::Outline | Variant::Ghost => theme.tokens.colors.background,
187        };
188        self.inner.text_color = resolved.text_color;
189        self.inner.selected_color = resolved.accent_color;
190        self
191    }
192
193    /// Assign the font used to render this select.
194    pub fn set_font(&mut self, font: &'static dyn FontMetrics) {
195        self.inner.set_font(font);
196    }
197
198    /// Immutable access to the trigger style.
199    pub fn style(&self) -> &rlvgl_core::style::Style {
200        &self.inner.style
201    }
202
203    /// Mutable access to the trigger style.
204    pub fn style_mut(&mut self) -> &mut rlvgl_core::style::Style {
205        &mut self.inner.style
206    }
207
208    /// Immutable access to the open-list item style.
209    pub fn item_style(&self) -> &rlvgl_core::style::Style {
210        &self.inner.item_style
211    }
212
213    /// Mutable access to the open-list item style.
214    pub fn item_style_mut(&mut self) -> &mut rlvgl_core::style::Style {
215        &mut self.inner.item_style
216    }
217}
218
219impl Widget for Select {
220    fn bounds(&self) -> Rect {
221        self.inner.bounds()
222    }
223
224    fn widget_font_mut(&mut self) -> Option<&mut WidgetFont> {
225        self.inner.widget_font_mut()
226    }
227
228    fn set_bounds(&mut self, bounds: Rect) {
229        self.inner.set_bounds(bounds);
230    }
231
232    fn draw(&self, renderer: &mut dyn Renderer) {
233        self.inner.draw(renderer);
234    }
235
236    fn handle_event(&mut self, event: &Event) -> bool {
237        let before = self.inner.selected();
238        let handled = self.inner.handle_event(event);
239        let after = self.inner.selected();
240        if handled
241            && before != after
242            && let Some(cb) = self.on_change.as_mut()
243        {
244            cb(after, self.inner.selected_text());
245        }
246        handled
247    }
248}
249
250#[cfg(test)]
251mod tests {
252    use super::*;
253    use alloc::rc::Rc;
254    use core::cell::Cell;
255
256    #[test]
257    fn select_options_and_callback_work() {
258        let selected = Rc::new(Cell::new(0usize));
259        let flag = selected.clone();
260        let mut select = Select::new(rect(0, 0, 120, 80))
261            .with_options(&["A", "B"])
262            .on_change(move |idx, _| flag.set(idx));
263
264        select.open();
265        assert!(select.handle_event(&Event::PressRelease { x: 5, y: 38 }));
266
267        assert_eq!(selected.get(), 1);
268        assert_eq!(select.selected_text(), "B");
269    }
270
271    #[test]
272    fn select_themed_sets_trigger_and_selected_color() {
273        let theme = Theme::material_light();
274        let select = Select::new(rect(0, 0, 120, 80)).themed(
275            &theme,
276            ColorScheme::Primary,
277            Variant::Subtle,
278            ComponentSize::Md,
279        );
280
281        assert_eq!(
282            select.item_style().bg_color,
283            theme.scheme(ColorScheme::Primary).subtle
284        );
285        assert_eq!(select.style().radius, theme.tokens.radii.md);
286    }
287
288    fn rect(x: i32, y: i32, width: i32, height: i32) -> Rect {
289        Rect {
290            x,
291            y,
292            width,
293            height,
294        }
295    }
296}