1use crate::widgets::ui::PickerState;
22use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub enum FieldKind {
27 Text,
30 MultilineText,
33 Choice,
35 Checkbox,
37 Picker { multi: bool },
41 Button,
43}
44
45impl FieldKind {
46 pub fn types(self) -> bool {
48 matches!(self, Self::Text | Self::MultilineText)
49 }
50}
51
52#[derive(Debug, Clone, Copy, PartialEq, Eq)]
54pub enum FormKey<F> {
55 Moved,
57 Submit,
59 Cancel,
61 Step(F, i8),
64 Act(F),
66 Text(F),
68 Other,
70}
71
72pub trait Form {
75 type Field: Copy + Eq + std::fmt::Debug;
77
78 fn fields(&self) -> Vec<(Self::Field, FieldKind)>;
81
82 fn focused(&self) -> Self::Field;
83
84 fn set_focused(&mut self, field: Self::Field);
87
88 fn text_edge(&self, _field: Self::Field) -> (bool, bool) {
91 (true, true)
92 }
93
94 fn list_row(&self, _field: Self::Field) -> bool {
99 false
100 }
101
102 fn focused_kind(&self) -> Option<FieldKind> {
104 let focused = self.focused();
105 self.fields()
106 .into_iter()
107 .find(|(field, _)| *field == focused)
108 .map(|(_, kind)| kind)
109 }
110
111 fn shown_picker(&mut self) -> Option<(&mut PickerState, bool)> {
114 None
115 }
116
117 fn dismiss_picker(&mut self) {}
119
120 fn pick(&mut self, _toggle: bool) {}
122
123 fn focus(&mut self, field: Self::Field) -> bool {
126 let shown = self.fields().iter().any(|(f, _)| *f == field);
127 if shown {
128 self.set_focused(field);
129 }
130 shown
131 }
132
133 fn move_focus(&mut self, delta: isize) {
136 let fields = self.fields();
137 if fields.is_empty() {
138 return;
139 }
140 let focused = self.focused();
141 let n = fields.len() as isize;
142 let next = match fields.iter().position(|(f, _)| *f == focused) {
143 Some(at) => (at as isize + delta).rem_euclid(n),
144 None if delta < 0 => n - 1,
145 None => 0,
146 };
147 self.set_focused(fields[next as usize].0);
148 }
149
150 fn focus_next(&mut self) {
151 self.move_focus(1);
152 }
153
154 fn focus_prev(&mut self) {
155 self.move_focus(-1);
156 }
157
158 fn settle_focus(&mut self) {
161 if self.focused_kind().is_none()
162 && let Some((first, _)) = self.fields().first().copied()
163 {
164 self.set_focused(first);
165 }
166 }
167}
168
169fn plain(event: &KeyEvent) -> bool {
170 event
171 .modifiers
172 .intersection(KeyModifiers::CONTROL | KeyModifiers::ALT)
173 .is_empty()
174}
175
176pub fn key<T: Form + ?Sized>(form: &mut T, event: &KeyEvent) -> FormKey<T::Field> {
179 let ctrl = event.modifiers.contains(KeyModifiers::CONTROL);
180 let shift = event.modifiers.contains(KeyModifiers::SHIFT);
181 let Some(kind) = form.focused_kind() else {
182 form.settle_focus();
185 return match event.code {
186 KeyCode::Esc => FormKey::Cancel,
187 KeyCode::Enter => FormKey::Submit,
188 _ => FormKey::Other,
189 };
190 };
191 let field = form.focused();
192 let code = match event.code {
194 KeyCode::Char('h') if !kind.types() && plain(event) => KeyCode::Left,
195 KeyCode::Char('j') if !kind.types() && plain(event) => KeyCode::Down,
196 KeyCode::Char('k') if !kind.types() && plain(event) => KeyCode::Up,
197 KeyCode::Char('l') if !kind.types() && plain(event) => KeyCode::Right,
198 code => code,
199 };
200 match code {
201 KeyCode::Esc => FormKey::Cancel,
202 KeyCode::Enter | KeyCode::Char('j' | 'J') if ctrl => FormKey::Submit,
204 KeyCode::Enter if kind == FieldKind::MultilineText => FormKey::Text(field),
205 KeyCode::Enter => FormKey::Submit,
206 KeyCode::BackTab => {
207 form.focus_prev();
208 FormKey::Moved
209 }
210 KeyCode::Tab if shift => {
211 form.focus_prev();
212 FormKey::Moved
213 }
214 KeyCode::Tab => {
215 form.focus_next();
216 FormKey::Moved
217 }
218 KeyCode::Up | KeyCode::Down if event.modifiers.is_empty() => {
219 let up = code == KeyCode::Up;
220 if kind == FieldKind::MultilineText {
221 let (first, last) = form.text_edge(field);
222 if (up && !first) || (!up && !last) {
223 return FormKey::Text(field);
224 }
225 }
226 form.move_focus(if up { -1 } else { 1 });
227 FormKey::Moved
228 }
229 KeyCode::Left | KeyCode::Right => {
230 let delta = if code == KeyCode::Left { -1 } else { 1 };
231 match kind {
232 FieldKind::Text | FieldKind::MultilineText => FormKey::Text(field),
233 FieldKind::Choice | FieldKind::Picker { multi: false } => {
234 FormKey::Step(field, delta)
235 }
236 FieldKind::Checkbox => FormKey::Act(field),
237 FieldKind::Picker { multi: true } | FieldKind::Button => FormKey::Other,
238 }
239 }
240 KeyCode::Char(' ') if plain(event) => match kind {
241 FieldKind::Text | FieldKind::MultilineText => FormKey::Text(field),
242 FieldKind::Choice => FormKey::Step(field, 1),
243 FieldKind::Checkbox | FieldKind::Picker { .. } | FieldKind::Button => {
244 FormKey::Act(field)
245 }
246 },
247 _ if kind.types() => FormKey::Text(field),
248 _ => FormKey::Other,
249 }
250}
251
252pub fn picker_form_key<T: Form + ?Sized>(form: &mut T, event: &KeyEvent) -> bool {
255 let Some((picker, multi)) = form.shown_picker() else {
256 return false;
257 };
258 match picker_key(picker, multi, event) {
259 PickerKey::Close => form.dismiss_picker(),
260 PickerKey::Choose => form.pick(false),
261 PickerKey::Toggle => form.pick(true),
262 PickerKey::ChooseAndMove(forward) => {
263 form.pick(false);
264 form.move_focus(if forward { 1 } else { -1 });
265 }
266 PickerKey::Handled | PickerKey::Other => {}
267 }
268 true
269}
270
271#[derive(Debug, Clone, Copy, PartialEq, Eq)]
274pub enum ListMove {
275 Up,
276 Down,
277 PageUp,
278 PageDown,
279 Home,
280 End,
281}
282
283impl ListMove {
284 pub fn from_key(event: &KeyEvent) -> Option<Self> {
286 if !event.is_press() {
287 return None;
288 }
289 Some(match event.code {
290 KeyCode::Up | KeyCode::Char('k') => Self::Up,
291 KeyCode::Down | KeyCode::Char('j') => Self::Down,
292 KeyCode::PageUp => Self::PageUp,
293 KeyCode::PageDown => Self::PageDown,
294 KeyCode::Home => Self::Home,
295 KeyCode::End => Self::End,
296 _ => return None,
297 })
298 }
299
300 pub fn delta(self, page: usize) -> isize {
302 let page = isize::try_from(page.max(1)).unwrap_or(isize::MAX);
303 match self {
304 Self::Up => -1,
305 Self::Down => 1,
306 Self::PageUp => -page,
307 Self::PageDown => page,
308 Self::Home => isize::MIN,
309 Self::End => isize::MAX,
310 }
311 }
312
313 pub fn apply(self, at: usize, len: usize, page: usize) -> usize {
315 let last = len.saturating_sub(1);
316 at.saturating_add_signed(self.delta(page)).min(last)
317 }
318}
319
320#[derive(Debug, Clone, Copy, PartialEq, Eq)]
322pub enum PickerKey {
323 Handled,
325 Close,
327 Choose,
330 Toggle,
332 ChooseAndMove(bool),
334 Other,
336}
337
338pub fn picker_key(picker: &mut PickerState, multi: bool, event: &KeyEvent) -> PickerKey {
344 let shift = event.modifiers.contains(KeyModifiers::SHIFT);
345 match event.code {
346 KeyCode::Esc => PickerKey::Close,
347 KeyCode::Enter => PickerKey::Choose,
348 KeyCode::BackTab => PickerKey::ChooseAndMove(false),
349 KeyCode::Tab => PickerKey::ChooseAndMove(!shift),
350 KeyCode::Up => {
351 picker.move_up();
352 PickerKey::Handled
353 }
354 KeyCode::Down => {
355 picker.move_down();
356 PickerKey::Handled
357 }
358 KeyCode::Char(' ') if multi => PickerKey::Toggle,
359 KeyCode::Char(' ') if picker.filter.is_empty() => PickerKey::Choose,
360 KeyCode::Backspace => {
361 picker.backspace();
362 PickerKey::Handled
363 }
364 KeyCode::Char(c) => {
365 picker.filter_key(c, event.modifiers);
366 PickerKey::Handled
367 }
368 _ => PickerKey::Other,
369 }
370}
371
372pub fn step_index(at: usize, len: usize, delta: i8) -> usize {
374 if len == 0 {
375 return 0;
376 }
377 (at as isize + delta as isize).rem_euclid(len as isize) as usize
378}
379
380pub fn step_value<T: Copy + PartialEq>(all: &[T], current: T, delta: i8) -> T {
383 let at = all.iter().position(|v| *v == current);
384 match at {
385 Some(at) => all[step_index(at, all.len(), delta)],
386 None => all[0],
387 }
388}
389
390#[cfg(test)]
391mod tests;