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
33pub 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 ;
61 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, ) {
99 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 pub fn empty() -> Self {
139 Self {
140 schedules: vec![],
141 match_key: None,
142 match_button: None,
143 }
144 }
145 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 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 pub fn match_key(mut self, yes: bool) -> Self {
164 self.match_key = Some(yes);
165 self
166 }
167
168 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 } 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 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 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
321fn 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 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 search.reset();
355 None
356 }
357 }
358 }
359 }
360 })
361}