1use crossterm::event::KeyCode;
2
3use crate::{
4 Component,
5 Event,
6 Focusable,
7 InputResult,
8 RenderError,
9 Rendered,
10 theme::{
11 Palette,
12 Style,
13 Theme,
14 stylize,
15 },
16};
17
18pub struct SelectList {
23 items: Vec<String>,
24 selected: usize,
25 max_visible: usize,
26 scroll: usize,
27 focused: bool,
28}
29
30impl SelectList {
31 pub fn new(items: Vec<String>, max_visible: usize) -> Self {
33 Self {
34 items,
35 selected: 0,
36 max_visible,
37 scroll: 0,
38 focused: false,
39 }
40 }
41
42 pub fn selected(&self) -> usize {
44 self.selected
45 }
46
47 pub fn set_selected(&mut self, index: usize) {
49 self.selected = index.min(self.items.len().saturating_sub(1));
50 self.scroll = self
51 .selected
52 .saturating_sub(self.max_visible.saturating_sub(1));
53 }
54
55 pub fn selected_item(&self) -> Option<&str> {
57 self.items.get(self.selected).map(|s| s.as_str())
58 }
59}
60
61impl Focusable for SelectList {
62 fn focused(&self) -> bool {
63 self.focused
64 }
65
66 fn set_focused(&mut self, focused: bool) {
67 self.focused = focused;
68 }
69}
70
71impl Component for SelectList {
72 fn render(&self, width: u16) -> Result<Rendered, RenderError> {
73 let theme = Theme::current();
74 let accent_style = Style::new().fg(theme.accent()).bold();
75 let primary_style = Style::new().fg(theme.text_primary());
76 let dim_style = Style::new().fg(theme.text_secondary());
77
78 let mut lines = Vec::new();
79 let visible_end = (self.scroll + self.max_visible).min(self.items.len());
80 for i in self.scroll..visible_end {
81 let is_selected = i == self.selected;
82 let style = if is_selected && self.focused {
83 &accent_style
84 } else if is_selected {
85 &primary_style
86 } else {
87 &dim_style
88 };
89
90 let prefix = if is_selected { "> " } else { " " };
91 let line = stylize(&format!("{}{}", prefix, self.items[i]), style);
92 lines.push(crate::utils::truncate_to_width(&line, width, "…"));
93 }
94 Ok(Rendered {
95 lines,
96 cursor: None,
97 images: Vec::new(),
98 })
99 }
100
101 fn handle_input(&mut self, event: &Event) -> InputResult {
102 use crossterm::event::KeyModifiers;
103 if let Event::Key(key) = event {
104 match key.code {
105 | KeyCode::Down => {
106 if self.selected + 1 < self.items.len() {
107 self.selected += 1;
108 if self.selected >= self.scroll + self.max_visible {
109 self.scroll += 1;
110 }
111 }
112 InputResult::Handled
113 },
114 | KeyCode::Up => {
115 if self.selected > 0 {
116 self.selected -= 1;
117 if self.selected < self.scroll {
118 self.scroll = self.scroll.saturating_sub(1);
119 }
120 }
121 InputResult::Handled
122 },
123 | KeyCode::Char('j') if !key.modifiers.contains(KeyModifiers::CONTROL) => {
124 if self.selected + 1 < self.items.len() {
125 self.selected += 1;
126 if self.selected >= self.scroll + self.max_visible {
127 self.scroll += 1;
128 }
129 }
130 InputResult::Handled
131 },
132 | KeyCode::Char('k') if !key.modifiers.contains(KeyModifiers::CONTROL) => {
133 if self.selected > 0 {
134 self.selected -= 1;
135 if self.selected < self.scroll {
136 self.scroll = self.scroll.saturating_sub(1);
137 }
138 }
139 InputResult::Handled
140 },
141 | KeyCode::Enter => InputResult::RequestRender,
142 | _ => InputResult::Ignored,
143 }
144 } else {
145 InputResult::Ignored
146 }
147 }
148
149 fn as_focusable(&self) -> Option<&dyn Focusable> {
150 Some(self)
151 }
152
153 fn as_focusable_mut(&mut self) -> Option<&mut dyn Focusable> {
154 Some(self)
155 }
156}
157
158#[cfg(test)]
159mod tests {
160 use crossterm::event::KeyCode;
161
162 use super::*;
163
164 #[test]
165 fn select_list_renders() {
166 let list = SelectList::new(vec!["a".into(), "b".into()], 10);
167 let r = list.render(10).unwrap();
168 assert_eq!(r.lines.len(), 2);
169 assert!(r.lines[0].contains("> "));
170 }
171
172 #[test]
173 fn select_list_navigation() {
174 let mut list = SelectList::new(vec!["a".into(), "b".into(), "c".into()], 2);
175 list.set_focused(true);
176 list.handle_input(&Event::Key(crossterm::event::KeyEvent::new(
177 KeyCode::Down,
178 crossterm::event::KeyModifiers::empty(),
179 )));
180 assert_eq!(list.selected(), 1);
181 list.handle_input(&Event::Key(crossterm::event::KeyEvent::new(
182 KeyCode::Down,
183 crossterm::event::KeyModifiers::empty(),
184 )));
185 assert_eq!(list.selected(), 2);
186 assert_eq!(list.scroll, 1);
187 }
188
189 #[test]
190 fn select_list_scroll_up() {
191 let mut list = SelectList::new(vec!["a".into(), "b".into(), "c".into()], 2);
192 list.set_focused(true);
193 list.handle_input(&Event::Key(crossterm::event::KeyEvent::new(
194 KeyCode::Down,
195 crossterm::event::KeyModifiers::empty(),
196 )));
197 list.handle_input(&Event::Key(crossterm::event::KeyEvent::new(
198 KeyCode::Down,
199 crossterm::event::KeyModifiers::empty(),
200 )));
201 assert_eq!(list.scroll, 1);
202 list.handle_input(&Event::Key(crossterm::event::KeyEvent::new(
203 KeyCode::Up,
204 crossterm::event::KeyModifiers::empty(),
205 )));
206 assert_eq!(list.scroll, 1);
208 list.handle_input(&Event::Key(crossterm::event::KeyEvent::new(
209 KeyCode::Up,
210 crossterm::event::KeyModifiers::empty(),
211 )));
212 assert_eq!(list.scroll, 0);
213 }
214
215 #[test]
216 fn select_list_j_k_navigation() {
217 let mut list = SelectList::new(vec!["a".into(), "b".into(), "c".into()], 2);
218 list.set_focused(true);
219 list.handle_input(&Event::Key(crossterm::event::KeyEvent::new(
220 KeyCode::Char('j'),
221 crossterm::event::KeyModifiers::empty(),
222 )));
223 assert_eq!(list.selected(), 1);
224 list.handle_input(&Event::Key(crossterm::event::KeyEvent::new(
225 KeyCode::Char('k'),
226 crossterm::event::KeyModifiers::empty(),
227 )));
228 assert_eq!(list.selected(), 0);
229 }
230
231 #[test]
232 fn select_list_j_scrolls() {
233 let mut list = SelectList::new(vec!["a".into(), "b".into(), "c".into()], 2);
234 list.set_focused(true);
235 list.handle_input(&Event::Key(crossterm::event::KeyEvent::new(
236 KeyCode::Char('j'),
237 crossterm::event::KeyModifiers::empty(),
238 )));
239 list.handle_input(&Event::Key(crossterm::event::KeyEvent::new(
240 KeyCode::Char('j'),
241 crossterm::event::KeyModifiers::empty(),
242 )));
243 assert_eq!(list.selected(), 2);
244 assert_eq!(list.scroll, 1);
245 }
246}