aven 0.1.12

Local-first task manager CLI and sync server
Documentation
use crossterm::event::{KeyCode, KeyEvent};

use crate::tui::navigation::scroll_with_delta;

pub(crate) struct ScrollState {
    pub(crate) scroll: u16,
    pub(crate) cap: u16,
}

pub(crate) enum ScrollKeyOutcome {
    Cancelled,
    Continue(ScrollState),
    Ignored,
}

pub(crate) fn handle_scroll_key(
    key: KeyEvent,
    state: ScrollState,
    close_keys: &[KeyCode],
    page_rows: u16,
) -> ScrollKeyOutcome {
    if close_keys.contains(&key.code) {
        return ScrollKeyOutcome::Cancelled;
    }

    let delta = match key.code {
        KeyCode::Char('j') | KeyCode::Down => Some(1),
        KeyCode::Char('k') | KeyCode::Up => Some(-1),
        KeyCode::PageDown => Some(page_rows as isize),
        KeyCode::PageUp => Some(-(page_rows as isize)),
        _ => None,
    };

    match delta {
        Some(delta) => ScrollKeyOutcome::Continue(ScrollState {
            scroll: scroll_with_delta(state.scroll, delta, state.cap),
            cap: state.cap,
        }),
        None => ScrollKeyOutcome::Ignored,
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crossterm::event::KeyModifiers;

    fn key(code: KeyCode) -> KeyEvent {
        KeyEvent::new(code, KeyModifiers::NONE)
    }

    #[test]
    fn close_key_cancels() {
        let result = handle_scroll_key(
            key(KeyCode::Esc),
            ScrollState { scroll: 5, cap: 10 },
            &[KeyCode::Esc, KeyCode::Enter],
            0,
        );
        assert!(matches!(result, ScrollKeyOutcome::Cancelled));
    }

    #[test]
    fn j_key_scrolls_down() {
        let result = handle_scroll_key(
            key(KeyCode::Char('j')),
            ScrollState { scroll: 0, cap: 10 },
            &[KeyCode::Esc],
            0,
        );
        assert!(matches!(result, ScrollKeyOutcome::Continue(s) if s.scroll == 1));
    }

    #[test]
    fn k_key_scrolls_up() {
        let result = handle_scroll_key(
            key(KeyCode::Char('k')),
            ScrollState { scroll: 3, cap: 10 },
            &[KeyCode::Esc],
            0,
        );
        assert!(matches!(result, ScrollKeyOutcome::Continue(s) if s.scroll == 2));
    }

    #[test]
    fn ignored_key_returns_ignored() {
        let result = handle_scroll_key(
            key(KeyCode::Char('z')),
            ScrollState { scroll: 0, cap: 10 },
            &[KeyCode::Esc],
            0,
        );
        assert!(matches!(result, ScrollKeyOutcome::Ignored));
    }

    #[test]
    fn scroll_caps_at_limit() {
        let result = handle_scroll_key(
            key(KeyCode::Char('j')),
            ScrollState {
                scroll: 10,
                cap: 10,
            },
            &[KeyCode::Esc],
            0,
        );
        assert!(matches!(result, ScrollKeyOutcome::Continue(s) if s.scroll == 10));
    }

    #[test]
    fn page_down_with_zero_rows_is_ignored() {
        let result = handle_scroll_key(
            key(KeyCode::PageDown),
            ScrollState { scroll: 0, cap: 10 },
            &[KeyCode::Esc],
            0,
        );
        assert!(matches!(result, ScrollKeyOutcome::Continue(s) if s.scroll == 0));
    }
}