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