inlet 0.4.0

Input management with configurable bindings for Bevy game engine.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
//! [`InputHandler`] related types.
use std::{
    fmt::Display,
    ops::{Deref, DerefMut},
    time::{Duration, Instant},
};

use bevy::{
    ecs::{component::Component, resource::Resource},
    input::{
        ButtonInput,
        gamepad::Gamepad,
        keyboard::KeyCode,
        mouse::{AccumulatedMouseMotion, AccumulatedMouseScroll, MouseButton},
    },
    platform::collections::{HashMap, hash_map::Entry},
};

use crate::{BevyAxisKind, BevyButtonKind, BevyInputKind, InputBinding, InputValue};

/// Current state of an input.
#[derive(Debug, Default)]
enum InputStateKind {
    /// Exactly 1 binding has been made to this input. Clash checks can be ignored.
    #[default]
    NoClash,
    /// State is currently inactive.
    Inactive,
    /// At least 1 input wants to
    Clashing(usize),
    /// Input is being buffered and is being reported as inactive, shall become released with
    /// the same `usize` for at least 1 frame.
    Buffered(Instant, usize),
    /// State is currently active if you meet the priority stored.
    Active(usize),
}

impl InputStateKind {
    fn inactive() -> Self {
        Self::Inactive
    }
    fn clashing(len: usize) -> Self {
        Self::Clashing(len)
    }
    fn buffered(len: usize) -> Self {
        Self::Buffered(Instant::now(), len)
    }
    fn buffered_with_instant(len: usize, i: Instant) -> Self {
        Self::Buffered(i, len)
    }
    fn active(len: usize) -> Self {
        Self::Active(len)
    }
    fn replace(&mut self, new: Self) {
        *self = new;
    }
}

impl Display for InputStateKind {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            InputStateKind::NoClash => write!(f, "NoClash"),
            InputStateKind::Inactive => write!(f, "Inactive"),
            InputStateKind::Clashing(len) => write!(f, "Clashing({len})"),
            InputStateKind::Buffered(_, len) => write!(f, "Buffered({len})"),
            InputStateKind::Active(len) => write!(f, "Active({len})"),
        }
    }
}

#[derive(Debug, Default)]
struct InputState {
    /// The last frame this was updated by a poll call.
    frame: usize,
    /// The actual state.
    kind: InputStateKind,
    /// The last input feed into the state.
    value: InputValue,
}

#[derive(Resource, Clone, Copy, Debug)]
pub struct DefaultClashSettings(pub ClashSettings);

impl Deref for DefaultClashSettings {
    type Target = ClashSettings;

    fn deref(&self) -> &Self::Target {
        &self.0
    }
}

impl DerefMut for DefaultClashSettings {
    fn deref_mut(&mut self) -> &mut Self::Target {
        &mut self.0
    }
}

/// The settings to use for resolving clashing inputs.
///
/// # Component
///
/// If inserted on an entity that has a InputHandler, the InputHandler will use new settings and remove the
///
/// # Resource
///
/// Defines a default settings that new [`InputHandler`] can pull from.
///
#[derive(Component, Clone, Copy, Debug)]
/// component to avoid extra checks.
#[derive(Default)]
pub enum ClashSettings {
    /// Does not buffer inputs, just detects clashes. Inputs that may clash will be re-checked after all inputs
    /// have had a chance to assert their priority.
    ///
    /// # Rules
    ///
    /// - If a high priority binding captures a button, that button must be released before a lower priority
    ///   binding can see it again.
    ///
    #[default]
    Unbuffered,
    /// Buffers inputs that can clash until a timer runs out or unpressed.
    ///
    /// # Rules
    ///
    /// - An input will NEVER be active/shown the first frame it is pressed. Unlike Unbuffered re-checks
    ///   are not necessary because every binding will have a chance to be prioritized for chords.
    /// - If a wait duration is provided the input will be inactive/hidden to all bindings until the button has been
    ///   active for at least that long.
    /// - If the timer runs out or the button is unpressed: the chord with the most active parts will be activated.
    /// - If a high priority binding captures a button, that button must be released before a lower priority
    ///   binding can see it again.
    /// - If a chord has multiple buffered inputs, all inputs start times will be set the the oldest.
    BufferClashing(Option<Duration>),
    /// Buffers all inputs until a timer runs out or unpressed.
    ///
    /// # Rules
    ///
    /// - An input will NEVER be active/shown the first frame it is pressed. Unlike Unbuffered re-checks
    ///   are not necessary because every binding will have a chance to be prioritized for chords.
    /// - If a wait duration is provided the input will be inactive/hidden to all bindings until the button has been
    ///   active for at least that long.
    /// - If the timer runs out or the button is unpressed: the chord with the most active parts will be activated.
    /// - If a high priority binding captures a button, that button must be released before a lower priority
    ///   binding can see it again.
    /// - If a chord has multiple buffered inputs, all inputs start times will be set the the oldest.
    BufferAll(Option<Duration>),
}

