Skip to main content

lv_tui/widgets/
select.rs

1use crate::component::{Component, EventCx, MeasureCx};
2use crate::event::Event;
3use crate::geom::{Rect, Size};
4use crate::layout::Constraint;
5use crate::render::RenderCx;
6use crate::style::Style;
7
8/// A dropdown select widget.
9///
10/// Displays the currently selected option when collapsed ("▼"), and expands
11/// inline to show all options with keyboard navigation when activated.
12pub struct Select {
13    /// List of option strings
14    options: Vec<String>,
15    /// Currently selected index
16    selected: usize,
17    /// Whether the dropdown is expanded
18    expanded: bool,
19    /// Whether this widget currently has keyboard focus
20    focused: bool,
21    /// Current layout rect
22    rect: Rect,
23    /// Collapsed/default style
24    style: Style,
25    /// Selected/focused option style
26    selected_style: Style,
27}
28
29impl Select {
30    /// Creates an empty select.
31    pub fn new() -> Self {
32        Self {
33            options: Vec::new(),
34            selected: 0,
35            expanded: false,
36            focused: false,
37            rect: Rect::default(),
38            style: Style::default(),
39            selected_style: Style::default().bg(crate::style::Color::White).fg(crate::style::Color::Black),
40        }
41    }
42
43    /// Sets the option list.
44    pub fn options(mut self, options: Vec<String>) -> Self {
45        self.options = options;
46        self
47    }
48
49    /// Sets the collapsed/default style.
50    pub fn style(mut self, style: Style) -> Self {
51        self.style = style;
52        self
53    }
54
55    /// Sets the selected option style.
56    pub fn selected_style(mut self, style: Style) -> Self {
57        self.selected_style = style;
58        self
59    }
60
61    /// Returns the currently selected index.
62    pub fn selected(&self) -> usize {
63        self.selected
64    }
65
66    /// Returns the text of the currently selected option.
67    pub fn selected_text(&self) -> &str {
68        self.options.get(self.selected).map(|s| s.as_str()).unwrap_or("")
69    }
70
71    /// Sets the selected option.
72    pub fn set_selected(&mut self, index: usize, cx: &mut EventCx) {
73        if index < self.options.len() {
74            self.selected = index;
75            cx.invalidate_paint();
76        }
77    }
78}
79
80impl Component for Select {
81    fn render(&self, cx: &mut RenderCx) {
82        if self.options.is_empty() {
83            return;
84        }
85
86        // First line: selected option + indicator
87        let indicator = if self.expanded { "▲" } else { "▼" };
88        let display = format!("{} {}", self.selected_text(), indicator);
89        if self.focused {
90            cx.set_style(self.selected_style.clone());
91        } else {
92            cx.set_style(self.style.clone());
93        }
94        cx.line(&display);
95
96        // Expanded: show all options
97        if self.expanded {
98            for (i, opt) in self.options.iter().enumerate() {
99                if i == self.selected {
100                    cx.set_style(self.selected_style.clone());
101                    cx.text("❯ ");
102                } else {
103                    cx.set_style(self.style.clone());
104                    cx.text("  ");
105                }
106                cx.line(opt);
107            }
108        }
109    }
110
111    fn measure(&self, _constraint: Constraint, _cx: &mut MeasureCx) -> Size {
112        if self.options.is_empty() {
113            return Size { width: 0, height: 0 };
114        }
115
116        let max_w: u16 = self.options.iter()
117            .map(|o| o.chars().map(|c| unicode_width::UnicodeWidthChar::width(c).unwrap_or(0) as u16).sum::<u16>())
118            .max()
119            .unwrap_or(0)
120            + 2; // pointer/spacing
121
122        let height = if self.expanded {
123            1u16.saturating_add(self.options.len() as u16)
124        } else {
125            1
126        };
127
128        Size { width: max_w, height }
129    }
130
131    fn event(&mut self, event: &Event, cx: &mut EventCx) {
132        match event {
133            Event::Focus => {
134                self.focused = true;
135                cx.invalidate_paint();
136                return;
137            }
138            Event::Blur => {
139                self.focused = false;
140                self.expanded = false;
141                cx.invalidate_layout();
142                return;
143            }
144            _ => {}
145        }
146
147        if self.options.is_empty() { return; }
148
149        // Only handle key events during Target phase
150        if cx.phase() != crate::event::EventPhase::Target { return; }
151
152        if let Event::Key(key_event) = event {
153            match &key_event.key {
154                crate::event::Key::Enter | crate::event::Key::Char(' ') => {
155                    if self.expanded {
156                        self.expanded = false;
157                    } else {
158                        self.expanded = true;
159                    }
160                    cx.invalidate_layout();
161                    return;
162                }
163                crate::event::Key::Esc => {
164                    if self.expanded {
165                        self.expanded = false;
166                        cx.invalidate_layout();
167                    }
168                    return;
169                }
170                crate::event::Key::Up => {
171                    if self.expanded {
172                        if self.selected > 0 {
173                            self.selected -= 1;
174                        } else {
175                            self.selected = self.options.len() - 1;
176                        }
177                        cx.invalidate_paint();
178                    }
179                    return;
180                }
181                crate::event::Key::Down => {
182                    if self.expanded {
183                        if self.selected + 1 < self.options.len() {
184                            self.selected += 1;
185                        } else {
186                            self.selected = 0;
187                        }
188                        cx.invalidate_paint();
189                    }
190                    return;
191                }
192                _ => {}
193            }
194        }
195    }
196
197    fn layout(&mut self, rect: Rect, _cx: &mut crate::component::LayoutCx) {
198        self.rect = rect;
199    }
200
201    fn focusable(&self) -> bool {
202        true
203    }
204
205    fn style(&self) -> Style {
206        self.style.clone()
207    }
208}