1use platform_core::{Key, NamedKey};
13
14impl Default for KeyNav {
16 fn default() -> Self {
17 Self {
18 vim: false,
19 horizontal: false,
20 grid: false,
21 }
22 }
23}
24
25#[derive(Clone, Copy, PartialEq, Eq, Debug)]
27pub enum KeyNavMove {
28 Next,
29 Previous,
30 First,
31 Last,
32 NextRow,
35 PreviousRow,
36 Activate,
38 Cancel,
40}
41
42#[derive(Clone, Copy)]
48pub struct KeyNav {
49 pub vim: bool,
50 pub horizontal: bool,
52 pub grid: bool,
55}
56
57impl KeyNav {
58 pub fn horizontal(mut self) -> Self {
59 self.horizontal = true;
60 self
61 }
62
63 pub fn grid(mut self) -> Self {
65 self.horizontal = true;
66 self.grid = true;
67 self
68 }
69
70 pub fn interpret(self, key: &Key) -> Option<KeyNavMove> {
73 let (forward, back) = if self.horizontal {
74 (NamedKey::ArrowRight, NamedKey::ArrowLeft)
75 } else {
76 (NamedKey::ArrowDown, NamedKey::ArrowUp)
77 };
78 if let Key::Named(named) = key {
79 if *named == forward {
80 return Some(KeyNavMove::Next);
81 }
82 if *named == back {
83 return Some(KeyNavMove::Previous);
84 }
85 if self.grid {
86 if *named == NamedKey::ArrowDown {
87 return Some(KeyNavMove::NextRow);
88 }
89 if *named == NamedKey::ArrowUp {
90 return Some(KeyNavMove::PreviousRow);
91 }
92 }
93 return match named {
94 NamedKey::Enter => Some(KeyNavMove::Activate),
95 NamedKey::Escape => Some(KeyNavMove::Cancel),
96 NamedKey::Home => Some(KeyNavMove::First),
97 NamedKey::End => Some(KeyNavMove::Last),
98 _ => None,
99 };
100 }
101 if !self.vim {
102 return None;
103 }
104 let (down, up) = if self.grid {
106 (KeyNavMove::NextRow, KeyNavMove::PreviousRow)
107 } else {
108 (KeyNavMove::Next, KeyNavMove::Previous)
109 };
110 match key {
111 Key::Char(c) => match c {
112 'j' => Some(down),
113 'k' => Some(up),
114 'h' if self.grid => Some(KeyNavMove::Previous),
115 'l' if self.grid => Some(KeyNavMove::Next),
116 'g' => Some(KeyNavMove::First),
117 'G' => Some(KeyNavMove::Last),
118 '\u{e}' => Some(down), '\u{10}' => Some(up), _ => None,
121 },
122 _ => None,
123 }
124 }
125}
126
127pub fn key_nav_apply(current: usize, count: usize, movement: KeyNavMove) -> usize {
132 key_nav_apply_grid(current, count, 1, movement)
133}
134
135pub fn key_nav_apply_grid(
141 current: usize,
142 count: usize,
143 columns: usize,
144 movement: KeyNavMove,
145) -> usize {
146 if count == 0 {
147 return 0;
148 }
149 let columns = columns.max(1);
150 let current = current.min(count - 1);
151 let last = count - 1;
152 match movement {
153 KeyNavMove::Next => (current + 1) % count,
154 KeyNavMove::Previous => (current + count - 1) % count,
155 KeyNavMove::First => 0,
156 KeyNavMove::Last => last,
157 KeyNavMove::NextRow => {
158 let below = current + columns;
159 if below <= last {
160 below
161 } else if current / columns < last / columns {
162 last
164 } else {
165 current % columns
166 }
167 }
168 KeyNavMove::PreviousRow => {
169 if current >= columns {
170 current - columns
171 } else {
172 let bottom = (last / columns) * columns + current % columns;
173 if bottom > last {
174 bottom - columns
175 } else {
176 bottom
177 }
178 }
179 }
180 KeyNavMove::Activate | KeyNavMove::Cancel => current,
181 }
182}
183
184#[cfg(test)]
185mod tests {
186 use super::*;
187
188 fn arrows() -> KeyNav {
189 KeyNav {
190 vim: false,
191 horizontal: false,
192 grid: false,
193 }
194 }
195
196 fn vim() -> KeyNav {
197 KeyNav {
198 vim: true,
199 horizontal: false,
200 grid: false,
201 }
202 }
203
204 fn named(key: NamedKey) -> Key {
205 Key::Named(key)
206 }
207
208 fn character(c: char) -> Key {
209 Key::Char(c)
210 }
211
212 #[test]
213 fn the_arrows_always_navigate_and_the_letters_only_do_in_vim_mode() {
214 assert_eq!(
215 arrows().interpret(&named(NamedKey::ArrowDown)),
216 Some(KeyNavMove::Next)
217 );
218 assert_eq!(
219 arrows().interpret(&named(NamedKey::ArrowUp)),
220 Some(KeyNavMove::Previous)
221 );
222 assert_eq!(
223 arrows().interpret(&named(NamedKey::Enter)),
224 Some(KeyNavMove::Activate)
225 );
226 assert_eq!(
227 arrows().interpret(&named(NamedKey::Escape)),
228 Some(KeyNavMove::Cancel)
229 );
230
231 assert_eq!(arrows().interpret(&character('j')), None);
233 assert_eq!(arrows().interpret(&character('k')), None);
234 assert_eq!(arrows().interpret(&character('G')), None);
235
236 assert_eq!(vim().interpret(&character('j')), Some(KeyNavMove::Next));
237 assert_eq!(vim().interpret(&character('k')), Some(KeyNavMove::Previous));
238 assert_eq!(vim().interpret(&character('g')), Some(KeyNavMove::First));
239 assert_eq!(vim().interpret(&character('G')), Some(KeyNavMove::Last));
240 assert_eq!(
241 vim().interpret(&character('\u{e}')),
242 Some(KeyNavMove::Next),
243 "Ctrl-N"
244 );
245 assert_eq!(
246 vim().interpret(&character('\u{10}')),
247 Some(KeyNavMove::Previous),
248 "Ctrl-P"
249 );
250 assert_eq!(
251 vim().interpret(&character('q')),
252 None,
253 "an unbound letter is still typing"
254 );
255 }
256
257 #[test]
258 fn a_horizontal_list_reads_the_other_pair_of_arrows() {
259 let row = arrows().horizontal();
260 assert_eq!(
261 row.interpret(&named(NamedKey::ArrowRight)),
262 Some(KeyNavMove::Next)
263 );
264 assert_eq!(
265 row.interpret(&named(NamedKey::ArrowLeft)),
266 Some(KeyNavMove::Previous)
267 );
268 assert_eq!(
269 row.interpret(&named(NamedKey::ArrowDown)),
270 None,
271 "down is not along a row, so it stays available to whatever else wants it"
272 );
273 }
274
275 #[test]
276 fn the_selection_wraps_at_both_ends_and_survives_a_list_that_shrank() {
277 assert_eq!(key_nav_apply(0, 3, KeyNavMove::Next), 1);
278 assert_eq!(
279 key_nav_apply(2, 3, KeyNavMove::Next),
280 0,
281 "wraps past the end"
282 );
283 assert_eq!(
284 key_nav_apply(0, 3, KeyNavMove::Previous),
285 2,
286 "and back past the start"
287 );
288 assert_eq!(key_nav_apply(1, 3, KeyNavMove::First), 0);
289 assert_eq!(key_nav_apply(1, 3, KeyNavMove::Last), 2);
290 assert_eq!(
291 key_nav_apply(1, 3, KeyNavMove::Activate),
292 1,
293 "activating moves nothing"
294 );
295
296 assert_eq!(
297 key_nav_apply(0, 0, KeyNavMove::Next),
298 0,
299 "an empty list has nowhere to go"
300 );
301 assert_eq!(key_nav_apply(9, 3, KeyNavMove::Next), 0);
303 assert_eq!(key_nav_apply(9, 3, KeyNavMove::Previous), 1);
304
305 assert_eq!(key_nav_apply(0, 3, KeyNavMove::NextRow), 1);
308 assert_eq!(key_nav_apply(2, 3, KeyNavMove::PreviousRow), 1);
309 assert_eq!(key_nav_apply(2, 3, KeyNavMove::NextRow), 0, "still wraps");
310 }
311
312 #[test]
313 fn a_grid_uses_both_pairs_of_arrows() {
314 let grid = arrows().grid();
315 assert_eq!(
316 grid.interpret(&named(NamedKey::ArrowRight)),
317 Some(KeyNavMove::Next)
318 );
319 assert_eq!(
320 grid.interpret(&named(NamedKey::ArrowLeft)),
321 Some(KeyNavMove::Previous)
322 );
323 assert_eq!(
324 grid.interpret(&named(NamedKey::ArrowDown)),
325 Some(KeyNavMove::NextRow)
326 );
327 assert_eq!(
328 grid.interpret(&named(NamedKey::ArrowUp)),
329 Some(KeyNavMove::PreviousRow)
330 );
331 assert_eq!(
332 grid.interpret(&named(NamedKey::Enter)),
333 Some(KeyNavMove::Activate)
334 );
335
336 assert_eq!(grid.interpret(&character('j')), None);
338 assert_eq!(grid.interpret(&character('h')), None);
339
340 let keys = vim().grid();
341 assert_eq!(keys.interpret(&character('j')), Some(KeyNavMove::NextRow));
342 assert_eq!(
343 keys.interpret(&character('k')),
344 Some(KeyNavMove::PreviousRow)
345 );
346 assert_eq!(keys.interpret(&character('l')), Some(KeyNavMove::Next));
347 assert_eq!(keys.interpret(&character('h')), Some(KeyNavMove::Previous));
348 }
349
350 #[test]
352 fn a_row_move_crosses_a_whole_row_and_a_partial_one_is_still_a_row() {
353 assert_eq!(key_nav_apply_grid(0, 9, 3, KeyNavMove::NextRow), 3);
357 assert_eq!(key_nav_apply_grid(4, 9, 3, KeyNavMove::PreviousRow), 1);
358 assert_eq!(
359 key_nav_apply_grid(1, 9, 3, KeyNavMove::Next),
360 2,
361 "along the row, not down it"
362 );
363 assert_eq!(
364 key_nav_apply_grid(7, 9, 3, KeyNavMove::NextRow),
365 1,
366 "wraps to the same column"
367 );
368 assert_eq!(
369 key_nav_apply_grid(1, 9, 3, KeyNavMove::PreviousRow),
370 7,
371 "and back to the bottom of it"
372 );
373
374 assert_eq!(key_nav_apply_grid(2, 5, 3, KeyNavMove::NextRow), 4);
378 assert_eq!(key_nav_apply_grid(1, 5, 3, KeyNavMove::NextRow), 4);
379 assert_eq!(key_nav_apply_grid(0, 5, 3, KeyNavMove::NextRow), 3);
380 assert_eq!(key_nav_apply_grid(2, 5, 3, KeyNavMove::PreviousRow), 2);
382 assert_eq!(key_nav_apply_grid(1, 5, 3, KeyNavMove::PreviousRow), 4);
383 assert_eq!(
384 key_nav_apply_grid(4, 5, 3, KeyNavMove::NextRow),
385 1,
386 "the last row wraps to the first"
387 );
388
389 assert_eq!(key_nav_apply_grid(1, 3, 3, KeyNavMove::NextRow), 1);
391 assert_eq!(key_nav_apply_grid(1, 3, 3, KeyNavMove::PreviousRow), 1);
392 assert_eq!(key_nav_apply_grid(0, 0, 4, KeyNavMove::NextRow), 0);
393 assert_eq!(
394 key_nav_apply_grid(9, 5, 3, KeyNavMove::NextRow),
395 1,
396 "a stale index is clamped first"
397 );
398 }
399}