use super::{ActionMap, ActionMapError, ActionValue, ButtonState};
pub const REPEAT_DELAY: f32 = 0.5;
pub const REPEAT_INTERVAL: f32 = 0.1;
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Repeat {
delay: f32,
interval: f32,
}
impl Repeat {
pub const UI: Self = Self {
delay: REPEAT_DELAY,
interval: REPEAT_INTERVAL,
};
#[must_use]
pub fn new(delay: f32, interval: f32) -> Option<Self> {
let valid = delay.is_finite() && delay >= 0.0 && interval.is_finite() && interval > 0.0;
valid.then_some(Self { delay, interval })
}
#[must_use]
pub const fn delay(self) -> f32 {
self.delay
}
#[must_use]
pub const fn interval(self) -> f32 {
self.interval
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum Cardinal {
Up,
Down,
Left,
Right,
}
impl Cardinal {
#[must_use]
pub fn of(x: f32, y: f32) -> Option<Self> {
if x == 0.0 && y == 0.0 {
None
} else if x.abs() > y.abs() {
Some(if x > 0.0 { Self::Right } else { Self::Left })
} else {
Some(if y > 0.0 { Self::Up } else { Self::Down })
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum Held {
Down,
Positive,
Negative,
Toward(Cardinal),
}
impl Held {
pub(crate) fn of(value: &ActionValue) -> Option<Self> {
match value {
ActionValue::Button(button) => matches!(
button.state,
ButtonState::Pressed | ButtonState::Held { .. }
)
.then_some(Self::Down),
ActionValue::Axis1(axis) if axis.value > 0.0 => Some(Self::Positive),
ActionValue::Axis1(axis) if axis.value < 0.0 => Some(Self::Negative),
ActionValue::Axis1(_) => None,
ActionValue::Axis2(axis) => Cardinal::of(axis.x, axis.y).map(Self::Toward),
}
}
}
#[derive(Clone, Debug)]
pub(crate) struct RepeatState {
pattern: Repeat,
held: Option<Held>,
next_at: f64,
pub(crate) fired: bool,
}
impl RepeatState {
pub(crate) fn reset(&mut self) {
self.held = None;
self.fired = false;
}
pub(crate) fn update(&mut self, value: &ActionValue, elapsed: f64) {
let now = Held::of(value);
if now.is_none() {
self.held = None;
return;
}
if now != self.held {
self.held = now;
self.fired = true;
self.next_at = elapsed + f64::from(self.pattern.delay);
return;
}
if elapsed >= self.next_at {
self.fired = true;
let interval = f64::from(self.pattern.interval);
let missed = ((elapsed - self.next_at) / interval).floor() + 1.0;
self.next_at += missed * interval;
if self.next_at <= elapsed {
self.next_at += interval;
}
}
}
}
impl ActionMap {
pub fn set_repeat(&mut self, name: &str, repeat: Option<Repeat>) -> Result<(), ActionMapError> {
let Some(&idx) = self.name_to_idx.get(name) else {
return Err(ActionMapError::UnknownAction(name.to_owned()));
};
let elapsed = self.elapsed;
let slot = &mut self.slots[idx];
slot.repeat = repeat.map(|pattern| RepeatState {
pattern,
held: Held::of(&slot.value),
next_at: elapsed + f64::from(pattern.delay),
fired: false,
});
Ok(())
}
#[must_use]
pub fn repeat(&self, name: &str) -> Option<Repeat> {
let &idx = self.name_to_idx.get(name)?;
self.slots[idx].repeat.as_ref().map(|state| state.pattern)
}
#[must_use]
pub fn repeated(&self, name: &str) -> bool {
self.name_to_idx
.get(name)
.and_then(|&idx| self.slots[idx].repeat.as_ref())
.is_some_and(|state| state.fired)
}
#[must_use]
pub fn cardinal(&self, name: &str) -> Option<Cardinal> {
let (x, y) = self.axis2(name);
Cardinal::of(x, y)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{ActionDecl, ActionKind, Binding};
use crcbl_core::input::KeyCode;
const TICK: f32 = 0.05;
fn map_with_repeat(kind: ActionKind, bindings: Vec<Binding>) -> ActionMap {
let mut map = ActionMap::new();
map.declare(ActionDecl {
name: "nav".to_owned(),
kind,
bindings,
});
map.set_repeat("nav", Some(Repeat::UI)).expect("declared");
map
}
fn pulses(map: &mut ActionMap, press: impl FnOnce(&mut ActionMap), ticks: u32) -> Vec<u32> {
let mut fired = Vec::new();
map.begin_tick(TICK);
press(map);
for tick in 0..ticks {
if tick > 0 {
map.begin_tick(TICK);
}
if map.repeated("nav") {
fired.push(tick);
}
}
fired
}
#[test]
fn a_held_button_repeats_after_the_delay_then_at_the_interval() {
let mut map = map_with_repeat(ActionKind::Button, vec![Binding::Key(KeyCode::Enter)]);
let fired = pulses(&mut map, |map| map.key_event(KeyCode::Enter, true), 17);
assert_eq!(fired, [0, 10, 12, 14, 16]);
}
#[test]
fn the_schedule_holds_at_sixty_hertz() {
let mut map = map_with_repeat(ActionKind::Button, vec![Binding::Key(KeyCode::Enter)]);
let mut fired = Vec::new();
map.begin_tick(1.0 / 60.0);
map.key_event(KeyCode::Enter, true);
for tick in 0..50 {
if tick > 0 {
map.begin_tick(1.0 / 60.0);
}
if map.repeated("nav") {
fired.push(tick);
}
}
assert_eq!(fired, [0, 30, 36, 42, 48]);
}
#[test]
fn a_release_ends_the_schedule() {
let mut map = map_with_repeat(ActionKind::Button, vec![Binding::Key(KeyCode::Enter)]);
let _ = pulses(&mut map, |map| map.key_event(KeyCode::Enter, true), 8);
map.key_event(KeyCode::Enter, false);
let fired = pulses(&mut map, |map| map.key_event(KeyCode::Enter, true), 11);
assert_eq!(
fired,
[0, 10],
"a fresh delay, not the old schedule's remainder"
);
}
#[test]
fn a_long_tick_pulses_once_and_leaves_no_backlog() {
let mut map = map_with_repeat(ActionKind::Button, vec![Binding::Key(KeyCode::Enter)]);
map.begin_tick(TICK);
map.key_event(KeyCode::Enter, true);
map.begin_tick(2.03);
assert!(map.repeated("nav"), "the long tick pulses");
map.begin_tick(TICK);
assert!(
!map.repeated("nav"),
"2.08 s: the missed repeats are gone rather than queued",
);
map.begin_tick(TICK);
assert!(map.repeated("nav"), "2.13 s: and the schedule carries on");
}
#[test]
fn a_direction_change_restarts_the_schedule() {
let wasd = Binding::Wasd {
up: KeyCode::KeyW,
down: KeyCode::KeyS,
left: KeyCode::KeyA,
right: KeyCode::KeyD,
};
let mut map = map_with_repeat(ActionKind::Axis2, vec![wasd]);
let _ = pulses(&mut map, |map| map.key_event(KeyCode::KeyD, true), 6);
assert_eq!(map.cardinal("nav"), Some(Cardinal::Right));
map.begin_tick(TICK);
map.key_event(KeyCode::KeyS, true);
assert_eq!(
map.cardinal("nav"),
Some(Cardinal::Down),
"a diagonal's tie goes to the vertical",
);
assert!(map.repeated("nav"), "turning is a new press");
let mut fired = Vec::new();
for tick in 1..11 {
map.begin_tick(TICK);
if map.repeated("nav") {
fired.push(tick);
}
}
assert_eq!(fired, [10], "the delay restarted at the turn");
}
#[test]
fn a_one_dimensional_axis_repeats_by_sign() {
let mut map = map_with_repeat(
ActionKind::Axis1,
vec![Binding::KeyAxis {
negative: KeyCode::KeyQ,
positive: KeyCode::KeyE,
}],
);
map.begin_tick(TICK);
map.key_event(KeyCode::KeyE, true);
assert!(map.repeated("nav"));
map.begin_tick(TICK);
map.key_event(KeyCode::KeyE, false);
map.key_event(KeyCode::KeyQ, true);
assert!(map.repeated("nav"), "the other sign is another press");
}
#[test]
fn only_runnable_schedules_exist() {
assert_eq!(Repeat::new(0.5, 0.1), Some(Repeat::UI));
assert!(Repeat::new(0.0, 0.1).is_some(), "no delay is a schedule");
for (delay, interval) in [
(0.5, 0.0),
(0.5, -1.0),
(-0.1, 0.1),
(f32::NAN, 0.1),
(0.5, f32::INFINITY),
] {
assert_eq!(Repeat::new(delay, interval), None, "({delay}, {interval})");
}
}
#[test]
fn attaching_does_not_pulse_and_no_schedule_never_does() {
let mut map = ActionMap::new();
map.declare(ActionDecl {
name: "nav".to_owned(),
kind: ActionKind::Button,
bindings: vec![Binding::Key(KeyCode::Enter)],
});
map.key_event(KeyCode::Enter, true);
assert!(!map.repeated("nav"), "no schedule");
map.set_repeat("nav", Some(Repeat::UI)).expect("declared");
assert!(!map.repeated("nav"));
assert_eq!(map.repeat("nav"), Some(Repeat::UI));
assert_eq!(
map.set_repeat("nope", None),
Err(ActionMapError::UnknownAction("nope".to_owned())),
);
}
}