use std::io::Write;
use crossterm::{
cursor::MoveTo,
queue,
style::{Color, Print, ResetColor, SetForegroundColor},
};
use rand::Rng;
use crate::box_chrome;
use crate::select_option::SelectOption;
use crate::{Component, Renderer};
use super::layout::{
MAX_VISIBLE_OPTIONS, SelectorLayout, render_partial_border, render_selector_items,
selector_border,
};
const QUESTION_REVEAL_GATE: f64 = 0.5;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ListNavigate {
Up,
Down,
}
#[derive(Debug, Clone)]
pub struct SelectorComponent {
pub options: Vec<SelectOption>,
pub selected: usize,
pub scroll_offset: usize,
pub visible: bool,
pub open: f64,
pub reveal: f64,
}
impl SelectorComponent {
pub fn hidden() -> Self {
Self {
options: Vec::new(),
selected: 0,
scroll_offset: 0,
visible: false,
open: 0.0,
reveal: 0.0,
}
}
pub fn from_options(options: Vec<SelectOption>, default_index: usize) -> Self {
Self {
options,
selected: default_index,
scroll_offset: 0,
visible: true,
open: 0.0,
reveal: 0.0,
}
}
pub fn is_fully_open(&self) -> bool {
self.open >= 1.0
}
pub fn is_fully_closed(&self) -> bool {
self.open <= 0.0
}
pub fn is_fully_revealed(&self) -> bool {
self.reveal >= 1.0
}
pub fn is_fully_hidden(&self) -> bool {
self.reveal <= 0.0
}
pub fn viewport_row_count(option_count: usize) -> usize {
option_count.min(MAX_VISIBLE_OPTIONS)
}
pub fn navigate(&mut self, direction: ListNavigate) {
let len = self.options.len();
if len == 0 {
return;
}
match direction {
ListNavigate::Up => {
self.selected = if self.selected == 0 {
len - 1
} else {
self.selected - 1
};
}
ListNavigate::Down => {
self.selected = (self.selected + 1) % len;
}
}
let max_visible = Self::viewport_row_count(len);
if self.selected < self.scroll_offset {
self.scroll_offset = self.selected;
} else if self.selected >= self.scroll_offset + max_visible {
self.scroll_offset = self.selected + 1 - max_visible;
}
if self.selected == 0 {
self.scroll_offset = 0;
} else if self.selected == len - 1 {
self.scroll_offset = len.saturating_sub(max_visible);
}
}
pub fn tick_decode(&mut self, question_reveal: f64, box_open_speed: f64, reveal_speed: f64) {
if !self.visible || question_reveal < QUESTION_REVEAL_GATE {
return;
}
self.open = (self.open + box_open_speed).min(1.0);
if self.open >= 1.0 {
self.reveal = (self.reveal + reveal_speed).min(1.0);
}
}
pub fn tick_transition_encode(
&mut self,
next_has_selector: bool,
encode_speed: f64,
morph_speed: f64,
) {
if !self.visible {
return;
}
self.reveal = (self.reveal - encode_speed).max(0.0);
if !next_has_selector && self.is_fully_hidden() {
self.open = (self.open - morph_speed).max(0.0);
}
}
pub fn tick_transition_decode(&mut self, encode_speed: f64, morph_speed: f64) {
if !self.visible {
return;
}
if self.open < 1.0 {
self.open = (self.open + morph_speed).min(1.0);
} else {
self.reveal = (self.reveal + encode_speed).min(1.0);
}
}
pub fn tick_exit_close(&mut self, encode_speed: f64, box_close_speed: f64) {
if !self.visible {
return;
}
self.reveal = (self.reveal - encode_speed).max(0.0);
if self.reveal <= 0.0 {
self.open = (self.open - box_close_speed).max(0.0);
}
}
}
#[cfg(not(tarpaulin_include))]
impl SelectorComponent {
fn render_fully_open<W: Write, R: Rng>(
&self,
writer: &mut W,
layout: &SelectorLayout,
start_row: u16,
rng: &mut R,
) -> std::io::Result<u16> {
let has_scroll_up = self.scroll_offset > 0;
let has_scroll_down = self.scroll_offset + layout.max_visible < self.options.len();
let mut row = start_row;
let top = selector_border(layout.inner_width, has_scroll_up, true);
queue!(
writer,
MoveTo(layout.col, row),
SetForegroundColor(Color::DarkGrey),
Print(&top),
ResetColor
)?;
row += 1;
render_selector_items(
writer,
self,
layout,
row,
has_scroll_up,
has_scroll_down,
rng,
)?;
row += layout.max_visible as u16;
let bottom = selector_border(layout.inner_width, has_scroll_down, false);
queue!(
writer,
MoveTo(layout.col, row),
SetForegroundColor(Color::DarkGrey),
Print(&bottom),
ResetColor
)?;
Ok((layout.max_visible as u16).saturating_add(box_chrome::BORDER_ROWS))
}
}
#[cfg(not(tarpaulin_include))]
impl Component for SelectorComponent {
fn render<W: Write>(&self, renderer: &mut Renderer<W>) -> std::io::Result<u16> {
if self.options.is_empty() {
return Ok(0);
}
renderer.with_panel(|writer, panel, rng| {
let layout = SelectorLayout::compute(self, panel.width);
if self.open < 1.0 {
return render_partial_border(writer, self.open, &layout, panel.row).map(|n| n as u16);
}
self.render_fully_open(writer, &layout, panel.row, rng)
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_helpers::select_option as option;
#[test]
fn hidden_is_invisible_with_empty_options() {
let s = SelectorComponent::hidden();
assert!(!s.visible);
assert!(s.options.is_empty());
assert_eq!(s.open, 0.0);
assert_eq!(s.reveal, 0.0);
}
#[test]
fn tick_decode_noop_when_invisible() {
let mut s = SelectorComponent::hidden();
s.tick_decode(1.0, 0.1, 0.1);
assert_eq!(s.open, 0.0);
}
#[test]
fn tick_decode_noop_below_question_gate() {
let mut s = SelectorComponent::from_options(vec![option("a", "a")], 0);
s.tick_decode(0.49, 0.1, 0.1);
assert_eq!(s.open, 0.0);
}
#[test]
fn tick_decode_opens_then_reveals() {
let mut s = SelectorComponent::from_options(vec![option("a", "a")], 0);
s.tick_decode(0.6, 0.5, 0.3);
assert!(s.open > 0.0);
assert_eq!(s.reveal, 0.0);
s.open = 1.0;
s.tick_decode(0.6, 0.5, 0.3);
assert!(s.reveal > 0.0);
}
#[test]
fn tick_decode_clamps_at_one() {
let mut s = SelectorComponent::from_options(vec![option("a", "a")], 0);
s.open = 1.0;
s.tick_decode(1.0, 1.0, 2.0);
assert_eq!(s.open, 1.0);
assert_eq!(s.reveal, 1.0);
}
#[test]
fn tick_transition_encode_noop_when_invisible() {
let mut s = SelectorComponent::hidden();
s.tick_transition_encode(true, 0.1, 0.1);
assert_eq!(s.reveal, 0.0);
}
#[test]
fn tick_transition_encode_fades_reveal_then_closes() {
let mut s = SelectorComponent::from_options(vec![option("a", "a")], 0);
s.open = 1.0;
s.reveal = 1.0;
s.tick_transition_encode(false, 0.5, 0.0);
assert!(s.reveal < 1.0);
s.reveal = 0.0;
s.tick_transition_encode(false, 0.5, 0.4);
assert!(s.open < 1.0);
}
#[test]
fn tick_transition_encode_keeps_box_when_next_has_selector() {
let mut s = SelectorComponent::from_options(vec![option("a", "a")], 0);
s.open = 1.0;
s.reveal = 0.0;
s.tick_transition_encode(true, 0.5, 0.5);
assert_eq!(s.open, 1.0);
}
#[test]
fn tick_transition_decode_noop_when_invisible() {
let mut s = SelectorComponent::hidden();
s.tick_transition_decode(0.1, 0.1);
assert_eq!(s.open, 0.0);
}
#[test]
fn tick_transition_decode_opens_then_reveals() {
let mut s = SelectorComponent::from_options(vec![option("a", "a")], 0);
s.open = 0.0;
s.tick_transition_decode(0.3, 0.5);
assert!(s.open > 0.0);
assert_eq!(s.reveal, 0.0);
s.open = 1.0;
s.tick_transition_decode(0.3, 0.5);
assert!(s.reveal > 0.0);
}
#[test]
fn tick_exit_close_noop_when_invisible() {
let mut s = SelectorComponent::hidden();
s.tick_exit_close(0.1, 0.1);
assert_eq!(s.open, 0.0);
}
#[test]
fn tick_exit_close_fades_reveal_then_closes_box() {
let mut s = SelectorComponent::from_options(vec![option("a", "a")], 0);
s.open = 1.0;
s.reveal = 1.0;
s.tick_exit_close(0.5, 0.5);
assert!(s.reveal < 1.0);
assert_eq!(s.open, 1.0);
s.reveal = 0.0;
s.tick_exit_close(0.5, 0.5);
assert!(s.open < 1.0);
}
#[test]
fn viewport_row_count_matches_render_cap() {
assert_eq!(SelectorComponent::viewport_row_count(0), 0);
assert_eq!(SelectorComponent::viewport_row_count(5), 5);
assert_eq!(SelectorComponent::viewport_row_count(8), 8);
assert_eq!(
SelectorComponent::viewport_row_count(200),
MAX_VISIBLE_OPTIONS
);
}
#[test]
fn is_fully_thresholds() {
let mut s = SelectorComponent::from_options(vec![option("a", "a")], 0);
s.open = 1.0;
assert!(s.is_fully_open());
s.open = 0.999;
assert!(!s.is_fully_open());
s.open = 0.0;
assert!(s.is_fully_closed());
s.open = -0.1;
assert!(s.is_fully_closed());
s.reveal = 1.0;
assert!(s.is_fully_revealed());
s.reveal = 0.999;
assert!(!s.is_fully_revealed());
s.reveal = 0.0;
assert!(s.is_fully_hidden());
s.reveal = 0.01;
assert!(!s.is_fully_hidden());
}
#[test]
fn navigate_down_increments_selection() {
let mut sel = SelectorComponent::from_options(
vec![option("A", "a"), option("B", "b"), option("C", "c")],
0,
);
sel.navigate(ListNavigate::Down);
assert_eq!(sel.selected, 1);
}
#[test]
fn navigate_down_wraps_to_first() {
let mut sel = SelectorComponent::from_options(vec![option("A", "a"), option("B", "b")], 1);
sel.navigate(ListNavigate::Down);
assert_eq!(sel.selected, 0);
}
#[test]
fn navigate_up_decrements_selection() {
let mut sel = SelectorComponent::from_options(
vec![option("A", "a"), option("B", "b"), option("C", "c")],
2,
);
sel.navigate(ListNavigate::Up);
assert_eq!(sel.selected, 1);
}
#[test]
fn navigate_up_wraps_to_last() {
let mut sel = SelectorComponent::from_options(
vec![option("A", "a"), option("B", "b"), option("C", "c")],
0,
);
sel.navigate(ListNavigate::Up);
assert_eq!(sel.selected, 2);
}
#[test]
fn navigate_empty_options_is_noop() {
let mut sel = SelectorComponent::hidden();
sel.navigate(ListNavigate::Down);
assert_eq!(sel.selected, 0);
}
}