use crate::widgets::ui::PickerState;
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FieldKind {
Text,
MultilineText,
Choice,
Checkbox,
Picker { multi: bool },
Button,
}
impl FieldKind {
pub fn types(self) -> bool {
matches!(self, Self::Text | Self::MultilineText)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FormKey<F> {
Moved,
Submit,
Cancel,
Step(F, i8),
Act(F),
Text(F),
Other,
}
pub trait Form {
type Field: Copy + Eq + std::fmt::Debug;
fn fields(&self) -> Vec<(Self::Field, FieldKind)>;
fn focused(&self) -> Self::Field;
fn set_focused(&mut self, field: Self::Field);
fn text_edge(&self, _field: Self::Field) -> (bool, bool) {
(true, true)
}
fn list_row(&self, _field: Self::Field) -> bool {
false
}
fn focused_kind(&self) -> Option<FieldKind> {
let focused = self.focused();
self.fields()
.into_iter()
.find(|(field, _)| *field == focused)
.map(|(_, kind)| kind)
}
fn shown_picker(&mut self) -> Option<(&mut PickerState, bool)> {
None
}
fn dismiss_picker(&mut self) {}
fn pick(&mut self, _toggle: bool) {}
fn focus(&mut self, field: Self::Field) -> bool {
let shown = self.fields().iter().any(|(f, _)| *f == field);
if shown {
self.set_focused(field);
}
shown
}
fn move_focus(&mut self, delta: isize) {
let fields = self.fields();
if fields.is_empty() {
return;
}
let focused = self.focused();
let n = fields.len() as isize;
let next = match fields.iter().position(|(f, _)| *f == focused) {
Some(at) => (at as isize + delta).rem_euclid(n),
None if delta < 0 => n - 1,
None => 0,
};
self.set_focused(fields[next as usize].0);
}
fn focus_next(&mut self) {
self.move_focus(1);
}
fn focus_prev(&mut self) {
self.move_focus(-1);
}
fn settle_focus(&mut self) {
if self.focused_kind().is_none()
&& let Some((first, _)) = self.fields().first().copied()
{
self.set_focused(first);
}
}
}
fn plain(event: &KeyEvent) -> bool {
event
.modifiers
.intersection(KeyModifiers::CONTROL | KeyModifiers::ALT)
.is_empty()
}
pub fn key<T: Form + ?Sized>(form: &mut T, event: &KeyEvent) -> FormKey<T::Field> {
let ctrl = event.modifiers.contains(KeyModifiers::CONTROL);
let shift = event.modifiers.contains(KeyModifiers::SHIFT);
let Some(kind) = form.focused_kind() else {
form.settle_focus();
return match event.code {
KeyCode::Esc => FormKey::Cancel,
KeyCode::Enter => FormKey::Submit,
_ => FormKey::Other,
};
};
let field = form.focused();
let code = match event.code {
KeyCode::Char('h') if !kind.types() && plain(event) => KeyCode::Left,
KeyCode::Char('j') if !kind.types() && plain(event) => KeyCode::Down,
KeyCode::Char('k') if !kind.types() && plain(event) => KeyCode::Up,
KeyCode::Char('l') if !kind.types() && plain(event) => KeyCode::Right,
code => code,
};
match code {
KeyCode::Esc => FormKey::Cancel,
KeyCode::Enter | KeyCode::Char('j' | 'J') if ctrl => FormKey::Submit,
KeyCode::Enter if kind == FieldKind::MultilineText => FormKey::Text(field),
KeyCode::Enter => FormKey::Submit,
KeyCode::BackTab => {
form.focus_prev();
FormKey::Moved
}
KeyCode::Tab if shift => {
form.focus_prev();
FormKey::Moved
}
KeyCode::Tab => {
form.focus_next();
FormKey::Moved
}
KeyCode::Up | KeyCode::Down if event.modifiers.is_empty() => {
let up = code == KeyCode::Up;
if kind == FieldKind::MultilineText {
let (first, last) = form.text_edge(field);
if (up && !first) || (!up && !last) {
return FormKey::Text(field);
}
}
form.move_focus(if up { -1 } else { 1 });
FormKey::Moved
}
KeyCode::Left | KeyCode::Right => {
let delta = if code == KeyCode::Left { -1 } else { 1 };
match kind {
FieldKind::Text | FieldKind::MultilineText => FormKey::Text(field),
FieldKind::Choice | FieldKind::Picker { multi: false } => {
FormKey::Step(field, delta)
}
FieldKind::Checkbox => FormKey::Act(field),
FieldKind::Picker { multi: true } | FieldKind::Button => FormKey::Other,
}
}
KeyCode::Char(' ') if plain(event) => match kind {
FieldKind::Text | FieldKind::MultilineText => FormKey::Text(field),
FieldKind::Choice => FormKey::Step(field, 1),
FieldKind::Checkbox | FieldKind::Picker { .. } | FieldKind::Button => {
FormKey::Act(field)
}
},
_ if kind.types() => FormKey::Text(field),
_ => FormKey::Other,
}
}
pub fn picker_form_key<T: Form + ?Sized>(form: &mut T, event: &KeyEvent) -> bool {
let Some((picker, multi)) = form.shown_picker() else {
return false;
};
match picker_key(picker, multi, event) {
PickerKey::Close => form.dismiss_picker(),
PickerKey::Choose => form.pick(false),
PickerKey::Toggle => form.pick(true),
PickerKey::ChooseAndMove(forward) => {
form.pick(false);
form.move_focus(if forward { 1 } else { -1 });
}
PickerKey::Handled | PickerKey::Other => {}
}
true
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ListMove {
Up,
Down,
PageUp,
PageDown,
Home,
End,
}
impl ListMove {
pub fn from_key(event: &KeyEvent) -> Option<Self> {
if !event.is_press() {
return None;
}
Some(match event.code {
KeyCode::Up | KeyCode::Char('k') => Self::Up,
KeyCode::Down | KeyCode::Char('j') => Self::Down,
KeyCode::PageUp => Self::PageUp,
KeyCode::PageDown => Self::PageDown,
KeyCode::Home => Self::Home,
KeyCode::End => Self::End,
_ => return None,
})
}
pub fn delta(self, page: usize) -> isize {
let page = isize::try_from(page.max(1)).unwrap_or(isize::MAX);
match self {
Self::Up => -1,
Self::Down => 1,
Self::PageUp => -page,
Self::PageDown => page,
Self::Home => isize::MIN,
Self::End => isize::MAX,
}
}
pub fn apply(self, at: usize, len: usize, page: usize) -> usize {
let last = len.saturating_sub(1);
at.saturating_add_signed(self.delta(page)).min(last)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PickerKey {
Handled,
Close,
Choose,
Toggle,
ChooseAndMove(bool),
Other,
}
pub fn picker_key(picker: &mut PickerState, multi: bool, event: &KeyEvent) -> PickerKey {
let shift = event.modifiers.contains(KeyModifiers::SHIFT);
match event.code {
KeyCode::Esc => PickerKey::Close,
KeyCode::Enter => PickerKey::Choose,
KeyCode::BackTab => PickerKey::ChooseAndMove(false),
KeyCode::Tab => PickerKey::ChooseAndMove(!shift),
KeyCode::Up => {
picker.move_up();
PickerKey::Handled
}
KeyCode::Down => {
picker.move_down();
PickerKey::Handled
}
KeyCode::Char(' ') if multi => PickerKey::Toggle,
KeyCode::Char(' ') if picker.filter.is_empty() => PickerKey::Choose,
KeyCode::Backspace => {
picker.backspace();
PickerKey::Handled
}
KeyCode::Char(c) => {
picker.filter_key(c, event.modifiers);
PickerKey::Handled
}
_ => PickerKey::Other,
}
}
pub fn step_index(at: usize, len: usize, delta: i8) -> usize {
if len == 0 {
return 0;
}
(at as isize + delta as isize).rem_euclid(len as isize) as usize
}
pub fn step_value<T: Copy + PartialEq>(all: &[T], current: T, delta: i8) -> T {
let at = all.iter().position(|v| *v == current);
match at {
Some(at) => all[step_index(at, all.len(), delta)],
None => all[0],
}
}
#[cfg(test)]
mod tests;