bevy_input_sequence/
plugin.rs

1use bevy::{
2    app::{App, Plugin, Update},
3    diagnostic::FrameCount,
4    ecs::{
5        entity::Entity,
6        intern::Interned,
7        prelude::In,
8        query::Added,
9        removal_detection::RemovedComponents,
10        schedule::{ScheduleLabel, SystemSet},
11        system::{Commands, Local, Query, Res, ResMut},
12    },
13    input::{
14        gamepad::{Gamepad, GamepadButton},
15        keyboard::KeyCode,
16        ButtonInput,
17    },
18    log::warn,
19    prelude::IntoScheduleConfigs,
20    time::Time,
21};
22use std::collections::{HashMap, VecDeque};
23
24use crate::{
25    cache::{ButtonSequenceCache, KeySequenceCache},
26    chord::{is_modifier, KeyChordQueue},
27    frame_time::FrameTime,
28    input_sequence::{ButtonSequence, InputSequence, KeySequence},
29    KeyChord, Modifiers,
30};
31use trie_rs::inc_search::{Answer, IncSearch};
32
33/// ButtonInput sequence plugin.
34pub struct InputSequencePlugin {
35    #[allow(clippy::type_complexity)]
36    schedules: Vec<(Interned<dyn ScheduleLabel>, Option<Interned<dyn SystemSet>>)>,
37    match_key: Option<bool>,
38    match_button: Option<bool>,
39}
40
41impl Default for InputSequencePlugin {
42    fn default() -> Self {
43        InputSequencePlugin {
44            schedules: vec![(Interned(Box::leak(Box::new(Update))), None)],
45            match_key: None,
46            match_button: None,
47        }
48    }
49}
50
51impl Plugin for InputSequencePlugin {
52    fn build(&self, app: &mut App) {
53        if self
54            .match_key
55            .unwrap_or(app.world().get_resource::<ButtonInput<KeyCode>>().is_some())
56        {
57            app
58                .register_type::<InputSequence<KeyChord, ()>>()
59                // .register_type::<InputSequenceCache<KeyChord, ()>>()
60                ;
61            // Add key sequence.
62            app.init_resource::<KeySequenceCache>();
63            app.init_resource::<KeyChordQueue>();
64
65            for (schedule, set) in &self.schedules {
66                if let Some(set) = set {
67                    app.add_systems(
68                        *schedule,
69                        (
70                            detect_key_removals,
71                            detect_key_additions,
72                            key_sequence_matcher,
73                        )
74                            .chain()
75                            .in_set(*set),
76                    );
77                } else {
78                    app.add_systems(
79                        *schedule,
80                        (
81                            detect_key_removals,
82                            detect_key_additions,
83                            key_sequence_matcher,
84                        )
85                            .chain(),
86                    );
87                }
88            }
89        } else {
90            warn!("No key sequence matcher added; consider adding DefaultPlugins.");
91        }
92
93        if self.match_button.unwrap_or(
94            false, // NOTE: Is there a way to detect whether gamepad input is available post 0.14?
95                  // app.world()
96                  //     .get_resource::<ButtonInput<GamepadButton>>()
97                  //     .is_some(),
98        ) {
99            // app
100            //     .register_type::<InputSequence<GamepadButton, In<Entity>>>()
101            //     // .register_type::<InputSequenceCache<GamepadButton, Gamepad>>()
102            //     ;
103            // Add button sequences.
104            app.init_resource::<ButtonSequenceCache>();
105
106            for (schedule, set) in &self.schedules {
107                if let Some(set) = set {
108                    app.add_systems(
109                        *schedule,
110                        (
111                            detect_button_removals,
112                            detect_button_additions,
113                            button_sequence_matcher,
114                        )
115                            .chain()
116                            .in_set(*set),
117                    );
118                } else {
119                    app.add_systems(
120                        *schedule,
121                        (
122                            detect_button_removals,
123                            detect_button_additions,
124                            button_sequence_matcher,
125                        )
126                            .chain(),
127                    );
128                }
129            }
130        } else {
131            warn!("No button sequence matcher added; consider adding DefaultPlugins.");
132        }
133    }
134}
135
136impl InputSequencePlugin {
137    /// Constructs an empty input sequence plugin with no default schedules.
138    pub fn empty() -> Self {
139        Self {
140            schedules: vec![],
141            match_key: None,
142            match_button: None,
143        }
144    }
145    /// Run the executor in a specific `Schedule`.
146    pub fn run_in(mut self, schedule: impl ScheduleLabel) -> Self {
147        self.schedules
148            .push((Interned(Box::leak(Box::new(schedule))), None));
149        self
150    }
151
152    /// Run the executor in a specific `Schedule` and `SystemSet`.
153    pub fn run_in_set(mut self, schedule: impl ScheduleLabel, set: impl SystemSet) -> Self {
154        self.schedules.push((
155            Interned(Box::leak(Box::new(schedule))),
156            Some(Interned(Box::leak(Box::new(set)))),
157        ));
158        self
159    }
160
161    /// Run systems to match keys. By default will match keys if resource
162    /// `ButtonInput<KeyCode>` exists.
163    pub fn match_key(mut self, yes: bool) -> Self {
164        self.match_key = Some(yes);
165        self
166    }
167
168    /// Run systems to match button. By default will match keys if resource
169    /// `ButtonInput<GamepadButton>` exists.
170    pub fn match_button(mut self, yes: bool) -> Self {
171        self.match_button = Some(yes);
172        self
173    }
174}
175
176fn detect_key_additions(
177    sequences: Query<&InputSequence<KeyChord, ()>, Added<InputSequence<KeyChord, ()>>>,
178    mut cache: ResMut<KeySequenceCache>,
179) {
180    if sequences.iter().next().is_some() {
181        cache.reset();
182    }
183}
184
185#[allow(clippy::type_complexity)]
186fn detect_button_additions(
187    sequences: Query<
188        &InputSequence<GamepadButton, In<Entity>>,
189        Added<InputSequence<GamepadButton, In<Entity>>>,
190    >,
191    mut cache: ResMut<ButtonSequenceCache>,
192) {
193    if sequences.iter().next().is_some() {
194        cache.reset();
195    }
196}
197
198fn detect_key_removals(
199    mut cache: ResMut<KeySequenceCache>,
200    mut removals: RemovedComponents<InputSequence<KeyChord, ()>>,
201) {
202    if removals.read().next().is_some() {
203        cache.reset();
204    }
205}
206
207fn detect_button_removals(
208    mut cache: ResMut<ButtonSequenceCache>,
209    mut removals: RemovedComponents<InputSequence<GamepadButton, In<Entity>>>,
210) {
211    if removals.read().next().is_some() {
212        cache.reset();
213    }
214}
215
216#[allow(clippy::too_many_arguments)]
217fn button_sequence_matcher(
218    sequences: Query<&ButtonSequence>,
219    time: Res<Time>,
220    mut last_times: Local<HashMap<Entity, VecDeque<FrameTime>>>,
221    mut cache: ResMut<ButtonSequenceCache>,
222    frame_count: Res<FrameCount>,
223    mut commands: Commands,
224    gamepads: Query<(Entity, &Gamepad)>,
225) {
226    let now = FrameTime {
227        frame: frame_count.0,
228        time: time.elapsed_secs(),
229    };
230    for (id, gamepad) in &gamepads {
231        for button in gamepad.get_just_pressed() {
232            let last_times = match last_times.get_mut(&id) {
233                Some(x) => x,
234                None => {
235                    last_times.insert(id, VecDeque::new());
236                    last_times.get_mut(&id).unwrap()
237                }
238            };
239
240            last_times.push_back(now.clone());
241            let start = &last_times[0];
242            let mut search = cache.recall(id, sequences.iter().by_ref());
243            for seq in inc_consume_input(&mut search, std::iter::once(*button)) {
244                if seq
245                    .time_limit
246                    .as_ref()
247                    .map(|limit| (&now - start).has_timedout(limit))
248                    .unwrap_or(false)
249                {
250                    // Sequence timed out.
251                } else {
252                    commands.run_system_with(seq.system_id, id);
253                }
254            }
255            let prefix_len = search.prefix_len();
256            let l = last_times.len();
257            let _ = last_times.drain(0..l - prefix_len);
258            let position = search.into();
259            cache.store(id, position);
260        }
261    }
262}
263
264#[allow(clippy::too_many_arguments)]
265fn key_sequence_matcher(
266    sequences: Query<&KeySequence>,
267    time: Res<Time>,
268    keys: Res<ButtonInput<KeyCode>>,
269    mut last_times: Local<VecDeque<FrameTime>>,
270    mut cache: ResMut<KeySequenceCache>,
271    frame_count: Res<FrameCount>,
272    mut commands: Commands,
273    mut keychord_queue: ResMut<KeyChordQueue>,
274) {
275    let mods = Modifiers::from(&keys);
276    let now = FrameTime {
277        frame: frame_count.0,
278        time: time.elapsed_secs(),
279    };
280    let maybe_start = last_times.front().cloned();
281    let mut input = keychord_queue
282        .drain(..)
283        .chain(
284            keys.get_just_pressed()
285                .filter(|k| !is_modifier(**k))
286                .map(|k| {
287                    let chord = KeyChord(mods, *k);
288                    last_times.push_back(now.clone());
289                    chord
290                }),
291        )
292        .peekable();
293    if input.peek().is_none() {
294        return;
295    }
296
297    let mut search = cache.recall(sequences.iter());
298
299    // eprintln!("maybe_start {maybe_start:?} now {now:?}");
300    for seq in inc_consume_input(&mut search, input) {
301        if let Some(ref start) = maybe_start {
302            if seq
303                .time_limit
304                .as_ref()
305                .map(|limit| (&now - start).has_timedout(limit))
306                .unwrap_or(false)
307            {
308                // Sequence timed out.
309                continue;
310            }
311        }
312        commands.run_system(seq.system_id);
313    }
314    let prefix_len = search.prefix_len();
315    let l = last_times.len();
316    let _ = last_times.drain(0..l.saturating_sub(prefix_len));
317    let position = search.into();
318    cache.store(position);
319}
320
321/// Incrementally consume the input.
322fn inc_consume_input<'a, 'b, K, V>(
323    search: &'b mut IncSearch<'a, K, V>,
324    input: impl Iterator<Item = K> + 'b,
325) -> impl Iterator<Item = &'a V> + 'b
326where
327    K: Clone + Eq + Ord,
328    'a: 'b,
329{
330    input.filter_map(move |k| {
331        match search.query(&k) {
332            Some(Answer::Match) => {
333                let result = Some(search.value().unwrap());
334                search.reset();
335                result
336            }
337            Some(Answer::PrefixAndMatch) => Some(search.value().unwrap()),
338            Some(Answer::Prefix) => None,
339            None => {
340                search.reset();
341                // This could be the start of a new sequence.
342                //
343                // Let's check it.
344                match search.query(&k) {
345                    Some(Answer::Match) => {
346                        let result = Some(search.value().unwrap());
347                        search.reset();
348                        result
349                    }
350                    Some(Answer::PrefixAndMatch) => Some(search.value().unwrap()),
351                    Some(Answer::Prefix) => None,
352                    None => {
353                        // This may not be necessary.
354                        search.reset();
355                        None
356                    }
357                }
358            }
359        }
360    })
361}