impl ClashSettings {
    /// Return new settings that use buffered clash resolution where `delay` is the amount of time to wait before
    /// resolving; if `delay` is `None` input will buffer for 1 frame.
    pub fn new_buffered(delay: Option<Duration>) -> Self {
        Self::BufferClashing(delay)
    }
    /// Returns new settings that use unbuffered clash resolution where inputs that might clash re-check after all
    /// bindings have been checked at least once.
    pub fn new_unbuffered() -> Self {
        Self::Unbuffered
    }
    fn buffer_all(&self) -> bool {
        matches!(self, Self::BufferAll(_))
    }
}


/// Management of a players bindings and the states.
#[derive(Component)]
#[derive(Default)]
pub struct InputHandler {
    /// a counter that is increased when ever `Self::tick` is called.
    frame: usize,
    /// All known bindings and the state of the input.
    clashables: HashMap<BevyInputKind, InputState>,
    /// The settings used for the resolution of clashing bindings.
    settings: ClashSettings,
}

impl From<ClashSettings> for InputHandler {
    fn from(value: ClashSettings) -> Self {
        Self {
            frame: 0,
            clashables: HashMap::default(),
            settings: value,
        }
    }
}


#[derive(PartialEq, Eq)]
enum Outy {
    Hide,
    Show,
    Repoll,
}

