use crate::input::{GbButton, InputState};
use crate::layout_engine::types::{DataContext, DataValue};
#[derive(Debug, Clone)]
pub struct TitleItem {
pub label: String,
pub enabled: bool,
}
impl TitleItem {
pub fn new(label: impl Into<String>) -> Self {
Self { label: label.into(), enabled: true }
}
pub fn with_enabled(label: impl Into<String>, enabled: bool) -> Self {
Self { label: label.into(), enabled }
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TitleEvent {
None,
Moved,
Selected(usize),
}
pub const DEFAULT_BLINK_PERIOD: u32 = 30;
#[derive(Debug, Clone)]
pub struct TitleMenu {
items: Vec<TitleItem>,
cursor: usize,
blink_period: u32,
blink_frame: u32,
}
impl TitleMenu {
pub fn new(items: Vec<TitleItem>) -> Self {
let mut menu = Self {
items,
cursor: 0,
blink_period: DEFAULT_BLINK_PERIOD,
blink_frame: 0,
};
if menu
.items
.first()
.map(|i| !i.enabled)
.unwrap_or(false)
{
if let Some(i) = menu.next_enabled(0, 1) {
menu.cursor = i;
}
}
menu
}
pub fn with_blink_period(mut self, frames: u32) -> Self {
self.blink_period = frames.max(1);
self
}
pub fn update(&mut self, input: &InputState) -> TitleEvent {
let cycle = (self.blink_period * 2).max(1);
self.blink_frame = (self.blink_frame + 1) % cycle;
if self.items.is_empty() {
return TitleEvent::None;
}
let up = input.is_just_pressed(GbButton::Up);
let down = input.is_just_pressed(GbButton::Down);
if up || down {
let dir = if up { -1 } else { 1 };
if let Some(next) = self.next_enabled(self.cursor, dir) {
if next != self.cursor {
self.cursor = next;
return TitleEvent::Moved;
}
}
}
if input.is_just_pressed(GbButton::A) || input.is_just_pressed(GbButton::Start) {
if self.items[self.cursor].enabled {
return TitleEvent::Selected(self.cursor);
}
}
TitleEvent::None
}
fn next_enabled(&self, from: usize, dir: i32) -> Option<usize> {
let n = self.items.len();
if n == 0 {
return None;
}
let mut i = from as i32;
for _ in 0..n {
i = (i + dir).rem_euclid(n as i32);
if self.items[i as usize].enabled {
return Some(i as usize);
}
}
None
}
pub fn cursor(&self) -> usize {
self.cursor
}
pub fn items(&self) -> &[TitleItem] {
&self.items
}
pub fn show_blink(&self) -> bool {
self.blink_frame < self.blink_period
}
pub fn write_context(&self, ctx: &mut DataContext) {
let labels: Vec<DataValue> = self
.items
.iter()
.map(|it| DataValue::from(it.label.as_str()))
.collect();
ctx.set("items", DataValue::List(labels));
ctx.set("cursor", self.cursor as i64);
ctx.set("show_blink", self.show_blink());
}
}
#[cfg(test)]
mod tests {
use super::*;
fn tap(menu: &mut TitleMenu, button: GbButton) -> TitleEvent {
let mut input = InputState::new();
input.begin_frame();
input.press(button);
menu.update(&input)
}
fn menu3() -> TitleMenu {
TitleMenu::new(vec![
TitleItem::new("新的征程"),
TitleItem::new("继续江湖"),
TitleItem::new("退出"),
])
}
#[test]
fn cursor_moves_and_wraps() {
let mut m = menu3();
assert_eq!(m.cursor(), 0);
assert_eq!(tap(&mut m, GbButton::Down), TitleEvent::Moved);
assert_eq!(m.cursor(), 1);
tap(&mut m, GbButton::Down);
assert_eq!(m.cursor(), 2);
assert_eq!(tap(&mut m, GbButton::Down), TitleEvent::Moved);
assert_eq!(m.cursor(), 0);
tap(&mut m, GbButton::Up);
assert_eq!(m.cursor(), 2);
}
#[test]
fn confirm_reports_selected_index() {
let mut m = menu3();
tap(&mut m, GbButton::Down); assert_eq!(tap(&mut m, GbButton::A), TitleEvent::Selected(1));
assert_eq!(tap(&mut m, GbButton::Start), TitleEvent::Selected(1));
}
#[test]
fn disabled_entries_are_skipped_and_unselectable() {
let mut m = TitleMenu::new(vec![
TitleItem::new("新的征程"),
TitleItem::with_enabled("继续江湖", false),
TitleItem::new("退出"),
]);
assert_eq!(m.cursor(), 0, "starts on the first enabled entry");
assert_eq!(tap(&mut m, GbButton::Down), TitleEvent::Moved);
assert_eq!(m.cursor(), 2, "Down skips the disabled entry");
assert_eq!(tap(&mut m, GbButton::A), TitleEvent::Selected(2));
}
#[test]
fn first_entry_disabled_seeks_forward() {
let m = TitleMenu::new(vec![
TitleItem::with_enabled("继续江湖", false),
TitleItem::new("新的征程"),
]);
assert_eq!(m.cursor(), 1, "cursor skips a disabled first entry");
}
#[test]
fn blink_toggles_over_the_period() {
let mut m = TitleMenu::new(vec![TitleItem::new("开始")]).with_blink_period(2);
assert!(m.show_blink());
let idle = InputState::new();
m.update(&idle); assert!(m.show_blink());
m.update(&idle); assert!(!m.show_blink());
m.update(&idle); assert!(!m.show_blink());
m.update(&idle); assert!(m.show_blink());
}
}