Skip to main content

crcbl_input/
repeat.rs

1//! The repeat pattern: an action held down pulses once when it goes down and
2//! then again on a schedule — the key-repeat every menu list expects.
3//!
4//! # On the tick clock
5//!
6//! The schedule runs on the clock [`ActionMap::begin_tick`] advances, never on
7//! wall time, so a scripted input sequence repeats on the same ticks every run.
8//! A pulse is an edge like [`ActionMap::just_pressed`]: [`ActionMap::repeated`]
9//! is true for the rest of the tick it fired on, and `begin_tick` clears it. A
10//! tick long enough to cover several repeats pulses once, and the schedule
11//! moves past the time that tick covered rather than firing a backlog on the
12//! ticks after it.
13//!
14//! # What "held" means per kind
15//!
16//! A button is held while it is down. A 1-D axis is held while its value is
17//! non-zero, and changing sign is letting go and pressing again. A 2-D axis is
18//! held toward its [`Cardinal`]: the larger component decides, a tie goes to
19//! the vertical, and turning to another cardinal restarts the schedule with a
20//! pulse. That is Unity's `InputSystemUIInputModule` rule — its move direction
21//! is horizontal only when `|x| > |y|`, and a new direction resets its repeat
22//! count — so a keyboard diagonal navigates vertically there and here.
23
24use super::{ActionMap, ActionMapError, ActionValue, ButtonState};
25
26/// How long an action is held before its first repeat, in seconds.
27///
28/// Unity's `InputSystemUIInputModule` default (`m_MoveRepeatDelay = 0.5f`):
29/// long enough that a deliberate single press never repeats, short enough that
30/// holding to scroll a list does not feel stuck.
31pub const REPEAT_DELAY: f32 = 0.5;
32
33/// The time between repeats once they start, in seconds.
34///
35/// Unity's `InputSystemUIInputModule` default (`m_MoveRepeatRate = 0.1f`),
36/// which that module documents as the delay between repeated moves — ten steps
37/// a second.
38pub const REPEAT_INTERVAL: f32 = 0.1;
39
40/// A repeat schedule: the first repeat after `delay` seconds held, then one
41/// every `interval`.
42///
43/// The fields are private so the only schedules that exist are ones the
44/// evaluator can run: a zero or non-finite interval would repeat every tick
45/// forever, or never advance.
46#[derive(Clone, Copy, Debug, PartialEq)]
47pub struct Repeat {
48    delay: f32,
49    interval: f32,
50}
51
52impl Repeat {
53    /// [`REPEAT_DELAY`] then [`REPEAT_INTERVAL`]: the schedule the reserved
54    /// `ui` context's navigation actions carry.
55    pub const UI: Self = Self {
56        delay: REPEAT_DELAY,
57        interval: REPEAT_INTERVAL,
58    };
59
60    /// A schedule, or `None` unless `delay` is finite and not negative and
61    /// `interval` is finite and positive.
62    #[must_use]
63    pub fn new(delay: f32, interval: f32) -> Option<Self> {
64        let valid = delay.is_finite() && delay >= 0.0 && interval.is_finite() && interval > 0.0;
65        valid.then_some(Self { delay, interval })
66    }
67
68    /// Seconds held before the first repeat.
69    #[must_use]
70    pub const fn delay(self) -> f32 {
71        self.delay
72    }
73
74    /// Seconds between repeats.
75    #[must_use]
76    pub const fn interval(self) -> f32 {
77        self.interval
78    }
79}
80
81/// One of four directions, with +Y up as [`Binding::Wasd`](crate::Binding::Wasd)
82/// has it.
83#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
84pub enum Cardinal {
85    /// +Y.
86    Up,
87    /// −Y.
88    Down,
89    /// −X.
90    Left,
91    /// +X.
92    Right,
93}
94
95impl Cardinal {
96    /// The direction `(x, y)` points in, or `None` for the zero vector: the
97    /// larger component decides and a tie is vertical — see the module docs.
98    #[must_use]
99    pub fn of(x: f32, y: f32) -> Option<Self> {
100        if x == 0.0 && y == 0.0 {
101            None
102        } else if x.abs() > y.abs() {
103            Some(if x > 0.0 { Self::Right } else { Self::Left })
104        } else {
105            Some(if y > 0.0 { Self::Up } else { Self::Down })
106        }
107    }
108}
109
110/// What an action is held as, for the schedule: a change is a new press.
111#[derive(Clone, Copy, Debug, PartialEq, Eq)]
112pub(crate) enum Held {
113    Down,
114    Positive,
115    Negative,
116    Toward(Cardinal),
117}
118
119impl Held {
120    pub(crate) fn of(value: &ActionValue) -> Option<Self> {
121        match value {
122            ActionValue::Button(button) => matches!(
123                button.state,
124                ButtonState::Pressed | ButtonState::Held { .. }
125            )
126            .then_some(Self::Down),
127            ActionValue::Axis1(axis) if axis.value > 0.0 => Some(Self::Positive),
128            ActionValue::Axis1(axis) if axis.value < 0.0 => Some(Self::Negative),
129            ActionValue::Axis1(_) => None,
130            ActionValue::Axis2(axis) => Cardinal::of(axis.x, axis.y).map(Self::Toward),
131        }
132    }
133}
134
135/// A slot's repeat schedule and where it is in it.
136#[derive(Clone, Debug)]
137pub(crate) struct RepeatState {
138    pattern: Repeat,
139    held: Option<Held>,
140    /// The clock reading the next repeat is due at.
141    next_at: f64,
142    /// Whether the action pulsed on this tick.
143    pub(crate) fired: bool,
144}
145
146impl RepeatState {
147    pub(crate) fn reset(&mut self) {
148        self.held = None;
149        self.fired = false;
150    }
151
152    /// Advance the schedule against the slot's freshly resolved value.
153    pub(crate) fn update(&mut self, value: &ActionValue, elapsed: f64) {
154        let now = Held::of(value);
155        if now.is_none() {
156            self.held = None;
157            return;
158        }
159        if now != self.held {
160            self.held = now;
161            self.fired = true;
162            self.next_at = elapsed + f64::from(self.pattern.delay);
163            return;
164        }
165        if elapsed >= self.next_at {
166            self.fired = true;
167            // Past `elapsed` in one step: a long tick pulses once.
168            let interval = f64::from(self.pattern.interval);
169            let missed = ((elapsed - self.next_at) / interval).floor() + 1.0;
170            self.next_at += missed * interval;
171            if self.next_at <= elapsed {
172                self.next_at += interval;
173            }
174        }
175    }
176}
177
178impl ActionMap {
179    /// Attach a repeat schedule to an action, or detach it with `None`.
180    ///
181    /// Attaching does not pulse: an action already held when its schedule is
182    /// attached is treated as having gone down now, and first repeats a
183    /// [`Repeat::delay`] later.
184    ///
185    /// # Errors
186    /// [`ActionMapError::UnknownAction`] if nothing with that name is declared.
187    pub fn set_repeat(&mut self, name: &str, repeat: Option<Repeat>) -> Result<(), ActionMapError> {
188        let Some(&idx) = self.name_to_idx.get(name) else {
189            return Err(ActionMapError::UnknownAction(name.to_owned()));
190        };
191        let elapsed = self.elapsed;
192        let slot = &mut self.slots[idx];
193        slot.repeat = repeat.map(|pattern| RepeatState {
194            pattern,
195            held: Held::of(&slot.value),
196            next_at: elapsed + f64::from(pattern.delay),
197            fired: false,
198        });
199        Ok(())
200    }
201
202    /// The repeat schedule attached to an action, if any.
203    #[must_use]
204    pub fn repeat(&self, name: &str) -> Option<Repeat> {
205        let &idx = self.name_to_idx.get(name)?;
206        self.slots[idx].repeat.as_ref().map(|state| state.pattern)
207    }
208
209    /// Whether an action pulsed on this tick: it went down (or turned to a new
210    /// direction), or a repeat fell due. `false` for an action with no
211    /// schedule, or none declared.
212    #[must_use]
213    pub fn repeated(&self, name: &str) -> bool {
214        self.name_to_idx
215            .get(name)
216            .and_then(|&idx| self.slots[idx].repeat.as_ref())
217            .is_some_and(|state| state.fired)
218    }
219
220    /// The [`Cardinal`] a 2-D axis action points in, or `None` when it is at
221    /// rest, absent or another kind.
222    #[must_use]
223    pub fn cardinal(&self, name: &str) -> Option<Cardinal> {
224        let (x, y) = self.axis2(name);
225        Cardinal::of(x, y)
226    }
227}
228
229#[cfg(test)]
230mod tests {
231    use super::*;
232    use crate::{ActionDecl, ActionKind, Binding};
233    use crcbl_core::input::KeyCode;
234
235    /// Twenty ticks a second, so the delay is ten ticks and the interval two.
236    ///
237    /// `0.05` is not exact in binary: as an `f32` it is a little over, so a
238    /// clock summed from it reaches each due time on the tick the arithmetic
239    /// says rather than one later — the same side sixty hertz's `f32` falls
240    /// on, which the next test pins separately.
241    const TICK: f32 = 0.05;
242
243    fn map_with_repeat(kind: ActionKind, bindings: Vec<Binding>) -> ActionMap {
244        let mut map = ActionMap::new();
245        map.declare(ActionDecl {
246            name: "nav".to_owned(),
247            kind,
248            bindings,
249        });
250        map.set_repeat("nav", Some(Repeat::UI)).expect("declared");
251        map
252    }
253
254    /// The ticks (counting the press as tick 0) on which the action pulsed,
255    /// over `ticks` ticks of holding it.
256    fn pulses(map: &mut ActionMap, press: impl FnOnce(&mut ActionMap), ticks: u32) -> Vec<u32> {
257        let mut fired = Vec::new();
258        map.begin_tick(TICK);
259        press(map);
260        for tick in 0..ticks {
261            if tick > 0 {
262                map.begin_tick(TICK);
263            }
264            if map.repeated("nav") {
265                fired.push(tick);
266            }
267        }
268        fired
269    }
270
271    /// **The press pulses, the first repeat lands `delay` later, and each one
272    /// after that `interval` later** — on ticks, whatever the wall clock did.
273    #[test]
274    fn a_held_button_repeats_after_the_delay_then_at_the_interval() {
275        let mut map = map_with_repeat(ActionKind::Button, vec![Binding::Key(KeyCode::Enter)]);
276        let fired = pulses(&mut map, |map| map.key_event(KeyCode::Enter, true), 17);
277        // 0.5 s / 0.05 s = tick 10, then every 0.1 s / 0.05 s = 2 ticks.
278        assert_eq!(fired, [0, 10, 12, 14, 16]);
279    }
280
281    /// The same schedule at sixty hertz, whose tick is not exact in binary:
282    /// the delay's thirty ticks and the interval's six.
283    #[test]
284    fn the_schedule_holds_at_sixty_hertz() {
285        let mut map = map_with_repeat(ActionKind::Button, vec![Binding::Key(KeyCode::Enter)]);
286        let mut fired = Vec::new();
287        map.begin_tick(1.0 / 60.0);
288        map.key_event(KeyCode::Enter, true);
289        for tick in 0..50 {
290            if tick > 0 {
291                map.begin_tick(1.0 / 60.0);
292            }
293            if map.repeated("nav") {
294                fired.push(tick);
295            }
296        }
297        assert_eq!(fired, [0, 30, 36, 42, 48]);
298    }
299
300    /// Releasing ends the schedule, and the next press starts a new one.
301    #[test]
302    fn a_release_ends_the_schedule() {
303        let mut map = map_with_repeat(ActionKind::Button, vec![Binding::Key(KeyCode::Enter)]);
304        let _ = pulses(&mut map, |map| map.key_event(KeyCode::Enter, true), 8);
305        map.key_event(KeyCode::Enter, false);
306        let fired = pulses(&mut map, |map| map.key_event(KeyCode::Enter, true), 11);
307        assert_eq!(
308            fired,
309            [0, 10],
310            "a fresh delay, not the old schedule's remainder"
311        );
312    }
313
314    /// A tick that covers several repeats pulses once, and does not leave a
315    /// backlog for the ticks after it.
316    #[test]
317    fn a_long_tick_pulses_once_and_leaves_no_backlog() {
318        let mut map = map_with_repeat(ActionKind::Button, vec![Binding::Key(KeyCode::Enter)]);
319        map.begin_tick(TICK);
320        map.key_event(KeyCode::Enter, true);
321        // Sixteen repeats fall due inside this tick, the last at 2.0 s held;
322        // the next is due at 2.1 s.
323        map.begin_tick(2.03);
324        assert!(map.repeated("nav"), "the long tick pulses");
325        map.begin_tick(TICK);
326        assert!(
327            !map.repeated("nav"),
328            "2.08 s: the missed repeats are gone rather than queued",
329        );
330        map.begin_tick(TICK);
331        assert!(map.repeated("nav"), "2.13 s: and the schedule carries on");
332    }
333
334    /// **A 2-D axis repeats per direction**: turning pulses at once and
335    /// restarts the delay, and a diagonal is vertical.
336    #[test]
337    fn a_direction_change_restarts_the_schedule() {
338        let wasd = Binding::Wasd {
339            up: KeyCode::KeyW,
340            down: KeyCode::KeyS,
341            left: KeyCode::KeyA,
342            right: KeyCode::KeyD,
343        };
344        let mut map = map_with_repeat(ActionKind::Axis2, vec![wasd]);
345        let _ = pulses(&mut map, |map| map.key_event(KeyCode::KeyD, true), 6);
346        assert_eq!(map.cardinal("nav"), Some(Cardinal::Right));
347
348        // S joins the held D: `(0.707, -0.707)`, a tie, so the vertical wins
349        // and the action turns from Right to Down without ever being released.
350        map.begin_tick(TICK);
351        map.key_event(KeyCode::KeyS, true);
352        assert_eq!(
353            map.cardinal("nav"),
354            Some(Cardinal::Down),
355            "a diagonal's tie goes to the vertical",
356        );
357        assert!(map.repeated("nav"), "turning is a new press");
358        let mut fired = Vec::new();
359        for tick in 1..11 {
360            map.begin_tick(TICK);
361            if map.repeated("nav") {
362                fired.push(tick);
363            }
364        }
365        assert_eq!(fired, [10], "the delay restarted at the turn");
366    }
367
368    /// A 1-D axis is held by sign.
369    #[test]
370    fn a_one_dimensional_axis_repeats_by_sign() {
371        let mut map = map_with_repeat(
372            ActionKind::Axis1,
373            vec![Binding::KeyAxis {
374                negative: KeyCode::KeyQ,
375                positive: KeyCode::KeyE,
376            }],
377        );
378        map.begin_tick(TICK);
379        map.key_event(KeyCode::KeyE, true);
380        assert!(map.repeated("nav"));
381        map.begin_tick(TICK);
382        map.key_event(KeyCode::KeyE, false);
383        map.key_event(KeyCode::KeyQ, true);
384        assert!(map.repeated("nav"), "the other sign is another press");
385    }
386
387    /// A schedule that could not advance is not constructible.
388    #[test]
389    fn only_runnable_schedules_exist() {
390        assert_eq!(Repeat::new(0.5, 0.1), Some(Repeat::UI));
391        assert!(Repeat::new(0.0, 0.1).is_some(), "no delay is a schedule");
392        for (delay, interval) in [
393            (0.5, 0.0),
394            (0.5, -1.0),
395            (-0.1, 0.1),
396            (f32::NAN, 0.1),
397            (0.5, f32::INFINITY),
398        ] {
399            assert_eq!(Repeat::new(delay, interval), None, "({delay}, {interval})");
400        }
401    }
402
403    /// Attaching to a held action does not pulse; no schedule never pulses.
404    #[test]
405    fn attaching_does_not_pulse_and_no_schedule_never_does() {
406        let mut map = ActionMap::new();
407        map.declare(ActionDecl {
408            name: "nav".to_owned(),
409            kind: ActionKind::Button,
410            bindings: vec![Binding::Key(KeyCode::Enter)],
411        });
412        map.key_event(KeyCode::Enter, true);
413        assert!(!map.repeated("nav"), "no schedule");
414        map.set_repeat("nav", Some(Repeat::UI)).expect("declared");
415        assert!(!map.repeated("nav"));
416        assert_eq!(map.repeat("nav"), Some(Repeat::UI));
417        assert_eq!(
418            map.set_repeat("nope", None),
419            Err(ActionMapError::UnknownAction("nope".to_owned())),
420        );
421    }
422}