use crate::direction::Direction;
use agb::fixnum::{Vector2D, vec2};
use agb::input::ButtonController;
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct MenuCursor {
idx: u8,
x: u8,
y: u8,
column_count: u8,
row_count: u8,
item_count: u8,
can_wrap: bool,
}
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub enum CursorResult {
NoChange,
Invalid,
Moved,
}
impl CursorResult {
#[inline]
pub const fn is_moved(self) -> bool {
matches!(self, CursorResult::Moved)
}
}
impl MenuCursor {
pub fn new(column_count: u8, item_count: u8) -> Self {
assert!(column_count > 0, "MenuCursor created with no columns");
assert!(item_count > 0, "MenuCursor created with no items");
let mut row_count = 0;
let mut remaining = item_count;
while remaining > 0 {
remaining = remaining.saturating_sub(column_count);
row_count += 1;
}
Self {
idx: 0,
x: 0,
y: 0,
column_count,
row_count,
item_count,
can_wrap: false,
}
}
pub fn with_wrapping(mut self) -> Self {
self.can_wrap = true;
self
}
pub fn set_wrapping(&mut self, can_wrap: bool) {
self.can_wrap = can_wrap;
}
#[inline]
pub fn can_wrap(&self) -> bool {
self.can_wrap
}
#[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)
}
#[inline]
pub fn column_count(&self) -> u8 {
self.column_count
}
#[inline]
pub fn row_count(&self) -> u8 {
self.row_count
}
#[inline]
pub fn item_count(&self) -> u8 {
self.item_count
}
#[inline]
pub fn is_first(&self) -> bool {
self.idx == 0
}
#[inline]
pub fn is_last(&self) -> bool {
self.idx + 1 == self.item_count
}
pub fn set_idx(&mut self, idx: usize) {
self.idx = idx.min((self.item_count - 1) as usize) as u8;
let mut x = self.idx;
let mut y = 0;
while x >= self.column_count {
x -= self.column_count;
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,
}
}
pub fn apply(&mut self, dir: Direction) -> CursorResult {
match dir {
Direction::Up => {
if self.y > 0 {
self.y -= 1;
self.idx -= self.column_count;
CursorResult::Moved
} else if self.can_wrap && self.row_count > 1 {
let mut idx = self.idx as u16;
let mut y = self.y;
while idx + (self.column_count as u16) < self.item_count as u16 {
idx += self.column_count as u16;
y += 1;
}
self.idx = idx as u8;
self.y = y;
CursorResult::Moved
} else {
CursorResult::Invalid
}
}
Direction::Down => {
if (self.idx as u16 + self.column_count as u16) < self.item_count as u16 {
self.y += 1;
self.idx += self.column_count;
CursorResult::Moved
} else if self.can_wrap && self.y > 0 {
self.idx = self.x;
self.y = 0;
CursorResult::Moved
} else {
CursorResult::Invalid
}
}
Direction::Left => {
if self.x > 0 {
self.x -= 1;
self.idx -= 1;
CursorResult::Moved
} else if self.can_wrap && self.column_count > 1 && self.item_count > 1 {
let row_end = self.idx as u16 + self.column_count as u16 - 1;
let last = (self.item_count as u16 - 1).min(row_end);
self.x = (last - self.idx as u16) as u8;
self.idx = last as u8;
if self.x == 0 {
CursorResult::Invalid
} else {
CursorResult::Moved
}
} else {
CursorResult::Invalid
}
}
Direction::Right => {
if (self.x + 1 < self.column_count) && (self.idx + 1 < self.item_count) {
self.x += 1;
self.idx += 1;
CursorResult::Moved
} else if self.can_wrap && self.x > 0 {
self.idx -= self.x;
self.x = 0;
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!(cursor.is_first());
assert_eq!(cursor.apply(Direction::Up), CursorResult::Invalid);
assert_eq!(cursor.apply(Direction::Left), CursorResult::Invalid);
assert!(cursor.apply(Direction::Right).is_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) {
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!(cursor.is_last());
assert_eq!(cursor.apply(Direction::Right), CursorResult::Invalid);
}
#[test_case]
fn no_overflow_on_large_grid(_gba: &mut agb::Gba) {
let mut cursor = MenuCursor::new(128, 255);
cursor.set_idx(200);
assert_eq!(cursor.apply(Direction::Down), CursorResult::Invalid);
assert_eq!(cursor.idx(), 200);
cursor.set_idx(254);
assert!(cursor.is_last());
assert_eq!(cursor.apply(Direction::Right), CursorResult::Invalid);
}
#[test_case]
fn wraps_within_full_grid(_gba: &mut agb::Gba) {
let mut cursor = MenuCursor::new(3, 9).with_wrapping();
assert!(cursor.can_wrap());
assert_eq!(cursor.apply(Direction::Up), CursorResult::Moved);
assert_eq!(cursor.pos(), (0, 2));
assert_eq!(cursor.apply(Direction::Down), CursorResult::Moved);
assert_eq!(cursor.pos(), (0, 0));
assert_eq!(cursor.apply(Direction::Left), CursorResult::Moved);
assert_eq!(cursor.pos(), (2, 0));
assert_eq!(cursor.idx(), 2);
assert_eq!(cursor.apply(Direction::Right), CursorResult::Moved);
assert_eq!(cursor.pos(), (0, 0));
assert_eq!(cursor.idx(), 0);
}
#[test_case]
fn wraps_on_partial_last_row(_gba: &mut agb::Gba) {
let mut cursor = MenuCursor::new(3, 8).with_wrapping();
cursor.set_idx(2);
assert_eq!(cursor.apply(Direction::Up), CursorResult::Moved);
assert_eq!(cursor.idx(), 5);
assert_eq!(cursor.pos(), (2, 1));
assert_eq!(cursor.apply(Direction::Down), CursorResult::Moved);
assert_eq!(cursor.idx(), 2);
cursor.set_idx(6);
assert_eq!(cursor.apply(Direction::Left), CursorResult::Moved);
assert_eq!(cursor.idx(), 7);
assert_eq!(cursor.pos(), (1, 2));
assert_eq!(cursor.apply(Direction::Right), CursorResult::Moved);
assert_eq!(cursor.idx(), 6);
assert_eq!(cursor.pos(), (0, 2));
}
#[test_case]
fn wrap_invalid_when_nowhere_to_go(_gba: &mut agb::Gba) {
let mut cursor = MenuCursor::new(1, 1).with_wrapping();
assert_eq!(cursor.apply(Direction::Up), CursorResult::Invalid);
assert_eq!(cursor.apply(Direction::Down), CursorResult::Invalid);
assert_eq!(cursor.apply(Direction::Left), CursorResult::Invalid);
assert_eq!(cursor.apply(Direction::Right), CursorResult::Invalid);
let mut cursor = MenuCursor::new(1, 4).with_wrapping();
assert_eq!(cursor.apply(Direction::Left), CursorResult::Invalid);
assert_eq!(cursor.apply(Direction::Up), CursorResult::Moved);
assert_eq!(cursor.idx(), 3);
let mut cursor = MenuCursor::new(4, 4).with_wrapping();
assert_eq!(cursor.apply(Direction::Up), CursorResult::Invalid);
assert_eq!(cursor.apply(Direction::Left), CursorResult::Moved);
assert_eq!(cursor.idx(), 3);
let mut cursor = MenuCursor::new(128, 255).with_wrapping();
cursor.set_idx(200);
assert_eq!(cursor.apply(Direction::Down), CursorResult::Moved);
assert_eq!(cursor.idx(), 72);
cursor.set_idx(0);
assert_eq!(cursor.apply(Direction::Left), CursorResult::Moved);
assert_eq!(cursor.idx(), 127);
}
#[test_case]
fn no_wrap_by_default(_gba: &mut agb::Gba) {
let mut cursor = MenuCursor::new(3, 9);
assert!(!cursor.can_wrap());
cursor.set_wrapping(true);
assert_eq!(cursor.apply(Direction::Up), CursorResult::Moved);
cursor.set_wrapping(false);
assert_eq!(cursor.apply(Direction::Down), CursorResult::Invalid);
}
#[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));
}
#[test_case]
fn counts(_gba: &mut agb::Gba) {
assert_eq!(MenuCursor::new(3, 9).row_count(), 3);
assert_eq!(MenuCursor::new(3, 8).row_count(), 3);
assert_eq!(MenuCursor::new(3, 1).row_count(), 1);
assert_eq!(MenuCursor::new(1, 255).row_count(), 255);
assert_eq!(MenuCursor::new(255, 255).row_count(), 1);
let cursor = MenuCursor::new(4, 10);
assert_eq!(cursor.column_count(), 4);
assert_eq!(cursor.item_count(), 10);
}
}