agb_eb_ext 0.25.3

AGB Extension methods
Documentation
use crate::direction::Direction;
use crate::menu_cursor::{CursorResult, MenuCursor};
use agb::fixnum::Vector2D;
use agb::input::ButtonController;
use core::ops::Range;

/// Cursor for vertically scrolling menu
///
/// Use [`MenuCursor`] if all options are on screen at once
///
/// # Usage
/// Draw every item in each column for the rows in [`visible_rows`](ScrollingCursor::visible_rows),
/// and draw the cursor at [`screen_row`](ScrollingCursor::screen_row)
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct ScrollingCursor {
    cursor: MenuCursor,
    first_visible_row: u8,
    visible_row_count: u8,
}

impl ScrollingCursor {
    /// Create a cursor at the first item with the first `visible_row_count` rows showing
    ///
    /// Panics if any argument is 0
    pub fn new(column_count: u8, item_count: u8, visible_row_count: u8) -> Self {
        assert!(
            visible_row_count > 0,
            "ScrollingCursor created with no visible rows"
        );
        Self {
            cursor: MenuCursor::new(column_count, item_count),
            first_visible_row: 0,
            visible_row_count,
        }
    }

    fn update_first_row(&mut self) {
        let (_, row) = self.cursor.pos();
        let end = self.first_visible_row as u16 + self.visible_row_count as u16;
        if row < self.first_visible_row {
            self.first_visible_row = row;
        } else if row as u16 >= end {
            self.first_visible_row = row - (self.visible_row_count - 1);
        }
    }

    /// Underlying grid cursor
    #[inline]
    pub fn cursor(&self) -> &MenuCursor {
        &self.cursor
    }

    /// Index of selected item
    #[inline]
    pub fn idx(&self) -> usize {
        self.cursor.idx()
    }

    /// Selected `(column, row)` in the whole list
    #[inline]
    pub fn pos(&self) -> (u8, u8) {
        self.cursor.pos()
    }

    /// Selected `(column, row)` in the whole list as `usize`
    #[inline]
    pub fn pos_usize(&self) -> (usize, usize) {
        self.cursor.pos_usize()
    }

    /// Selected `(column, row)` in the whole list as a vector
    #[inline]
    pub fn vec_pos(&self) -> Vector2D<i32> {
        self.cursor.vec_pos()
    }

    /// First row currently on screen
    #[inline]
    pub fn first_visible_row(&self) -> u8 {
        self.first_visible_row
    }

    /// Last row currently on screen (inclusive, clamped to the last row of items)
    #[inline]
    pub fn last_visible_row(&self) -> u8 {
        (self.first_visible_row as u16 + self.visible_row_count as u16 - 1)
            .min(self.cursor.row_count() as u16 - 1) as u8
    }

    /// Maximum number of rows on screen at once
    #[inline]
    pub fn visible_row_count(&self) -> u8 {
        self.visible_row_count
    }

    /// Rows currently on screen (clamped to the rows that exist)
    #[inline]
    pub fn visible_rows(&self) -> Range<u8> {
        self.first_visible_row..self.last_visible_row() + 1
    }

    /// Row on screen the cursor should be drawn at, `0..visible_row_count`
    #[inline]
    pub fn screen_row(&self) -> u8 {
        self.cursor.pos().1 - self.first_visible_row
    }

    /// Select item `idx`, clamped to the last item, scrolling if needed
    #[inline]
    pub fn set_idx(&mut self, idx: usize) {
        self.cursor.set_idx(idx);
        self.update_first_row();
    }

    /// Move the cursor based on any direction just pressed, scrolling if needed
    pub fn update(&mut self, button_controller: &ButtonController) -> CursorResult {
        match Direction::from_recent_input(button_controller) {
            Some(dir) => self.apply(dir),
            None => CursorResult::NoChange,
        }
    }

    /// Move the cursor in `dir`, as if that direction had just been pressed, scrolling if needed
    pub fn apply(&mut self, dir: Direction) -> CursorResult {
        let result = self.cursor.apply(dir);
        if result.is_moved() {
            self.update_first_row();
        }
        result
    }
}

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

    #[test_case]
    fn scrolls_to_keep_cursor_visible(_gba: &mut agb::Gba) {
        // 1 column, 10 items, 3 visible rows
        let mut cursor = ScrollingCursor::new(1, 10, 3);
        assert_eq!(cursor.first_visible_row(), 0);
        assert_eq!(cursor.visible_rows(), 0..3);
        cursor.set_idx(2);
        assert_eq!(cursor.first_visible_row(), 0);
        assert_eq!(cursor.screen_row(), 2);
        cursor.set_idx(5);
        assert_eq!(cursor.first_visible_row(), 3);
        assert_eq!(cursor.screen_row(), 2);
        cursor.set_idx(9);
        assert_eq!(cursor.first_visible_row(), 7);
        assert_eq!(cursor.last_visible_row(), 9);
        cursor.set_idx(7);
        assert_eq!(cursor.first_visible_row(), 7);
        assert_eq!(cursor.screen_row(), 0);
        cursor.set_idx(0);
        assert_eq!(cursor.first_visible_row(), 0);
    }

    #[test_case]
    fn apply_scrolls(_gba: &mut agb::Gba) {
        // 2 columns, 7 items (4 rows), 2 visible rows
        let mut cursor = ScrollingCursor::new(2, 7, 2);
        assert_eq!(cursor.apply(Direction::Down), CursorResult::Moved);
        assert_eq!(cursor.first_visible_row(), 0);
        assert_eq!(cursor.apply(Direction::Down), CursorResult::Moved);
        assert_eq!(cursor.first_visible_row(), 1);
        assert_eq!(cursor.apply(Direction::Right), CursorResult::Moved);
        assert_eq!(cursor.apply(Direction::Down), CursorResult::Invalid);
        assert_eq!(cursor.first_visible_row(), 1);
        assert_eq!(cursor.apply(Direction::Left), CursorResult::Moved);
        assert_eq!(cursor.apply(Direction::Down), CursorResult::Moved);
        assert_eq!(cursor.idx(), 6);
        assert_eq!(cursor.first_visible_row(), 2);
        assert_eq!(cursor.visible_rows(), 2..4);
    }

    #[test_case]
    fn no_overflow_near_end_of_long_list(_gba: &mut agb::Gba) {
        let mut cursor = ScrollingCursor::new(1, 255, 3);
        cursor.set_idx(254);
        assert_eq!(cursor.first_visible_row(), 252);
        assert_eq!(cursor.apply(Direction::Down), CursorResult::Invalid);
        assert_eq!(cursor.visible_rows(), 252..255);
        assert_eq!(cursor.screen_row(), 2);
    }

    #[test_case]
    fn visible_rows_clamped_to_items(_gba: &mut agb::Gba) {
        let cursor = ScrollingCursor::new(1, 2, 5);
        assert_eq!(cursor.visible_rows(), 0..2);
        assert_eq!(cursor.last_visible_row(), 1);
    }
}