use super::*;
pub(super) trait Tabs: Copy + Default + PartialEq {
fn next(self) -> Self;
fn prev(self) -> Self;
}
#[derive(Default)]
pub(super) struct Nav<T> {
pub(super) sel: usize,
pub(super) top: usize,
pub(super) query: String,
pub(super) filtering: bool,
pub(super) help: bool,
pub(super) tab: T,
pub(super) tab_hits: Vec<(std::ops::Range<u16>, T)>,
pub(super) last_click: Option<(std::time::Instant, usize)>,
g: bool,
pub(super) page: usize,
}
pub(super) fn cards_fit(rows: usize, height: u16) -> usize {
height.saturating_sub(2) as usize / rows.max(1)
}
impl<T: Tabs> Nav<T> {
pub(super) fn card(&self, rows: usize, area: Rect) -> Rect {
Rect { y: area.y + 1 + (self.sel.saturating_sub(self.top) * rows) as u16, height: rows as u16, ..area }
}
pub(super) fn status(&self, msg: &str, fields: &str, idle: &str) -> String {
match (self.filtering, self.query.is_empty(), msg.is_empty()) {
(true, ..) => format!(" /{}▏ {fields} -x negates", self.query),
(_, _, false) => format!(" {msg}"),
(_, false, _) => format!(" filter: {} (Esc clears)", self.query),
_ => idle.into(),
}
}
pub(super) fn go(&mut self, tab: T) {
(self.tab, self.sel, self.top) = (tab, 0, 0);
}
pub(super) fn clamp(&mut self, n: usize) {
self.sel = self.sel.min(n.saturating_sub(1));
}
pub(super) fn step(&mut self, down: bool, n: usize) {
self.sel = if down { self.sel + 1 } else { self.sel.saturating_sub(1) };
self.clamp(n);
}
pub(super) fn keep(&mut self, n: usize, h: usize) {
self.clamp(n);
self.page = h;
self.top = self.top.min(self.sel).max((self.sel + 1).saturating_sub(h));
}
pub(super) fn filter_key(&mut self, k: KeyEvent) -> bool {
if !self.filtering {
return false;
}
match k.code {
KeyCode::Esc => (self.query.clear(), self.filtering = false).1,
KeyCode::Enter => self.filtering = false,
KeyCode::Backspace => _ = self.query.pop(),
KeyCode::Char(c) => self.query.push(c),
_ => {}
}
self.sel = 0;
true
}
pub(super) fn key(&mut self, k: KeyEvent, n: usize) -> bool {
let ctrl = k.modifiers.contains(KeyModifiers::CONTROL);
let (last, half) = (n.saturating_sub(1), (self.page / 2).max(1));
let g = std::mem::take(&mut self.g);
match (g, k.code) {
(_, KeyCode::Char('d')) if ctrl => self.sel = (self.sel + half).min(last),
(_, KeyCode::Char('u')) if ctrl => self.sel = self.sel.saturating_sub(half),
(_, KeyCode::Char('j') | KeyCode::Down) => self.step(true, n),
(_, KeyCode::Char('k') | KeyCode::Up) => self.step(false, n),
(false, KeyCode::Char('g')) => self.g = true,
(true, KeyCode::Esc) => {}
(false, KeyCode::Esc) if !self.query.is_empty() => (self.query.clear(), self.sel = 0).1,
(true, KeyCode::Char('g')) => self.sel = 0,
(true, KeyCode::Char('e')) => self.sel = last,
(true, KeyCode::Char('n')) | (false, KeyCode::Char('l')) => self.go(self.tab.next()),
(true, KeyCode::Char('p')) | (false, KeyCode::Char('h')) => self.go(self.tab.prev()),
(_, KeyCode::Char('/')) => self.filtering = true, (_, KeyCode::Char('?')) => self.help = true,
_ => return false,
}
true
}
pub(super) fn click(&mut self, x: u16, y: u16, n: usize, rows: usize) -> bool {
if y == 0 {
if let Some(&(_, tab)) = self.tab_hits.iter().find(|(r, _)| r.contains(&x)) {
self.go(tab);
}
return false;
}
let k = (y as usize - 1) / rows.max(1);
let i = self.top + k;
if k >= self.page || i >= n {
return false;
}
let double = self.last_click.is_some_and(|(at, j)| j == i && at.elapsed() < Duration::from_millis(400));
self.last_click = if double { None } else { Some((std::time::Instant::now(), i)) };
self.sel = i;
double
}
}
#[cfg(test)]
impl Tabs for u8 {
fn next(self) -> u8 {
(self + 1) % 3
}
fn prev(self) -> u8 {
(self + 2) % 3
}
}
#[test]
fn moving_about_a_list() {
let key = |c| KeyEvent::new(KeyCode::Char(c), KeyModifiers::NONE);
let mut nav = Nav::<u8> { page: 2, ..Default::default() };
(0..5).for_each(|_| _ = nav.key(key('j'), 3));
assert_eq!(nav.sel, 2);
assert!(nav.key(key('g'), 3) && nav.key(key('g'), 3) && nav.sel == 0);
assert!(nav.key(key('g'), 3) && nav.key(key('e'), 3) && nav.sel == 2);
assert!(nav.key(key('h'), 3) && (nav.tab, nav.sel) == (2, 0));
let esc = KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE);
assert!(nav.key(key('g'), 3) && nav.key(esc, 3) && nav.key(key('l'), 3) && nav.tab == 0);
assert!(!nav.key(key('x'), 3) && !nav.key(esc, 3));
assert!(nav.key(key('/'), 3) && nav.filter_key(key('a')) && nav.query == "a");
assert!(nav.filter_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE)) && !nav.filter_key(key('b')));
assert!(nav.key(esc, 3) && nav.query.is_empty());
nav.page = 4;
assert!(!nav.click(0, 4, 5, 3) && nav.sel == 1);
assert!(nav.click(0, 5, 5, 3));
nav.sel = 9;
nav.keep(10, 4);
assert_eq!(nav.top, 6);
}