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