use crossterm::event::{KeyCode, KeyEvent, KeyEventKind, KeyModifiers};
use crate::prelude::*;
#[derive(Debug, Default, Clone, PartialEq, Eq, Hash)]
pub struct SelectState {
status: Status,
focus: FocusState,
pub(crate) focused_index: usize,
pub(crate) option_count: usize,
}
impl SelectState {
#[must_use]
pub const fn new() -> Self {
Self {
status: Status::Pending,
focus: FocusState::Unfocused,
focused_index: 0,
option_count: 0,
}
}
pub fn move_up(&mut self) {
if self.focused_index > 0 {
self.focused_index -= 1;
}
}
pub fn move_down(&mut self) {
if self.focused_index < self.option_count.saturating_sub(1) {
self.focused_index += 1;
}
}
#[must_use]
pub const fn with_status(mut self, status: Status) -> Self {
self.status = status;
self
}
#[must_use]
pub const fn with_focus(mut self, focus: FocusState) -> Self {
self.focus = focus;
self
}
#[must_use]
pub const fn focused_index(&self) -> usize {
self.focused_index
}
pub fn set_focused_index(&mut self, index: usize) {
self.focused_index = self.clamp_focused_index(index);
}
#[must_use]
pub const fn is_finished(&self) -> bool {
self.status.is_finished()
}
#[must_use]
pub const fn status(&self) -> Status {
self.status
}
pub fn focus(&mut self) {
self.focus = FocusState::Focused;
}
pub fn blur(&mut self) {
self.focus = FocusState::Unfocused;
}
#[must_use]
pub fn is_focused(&self) -> bool {
self.focus == FocusState::Focused
}
pub fn complete(&mut self) {
self.status = Status::Done;
}
pub fn abort(&mut self) {
self.status = Status::Aborted;
}
pub fn handle_key_event(&mut self, key: KeyEvent) {
if key.kind == KeyEventKind::Release || self.status.is_finished() {
return;
}
match (key.code, key.modifiers) {
(KeyCode::Up, _) => self.move_up(),
(KeyCode::Down, _) => self.move_down(),
(KeyCode::Enter, _) if self.option_count > 0 => self.complete(),
(KeyCode::Esc, _) | (KeyCode::Char('c'), KeyModifiers::CONTROL) => self.abort(),
_ => {}
}
}
pub(crate) const fn clamp_focused_index(&self, index: usize) -> usize {
if self.option_count == 0 {
index
} else if index >= self.option_count {
self.option_count - 1
} else {
index
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn key(code: KeyCode, kind: KeyEventKind) -> KeyEvent {
KeyEvent::new_with_kind(code, KeyModifiers::NONE, kind)
}
fn ctrl_key(code: KeyCode) -> KeyEvent {
KeyEvent::new_with_kind(code, KeyModifiers::CONTROL, KeyEventKind::Press)
}
#[test]
fn render_option_count_clamps_focused_index() {
let mut state = SelectState::new();
state.set_focused_index(5);
state.option_count = 3;
state.focused_index = state.clamp_focused_index(state.focused_index);
assert_eq!(state.focused_index(), 2);
}
#[test]
fn set_focused_index_clamps_when_option_count_is_known() {
let mut state = SelectState::new();
state.option_count = 3;
state.set_focused_index(5);
assert_eq!(state.focused_index(), 2);
}
#[test]
fn move_down_stops_at_last_option() {
let mut state = SelectState::new();
state.option_count = 2;
state.move_down();
state.move_down();
assert_eq!(state.focused_index(), 1);
}
#[test]
fn handle_key_event_accepts_repeated_navigation() {
let mut state = SelectState::new();
state.option_count = 2;
state.handle_key_event(key(KeyCode::Down, KeyEventKind::Repeat));
assert_eq!(state.focused_index(), 1);
}
#[test]
fn handle_key_event_ignores_key_release() {
let mut state = SelectState::new();
state.option_count = 2;
state.handle_key_event(key(KeyCode::Down, KeyEventKind::Release));
assert_eq!(state.focused_index(), 0);
}
#[test]
fn handle_key_event_aborts_on_ctrl_c() {
let mut state = SelectState::new();
state.handle_key_event(ctrl_key(KeyCode::Char('c')));
assert_eq!(state.status(), Status::Aborted);
}
#[test]
fn handle_key_event_ignores_events_after_completion() {
let mut state = SelectState::new();
state.option_count = 2;
state.handle_key_event(key(KeyCode::Enter, KeyEventKind::Press));
state.handle_key_event(key(KeyCode::Down, KeyEventKind::Press));
state.handle_key_event(key(KeyCode::Esc, KeyEventKind::Press));
assert_eq!(state.focused_index(), 0);
assert_eq!(state.status(), Status::Done);
}
#[test]
fn handle_key_event_ignores_events_after_abort() {
let mut state = SelectState::new();
state.option_count = 2;
state.handle_key_event(key(KeyCode::Esc, KeyEventKind::Press));
state.handle_key_event(key(KeyCode::Enter, KeyEventKind::Press));
assert_eq!(state.status(), Status::Aborted);
}
#[test]
fn handle_key_event_does_not_complete_without_visible_options() {
let mut state = SelectState::new();
state.handle_key_event(key(KeyCode::Enter, KeyEventKind::Press));
assert_eq!(state.status(), Status::Pending);
}
}