agb_eb_ext 0.25.0

AGB Extension methods
use crate::direction::Direction;
use agb::fixnum::{Vector2D, vec2};
use agb::input::ButtonController;

/// Cursor for grid/list menu with every option on screen
///
/// Use [`ScrollingCursor`](crate::scrolling_cursor::ScrollingCursor) if menu scrolls
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct MenuCursor {
    idx: u8,
    x: u8,
    y: u8,
    max_x: u8,
    total: u8,
}

#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub enum CursorResult {
    NoChange,
    Invalid,
    Moved,
}

impl MenuCursor {
    pub fn new(column_count: u8, count: u8) -> Self {
        assert!(
            column_count > 0,
            "MenuCursor created with no columns {}, {}",
            column_count,
            count
        );
        assert!(count > 0, "MenuCursor created with no items");
        Self {
            idx: 0,
            x: 0,
            y: 0,
            max_x: column_count,
            total: count,
        }
    }
}

impl MenuCursor {
    #[inline]
    pub fn idx(&self) -> usize {
        self.idx as usize
    }

    #[inline]
    pub fn pos(&self) -> (u8, u8) {
        (self.x, self.y)
    }

    #[inline]
    pub fn vec_pos(&self) -> Vector2D<i32> {
        vec2(self.x as i32, self.y as i32)
    }

    #[inline]
    pub fn pos_usize(&self) -> (usize, usize) {
        (self.x as usize, self.y as usize)
    }

    pub fn set_idx(&mut self, idx: usize) {
        self.idx = idx.min((self.total - 1) as usize) as u8;
        let mut x = self.idx;
        let mut y = 0;
        while x >= self.max_x {
            x -= self.max_x;
            y += 1;
        }
        self.x = x;
        self.y = y;
    }

    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
    pub fn apply(&mut self, dir: Direction) -> CursorResult {
        match dir {
            Direction::Up => {
                if self.y > 0 {
                    self.y -= 1;
                    self.idx -= self.max_x;
                    CursorResult::Moved
                } else {
                    CursorResult::Invalid
                }
            }
            Direction::Down => {
                if (self.idx as u16 + self.max_x as u16) < self.total as u16 {
                    self.y += 1;
                    self.idx += self.max_x;
                    CursorResult::Moved
                } else {
                    CursorResult::Invalid
                }
            }
            Direction::Left => {
                if self.x > 0 {
                    self.x -= 1;
                    self.idx -= 1;
                    CursorResult::Moved
                } else {
                    CursorResult::Invalid
                }
            }
            Direction::Right => {
                if (self.x + 1 < self.max_x) && (self.idx + 1 < self.total) {
                    self.x += 1;
                    self.idx += 1;
                    CursorResult::Moved
                } else {
                    CursorResult::Invalid
                }
            }
        }
    }
}

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

    #[test_case]
    fn moves_within_grid(_gba: &mut agb::Gba) {
        let mut cursor = MenuCursor::new(3, 9);
        assert_eq!(cursor.apply(Direction::Up), CursorResult::Invalid);
        assert_eq!(cursor.apply(Direction::Left), CursorResult::Invalid);
        assert_eq!(cursor.apply(Direction::Right), CursorResult::Moved);
        assert_eq!(cursor.pos(), (1, 0));
        assert_eq!(cursor.apply(Direction::Down), CursorResult::Moved);
        assert_eq!(cursor.idx(), 4);
        assert_eq!(cursor.vec_pos(), vec2(1, 1));
        assert_eq!(cursor.apply(Direction::Up), CursorResult::Moved);
        assert_eq!(cursor.apply(Direction::Left), CursorResult::Moved);
        assert_eq!(cursor.pos(), (0, 0));
    }

    #[test_case]
    fn blocked_on_partial_last_row(_gba: &mut agb::Gba) {
        // 3 columns, 8 items: last row only has 2 entries
        let mut cursor = MenuCursor::new(3, 8);
        cursor.set_idx(5);
        assert_eq!(cursor.apply(Direction::Down), CursorResult::Invalid);
        cursor.set_idx(4);
        assert_eq!(cursor.apply(Direction::Down), CursorResult::Moved);
        assert_eq!(cursor.idx(), 7);
        assert_eq!(cursor.apply(Direction::Right), CursorResult::Invalid);
    }

    #[test_case]
    fn no_overflow_on_large_grid(_gba: &mut agb::Gba) {
        // idx + max_x exceeds u8::MAX; must report Invalid, not panic or wrap
        let mut cursor = MenuCursor::new(128, 255);
        cursor.set_idx(200);
        assert_eq!(cursor.apply(Direction::Down), CursorResult::Invalid);
        assert_eq!(cursor.idx(), 200);
    }

    #[test_case]
    fn set_idx_clamps(_gba: &mut agb::Gba) {
        let mut cursor = MenuCursor::new(3, 9);
        cursor.set_idx(500);
        assert_eq!(cursor.idx(), 8);
        assert_eq!(cursor.pos(), (2, 2));
        cursor.set_idx(4);
        assert_eq!(cursor.pos_usize(), (1, 1));
    }
}