1use std::io::Write;
2
3use crossterm::{
4 cursor::MoveTo,
5 queue,
6 style::{Color, Print, ResetColor, SetForegroundColor},
7};
8use rand::Rng;
9
10use crate::box_chrome;
11use crate::select_option::SelectOption;
12use crate::{Component, Renderer};
13
14use super::layout::{
15 MAX_VISIBLE_OPTIONS, SelectorLayout, render_partial_border, render_selector_items,
16 selector_border,
17};
18
19const QUESTION_REVEAL_GATE: f64 = 0.5;
21
22#[derive(Clone, Copy, Debug, PartialEq, Eq)]
24pub enum ListNavigate {
25 Up,
26 Down,
27}
28
29#[derive(Debug, Clone)]
31pub struct SelectorComponent {
32 pub options: Vec<SelectOption>,
33 pub selected: usize,
34 pub scroll_offset: usize,
35 pub visible: bool,
36 pub open: f64,
37 pub reveal: f64,
38}
39
40impl SelectorComponent {
41 pub fn hidden() -> Self {
42 Self {
43 options: Vec::new(),
44 selected: 0,
45 scroll_offset: 0,
46 visible: false,
47 open: 0.0,
48 reveal: 0.0,
49 }
50 }
51
52 pub fn from_options(options: Vec<SelectOption>, default_index: usize) -> Self {
53 Self {
54 options,
55 selected: default_index,
56 scroll_offset: 0,
57 visible: true,
58 open: 0.0,
59 reveal: 0.0,
60 }
61 }
62
63 pub fn is_fully_open(&self) -> bool {
64 self.open >= 1.0
65 }
66
67 pub fn is_fully_closed(&self) -> bool {
68 self.open <= 0.0
69 }
70
71 pub fn is_fully_revealed(&self) -> bool {
72 self.reveal >= 1.0
73 }
74
75 pub fn is_fully_hidden(&self) -> bool {
76 self.reveal <= 0.0
77 }
78
79 pub fn viewport_row_count(option_count: usize) -> usize {
81 option_count.min(MAX_VISIBLE_OPTIONS)
82 }
83
84 pub fn navigate(&mut self, direction: ListNavigate) {
85 let len = self.options.len();
86 if len == 0 {
87 return;
88 }
89
90 match direction {
91 ListNavigate::Up => {
92 self.selected = if self.selected == 0 {
93 len - 1
94 } else {
95 self.selected - 1
96 };
97 }
98 ListNavigate::Down => {
99 self.selected = (self.selected + 1) % len;
100 }
101 }
102
103 let max_visible = Self::viewport_row_count(len);
104 if self.selected < self.scroll_offset {
105 self.scroll_offset = self.selected;
106 } else if self.selected >= self.scroll_offset + max_visible {
107 self.scroll_offset = self.selected + 1 - max_visible;
108 }
109
110 if self.selected == 0 {
111 self.scroll_offset = 0;
112 } else if self.selected == len - 1 {
113 self.scroll_offset = len.saturating_sub(max_visible);
114 }
115 }
116
117 pub fn tick_decode(&mut self, question_reveal: f64, box_open_speed: f64, reveal_speed: f64) {
118 if !self.visible || question_reveal < QUESTION_REVEAL_GATE {
119 return;
120 }
121 self.open = (self.open + box_open_speed).min(1.0);
122 if self.open >= 1.0 {
123 self.reveal = (self.reveal + reveal_speed).min(1.0);
124 }
125 }
126
127 pub fn tick_transition_encode(
128 &mut self,
129 next_has_selector: bool,
130 encode_speed: f64,
131 morph_speed: f64,
132 ) {
133 if !self.visible {
134 return;
135 }
136 self.reveal = (self.reveal - encode_speed).max(0.0);
137 if !next_has_selector && self.is_fully_hidden() {
138 self.open = (self.open - morph_speed).max(0.0);
139 }
140 }
141
142 pub fn tick_transition_decode(&mut self, encode_speed: f64, morph_speed: f64) {
143 if !self.visible {
144 return;
145 }
146 if self.open < 1.0 {
147 self.open = (self.open + morph_speed).min(1.0);
148 } else {
149 self.reveal = (self.reveal + encode_speed).min(1.0);
150 }
151 }
152
153 pub fn tick_exit_close(&mut self, encode_speed: f64, box_close_speed: f64) {
154 if !self.visible {
155 return;
156 }
157 self.reveal = (self.reveal - encode_speed).max(0.0);
158 if self.reveal <= 0.0 {
159 self.open = (self.open - box_close_speed).max(0.0);
160 }
161 }
162}
163
164#[cfg(not(tarpaulin_include))]
165impl SelectorComponent {
166 fn render_fully_open<W: Write, R: Rng>(
167 &self,
168 writer: &mut W,
169 layout: &SelectorLayout,
170 start_row: u16,
171 rng: &mut R,
172 ) -> std::io::Result<u16> {
173 let has_scroll_up = self.scroll_offset > 0;
174 let has_scroll_down = self.scroll_offset + layout.max_visible < self.options.len();
175 let mut row = start_row;
176
177 let top = selector_border(layout.inner_width, has_scroll_up, true);
178 queue!(
179 writer,
180 MoveTo(layout.col, row),
181 SetForegroundColor(Color::DarkGrey),
182 Print(&top),
183 ResetColor
184 )?;
185 row += 1;
186
187 render_selector_items(
188 writer,
189 self,
190 layout,
191 row,
192 has_scroll_up,
193 has_scroll_down,
194 rng,
195 )?;
196 row += layout.max_visible as u16;
197
198 let bottom = selector_border(layout.inner_width, has_scroll_down, false);
199 queue!(
200 writer,
201 MoveTo(layout.col, row),
202 SetForegroundColor(Color::DarkGrey),
203 Print(&bottom),
204 ResetColor
205 )?;
206
207 Ok((layout.max_visible as u16).saturating_add(box_chrome::BORDER_ROWS))
208 }
209}
210
211#[cfg(not(tarpaulin_include))]
212impl Component for SelectorComponent {
213 fn render<W: Write>(&self, renderer: &mut Renderer<W>) -> std::io::Result<u16> {
214 if self.options.is_empty() {
215 return Ok(0);
216 }
217 renderer.with_panel(|writer, panel, rng| {
218 let layout = SelectorLayout::compute(self, panel.width);
219
220 if self.open < 1.0 {
221 return render_partial_border(writer, self.open, &layout, panel.row).map(|n| n as u16);
222 }
223
224 self.render_fully_open(writer, &layout, panel.row, rng)
225 })
226 }
227}
228
229#[cfg(test)]
230mod tests {
231 use super::*;
232 use crate::test_helpers::select_option as option;
233
234 #[test]
235 fn hidden_is_invisible_with_empty_options() {
236 let s = SelectorComponent::hidden();
237 assert!(!s.visible);
238 assert!(s.options.is_empty());
239 assert_eq!(s.open, 0.0);
240 assert_eq!(s.reveal, 0.0);
241 }
242
243 #[test]
244 fn tick_decode_noop_when_invisible() {
245 let mut s = SelectorComponent::hidden();
246 s.tick_decode(1.0, 0.1, 0.1);
247 assert_eq!(s.open, 0.0);
248 }
249
250 #[test]
251 fn tick_decode_noop_below_question_gate() {
252 let mut s = SelectorComponent::from_options(vec![option("a", "a")], 0);
253 s.tick_decode(0.49, 0.1, 0.1);
254 assert_eq!(s.open, 0.0);
255 }
256
257 #[test]
258 fn tick_decode_opens_then_reveals() {
259 let mut s = SelectorComponent::from_options(vec![option("a", "a")], 0);
260
261 s.tick_decode(0.6, 0.5, 0.3);
262 assert!(s.open > 0.0);
263 assert_eq!(s.reveal, 0.0);
264
265 s.open = 1.0;
266 s.tick_decode(0.6, 0.5, 0.3);
267 assert!(s.reveal > 0.0);
268 }
269
270 #[test]
271 fn tick_decode_clamps_at_one() {
272 let mut s = SelectorComponent::from_options(vec![option("a", "a")], 0);
273 s.open = 1.0;
274 s.tick_decode(1.0, 1.0, 2.0);
275 assert_eq!(s.open, 1.0);
276 assert_eq!(s.reveal, 1.0);
277 }
278
279 #[test]
280 fn tick_transition_encode_noop_when_invisible() {
281 let mut s = SelectorComponent::hidden();
282 s.tick_transition_encode(true, 0.1, 0.1);
283 assert_eq!(s.reveal, 0.0);
284 }
285
286 #[test]
287 fn tick_transition_encode_fades_reveal_then_closes() {
288 let mut s = SelectorComponent::from_options(vec![option("a", "a")], 0);
289 s.open = 1.0;
290 s.reveal = 1.0;
291
292 s.tick_transition_encode(false, 0.5, 0.0);
293 assert!(s.reveal < 1.0);
294
295 s.reveal = 0.0;
296 s.tick_transition_encode(false, 0.5, 0.4);
297 assert!(s.open < 1.0);
298 }
299
300 #[test]
301 fn tick_transition_encode_keeps_box_when_next_has_selector() {
302 let mut s = SelectorComponent::from_options(vec![option("a", "a")], 0);
303 s.open = 1.0;
304 s.reveal = 0.0;
305
306 s.tick_transition_encode(true, 0.5, 0.5);
307 assert_eq!(s.open, 1.0);
308 }
309
310 #[test]
311 fn tick_transition_decode_noop_when_invisible() {
312 let mut s = SelectorComponent::hidden();
313 s.tick_transition_decode(0.1, 0.1);
314 assert_eq!(s.open, 0.0);
315 }
316
317 #[test]
318 fn tick_transition_decode_opens_then_reveals() {
319 let mut s = SelectorComponent::from_options(vec![option("a", "a")], 0);
320 s.open = 0.0;
321
322 s.tick_transition_decode(0.3, 0.5);
323 assert!(s.open > 0.0);
324 assert_eq!(s.reveal, 0.0);
325
326 s.open = 1.0;
327 s.tick_transition_decode(0.3, 0.5);
328 assert!(s.reveal > 0.0);
329 }
330
331 #[test]
332 fn tick_exit_close_noop_when_invisible() {
333 let mut s = SelectorComponent::hidden();
334 s.tick_exit_close(0.1, 0.1);
335 assert_eq!(s.open, 0.0);
336 }
337
338 #[test]
339 fn tick_exit_close_fades_reveal_then_closes_box() {
340 let mut s = SelectorComponent::from_options(vec![option("a", "a")], 0);
341 s.open = 1.0;
342 s.reveal = 1.0;
343
344 s.tick_exit_close(0.5, 0.5);
345 assert!(s.reveal < 1.0);
346 assert_eq!(s.open, 1.0);
347
348 s.reveal = 0.0;
349 s.tick_exit_close(0.5, 0.5);
350 assert!(s.open < 1.0);
351 }
352
353 #[test]
354 fn viewport_row_count_matches_render_cap() {
355 assert_eq!(SelectorComponent::viewport_row_count(0), 0);
356 assert_eq!(SelectorComponent::viewport_row_count(5), 5);
357 assert_eq!(SelectorComponent::viewport_row_count(8), 8);
358 assert_eq!(
359 SelectorComponent::viewport_row_count(200),
360 MAX_VISIBLE_OPTIONS
361 );
362 }
363
364 #[test]
365 fn is_fully_thresholds() {
366 let mut s = SelectorComponent::from_options(vec![option("a", "a")], 0);
367 s.open = 1.0;
368 assert!(s.is_fully_open());
369 s.open = 0.999;
370 assert!(!s.is_fully_open());
371 s.open = 0.0;
372 assert!(s.is_fully_closed());
373 s.open = -0.1;
374 assert!(s.is_fully_closed());
375
376 s.reveal = 1.0;
377 assert!(s.is_fully_revealed());
378 s.reveal = 0.999;
379 assert!(!s.is_fully_revealed());
380 s.reveal = 0.0;
381 assert!(s.is_fully_hidden());
382 s.reveal = 0.01;
383 assert!(!s.is_fully_hidden());
384 }
385
386 #[test]
387 fn navigate_down_increments_selection() {
388 let mut sel = SelectorComponent::from_options(
389 vec![option("A", "a"), option("B", "b"), option("C", "c")],
390 0,
391 );
392 sel.navigate(ListNavigate::Down);
393 assert_eq!(sel.selected, 1);
394 }
395
396 #[test]
397 fn navigate_down_wraps_to_first() {
398 let mut sel = SelectorComponent::from_options(vec![option("A", "a"), option("B", "b")], 1);
399 sel.navigate(ListNavigate::Down);
400 assert_eq!(sel.selected, 0);
401 }
402
403 #[test]
404 fn navigate_up_decrements_selection() {
405 let mut sel = SelectorComponent::from_options(
406 vec![option("A", "a"), option("B", "b"), option("C", "c")],
407 2,
408 );
409 sel.navigate(ListNavigate::Up);
410 assert_eq!(sel.selected, 1);
411 }
412
413 #[test]
414 fn navigate_up_wraps_to_last() {
415 let mut sel = SelectorComponent::from_options(
416 vec![option("A", "a"), option("B", "b"), option("C", "c")],
417 0,
418 );
419 sel.navigate(ListNavigate::Up);
420 assert_eq!(sel.selected, 2);
421 }
422
423 #[test]
424 fn navigate_empty_options_is_noop() {
425 let mut sel = SelectorComponent::hidden();
426 sel.navigate(ListNavigate::Down);
427 assert_eq!(sel.selected, 0);
428 }
429}