use super::{ActionMap, ActionMapError, ActionValue};
use crate::Pattern;
use crate::repeat::Held;
pub const TAP_TIME: f32 = 0.25;
pub const HOLD_TIME: f32 = 0.4;
pub const DOUBLE_TAP_WINDOW: f32 = 0.25;
fn is_duration(seconds: f32) -> bool {
seconds.is_finite() && seconds > 0.0
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Tap {
time: f32,
}
impl Tap {
#[must_use]
pub fn new(time: f32) -> Option<Self> {
is_duration(time).then_some(Self { time })
}
#[must_use]
pub const fn time(self) -> f32 {
self.time
}
}
impl Default for Tap {
fn default() -> Self {
Self { time: TAP_TIME }
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Hold {
time: f32,
}
impl Hold {
#[must_use]
pub fn new(time: f32) -> Option<Self> {
is_duration(time).then_some(Self { time })
}
#[must_use]
pub const fn time(self) -> f32 {
self.time
}
}
impl Default for Hold {
fn default() -> Self {
Self { time: HOLD_TIME }
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct DoubleTap {
tap_time: f32,
window: f32,
on_release: bool,
}
impl DoubleTap {
#[must_use]
pub fn new(tap_time: f32, window: f32) -> Option<Self> {
(is_duration(tap_time) && is_duration(window)).then_some(Self {
tap_time,
window,
on_release: false,
})
}
#[must_use]
pub const fn on_release(self) -> Self {
Self {
on_release: true,
..self
}
}
#[must_use]
pub const fn fires_on_release(self) -> bool {
self.on_release
}
#[must_use]
pub const fn tap_time(self) -> f32 {
self.tap_time
}
#[must_use]
pub const fn window(self) -> f32 {
self.window
}
}
impl Default for DoubleTap {
fn default() -> Self {
Self {
tap_time: TAP_TIME,
window: DOUBLE_TAP_WINDOW,
on_release: false,
}
}
}
#[derive(Clone, Copy, Debug, Default)]
enum Press {
#[default]
Up,
Down { at: f64, hold_fired: bool },
Spent,
Second { at: f64 },
}
#[derive(Clone, Copy, Debug)]
struct Pending {
released_at: f64,
tap: bool,
}
#[derive(Clone, Debug, Default)]
pub(crate) struct PatternState {
tap: Option<Tap>,
hold: Option<Hold>,
double_tap: Option<DoubleTap>,
press: Press,
pending: Option<Pending>,
tapped: bool,
hold_fired: bool,
double_tapped: bool,
pub(crate) emits: [Option<usize>; Pattern::ALL.len()],
}
impl PatternState {
pub(crate) const fn fired(&self) -> [bool; Pattern::ALL.len()] {
[self.tapped, self.hold_fired, self.double_tapped]
}
pub(crate) fn queue_emits(&self, before: [bool; Pattern::ALL.len()], queue: &mut Vec<usize>) {
for ((was, now), target) in before.into_iter().zip(self.fired()).zip(self.emits) {
if now && !was {
queue.extend(target);
}
}
}
pub(crate) fn clear_fired(&mut self) {
self.tapped = false;
self.hold_fired = false;
self.double_tapped = false;
}
pub(crate) fn cancel(&mut self) {
if !matches!(self.press, Press::Up) {
self.press = Press::Spent;
}
self.pending = None;
}
pub(crate) fn reset(&mut self) {
self.press = Press::Up;
self.pending = None;
self.clear_fired();
}
pub(crate) fn update(&mut self, value: &ActionValue, elapsed: f64) {
if let (Some(pending), Some(double_tap)) = (self.pending, self.double_tap)
&& elapsed - pending.released_at > f64::from(double_tap.window)
{
self.pending = None;
self.tapped |= pending.tap;
}
let down = Held::of(value).is_some();
match (self.press, down) {
(Press::Up, true) => {
self.press = if self.pending.take().is_some() {
if self.double_tap.is_some_and(DoubleTap::fires_on_release) {
Press::Second { at: elapsed }
} else {
self.double_tapped = true;
Press::Spent
}
} else {
Press::Down {
at: elapsed,
hold_fired: false,
}
};
}
(
Press::Down {
at,
hold_fired: false,
},
true,
) => {
if self
.hold
.is_some_and(|hold| elapsed - at >= f64::from(hold.time))
{
self.hold_fired = true;
self.press = Press::Down {
at,
hold_fired: true,
};
}
}
(Press::Down { at, hold_fired }, false) => {
self.press = Press::Up;
if hold_fired {
return;
}
let lasted = elapsed - at;
let tap = self.tap.is_some_and(|tap| lasted <= f64::from(tap.time));
let first_of_two = self
.double_tap
.is_some_and(|double_tap| lasted <= f64::from(double_tap.tap_time));
if first_of_two {
self.pending = Some(Pending {
released_at: elapsed,
tap,
});
} else {
self.tapped |= tap;
}
}
(Press::Second { at }, false) => {
self.press = Press::Up;
self.double_tapped |= self
.double_tap
.is_some_and(|double_tap| elapsed - at <= f64::from(double_tap.tap_time));
}
(Press::Spent, false) => self.press = Press::Up,
(Press::Up, false)
| (Press::Down { .. } | Press::Spent | Press::Second { .. }, true) => {}
}
}
fn hold_progress(&self, elapsed: f64) -> f32 {
match (self.press, self.hold) {
(
Press::Down {
hold_fired: true, ..
},
_,
) => 1.0,
(Press::Down { at, .. }, Some(hold)) => {
((elapsed - at) / f64::from(hold.time)).min(1.0) as f32
}
_ => 0.0,
}
}
}
impl ActionMap {
pub fn set_tap(&mut self, name: &str, tap: Option<Tap>) -> Result<(), ActionMapError> {
let patterns = self.patterns_mut(name)?;
patterns.tap = tap;
patterns.cancel();
Ok(())
}
pub fn set_hold(&mut self, name: &str, hold: Option<Hold>) -> Result<(), ActionMapError> {
let patterns = self.patterns_mut(name)?;
patterns.hold = hold;
patterns.cancel();
Ok(())
}
pub fn set_double_tap(
&mut self,
name: &str,
double_tap: Option<DoubleTap>,
) -> Result<(), ActionMapError> {
let patterns = self.patterns_mut(name)?;
patterns.double_tap = double_tap;
patterns.cancel();
Ok(())
}
pub fn cancel_patterns(&mut self, name: &str) -> Result<(), ActionMapError> {
self.patterns_mut(name)?.cancel();
Ok(())
}
#[must_use]
pub fn tap(&self, name: &str) -> Option<Tap> {
self.patterns(name)?.tap
}
#[must_use]
pub fn hold(&self, name: &str) -> Option<Hold> {
self.patterns(name)?.hold
}
#[must_use]
pub fn double_tap(&self, name: &str) -> Option<DoubleTap> {
self.patterns(name)?.double_tap
}
#[must_use]
pub fn tapped(&self, name: &str) -> bool {
self.patterns(name).is_some_and(|patterns| patterns.tapped)
}
#[must_use]
pub fn hold_fired(&self, name: &str) -> bool {
self.patterns(name)
.is_some_and(|patterns| patterns.hold_fired)
}
#[must_use]
pub fn double_tapped(&self, name: &str) -> bool {
self.patterns(name)
.is_some_and(|patterns| patterns.double_tapped)
}
#[must_use]
pub fn hold_progress(&self, name: &str) -> f32 {
self.patterns(name)
.map_or(0.0, |patterns| patterns.hold_progress(self.elapsed))
}
fn patterns(&self, name: &str) -> Option<&PatternState> {
let &idx = self.name_to_idx.get(name)?;
Some(&self.slots[idx].patterns)
}
fn patterns_mut(&mut self, name: &str) -> Result<&mut PatternState, ActionMapError> {
let Some(&idx) = self.name_to_idx.get(name) else {
return Err(ActionMapError::UnknownAction(name.to_owned()));
};
Ok(&mut self.slots[idx].patterns)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{ActionDecl, ActionKind, Binding, Repeat};
use crcbl_core::input::KeyCode;
const TICK: f32 = 0.0625;
const QUARTER: f32 = 0.25;
const KEY: KeyCode = KeyCode::Space;
fn map_with(attach: impl FnOnce(&mut ActionMap)) -> ActionMap {
let mut map = ActionMap::new();
map.declare(ActionDecl {
name: "act".to_owned(),
kind: ActionKind::Button,
bindings: vec![Binding::Key(KEY)],
});
attach(&mut map);
map
}
fn tap(time: f32) -> Option<Tap> {
Some(Tap::new(time).expect("a runnable tap"))
}
fn hold(time: f32) -> Option<Hold> {
Some(Hold::new(time).expect("a runnable hold"))
}
fn double_tap(tap_time: f32, window: f32) -> Option<DoubleTap> {
Some(DoubleTap::new(tap_time, window).expect("a runnable double tap"))
}
#[derive(Debug, Default, PartialEq)]
struct Fires {
tap: Vec<u32>,
hold: Vec<u32>,
double_tap: Vec<u32>,
}
fn fires(map: &mut ActionMap, edges: &[(u32, bool)], ticks: u32) -> Fires {
let mut fires = Fires::default();
for tick in 0..ticks {
map.begin_tick(TICK);
for &(_, down) in edges.iter().filter(|(at, _)| *at == tick) {
map.key_event(KEY, down);
}
if map.tapped("act") {
fires.tap.push(tick);
}
if map.hold_fired("act") {
fires.hold.push(tick);
}
if map.double_tapped("act") {
fires.double_tap.push(tick);
}
}
fires
}
fn taps(tap: Vec<u32>) -> Fires {
Fires {
tap,
..Fires::default()
}
}
#[test]
fn a_tap_fires_on_a_release_within_its_time() {
for (released, expected) in [(3, vec![3]), (4, vec![4]), (5, vec![])] {
let mut map = map_with(|map| map.set_tap("act", tap(QUARTER)).expect("declared"));
assert_eq!(
fires(&mut map, &[(0, true), (released, false)], 8),
taps(expected),
"released after {released} ticks",
);
}
}
#[test]
fn a_hold_fires_once_at_its_time_and_a_release_cancels_it() {
let mut map = map_with(|map| map.set_hold("act", hold(QUARTER)).expect("declared"));
let held = fires(&mut map, &[(0, true)], 10);
assert_eq!(
held,
Fires {
hold: vec![4],
..Fires::default()
},
);
let mut map = map_with(|map| map.set_hold("act", hold(QUARTER)).expect("declared"));
let let_go = fires(&mut map, &[(0, true), (3, false)], 10);
assert_eq!(let_go, Fires::default(), "released one tick short");
}
#[test]
fn hold_progress_fills_to_one_and_empties_on_release() {
let mut map = map_with(|map| map.set_hold("act", hold(QUARTER)).expect("declared"));
map.begin_tick(TICK);
assert_eq!(map.hold_progress("act"), 0.0, "nothing down");
map.key_event(KEY, true);
let mut progress = vec![map.hold_progress("act")];
for _ in 0..5 {
map.begin_tick(TICK);
progress.push(map.hold_progress("act"));
}
assert_eq!(progress, [0.0, 0.25, 0.5, 0.75, 1.0, 1.0]);
map.key_event(KEY, false);
assert_eq!(map.hold_progress("act"), 0.0);
assert_eq!(map.hold_progress("nope"), 0.0);
}
#[test]
fn a_press_that_held_does_not_tap() {
let mut map = map_with(|map| {
map.set_tap("act", tap(0.5)).expect("declared");
map.set_hold("act", hold(QUARTER)).expect("declared");
});
assert_eq!(
fires(&mut map, &[(0, true), (6, false)], 10),
Fires {
hold: vec![4],
..Fires::default()
},
"released at 0.375 s, inside the tap's 0.5 s",
);
}
#[test]
fn a_release_on_the_hold_tick_is_a_hold() {
let mut map = map_with(|map| {
map.set_tap("act", tap(QUARTER)).expect("declared");
map.set_hold("act", hold(QUARTER)).expect("declared");
});
assert_eq!(
fires(&mut map, &[(0, true), (4, false)], 8),
Fires {
hold: vec![4],
..Fires::default()
},
);
}
#[test]
fn tap_and_hold_split_presses_by_length() {
for (released, expected) in [
(3, taps(vec![3])),
(5, Fires::default()),
(
8,
Fires {
hold: vec![7],
..Fires::default()
},
),
] {
let mut map = map_with(|map| {
map.set_tap("act", Some(Tap::default())).expect("declared");
map.set_hold("act", Some(Hold::default()))
.expect("declared");
});
assert_eq!(
fires(&mut map, &[(0, true), (released, false)], 12),
expected,
"released after {released} ticks",
);
}
}
#[test]
fn a_double_tap_fires_on_a_second_press_inside_the_window() {
for (gap, expected) in [(3, vec![5]), (4, vec![6]), (5, vec![])] {
let mut map = map_with(|map| {
map.set_double_tap("act", double_tap(QUARTER, QUARTER))
.expect("declared");
});
let second = 2 + gap;
let fired = fires(
&mut map,
&[(0, true), (2, false), (second, true), (second + 1, false)],
14,
);
assert_eq!(
fired,
Fires {
double_tap: expected,
..Fires::default()
},
"second press {gap} ticks after the first release",
);
}
}
#[test]
fn a_long_first_press_does_not_start_a_double_tap() {
let mut map = map_with(|map| {
map.set_double_tap("act", double_tap(QUARTER, QUARTER))
.expect("declared");
});
let fired = fires(&mut map, &[(0, true), (5, false), (6, true)], 10);
assert_eq!(fired, Fires::default());
}
#[test]
fn a_tap_waits_for_the_double_tap_window() {
let attach = |map: &mut ActionMap| {
map.set_tap("act", tap(QUARTER)).expect("declared");
map.set_double_tap("act", double_tap(QUARTER, QUARTER))
.expect("declared");
};
let mut map = map_with(attach);
let single = fires(&mut map, &[(0, true), (2, false)], 10);
assert_eq!(
single,
taps(vec![7]),
"released at tick 2, the window closes after tick 6",
);
let mut map = map_with(attach);
let double = fires(
&mut map,
&[(0, true), (2, false), (4, true), (5, false)],
12,
);
assert_eq!(
double,
Fires {
double_tap: vec![4],
..Fires::default()
},
);
}
#[test]
fn a_context_push_cancels_the_press_in_progress() {
let mut map = map_with(|map| {
map.set_tap("act", tap(QUARTER)).expect("declared");
map.set_hold("act", hold(QUARTER)).expect("declared");
});
map.declare_in(
"menu",
ActionDecl {
name: "confirm".to_owned(),
kind: ActionKind::Button,
bindings: vec![Binding::Key(KEY)],
},
);
map.begin_tick(TICK);
map.key_event(KEY, true);
map.begin_tick(TICK);
map.push_context("menu").expect("declared");
assert!(!map.button_held("act"), "the menu took the key");
assert!(!map.tapped("act"), "a press taken away is not a tap");
for _ in 0..6 {
map.begin_tick(TICK);
assert!(!map.hold_fired("act"));
}
}
#[test]
fn a_context_switch_spends_a_press_it_does_not_take() {
let mut map = map_with(|map| {
map.set_tap("act", tap(QUARTER)).expect("declared");
map.set_hold("act", hold(QUARTER)).expect("declared");
});
map.declare_in(
"menu",
ActionDecl {
name: "other".to_owned(),
kind: ActionKind::Button,
bindings: vec![Binding::Key(KeyCode::Enter)],
},
);
map.begin_tick(TICK);
map.key_event(KEY, true);
map.push_context("menu").expect("declared");
assert!(map.button_held("act"), "the key is still the action's");
let mut fired = false;
for _ in 0..6 {
map.begin_tick(TICK);
fired |= map.hold_fired("act");
}
map.key_event(KEY, false);
fired |= map.tapped("act");
assert!(!fired, "neither a hold nor a tap");
}
#[test]
fn a_context_switch_drops_a_waiting_first_tap() {
let mut map = map_with(|map| {
map.set_double_tap("act", double_tap(QUARTER, QUARTER))
.expect("declared");
});
map.declare_in(
"menu",
ActionDecl {
name: "other".to_owned(),
kind: ActionKind::Button,
bindings: vec![Binding::Key(KeyCode::Enter)],
},
);
map.begin_tick(TICK);
map.key_event(KEY, true);
map.begin_tick(TICK);
map.key_event(KEY, false);
map.push_context("menu").expect("declared");
map.pop_context("menu").expect("on top");
map.begin_tick(TICK);
map.key_event(KEY, true);
assert!(!map.double_tapped("act"));
}
#[test]
fn disabling_cancels_the_press() {
let mut map = map_with(|map| map.set_tap("act", tap(QUARTER)).expect("declared"));
map.begin_tick(TICK);
map.key_event(KEY, true);
map.set_enabled("act", false);
map.begin_tick(TICK);
map.key_event(KEY, false);
map.set_enabled("act", true);
map.begin_tick(TICK);
assert!(!map.tapped("act"), "released while disabled");
}
#[test]
fn attaching_mid_press_fires_nothing() {
let mut map = map_with(|_| {});
map.begin_tick(TICK);
map.key_event(KEY, true);
map.set_tap("act", tap(QUARTER)).expect("declared");
map.begin_tick(TICK);
map.key_event(KEY, false);
assert!(!map.tapped("act"));
assert_eq!(map.tap("act"), tap(QUARTER));
assert_eq!(map.hold("act"), None);
assert_eq!(map.double_tap("act"), None);
assert_eq!(
map.set_hold("nope", None),
Err(ActionMapError::UnknownAction("nope".to_owned())),
);
}
#[test]
fn a_fire_lasts_one_tick() {
let mut map = map_with(|map| map.set_tap("act", tap(QUARTER)).expect("declared"));
map.begin_tick(TICK);
map.key_event(KEY, true);
map.key_event(KEY, false);
assert!(map.tapped("act"));
map.begin_tick(TICK);
assert!(!map.tapped("act"));
}
fn repeat_pulses(map: &mut ActionMap) -> Vec<u32> {
let edges = [(0, true), (1, false), (3, true), (4, false), (6, true)];
let mut pulses = Vec::new();
for tick in 0..24 {
map.begin_tick(TICK);
for &(_, down) in edges.iter().filter(|(at, _)| *at == tick) {
map.key_event(KEY, down);
}
if map.repeated("act") {
pulses.push(tick);
}
}
pulses
}
#[test]
fn patterns_leave_repeat_alone() {
let mut bare = map_with(|map| map.set_repeat("act", Some(Repeat::UI)).expect("declared"));
let mut patterned = map_with(|map| {
map.set_repeat("act", Some(Repeat::UI)).expect("declared");
map.set_tap("act", Some(Tap::default())).expect("declared");
map.set_hold("act", Some(Hold::default()))
.expect("declared");
map.set_double_tap("act", Some(DoubleTap::default()))
.expect("declared");
});
let expected = repeat_pulses(&mut bare);
assert_eq!(expected[..4], [0, 3, 6, 14]);
assert_eq!(repeat_pulses(&mut patterned), expected);
}
#[test]
fn cancel_patterns_with_nothing_in_flight_spends_nothing() {
let mut map = map_with(|map| map.set_tap("act", tap(QUARTER)).expect("declared"));
map.begin_tick(TICK);
map.cancel_patterns("act").expect("declared");
map.key_event(KEY, true);
map.begin_tick(TICK);
map.key_event(KEY, false);
assert!(map.tapped("act"));
assert_eq!(
map.cancel_patterns("nope"),
Err(ActionMapError::UnknownAction("nope".to_owned())),
);
}
#[test]
fn cancel_patterns_drops_a_waiting_first_tap() {
let mut map = map_with(|map| {
map.set_tap("act", tap(QUARTER)).expect("declared");
map.set_double_tap("act", double_tap(QUARTER, QUARTER))
.expect("declared");
});
map.begin_tick(TICK);
map.key_event(KEY, true);
map.begin_tick(TICK);
map.key_event(KEY, false);
map.cancel_patterns("act").expect("declared");
let fired = fires(&mut map, &[(0, true)], 8);
assert_eq!(
fired,
Fires::default(),
"neither the double nor the dropped tap fires"
);
}
#[test]
fn cancel_patterns_spends_the_press_in_progress() {
let mut map = map_with(|map| {
map.set_tap("act", tap(QUARTER)).expect("declared");
map.set_hold("act", hold(QUARTER)).expect("declared");
});
map.begin_tick(TICK);
map.key_event(KEY, true);
map.cancel_patterns("act").expect("declared");
assert!(map.button_held("act"), "only the patterns are cancelled");
let mut fired = false;
for _ in 0..6 {
map.begin_tick(TICK);
fired |= map.hold_fired("act");
}
map.key_event(KEY, false);
fired |= map.tapped("act");
assert!(!fired, "neither a hold nor a tap");
assert_eq!(fires(&mut map, &[(0, true), (1, false)], 2), taps(vec![1]));
}
fn released_double(tap_time: f32, window: f32) -> Option<DoubleTap> {
double_tap(tap_time, window).map(DoubleTap::on_release)
}
fn doubles(double_tap: Vec<u32>) -> Fires {
Fires {
double_tap,
..Fires::default()
}
}
#[test]
fn an_on_release_double_tap_fires_on_the_second_release() {
for (gap, expected) in [(3, vec![7]), (4, vec![8]), (5, vec![])] {
let mut map = map_with(|map| {
map.set_double_tap("act", released_double(QUARTER, QUARTER))
.expect("declared");
});
let second = 2 + gap;
let fired = fires(
&mut map,
&[(0, true), (2, false), (second, true), (second + 2, false)],
16,
);
assert_eq!(
fired,
doubles(expected),
"second press {gap} ticks after the first release"
);
}
}
#[test]
fn an_on_release_double_tap_needs_a_short_second_press() {
for (held, expected) in [(4, vec![7]), (5, vec![])] {
let mut map = map_with(|map| {
map.set_tap("act", tap(QUARTER)).expect("declared");
map.set_hold("act", hold(QUARTER)).expect("declared");
map.set_double_tap("act", released_double(QUARTER, QUARTER))
.expect("declared");
});
let fired = fires(
&mut map,
&[(0, true), (2, false), (3, true), (3 + held, false)],
14,
);
assert_eq!(
fired,
doubles(expected),
"second press held {held} ticks: never a hold or a tap either",
);
}
}
#[test]
fn a_cancelled_second_press_does_not_fire_on_release() {
let mut map = map_with(|map| {
map.set_double_tap("act", released_double(QUARTER, QUARTER))
.expect("declared");
});
map.begin_tick(TICK);
map.key_event(KEY, true);
map.begin_tick(TICK);
map.key_event(KEY, false);
map.begin_tick(TICK);
map.key_event(KEY, true);
map.cancel_patterns("act").expect("declared");
map.begin_tick(TICK);
map.key_event(KEY, false);
assert!(!map.double_tapped("act"));
}
#[test]
fn a_double_tap_fires_on_the_press_unless_asked_otherwise() {
let press = DoubleTap::new(QUARTER, 0.5).expect("a runnable double tap");
assert!(!press.fires_on_release());
assert!(!DoubleTap::default().fires_on_release());
let release = press.on_release();
assert!(release.fires_on_release());
assert_eq!(
(release.tap_time(), release.window()),
(QUARTER, 0.5),
"same times"
);
assert_ne!(press, release);
}
#[test]
fn only_timeable_patterns_exist() {
for time in [0.0, -0.1, f32::NAN, f32::INFINITY] {
assert_eq!(Tap::new(time), None, "{time}");
assert_eq!(Hold::new(time), None, "{time}");
assert_eq!(DoubleTap::new(time, QUARTER), None, "{time}");
assert_eq!(DoubleTap::new(QUARTER, time), None, "{time}");
}
assert_eq!(Tap::new(TAP_TIME), Some(Tap::default()));
assert_eq!(Hold::new(HOLD_TIME), Some(Hold::default()));
assert_eq!(
DoubleTap::new(TAP_TIME, DOUBLE_TAP_WINDOW),
Some(DoubleTap::default()),
);
}
}