impl InputHandler {
    /// The settings used for clash handling.
    pub fn settings(&self) -> &ClashSettings {
        &self.settings
    }
    /// Please update_list after using this, because some input may be in a state that will not
    /// allow the input to enter a state that is correct for the new settings.
    pub(crate) fn set_settings(&mut self, new: ClashSettings) {
        self.settings = new;
    }
    /// Does some internal cleaning that is only possible between bindings checking for their inputs
    /// because we can assume that all (or none) of the inputs have been given a change to fight for priority.
    ///
    /// - if the input state has a frame not equal to the current frame: change to inactive.
    /// - else if the input state is buffered and the timer has expired: change to active.
    /// - else if the input state is clashing : change to active.
    /// - increases the internal counter for "frames" after all above steps.
    ///
    pub fn tick(&mut self) {
        for (_c, state) in self.clashables.iter_mut() {
            let new = if state.frame != self.frame {
                if matches!(
                    state.kind,
                    InputStateKind::Inactive | InputStateKind::NoClash
                ) {
                    None
                } else {
                    Some(InputStateKind::inactive())
                }
            } else if let ClashSettings::BufferClashing(duration)
            | ClashSettings::BufferAll(duration) = &self.settings
                && let InputStateKind::Buffered(start, len) = &state.kind
            {
                if let Some(d) = duration {
                    if start.elapsed() >= *d {
                        Some(InputStateKind::active(*len))
                    } else {
                        None
                    }
                } else {
                    Some(InputStateKind::active(*len))
                }
            } else if let InputStateKind::Clashing(priority) = &state.kind {
                Some(InputStateKind::Active(*priority))
            } else {
                None
            };
            if let Some(new) = new {
                state.kind.replace(new);
            }
        }
        self.frame += 1;
    }
    /// Updates the internal binding map and resets all states.
    pub fn update_list<K, T>(&mut self, map: &HashMap<K, InputBinding<T>>) {
        let clashables: Vec<BevyInputKind> =
            map.values().flat_map(|asdf| asdf.input_kinds()).collect();
        self.clashables.clear();
        for c in clashables.clone().into_iter() {
            match self.clashables.entry(c) {
                Entry::Occupied(mut o) => {
                    let state = o.get_mut();
                    if matches!(state.kind, InputStateKind::NoClash) {
                        state.kind = InputStateKind::Inactive;
                    }
                }
                Entry::Vacant(v) => {
                    v.insert_entry(InputState {
                        frame: self.frame,
                        kind: InputStateKind::default(),
                        value: InputValue::default(),
                    });
                }
            }
        }
    }
    /// Tries to return the newest value associated with the binding.
    ///
    /// If `None` is returned then you must [`Self::repoll`] after all inputs have been polled
    pub(crate) fn poll(&mut self, clashable: &[BevyInputKind]) -> Option<InputValue> {
        if clashable.is_empty() {
            return Some(InputValue::default());
        }
        // Are all inputs pressed
        let mut pressed = true;
        // the buffered input with the oldest instant.
        let mut oldest_press = Ok(Instant::now());
        for c in clashable.iter() {
            match self.clashables.entry(*c) {
                Entry::Occupied(o) => {
                    if o.get().value.is_pressed() {
                        if let InputStateKind::Buffered(start, _) = &o.get().kind {
                            match oldest_press {
                                Err(oldest) | Ok(oldest) => {
                                    if oldest > *start {
                                        oldest_press = Err(*start);
                                    }
                                }
                            }
                        }
                    } else {
                        pressed = false;
                    }
                }
                Entry::Vacant(v) => {
                    bevy::log::warn!("polled unregistered bevy input in manager.");
                    v.insert(InputState {
                        frame: self.frame,
                        kind: InputStateKind::inactive(),
                        value: InputValue::default(),
                    });
                }
            }
        }
        let oldest_press = oldest_press.err();
        let mut repoll = if pressed { Outy::Show } else { Outy::Hide };
        let chord_length = clashable.len();
        for c in clashable.iter() {
            // UNWRAP the first for loop pass should insure that all clashables are in the map.
            let state = self.clashables.get_mut(c).unwrap();
            let new_state = if pressed {
                match &mut state.kind {
                    InputStateKind::NoClash => {
                        if self.settings.buffer_all() {
                            Some(InputStateKind::buffered(chord_length))
                        } else {
                            None
                        }
                    }
                    InputStateKind::Inactive => match self.settings {
                        ClashSettings::Unbuffered => Some(InputStateKind::clashing(chord_length)),
                        ClashSettings::BufferAll(_) | ClashSettings::BufferClashing(_) => {
                            Some(InputStateKind::buffered(chord_length))
                        }
                    },
                    InputStateKind::Clashing(len) => {
                        if chord_length > *len {
                            Some(InputStateKind::clashing(chord_length))
                        } else {
                            None
                        }
                    }
                    InputStateKind::Buffered(instant, len) => {
                        if let Some(oldest) = oldest_press
                            && oldest < *instant {
                                *instant = oldest;
                            }
                        if chord_length > *len {
                            Some(InputStateKind::buffered_with_instant(
                                chord_length,
                                *instant,
                            ))
                        } else {
                            None
                        }
                    }
                    InputStateKind::Active(len) => {
                        if chord_length > *len {
                            Some(InputStateKind::active(chord_length))
                        } else {
                            None
                        }
                    }
                }
            } else {
                None
            };
            if let Some(new) = new_state {
                state.kind.replace(new);
            }
            if pressed && state.frame != self.frame {
                state.frame = self.frame;
            }
            match &state.kind {
                InputStateKind::Clashing(_) => {
                    if matches!(repoll, Outy::Show) {
                        repoll = Outy::Repoll;
                    }
                }
                InputStateKind::Buffered(_, _) => {
                    if matches!(repoll, Outy::Show | Outy::Repoll) {
                        repoll = Outy::Hide;
                    }
                }
                InputStateKind::Active(len) => {
                    if *len != chord_length && matches!(repoll, Outy::Show | Outy::Repoll) {
                        repoll = Outy::Hide;
                    }
                }
                InputStateKind::NoClash | InputStateKind::Inactive => {}
            }
        }

        match repoll {
            Outy::Hide => Some(InputValue::default()),
            Outy::Show => {
                // UNWRAP the first for loop pass should insure that all clashables are in the map.
                let val = self
                    .clashables
                    .get(&clashable[0])
                    .map(|asdf| asdf.value.clone())
                    .unwrap_or_default();
                Some(val)
            }
            Outy::Repoll => None,
        }
    }
    /// Only preforms a check of what input to use, does not preform any state changing.
    ///
    /// It is expected that this is only ever called on inputs that got a `None` from [`Self::poll`].
    pub(crate) fn repoll(&self, clashable: &[BevyInputKind]) -> InputValue {
        if clashable.is_empty() {
            return InputValue::default();
        }
        for c in clashable.iter() {
            if let Some(state) = self.clashables.get(c) {
                match &state.kind {
                    InputStateKind::Inactive | InputStateKind::Buffered(_, _) => {
                        return InputValue::default();
                    }
                    InputStateKind::NoClash => {}
                    InputStateKind::Clashing(len) | InputStateKind::Active(len) => {
                        if clashable.len() != *len {
                            return InputValue::default();
                        }
                    }
                }
            }
        }
        self.clashables
            .get(&clashable[0])
            .map(|asdf| asdf.value.clone())
            .unwrap_or_default()
    }
    /// Updates values for input types from `bevy_input`.
    pub(crate) fn update(
        &mut self,
        gamepads: &[&Gamepad],
        keycodes: &ButtonInput<KeyCode>,
        // keys: &ButtonInput<Key>,
        mouse: &ButtonInput<MouseButton>,
        accumulated_mouse_motion: &AccumulatedMouseMotion,
        accumulated_mouse_scroll: &AccumulatedMouseScroll,
    ) {
        for (kind, state) in self.clashables.iter_mut() {
            let new_value = match kind {
                BevyInputKind::Axis(bevy_axis_kind) => match bevy_axis_kind {
                    BevyAxisKind::MouseAxis(mouse_axis) => InputValue::Value(match mouse_axis {
                        crate::axis::MouseAxis::MotionX => accumulated_mouse_motion.delta.x,
                        crate::axis::MouseAxis::MotionY => accumulated_mouse_motion.delta.y,
                        crate::axis::MouseAxis::ScrollX => accumulated_mouse_scroll.delta.x,
                        crate::axis::MouseAxis::ScrollY => accumulated_mouse_scroll.delta.y,
                    }),
                    BevyAxisKind::GamepadAxis(gamepad_axis) => {
                        let mut value = 0.;
                        let mut count = 0;
                        for gpad in gamepads {
                            if let Some(v) = gpad.get(*gamepad_axis)
                                && v != 0.
                            {
                                value += v;
                                count += 1;
                            }
                        }
                        InputValue::Value(if count == 0 {
                            0.
                        } else {
                            value / (count as f32)
                        })
                    }
                    BevyAxisKind::GamepadButton(gamepad_button) => {
                        let mut value = 0.;
                        let mut count = 0;
                        for gpad in gamepads {
                            if let Some(v) = gpad.get(*gamepad_button)
                                && v != 0.
                            {
                                value += v;
                                count += 1;
                            }
                        }
                        InputValue::Value(if count == 0 {
                            0.
                        } else {
                            value / (count as f32)
                        })
                    }
                },
                BevyInputKind::Button(bevy_button_kind) => match bevy_button_kind {
                    BevyButtonKind::GamepadButton(gamepad_button) => {
                        let mut out = false;
                        for gpad in gamepads {
                            if gpad.pressed(*gamepad_button) {
                                out |= true;
                                break;
                            }
                        }
                        InputValue::Pressed(out)
                    }
                    BevyButtonKind::KeyCode(key_code) => {
                        InputValue::Pressed(keycodes.pressed(*key_code))
                    }
                    BevyButtonKind::MouseButton(mouse_button) => {
                        InputValue::Pressed(mouse.pressed(*mouse_button))
                    }
                },
            };
            state.value = new_value;
        }
    }
}