├── .gitignore ├── src ├── cache │ ├── mod.rs │ ├── key.rs │ └── button.rs ├── time_limit.rs ├── frame_time.rs ├── lib.rs ├── action.rs ├── chord.rs ├── cond_system.rs ├── input_sequence.rs └── plugin.rs ├── tests ├── compile_fail │ ├── too_many_keys.rs │ ├── use_uppercase.rs │ ├── too_many_keys.stderr │ └── use_uppercase.stderr ├── try_build_tests.rs ├── version-numbers.rs ├── macros.rs ├── act.rs └── simulated.rs ├── examples ├── minimal.rs ├── keymod.rs ├── keycode.rs ├── gamepad_button.rs ├── run_if.rs ├── only_if.rs └── multiple_input.rs ├── .github └── workflows │ └── main.yml ├── LICENSE-MIT ├── Cargo.toml ├── CHANGELOG.md ├── README.md └── LICENSE-APACHE2 /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /.idea 3 | /.vscode 4 | /macro/target/ 5 | -------------------------------------------------------------------------------- /src/cache/mod.rs: -------------------------------------------------------------------------------- 1 | //! Cache for input sequences 2 | mod button; 3 | pub use button::*; 4 | mod key; 5 | pub use key::*; 6 | -------------------------------------------------------------------------------- /tests/compile_fail/too_many_keys.rs: -------------------------------------------------------------------------------- 1 | use bevy_input_sequence::*; 2 | 3 | fn main() { 4 | let _ = key!(A B); 5 | } 6 | -------------------------------------------------------------------------------- /tests/compile_fail/use_uppercase.rs: -------------------------------------------------------------------------------- 1 | use bevy_input_sequence::*; 2 | 3 | fn main() { 4 | let _ = key!(a); 5 | } 6 | -------------------------------------------------------------------------------- /tests/try_build_tests.rs: -------------------------------------------------------------------------------- 1 | #[test] 2 | fn try_build() { 3 | let t = trybuild::TestCases::new(); 4 | t.compile_fail("tests/compile_fail/*.rs"); 5 | } 6 | -------------------------------------------------------------------------------- /tests/compile_fail/too_many_keys.stderr: -------------------------------------------------------------------------------- 1 | error: Too many tokens; use keyseq! for multiple keys 2 | --> tests/compile_fail/too_many_keys.rs:4:20 3 | | 4 | 4 | let _ = key!(A B); 5 | | ^ 6 | -------------------------------------------------------------------------------- /tests/version-numbers.rs: -------------------------------------------------------------------------------- 1 | #[test] 2 | fn test_readme_deps() { 3 | version_sync::assert_markdown_deps_updated!("README.md"); 4 | } 5 | 6 | #[test] 7 | fn test_html_root_url() { 8 | version_sync::assert_html_root_url_updated!("src/lib.rs"); 9 | } 10 | -------------------------------------------------------------------------------- /tests/compile_fail/use_uppercase.stderr: -------------------------------------------------------------------------------- 1 | error: Use uppercase key names for physical keys 2 | --> tests/compile_fail/use_uppercase.rs:4:13 3 | | 4 | 4 | let _ = key!(a); 5 | | ^^^^^^^ 6 | | 7 | = note: this error originates in the macro `key` (in Nightly builds, run with -Z macro-backtrace for more info) 8 | -------------------------------------------------------------------------------- /examples/minimal.rs: -------------------------------------------------------------------------------- 1 | use bevy::prelude::*; 2 | use bevy_input_sequence::prelude::*; 3 | 4 | fn main() { 5 | App::new() 6 | .add_plugins(DefaultPlugins) 7 | .add_plugins(InputSequencePlugin::default()) 8 | .add_systems(Startup, setup) 9 | .run(); 10 | } 11 | 12 | fn setup(mut commands: Commands) { 13 | info!("Type H I or \"hi\"."); 14 | commands.queue(KeySequence::new(say_hi, keyseq! { H I })); 15 | } 16 | 17 | fn say_hi() { 18 | info!("hi"); 19 | } 20 | -------------------------------------------------------------------------------- /src/time_limit.rs: -------------------------------------------------------------------------------- 1 | use bevy::reflect::Reflect; 2 | use std::time::Duration; 3 | /// A time limit specified as frame counts or duration. 4 | #[derive(Clone, Debug, Reflect)] 5 | pub enum TimeLimit { 6 | /// Time limit for frame count 7 | Frames(u32), 8 | /// Time limit for duration 9 | Duration(Duration), 10 | } 11 | 12 | impl From for TimeLimit { 13 | #[inline(always)] 14 | fn from(duration: Duration) -> Self { 15 | Self::Duration(duration) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/frame_time.rs: -------------------------------------------------------------------------------- 1 | use crate::time_limit::TimeLimit; 2 | 3 | #[derive(Clone, Debug)] 4 | pub(crate) struct FrameTime { 5 | pub(crate) frame: u32, 6 | pub(crate) time: f32, 7 | } 8 | 9 | impl std::ops::Sub for &FrameTime { 10 | type Output = FrameTime; 11 | 12 | fn sub(self, other: Self) -> Self::Output { 13 | FrameTime { 14 | frame: self.frame - other.frame, 15 | time: self.time - other.time, 16 | } 17 | } 18 | } 19 | 20 | impl FrameTime { 21 | pub(crate) fn has_timedout(&self, time_limit: &TimeLimit) -> bool { 22 | match time_limit { 23 | TimeLimit::Frames(f) => self.frame > *f, 24 | TimeLimit::Duration(d) => self.time > d.as_secs_f32(), 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![doc(html_root_url = "https://docs.rs/bevy-input-sequence/0.8.1")] 2 | #![doc = include_str!("../README.md")] 3 | #![forbid(missing_docs)] 4 | 5 | pub mod action; 6 | pub mod cache; 7 | mod chord; 8 | pub mod cond_system; 9 | mod frame_time; 10 | pub mod input_sequence; 11 | mod plugin; 12 | mod time_limit; 13 | 14 | pub use chord::{KeyChord, KeyChordQueue}; 15 | pub use plugin::InputSequencePlugin; 16 | pub use time_limit::TimeLimit; 17 | 18 | pub use keyseq::{ 19 | bevy::{pkey as key, pkeyseq as keyseq}, 20 | Modifiers, 21 | }; 22 | 23 | /// Convenient glob import 24 | pub mod prelude { 25 | pub use super::cond_system::IntoCondSystem; 26 | pub use super::input_sequence::{ButtonSequence, InputSequence, KeySequence}; 27 | pub use super::{action, keyseq, InputSequencePlugin, Modifiers, TimeLimit}; 28 | pub use super::{KeyChord, KeyChordQueue}; 29 | pub use std::time::Duration; 30 | } 31 | -------------------------------------------------------------------------------- /examples/keymod.rs: -------------------------------------------------------------------------------- 1 | use bevy::prelude::*; 2 | use bevy_input_sequence::prelude::*; 3 | 4 | #[derive(Event, Clone, Debug)] 5 | struct MyEvent; 6 | 7 | fn main() { 8 | App::new() 9 | .add_plugins(DefaultPlugins) 10 | .add_event::() 11 | .add_plugins(InputSequencePlugin::default()) 12 | .add_systems(Startup, setup) 13 | .add_systems(Update, input_sequence_event_system) 14 | .run(); 15 | } 16 | 17 | fn setup(mut commands: Commands) { 18 | commands.queue( 19 | KeySequence::new( 20 | action::send_event(MyEvent), 21 | keyseq! { Ctrl-W Ctrl-D Ctrl-S Ctrl-A }, 22 | ) 23 | .time_limit(Duration::from_secs(1)), 24 | ); 25 | println!("Press Ctrl-W Ctrl-D Ctrl-S Ctrl-A to emit event."); 26 | } 27 | 28 | fn input_sequence_event_system(mut er: EventReader) { 29 | for e in er.read() { 30 | println!("{e:?} emitted."); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Unit Test 2 | run-name: ${{ github.actor }} is testing 3 | on: [ push, pull_request ] 4 | jobs: 5 | lint: 6 | runs-on: ubuntu-latest 7 | steps: 8 | - name: Install 9 | run: sudo apt-get update; sudo apt-get install --no-install-recommends libasound2-dev libudev-dev libwayland-dev libxkbcommon-dev 10 | - name: Checkout 11 | uses: actions/checkout@v3 12 | - name: Toolchain 13 | uses: dtolnay/rust-toolchain@stable 14 | with: 15 | components: clippy 16 | - name: Clippy 17 | run: cargo clippy --tests 18 | test: 19 | runs-on: ubuntu-latest 20 | steps: 21 | - name: Install 22 | run: sudo apt-get update; sudo apt-get install --no-install-recommends libasound2-dev libudev-dev 23 | - name: Checkout 24 | uses: actions/checkout@v3 25 | - name: Toolchain 26 | uses: dtolnay/rust-toolchain@stable 27 | - name: Test 28 | run: cargo test -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 elm 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "bevy-input-sequence" 3 | description = "Recognizes and acts on input sequences" 4 | version = "0.8.1" 5 | edition = "2021" 6 | authors = ["elm", "Shane Celis "] 7 | keywords = [ 8 | "gamedev", 9 | "bevy", 10 | "hotkey", 11 | "cheat-code", 12 | "input" 13 | ] 14 | categories = [ 15 | "game-engines", 16 | ] 17 | readme = "README.md" 18 | license = "MIT OR Apache-2.0" 19 | repository = "https://github.com/elm-register/bevy-input-sequence" 20 | 21 | [[example]] 22 | name = "keycode" 23 | path = "examples/keycode.rs" 24 | 25 | [[example]] 26 | name = "gamepad_button" 27 | path = "examples/gamepad_button.rs" 28 | 29 | [[example]] 30 | name = "multiple_input" 31 | path = "examples/multiple_input.rs" 32 | 33 | [dependencies] 34 | bevy = { version = "0.16", default-features = false, features = ["std", "async_executor", "bevy_log"] } 35 | trie-rs = { version = "0.4" } 36 | keyseq = { version = "0.6.0", features = [ "bevy" ] } 37 | 38 | [dev-dependencies] 39 | bevy = "0.16" 40 | trybuild = "1.0" 41 | version-sync = "0.9" 42 | 43 | [features] 44 | x11 = ["bevy/x11"] 45 | 46 | [package.metadata.docs.rs] 47 | features = ["x11"] 48 | -------------------------------------------------------------------------------- /examples/keycode.rs: -------------------------------------------------------------------------------- 1 | use bevy::prelude::*; 2 | use bevy_input_sequence::prelude::*; 3 | 4 | #[derive(Clone, Debug)] 5 | enum Direction { 6 | Clockwise, 7 | CounterClockwise, 8 | } 9 | 10 | #[derive(Event, Clone, Debug)] 11 | #[allow(dead_code)] 12 | struct MyEvent(Direction); 13 | 14 | fn main() { 15 | App::new() 16 | .add_plugins(DefaultPlugins) 17 | .add_plugins(InputSequencePlugin::default()) 18 | .add_event::() 19 | .add_systems(Startup, setup) 20 | .add_systems(Update, event_listener) 21 | .run(); 22 | } 23 | 24 | #[rustfmt::skip] 25 | fn setup(mut commands: Commands) { 26 | // Specify key codes directly. 27 | commands.queue( 28 | KeySequence::new( 29 | action::send_event(MyEvent(Direction::Clockwise)), 30 | [KeyCode::KeyW, 31 | KeyCode::KeyD, 32 | KeyCode::KeyS, 33 | KeyCode::KeyA], 34 | ) 35 | .time_limit(Duration::from_secs(1)), 36 | ); 37 | 38 | // Use keyseq! macro. 39 | commands.queue( 40 | KeySequence::new( 41 | action::send_event(MyEvent(Direction::CounterClockwise)), 42 | keyseq!{ W A S D }, 43 | ) 44 | .time_limit(Duration::from_secs(1)), 45 | ); 46 | 47 | println!("Press W D S A to emit clockwise event."); 48 | println!("Press W A S D to emit counter clockwise event."); 49 | } 50 | 51 | fn event_listener(mut er: EventReader) { 52 | for e in er.read() { 53 | println!("{:?} emitted.", e); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /examples/gamepad_button.rs: -------------------------------------------------------------------------------- 1 | use bevy::prelude::*; 2 | use bevy_input_sequence::prelude::*; 3 | 4 | #[derive(Event, Clone, Debug)] 5 | struct MyEvent(usize, Entity); 6 | 7 | fn main() { 8 | App::new() 9 | .add_plugins(DefaultPlugins) 10 | .add_plugins(InputSequencePlugin::default().match_button(true)) 11 | .add_event::() 12 | .add_systems(Startup, setup) 13 | .add_systems(Update, input_sequence_event_system) 14 | .run(); 15 | } 16 | 17 | fn setup(mut commands: Commands) { 18 | commands.queue( 19 | ButtonSequence::new( 20 | action::send_event_with_input(|gamepad| MyEvent(0, gamepad)), 21 | [ 22 | GamepadButton::North, 23 | GamepadButton::East, 24 | GamepadButton::South, 25 | GamepadButton::West, 26 | ], 27 | ) 28 | .time_limit(Duration::from_secs(1)), 29 | ); 30 | 31 | commands.queue( 32 | ButtonSequence::new( 33 | action::send_event_with_input(|gamepad| MyEvent(1, gamepad)), 34 | [ 35 | GamepadButton::North, 36 | GamepadButton::West, 37 | GamepadButton::South, 38 | GamepadButton::East, 39 | ], 40 | ) 41 | .time_limit(Duration::from_secs(1)), 42 | ); 43 | 44 | println!("Press north, east, south, west to emit MyEvent 0."); 45 | println!("Press north, west, south, east to emit MyEvent 1."); 46 | } 47 | 48 | fn input_sequence_event_system(mut er: EventReader) { 49 | for e in er.read() { 50 | println!("{:?} emitted from gamepad {:?}", e.0, e.1); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/action.rs: -------------------------------------------------------------------------------- 1 | //! Common actions to do on key sequence matches 2 | use bevy::ecs::{ 3 | event::{Event, EventWriter}, 4 | observer::TriggerTargets, 5 | prelude::Commands, 6 | system::In, 7 | }; 8 | 9 | /// Send this event. 10 | /// 11 | /// ```rust 12 | /// use bevy::prelude::*; 13 | /// use bevy_input_sequence::prelude::*; 14 | /// 15 | /// #[derive(Debug, Clone, Eq, PartialEq, Hash, States)] 16 | /// enum AppState { Menu, Game } 17 | /// #[derive(Event, Clone, Debug)] 18 | /// struct MyEvent; 19 | /// 20 | /// KeySequence::new( 21 | /// action::send_event(MyEvent), 22 | /// keyseq! { Space }); 23 | /// ``` 24 | pub fn send_event(event: E) -> impl FnMut(EventWriter) { 25 | move |mut writer: EventWriter| { 26 | writer.write(event.clone()); 27 | } 28 | } 29 | 30 | /// Trigger an event. 31 | pub fn trigger(event: E) -> impl FnMut(Commands) { 32 | move |mut commands: Commands| { 33 | commands.trigger(event.clone()); 34 | } 35 | } 36 | 37 | /// Trigger an event with targets. 38 | pub fn trigger_targets( 39 | event: E, 40 | targets: T, 41 | ) -> impl FnMut(Commands) { 42 | move |mut commands: Commands| { 43 | commands.trigger_targets(event.clone(), targets.clone()); 44 | } 45 | } 46 | 47 | /// Sends an event with input, .e.g, [ButtonSequence](crate::input_sequence::ButtonSequence) provides a [Gamepad](bevy::input::gamepad::Gamepad) identifier. 48 | pub fn send_event_with_input E>( 49 | mut f: F, 50 | ) -> impl FnMut(In, EventWriter) { 51 | move |In(x), mut writer: EventWriter| { 52 | writer.write(f(x)); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /examples/run_if.rs: -------------------------------------------------------------------------------- 1 | use bevy::prelude::*; 2 | use bevy_input_sequence::prelude::*; 3 | 4 | #[derive(Event, Clone, Debug)] 5 | struct MyEvent; 6 | 7 | #[derive(Debug, Clone, Copy, Default, Eq, PartialEq, Hash, States)] 8 | enum AppState { 9 | Menu, 10 | #[default] 11 | Game, 12 | } 13 | 14 | #[derive(SystemSet, Debug, Clone, PartialEq, Eq, Hash)] 15 | pub struct MySet; 16 | 17 | fn main() { 18 | App::new() 19 | .add_plugins(DefaultPlugins) 20 | .init_state::() 21 | .add_plugins(InputSequencePlugin::empty().run_in_set(Update, MySet)) 22 | .add_event::() 23 | .configure_sets(Update, MySet.run_if(in_state(AppState::Game))) 24 | .add_systems(Startup, setup) 25 | .add_systems(Update, listen_for_myevent) 26 | .add_systems(Update, listen_for_escape_key) 27 | .run(); 28 | } 29 | 30 | fn setup(mut commands: Commands) { 31 | commands.queue( 32 | KeySequence::new( 33 | action::send_event(MyEvent).only_if(in_state(AppState::Game)), 34 | keyseq! { Space }, 35 | ) 36 | .time_limit(Duration::from_secs(1)), 37 | ); 38 | println!("Press Space to emit event in game mode."); 39 | println!("Press Escape to switch between Game and Menu mode; currently in Game mode."); 40 | } 41 | 42 | fn listen_for_escape_key( 43 | keys: Res>, 44 | state: Res>, 45 | mut next_state: ResMut>, 46 | ) { 47 | if keys.just_pressed(KeyCode::Escape) { 48 | let new_state = match state.get() { 49 | AppState::Menu => AppState::Game, 50 | AppState::Game => AppState::Menu, 51 | }; 52 | println!("Going to state {:?}.", new_state); 53 | next_state.set(new_state); 54 | } 55 | } 56 | 57 | fn listen_for_myevent(mut er: EventReader) { 58 | for e in er.read() { 59 | println!("{e:?} emitted."); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /examples/only_if.rs: -------------------------------------------------------------------------------- 1 | use bevy::prelude::*; 2 | use bevy_input_sequence::prelude::*; 3 | 4 | #[derive(Event, Clone, Debug)] 5 | struct MyEvent; 6 | 7 | #[derive(Event, Clone, Debug)] 8 | struct GlobalEvent; 9 | 10 | #[derive(Debug, Clone, Copy, Default, Eq, PartialEq, Hash, States)] 11 | enum AppState { 12 | Menu, 13 | #[default] 14 | Game, 15 | } 16 | 17 | fn main() { 18 | App::new() 19 | .add_plugins(DefaultPlugins) 20 | .init_state::() 21 | .add_plugins(InputSequencePlugin::default()) 22 | .add_event::() 23 | .add_event::() 24 | .add_systems(Startup, setup) 25 | .add_systems(Update, listen_for_myevent) 26 | .add_systems(Update, listen_for_global_event) 27 | .run(); 28 | } 29 | 30 | fn setup(mut commands: Commands) { 31 | commands.queue(KeySequence::new( 32 | action::send_event(GlobalEvent), 33 | keyseq! { Escape }, 34 | )); 35 | commands.queue( 36 | KeySequence::new( 37 | action::send_event(MyEvent).only_if(in_state(AppState::Game)), 38 | keyseq! { Space }, 39 | ) 40 | .time_limit(Duration::from_secs(1)), 41 | ); 42 | println!("Press Space to emit event in game mode."); 43 | println!("Press Escape to switch between Game and Menu mode; currently in Game mode."); 44 | } 45 | 46 | fn listen_for_global_event( 47 | mut er: EventReader, 48 | state: Res>, 49 | mut next_state: ResMut>, 50 | ) { 51 | for _ in er.read() { 52 | let new_state = match state.get() { 53 | AppState::Menu => AppState::Game, 54 | AppState::Game => AppState::Menu, 55 | }; 56 | println!("Going to state {:?}.", new_state); 57 | next_state.set(new_state); 58 | } 59 | } 60 | 61 | fn listen_for_myevent(mut er: EventReader) { 62 | for e in er.read() { 63 | println!("{e:?} emitted."); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/chord.rs: -------------------------------------------------------------------------------- 1 | use bevy::{ 2 | input::keyboard::KeyCode, 3 | prelude::{Deref, DerefMut, ReflectResource, Resource}, 4 | reflect::{Enum, Reflect}, 5 | }; 6 | 7 | use std::{collections::VecDeque, fmt}; 8 | 9 | use keyseq::Modifiers; 10 | 11 | /// Represents a key chord, i.e., a set of modifiers and a key code. 12 | #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Reflect)] 13 | pub struct KeyChord(pub Modifiers, pub KeyCode); 14 | 15 | impl fmt::Display for KeyChord { 16 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 17 | use KeyCode::*; 18 | self.0.fmt(f)?; 19 | if !self.0.is_empty() { 20 | f.write_str("-")?; 21 | } 22 | let key_repr = match self.1 { 23 | Semicolon => ";", 24 | Period => ".", 25 | Equal => "=", 26 | Slash => "/", 27 | Minus => "-", 28 | BracketLeft => "[", 29 | BracketRight => "]", 30 | Quote => "'", 31 | Backquote => "`", 32 | key_code => { 33 | let mut key = key_code.variant_name(); 34 | key = key.strip_prefix("Key").unwrap_or(key); 35 | key = key.strip_prefix("Digit").unwrap_or(key); 36 | return f.write_str(key); 37 | } 38 | }; 39 | f.write_str(key_repr) 40 | } 41 | } 42 | 43 | impl From<(Modifiers, KeyCode)> for KeyChord { 44 | #[inline(always)] 45 | fn from((mods, key): (Modifiers, KeyCode)) -> Self { 46 | KeyChord(mods, key) 47 | } 48 | } 49 | 50 | impl From for KeyChord { 51 | #[inline(always)] 52 | fn from(key: KeyCode) -> Self { 53 | KeyChord(Modifiers::empty(), key) 54 | } 55 | } 56 | 57 | pub(crate) fn is_modifier(key: KeyCode) -> bool { 58 | !Modifiers::from(key).is_empty() 59 | } 60 | 61 | /// Manually add key chords to be processed as through they were pressed by the 62 | /// user. 63 | /// 64 | /// Normally this does not need to be manipulated. It is a kind of escape hatch. 65 | #[derive(Resource, Debug, Deref, DerefMut, Default, Reflect)] 66 | #[reflect(Resource)] 67 | pub struct KeyChordQueue(pub VecDeque); 68 | -------------------------------------------------------------------------------- /src/cache/key.rs: -------------------------------------------------------------------------------- 1 | //! Cache the trie for reuse. 2 | use crate::{input_sequence::InputSequence, KeyChord}; 3 | use bevy::ecs::prelude::Resource; 4 | use trie_rs::{ 5 | inc_search::{IncSearch, Position}, 6 | map::{Trie, TrieBuilder}, 7 | }; 8 | 9 | /// Contains the trie for gamepad button sequences. 10 | #[derive(Resource, Default)] 11 | pub struct KeySequenceCache { 12 | trie: Option>>, 13 | position: Option, 14 | } 15 | 16 | impl KeySequenceCache { 17 | /// Retrieve the cached trie without iterating through `sequences`. Or if 18 | /// the cache has been invalidated, build and cache a new trie using the 19 | /// `sequences` iterator. 20 | pub fn trie<'a>( 21 | &mut self, 22 | sequences: impl Iterator>, 23 | ) -> &Trie> { 24 | self.trie.get_or_insert_with(|| { 25 | let mut builder: TrieBuilder> = 26 | TrieBuilder::new(); 27 | for sequence in sequences { 28 | builder.insert(sequence.acts.clone(), sequence.clone()); 29 | } 30 | // info!( 31 | // "Building trie for {} input sequences.", 32 | // A::short_type_path() 33 | // ); 34 | builder.build() 35 | }) 36 | } 37 | 38 | /// Store a search. 39 | pub fn store(&mut self, position: Position) { 40 | self.position = Some(position) 41 | } 42 | 43 | /// Recall a search OR create a new search. 44 | pub fn recall<'a, 'b>( 45 | &'b mut self, 46 | sequences: impl Iterator>, 47 | ) -> IncSearch<'a, KeyChord, InputSequence> 48 | where 49 | 'b: 'a, 50 | { 51 | let position = self.position; 52 | let trie = self.trie(sequences); 53 | position 54 | .map(move |p| IncSearch::resume(trie, p)) 55 | .unwrap_or_else(move || trie.inc_search()) 56 | } 57 | 58 | /// Clears the cache. 59 | pub fn reset(&mut self) { 60 | self.trie = None; 61 | self.position = None; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /examples/multiple_input.rs: -------------------------------------------------------------------------------- 1 | use bevy::prelude::*; 2 | use bevy_input_sequence::prelude::*; 3 | 4 | #[derive(Event, Clone, Debug)] 5 | #[allow(dead_code)] 6 | struct MyEvent(u8, Option); 7 | 8 | fn main() { 9 | App::new() 10 | .add_plugins(DefaultPlugins) 11 | .add_plugins(InputSequencePlugin::default().match_button(true)) 12 | .add_event::() 13 | .add_systems(Startup, setup) 14 | .add_systems(Update, input_sequence_event_system) 15 | .run(); 16 | } 17 | 18 | fn setup(mut commands: Commands) { 19 | commands.queue( 20 | KeySequence::new(action::send_event(MyEvent(1, None)), keyseq! { W D S A }) 21 | .time_limit(Duration::from_secs(5)), 22 | ); 23 | 24 | commands.queue( 25 | ButtonSequence::new( 26 | action::send_event_with_input(|gamepad| MyEvent(2, Some(gamepad))), 27 | [ 28 | GamepadButton::North, 29 | GamepadButton::East, 30 | GamepadButton::South, 31 | GamepadButton::West, 32 | ], 33 | ) 34 | .time_limit(Duration::from_secs(5)), 35 | ); 36 | 37 | commands.queue( 38 | KeySequence::new(action::send_event(MyEvent(3, None)), keyseq! { W A S D }) 39 | .time_limit(Duration::from_secs(5)), 40 | ); 41 | 42 | commands.queue( 43 | ButtonSequence::new( 44 | action::send_event_with_input(|gamepad| MyEvent(4, Some(gamepad))), 45 | [ 46 | GamepadButton::North, 47 | GamepadButton::West, 48 | GamepadButton::South, 49 | GamepadButton::East, 50 | ], 51 | ) 52 | .time_limit(Duration::from_secs(5)), 53 | ); 54 | 55 | println!("Press W D S A or north east south west to emit event 1 and 2."); 56 | println!("Press W A S D or north west south east to emit event 3 and 4."); 57 | } 58 | 59 | fn input_sequence_event_system(mut er: EventReader) { 60 | for e in er.read() { 61 | println!( 62 | "{e:?} emitted {}", 63 | e.1.map(|x| format!("from gamepad id {}", x)) 64 | .unwrap_or("not from gamepad".into()) 65 | ); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /tests/macros.rs: -------------------------------------------------------------------------------- 1 | use bevy::prelude::*; 2 | use bevy_input_sequence::*; 3 | 4 | #[rustfmt::skip] 5 | #[test] 6 | fn before_cargo_format() { 7 | assert_eq!( 8 | [key![Ctrl-A], 9 | key! [Ctrl-A], 10 | key! [ Ctrl-A ], 11 | key!{Ctrl-A}, 12 | key! {Ctrl-A}, 13 | key! { Ctrl-A }, 14 | key!(Ctrl-A), 15 | key! (Ctrl-A), 16 | key! ( Ctrl-A ), 17 | ], 18 | [ 19 | (Modifiers::CONTROL, KeyCode::KeyA), 20 | (Modifiers::CONTROL, KeyCode::KeyA), 21 | (Modifiers::CONTROL, KeyCode::KeyA), 22 | (Modifiers::CONTROL, KeyCode::KeyA), 23 | (Modifiers::CONTROL, KeyCode::KeyA), 24 | (Modifiers::CONTROL, KeyCode::KeyA), 25 | (Modifiers::CONTROL, KeyCode::KeyA), 26 | (Modifiers::CONTROL, KeyCode::KeyA), 27 | (Modifiers::CONTROL, KeyCode::KeyA), 28 | ] 29 | ); 30 | } 31 | 32 | #[test] 33 | fn after_cargo_format() { 34 | assert_eq!( 35 | [ 36 | key![Ctrl - A], 37 | key![Ctrl - A], 38 | key![Ctrl - A], 39 | key! {Ctrl-A}, 40 | key! {Ctrl-A}, 41 | key! { Ctrl-A }, 42 | key!(Ctrl - A), 43 | key!(Ctrl - A), 44 | key!(Ctrl - A), 45 | ], 46 | [ 47 | (Modifiers::CONTROL, KeyCode::KeyA), 48 | (Modifiers::CONTROL, KeyCode::KeyA), 49 | (Modifiers::CONTROL, KeyCode::KeyA), 50 | (Modifiers::CONTROL, KeyCode::KeyA), 51 | (Modifiers::CONTROL, KeyCode::KeyA), 52 | (Modifiers::CONTROL, KeyCode::KeyA), 53 | (Modifiers::CONTROL, KeyCode::KeyA), 54 | (Modifiers::CONTROL, KeyCode::KeyA), 55 | (Modifiers::CONTROL, KeyCode::KeyA), 56 | ] 57 | ); 58 | } 59 | 60 | #[test] 61 | fn test_keyseq_doc() { 62 | assert_eq!( 63 | keyseq! { A B }, 64 | [ 65 | (Modifiers::empty(), KeyCode::KeyA), 66 | (Modifiers::empty(), KeyCode::KeyB) 67 | ] 68 | ); 69 | assert_eq!( 70 | keyseq! { Ctrl-A B }, 71 | [ 72 | (Modifiers::CONTROL, KeyCode::KeyA), 73 | (Modifiers::empty(), KeyCode::KeyB) 74 | ] 75 | ); 76 | assert_eq!( 77 | keyseq! { Ctrl-Alt-A Escape }, 78 | [ 79 | (Modifiers::ALT | Modifiers::CONTROL, KeyCode::KeyA), 80 | (Modifiers::empty(), KeyCode::Escape) 81 | ] 82 | ); 83 | assert_eq!( 84 | keyseq! { Ctrl-; }, 85 | [(Modifiers::CONTROL, KeyCode::Semicolon)] 86 | ); 87 | assert_eq!( 88 | keyseq! { Ctrl-Semicolon }, 89 | [(Modifiers::CONTROL, KeyCode::Semicolon)] 90 | ); 91 | } 92 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | ## [unreleased] 6 | 7 | ## [0.8.1] - 2025-04-28 8 | 9 | - Fix docs.rs build 10 | 11 | ## [0.8.0] - 2025-04-27 12 | 13 | - Add support for bevy v0.16 14 | 15 | ## [0.7.0] - 2024-12-09 16 | 17 | - Add support for bevy v0.15 18 | 19 | ## [0.6.0] - 2024-12-08 20 | 21 | - Add `action::trigger` and `action::trigger_targets` convenience systems. 22 | - Bump keyseq to 0.4.0, which changes notation to be more standard: `Ctrl-A` 23 | instead of `ctrl-A`. 24 | 25 | ## [0.5.0] - 2024-06-05 26 | 27 | ### Features 28 | - Optimize look ups to incrementally search using O(log n) instead of O(m^2 log n). See [PR #7](https://github.com/not-elm/bevy-input-sequence/pull/7) for more details. 29 | 30 | ### Bugs 31 | - Fix bug where "W A S D" and "A S" sequences would match the latter pattern when "W A S P" was typed. 32 | 33 | ## [0.4.0] - 2024-04-23 34 | 35 | ### Features 36 | 37 | - Generalize to running one-shot systems instead of firing events. 38 | - Use plugin. 39 | - Can configure schedule and system set. 40 | - Remove `GamepadEvent`; use system with input `In`. 41 | - Add `IntoCondSystem` that adds `only_if()` conditi:ons to `IntoSystems`. 42 | - Add `only_if` example. 43 | - Add `prelude` module for glob imports. 44 | 45 | ### Refactor 46 | 47 | - Hollow out lib so it's just `mod` and `pub use` statements. 48 | - Extract sets of functionality from `lib.rs` into `chord.rs`, `cache.rs` and `plugin.rs`. 49 | - Extract tests into `tests/simulated.rs`. 50 | 51 | ## [0.3.0] - 2024-03-08 52 | 53 | ### Features 54 | 55 | - Removed `Act::Any`, so we can no longer define an act composed of multiple buttons(or keycodes). 56 | - The keyboard and gamepad now use different sequences. 57 | 58 | 59 | ## [0.2.0] - 2024-02-24 60 | 61 | ### Features 62 | 63 | - Add support for bevy v0.12 64 | - Add key chord support 65 | - Add `key!` and `keyseq!` macro from `keyseq` crate. 66 | - Make controllers' sequences independent of one another. 67 | - Add `add_input_sequence_run_if()` variant. 68 | - Add "keymod" example that uses key chords. 69 | - Add "run_if" example 70 | 71 | ### Refactor 72 | 73 | - Make timeout setting more ergonomic. 74 | - Use a trie for sequence detection. 75 | 76 | Changes the algorithmic complexity each tick from `O(number of key_sequences)` 77 | to `O(length of longest key sequence)`. 78 | 79 | - Make `new()` generic: `new>(inputs: [T])` 80 | 81 | ### Documentation 82 | 83 | - Describe examples in README 84 | - Add installation, usage, and license sections to README 85 | 86 | ### Test 87 | 88 | - The `multiple_inputs` test is disabled. 89 | 90 | This test includes keyboard and controller inputs. It's not clear how to 91 | manage these with controller sequences being independent from one another. 92 | 93 | ## [0.1.0] - 2023-08-19 94 | 95 | - 一通りの機能の実装 96 | -------------------------------------------------------------------------------- /src/cond_system.rs: -------------------------------------------------------------------------------- 1 | //! Extend [IntoSystem] for conditional execution 2 | use bevy::ecs::system::{CombinatorSystem, Combine, IntoSystem, System, SystemIn, SystemInput}; 3 | use std::borrow::Cow; 4 | 5 | /// Extend [IntoSystem] to allow for some conditional execution. Probably only 6 | /// appropriate for one-shot systems. Prefer 7 | /// [`run_if()`](bevy::ecs::schedule::IntoScheduleConfigs::run_if()) when directly 8 | /// adding to the scheduler. 9 | pub trait IntoCondSystem: IntoSystem 10 | where 11 | I: SystemInput, 12 | { 13 | /// Only run self's system if the given `system` parameter returns true. No 14 | /// output is provided. (This is convenient for running systems with 15 | /// [bevy::prelude::Commands::run_system]). 16 | fn only_if(self, system: B) -> SilentCondSystem 17 | where 18 | B: IntoSystem<(), bool, MarkerB>, 19 | { 20 | let system_a = IntoSystem::into_system(self); 21 | let system_b = IntoSystem::into_system(system); 22 | let name = format!("SilentCond({}, {})", system_a.name(), system_b.name()); 23 | SilentCondSystem::new(system_a, system_b, Cow::Owned(name)) 24 | } 25 | 26 | /// Only run self's system if the given `system` parameter returns true. The 27 | /// output is an `Option`. `None` is returned when the condition 28 | /// returns false. 29 | fn only_if_with_output(self, system: B) -> CondSystem 30 | where 31 | B: IntoSystem<(), bool, MarkerB>, 32 | { 33 | let system_a = IntoSystem::into_system(self); 34 | let system_b = IntoSystem::into_system(system); 35 | let name = format!("Cond({}, {})", system_a.name(), system_b.name()); 36 | CondSystem::new(system_a, system_b, Cow::Owned(name)) 37 | } 38 | } 39 | 40 | impl IntoCondSystem for T 41 | where 42 | T: IntoSystem, 43 | I: SystemInput, 44 | { 45 | } 46 | 47 | /// A one-shot conditional system comprised of consequent `SystemA` and 48 | /// conditional `SystemB`. 49 | pub type CondSystem = CombinatorSystem; 50 | 51 | #[doc(hidden)] 52 | pub struct Cond; 53 | 54 | impl Combine for Cond 55 | where 56 | B: System, 57 | A: System, 58 | { 59 | type In = A::In; 60 | type Out = Option; 61 | 62 | fn combine( 63 | input: ::Inner<'_>, 64 | a: impl FnOnce(SystemIn<'_, A>) -> A::Out, 65 | b: impl FnOnce(SystemIn<'_, B>) -> B::Out, 66 | ) -> Self::Out { 67 | b(()).then(|| a(input)) 68 | } 69 | } 70 | 71 | /// A one-shot conditional system comprised of consequent `SystemA` and 72 | /// conditional `SystemB` with no output. 73 | pub type SilentCondSystem = CombinatorSystem; 74 | 75 | #[doc(hidden)] 76 | pub struct SilentCond; 77 | 78 | impl Combine for SilentCond 79 | where 80 | B: System, 81 | A: System, 82 | { 83 | type In = A::In; 84 | type Out = (); 85 | 86 | fn combine( 87 | input: ::Inner<'_>, 88 | a: impl FnOnce(SystemIn<'_, A>) -> A::Out, 89 | b: impl FnOnce(SystemIn<'_, B>) -> B::Out, 90 | ) -> Self::Out { 91 | if b(()) { 92 | a(input); 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/cache/button.rs: -------------------------------------------------------------------------------- 1 | //! Cache the trie for reuse. 2 | use crate::input_sequence::InputSequence; 3 | use bevy::{ 4 | ecs::prelude::Resource, 5 | prelude::{Entity, GamepadButton, In}, 6 | }; 7 | use std::collections::HashMap; 8 | use trie_rs::{ 9 | inc_search::{IncSearch, Position}, 10 | map::{Trie, TrieBuilder}, 11 | }; 12 | 13 | /// Contains the trie for gamepad button sequences. 14 | #[derive(Resource, Default)] 15 | pub struct ButtonSequenceCache { 16 | trie: Option>>>, 17 | position: HashMap, 18 | } 19 | 20 | impl ButtonSequenceCache { 21 | /// Retrieve the cached trie without iterating through `sequences`. Or if 22 | /// the cache has been invalidated, build and cache a new trie using the 23 | /// `sequences` iterator. 24 | pub fn trie<'a>( 25 | &mut self, 26 | sequences: impl Iterator>>, 27 | ) -> &Trie>> { 28 | self.trie.get_or_insert_with(|| { 29 | let mut builder: TrieBuilder>> = 30 | TrieBuilder::new(); 31 | for sequence in sequences { 32 | builder.insert(sequence.acts.clone(), sequence.clone()); 33 | } 34 | // info!( 35 | // "Building trie for {} input sequences.", 36 | // A::short_type_path() 37 | // ); 38 | assert!( 39 | self.position.is_empty(), 40 | "Position should be none when rebuilding trie" 41 | ); 42 | builder.build() 43 | }) 44 | } 45 | 46 | /// Store a search. 47 | pub fn store(&mut self, key: Entity, position: Position) { 48 | self.position.insert(key, position); 49 | } 50 | 51 | /// Recall a search OR create a new search. 52 | pub fn recall<'a, 'b>( 53 | &'b mut self, 54 | key: Entity, 55 | sequences: impl Iterator>>, 56 | ) -> IncSearch<'a, GamepadButton, InputSequence>> 57 | where 58 | 'b: 'a, 59 | { 60 | let position = self.position.get(&key).cloned(); 61 | let trie = self.trie(sequences); 62 | position 63 | .map(move |p| IncSearch::resume(trie, p)) 64 | .unwrap_or_else(move || trie.inc_search()) 65 | } 66 | 67 | // impl<'i, A, I> ButtonSequenceCache<'i, A, I> 68 | // where 69 | // A: Ord + Clone + Send + Sync + TypePath + 'static, 70 | // I: SystemInput + Send + Sync, 71 | // I::Inner<'i>: Clone + Eq + Hash + 'static, 72 | // { 73 | // // /// Retrieve the cached trie without iterating through `sequences`. Or if 74 | // // /// the cache has been invalidated, build and cache a new trie using the 75 | // // /// `sequences` iterator. 76 | // // pub fn trie<'a>( 77 | // // &mut self, 78 | // // sequences: impl Iterator>, 79 | // // ) -> &Trie> { 80 | // // self.trie.get_or_insert_with(|| { 81 | // // let mut builder: TrieBuilder> = TrieBuilder::new(); 82 | // // for sequence in sequences { 83 | // // builder.insert(sequence.acts.clone(), sequence.clone()); 84 | // // } 85 | // // // info!( 86 | // // // "Building trie for {} input sequences.", 87 | // // // A::short_type_path() 88 | // // // ); 89 | // // assert!( 90 | // // self.position.is_empty(), 91 | // // "Position should be none when rebuilding trie" 92 | // // ); 93 | // // builder.build() 94 | // // }) 95 | // // } 96 | 97 | // // /// Store a search. 98 | // // pub fn store(&mut self, key: I, position: Position) { 99 | // // self.position.insert(key, position); 100 | // // } 101 | 102 | // // /// Recall a search OR create a new search. 103 | // // pub fn recall<'a, 'b>( 104 | // // &'b mut self, 105 | // // key: I, 106 | // // sequences: impl Iterator>, 107 | // // ) -> IncSearch<'a, A, InputSequence> 108 | // // where 109 | // // 'b: 'a, 110 | // // { 111 | // // let position = self.position.get(&key).cloned(); 112 | // // let trie = self.trie(sequences); 113 | // // position 114 | // // .map(move |p| IncSearch::resume(trie, p)) 115 | // // .unwrap_or_else(move || trie.inc_search()) 116 | // // } 117 | // } 118 | 119 | /// Clears the cache. 120 | pub fn reset(&mut self) { 121 | self.trie = None; 122 | self.position.clear(); 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/input_sequence.rs: -------------------------------------------------------------------------------- 1 | //! Input sequences for keys and gamepad buttons 2 | use crate::{cond_system::IntoCondSystem, time_limit::TimeLimit, KeyChord}; 3 | use std::{fmt, marker::PhantomData}; 4 | 5 | use bevy::{ 6 | ecs::{ 7 | component::Component, 8 | entity::Entity, 9 | prelude::In, 10 | system::{IntoSystem, System, SystemId, SystemInput}, 11 | world::World, 12 | }, 13 | input::gamepad::GamepadButton, 14 | prelude::{ChildOf, EntityWorldMut}, 15 | reflect::Reflect, 16 | }; 17 | 18 | /// An input sequence is a series of acts that fires an event when matched with 19 | /// inputs within the given time limit. 20 | /// 21 | /// `InputSequence`, a keychord sequence doesn't have an input, 22 | /// but a gamepad `InputSequence>` provides an entity 23 | /// for the controller the input came from. 24 | #[derive(Component, Reflect)] 25 | #[reflect(from_reflect = false)] 26 | pub struct InputSequence { 27 | /// Event emitted 28 | #[reflect(ignore)] 29 | pub system_id: SystemId, 30 | /// Sequence of acts that trigger input sequence 31 | pub acts: Vec, 32 | /// Optional time limit after first match 33 | pub time_limit: Option, 34 | } 35 | 36 | impl Clone for InputSequence { 37 | fn clone(&self) -> Self { 38 | Self { 39 | system_id: self.system_id, 40 | acts: self.acts.clone(), 41 | time_limit: self.time_limit.clone(), 42 | } 43 | } 44 | } 45 | 46 | impl Clone for InputSequence> { 47 | fn clone(&self) -> Self { 48 | Self { 49 | system_id: self.system_id, 50 | acts: self.acts.clone(), 51 | time_limit: self.time_limit.clone(), 52 | } 53 | } 54 | } 55 | 56 | impl fmt::Debug for InputSequence { 57 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { 58 | #[derive(Debug)] 59 | #[allow(dead_code)] 60 | struct InputSequence<'a, Act> { 61 | // system_id: SystemId, 62 | acts: &'a Vec, 63 | time_limit: &'a Option, 64 | } 65 | 66 | let Self { 67 | acts, 68 | time_limit, 69 | system_id: _, 70 | } = self; 71 | 72 | fmt::Debug::fmt(&InputSequence { acts, time_limit }, f) 73 | } 74 | } 75 | 76 | /// An input sequence builder. 77 | pub struct InputSequenceBuilder { 78 | /// The action when to run when sequence matches 79 | pub system: S, 80 | /// Sequence of acts that trigger input sequence 81 | pub acts: Vec, 82 | /// Optional time limit after first match 83 | pub time_limit: Option, 84 | input: PhantomData, 85 | } 86 | 87 | impl InputSequenceBuilder { 88 | /// Create new input sequence. Not operant until added to an entity. 89 | pub fn new(system: C) -> InputSequenceBuilder 90 | where 91 | C: IntoCondSystem + 'static, 92 | I: SystemInput + Send + Sync + 'static, 93 | { 94 | InputSequenceBuilder { 95 | acts: Vec::new(), 96 | system: IntoSystem::into_system(system), 97 | time_limit: None, 98 | input: PhantomData, 99 | } 100 | } 101 | } 102 | 103 | impl InputSequenceBuilder 104 | where 105 | S: System, 106 | { 107 | /// Specify a time limit from the start of the first matching input. 108 | pub fn time_limit(mut self, time_limit: impl Into) -> Self { 109 | self.time_limit = Some(time_limit.into()); 110 | self 111 | } 112 | 113 | /// Build the InputSequence. Requires world to register the system. 114 | pub fn build(self, world: &mut World) -> InputSequence { 115 | InputSequence { 116 | system_id: world.register_system(self.system), 117 | acts: self.acts, 118 | time_limit: self.time_limit, 119 | } 120 | } 121 | } 122 | 123 | impl bevy::prelude::Command for InputSequenceBuilder 124 | where 125 | Act: Send + Sync + 'static, 126 | S: System + Send + Sync + 'static, 127 | I: SystemInput + Send + Sync + 'static, 128 | { 129 | fn apply(self, world: &mut World) { 130 | let act = self.build(world); 131 | let system_entity = act.system_id.entity(); 132 | let id = world.spawn(act).id(); 133 | world.entity_mut(system_entity).insert(ChildOf(id)); 134 | } 135 | } 136 | 137 | impl bevy::ecs::system::EntityCommand for InputSequenceBuilder 138 | where 139 | Act: Send + Sync + 'static, 140 | S: System + Send + Sync + 'static, 141 | I: SystemInput + Send + Sync + 'static, 142 | { 143 | fn apply(self, mut entity_world: EntityWorldMut) { 144 | let id = entity_world.id(); 145 | entity_world.world_scope(move |world: &mut World| { 146 | let act = self.build(world); 147 | let system_entity = act.system_id.entity(); 148 | let mut entity = world.get_entity_mut(id).unwrap(); 149 | entity.insert(act); 150 | world.entity_mut(system_entity).insert(ChildOf(id)); 151 | }); 152 | } 153 | } 154 | 155 | impl InputSequence 156 | where 157 | In: 'static, 158 | { 159 | /// Create new input sequence. Not operant until added to an entity. 160 | #[inline(always)] 161 | #[allow(clippy::new_ret_no_self)] 162 | pub fn new( 163 | system: C, 164 | acts: impl IntoIterator, 165 | ) -> InputSequenceBuilder 166 | where 167 | C: IntoCondSystem + 'static, 168 | Act: From, 169 | { 170 | let mut builder = InputSequenceBuilder::new(system); 171 | builder.acts = Vec::from_iter(acts.into_iter().map(Act::from)); 172 | builder 173 | } 174 | } 175 | 176 | /// Represents a key sequence 177 | pub type KeySequence = InputSequence; 178 | /// Represents a key sequence builder 179 | pub type KeySequenceBuilder = InputSequenceBuilder; 180 | 181 | /// Represents a gamepad button sequence 182 | pub type ButtonSequence = InputSequence>; 183 | -------------------------------------------------------------------------------- /tests/act.rs: -------------------------------------------------------------------------------- 1 | use bevy::prelude::*; 2 | use bevy_input_sequence::*; 3 | 4 | #[allow(unused_must_use)] 5 | #[test] 6 | fn test_key_eq() { 7 | let a: KeyChord = KeyCode::KeyA.into(); 8 | let b: KeyChord = KeyCode::KeyA.into(); 9 | assert_eq!(a, b); 10 | assert!(a == b); 11 | } 12 | 13 | #[allow(unused_must_use)] 14 | #[test] 15 | fn test_keyseq_macro() { 16 | assert_eq!(vec![(Modifiers::empty(), KeyCode::KeyA)], keyseq! { A }); 17 | assert_eq!( 18 | vec![ 19 | (Modifiers::empty(), KeyCode::KeyA), 20 | (Modifiers::empty(), KeyCode::KeyB), 21 | ], 22 | keyseq! { A B } 23 | ); 24 | } 25 | 26 | #[test] 27 | fn eq_if_contains_key_in_lhs() { 28 | let lhs = KeyChord(Modifiers::CONTROL, KeyCode::KeyA); 29 | let rhs = KeyChord(Modifiers::CONTROL, KeyCode::KeyA); 30 | assert!(lhs == rhs); 31 | assert!(rhs == lhs); 32 | } 33 | 34 | // #[test] 35 | // fn test_shifted_key_macro() { 36 | // assert_eq!((Modifiers::CONTROL, KeyCode::KeyB), key! { Ctrl-* }); 37 | // } 38 | 39 | /// XXX: This doc test isn't working. 40 | /// 41 | /// ``` 42 | /// assert_eq!((Modifiers::CONTROL, KeyCode::F2), key!{ Ctrl-F2 }); 43 | /// ``` 44 | /// 45 | /// ``` 46 | /// let _ = key! { Ctrl-* }); 47 | /// ``` 48 | #[allow(unused_must_use)] 49 | #[test] 50 | fn test_key_macro() { 51 | assert_eq!((Modifiers::CONTROL, KeyCode::KeyB), key! { Ctrl-B }); 52 | assert_eq!((Modifiers::CONTROL, KeyCode::Digit1), key! { Ctrl-1 }); 53 | assert_eq!((Modifiers::CONTROL, KeyCode::Digit2), key! { Ctrl-2 }); 54 | assert_eq!((Modifiers::CONTROL, KeyCode::F2), key! { Ctrl-F2 }); 55 | // assert_eq!((Modifiers::CONTROL, KeyCode::F2), key!{ Ctrl-f2 }); 56 | assert_eq!((Modifiers::CONTROL, KeyCode::Semicolon), key! { Ctrl-; }); 57 | // assert_eq!((Modifiers::CONTROL, KeyCode::Caret), key! { Ctrl-^ }); 58 | // assert_eq!((Modifiers::CONTROL, KeyCode::Colon), key! { Ctrl-: }); 59 | assert_eq!((Modifiers::CONTROL, KeyCode::Equal), key! { Ctrl-= }); 60 | assert_eq!((Modifiers::CONTROL, KeyCode::Comma), key! { Ctrl-, }); 61 | assert_eq!((Modifiers::CONTROL, KeyCode::Period), key! { Ctrl-. }); 62 | assert_eq!((Modifiers::CONTROL, KeyCode::Slash), key! { Ctrl-/ }); 63 | assert_eq!((Modifiers::CONTROL, KeyCode::Enter), key! { Ctrl-Enter }); 64 | assert_eq!((Modifiers::CONTROL, KeyCode::Space), key! { Ctrl-Space }); 65 | assert_eq!((Modifiers::CONTROL, KeyCode::Tab), key! { Ctrl-Tab }); 66 | assert_eq!((Modifiers::CONTROL, KeyCode::Delete), key! { Ctrl-Delete }); 67 | assert_eq!((Modifiers::CONTROL, KeyCode::Minus), key! { Ctrl-- }); 68 | assert_eq!( 69 | (Modifiers::CONTROL | Modifiers::SHIFT, KeyCode::Minus), 70 | key! { Ctrl-Shift-- } 71 | ); 72 | // assert_eq!((Modifiers::CONTROL, KeyCode::Underline), key! { Ctrl-_ }); 73 | // No colon key. 74 | // assert_eq!((Modifiers::CONTROL, KeyCode::Colon), key! { Ctrl-: }); 75 | assert_eq!( 76 | (Modifiers::CONTROL | Modifiers::SHIFT, KeyCode::Semicolon), 77 | key! { Ctrl-Shift-; } 78 | ); 79 | assert_eq!((Modifiers::CONTROL, KeyCode::Quote), key! { Ctrl-'\'' }); 80 | 81 | assert_eq!( 82 | (Modifiers::CONTROL | Modifiers::SHIFT, KeyCode::KeyA), 83 | key! { Ctrl-Shift-A } 84 | ); 85 | // assert_eq!((Modifiers::CONTROL, KeyCode::KeyA), key!{ Ctrl-A }); 86 | assert_eq!((Modifiers::SUPER, KeyCode::KeyA), key! { Super-A }); 87 | assert_eq!((Modifiers::CONTROL, KeyCode::KeyA), key! { Ctrl-A }); // Allow lowercase or demand lowercase? 88 | assert_eq!((Modifiers::empty(), KeyCode::KeyA), key! { A }); 89 | let k = (Modifiers::empty(), KeyCode::KeyA); 90 | assert_eq!(k, key! { A }); 91 | // assert_eq!( 92 | // (Modifiers::CONTROL, KeyCode::Asterisk), 93 | // key! { Ctrl-Asterisk } 94 | // ); 95 | assert_eq!( 96 | (Modifiers::CONTROL | Modifiers::SHIFT, KeyCode::Digit8), 97 | key! { Ctrl-Shift-8 } 98 | ); 99 | 100 | assert_eq!( 101 | (Modifiers::CONTROL | Modifiers::SHIFT, KeyCode::Digit8), 102 | key! { Ctrl-Shift-Digit8 } 103 | ); 104 | // All bevy KeyCode names work. 105 | // assert_eq!((Modifiers::CONTROL, KeyCode::Asterisk), key! { Ctrl-* }); // with some short hand. 106 | 107 | // assert_eq!((Modifiers::CONTROL, KeyCode::Plus), key! { Ctrl-+ }); 108 | assert_eq!( 109 | (Modifiers::CONTROL | Modifiers::SHIFT, KeyCode::Equal), 110 | key! { Ctrl-Shift-= } 111 | ); 112 | // assert_eq!((Modifiers::CONTROL, KeyCode::At), key! { Ctrl-@ }); 113 | assert_eq!( 114 | (Modifiers::CONTROL, KeyCode::BracketLeft), 115 | key! { Ctrl-'[' } 116 | ); 117 | assert_eq!( 118 | (Modifiers::CONTROL, KeyCode::BracketRight), 119 | key! { Ctrl-']' } 120 | ); 121 | assert_eq!( 122 | (Modifiers::CONTROL, KeyCode::BracketRight), 123 | key! { Ctrl-']' } 124 | ); 125 | assert_eq!((Modifiers::CONTROL, KeyCode::Backquote), key! { Ctrl-'`' }); 126 | assert_eq!((Modifiers::CONTROL, KeyCode::Backslash), key! { Ctrl-'\\' }); 127 | assert_eq!((Modifiers::CONTROL, KeyCode::Escape), key! { Ctrl-Escape }); 128 | // assert_eq!((Modifiers::CONTROL, KeyCode::Escape), key!{ Ctrl-Esc }); 129 | assert_eq!( 130 | (Modifiers::CONTROL | Modifiers::ALT, KeyCode::KeyA), 131 | key! { Ctrl-Alt-A } 132 | ); 133 | 134 | assert_eq!((Modifiers::empty(), KeyCode::KeyA), key! { A }); 135 | assert_eq!( 136 | (Modifiers::CONTROL | Modifiers::ALT, KeyCode::KeyA), 137 | key! { Ctrl-Alt-A } 138 | ); 139 | assert_eq!( 140 | (Modifiers::CONTROL | Modifiers::ALT, KeyCode::KeyA), 141 | key! { Ctrl-Alt-A } 142 | ); 143 | assert_eq!( 144 | (Modifiers::CONTROL | Modifiers::ALT, KeyCode::Semicolon), 145 | key! { Ctrl-Alt-Semicolon } 146 | ); 147 | assert_eq!( 148 | (Modifiers::CONTROL | Modifiers::ALT, KeyCode::Semicolon), 149 | key! { Ctrl-Alt-; } 150 | ); 151 | assert_eq!( 152 | ( 153 | Modifiers::CONTROL | Modifiers::ALT | Modifiers::SHIFT, 154 | KeyCode::Semicolon 155 | ), 156 | key! { Ctrl-Alt-Shift-; } // Ctrl-Alt-: 157 | ); 158 | assert_eq!( 159 | (Modifiers::CONTROL | Modifiers::ALT, KeyCode::Slash), 160 | key! { Ctrl-Alt-/ } 161 | ); 162 | } 163 | 164 | #[allow(unused_must_use)] 165 | #[test] 166 | fn test_keyseq() { 167 | assert_eq!( 168 | vec![(Modifiers::CONTROL, KeyCode::KeyA)], 169 | keyseq! { Ctrl-A } 170 | ); 171 | assert_eq!( 172 | vec![(Modifiers::CONTROL, KeyCode::KeyA)], 173 | keyseq! { Ctrl-Ctrl-A } 174 | ); 175 | assert_eq!( 176 | vec![ 177 | (Modifiers::CONTROL, KeyCode::KeyA), 178 | (Modifiers::ALT, KeyCode::KeyB) 179 | ], 180 | keyseq! { Ctrl-A Alt-B } 181 | ); 182 | 183 | assert_eq!( 184 | vec![ 185 | (Modifiers::empty(), KeyCode::KeyA), 186 | (Modifiers::empty(), KeyCode::KeyB) 187 | ], 188 | keyseq! { A B } 189 | ); 190 | } 191 | 192 | #[test] 193 | fn test_key_eq_not() { 194 | let a: KeyChord = KeyCode::KeyA.into(); 195 | let b: KeyChord = KeyCode::KeyB.into(); 196 | assert!(a != b); 197 | } 198 | 199 | #[test] 200 | fn test_key_eq_vec() { 201 | let a: Vec = vec![KeyCode::KeyA.into()]; 202 | let b: Vec = vec![KeyCode::KeyB.into()]; 203 | let c: Vec = vec![KeyCode::KeyA.into()]; 204 | let e: Vec = vec![]; 205 | assert!(a != b); 206 | assert!(a == c); 207 | assert_eq!(a, c); 208 | assert!(e != a); 209 | assert!(e != b); 210 | assert!(e != c); 211 | } 212 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # bevy-input-sequence 2 | 3 | Recognizes and acts on input sequences from the keyboard or a gamepad. 4 | 5 | # Use Cases 6 | 7 | * Hotkeys 8 | * Cheat codes 9 | * Developer UI 10 | 11 | # Installation 12 | 13 | ``` sh 14 | cargo install bevy-input-sequence 15 | ``` 16 | 17 | # Code Examples 18 | 19 | Here are some code snippets. These also run as doctests so they do a few things 20 | differently than a regular runnable example: 21 | 22 | - Instead of `DefaultPlugins` they use `MinimalPlugins`. 23 | - Instead of `app.run()` they call `app.update()`. 24 | 25 | The next section describes the runnable examples that come with the crate. 26 | 27 | ## Run a System on a Key Sequence 28 | 29 | Run a system whenever the user presses the key sequence `H I` or "hi" within a 30 | time limit. 31 | 32 | ```rust 33 | use bevy::prelude::*; 34 | use bevy_input_sequence::prelude::*; 35 | 36 | fn main() { 37 | App::new() 38 | .add_plugins(MinimalPlugins) 39 | .add_plugins(InputSequencePlugin::default()) 40 | .add_systems(Startup, setup) 41 | .update(); // Normally you'd run it here. 42 | } 43 | 44 | fn setup(mut commands: Commands) { 45 | // Add key sequence. 46 | commands.queue( 47 | KeySequence::new(say_hello, 48 | keyseq! { H I }) 49 | .time_limit(Duration::from_secs(2)) 50 | ); 51 | } 52 | 53 | fn say_hello() { 54 | info!("hello"); 55 | } 56 | ``` 57 | 58 | ## Send an Event on Key Sequence 59 | 60 | Originally `bevy-input-sequence` always sent an event. You can still do that 61 | with `action::send_event()`. 62 | 63 | ```rust 64 | use bevy::prelude::*; 65 | use bevy_input_sequence::prelude::*; 66 | 67 | /// Define an event. 68 | #[derive(Event, Clone, Debug)] 69 | struct MyEvent; 70 | 71 | /// Add event as an key sequence. 72 | fn main() { 73 | App::new() 74 | .add_plugins(MinimalPlugins) 75 | .add_plugins(InputSequencePlugin::default()) 76 | .add_event::() 77 | .add_systems(Startup, setup) 78 | .update(); // Normally you'd run it here. 79 | } 80 | 81 | fn setup(mut commands: Commands) { 82 | commands.queue( 83 | KeySequence::new(action::send_event(MyEvent), 84 | keyseq! { Ctrl-E Alt-L Shift-M }) 85 | ); 86 | } 87 | 88 | fn check_events(mut events: EventReader) { 89 | for event in events.read() { 90 | info!("got event {event:?}"); 91 | } 92 | } 93 | ``` 94 | 95 | ## Send an Event on Gamepad Button Sequence 96 | 97 | Gamepads have something that keyboards don't: identity problems. Which player 98 | hit the button sequence may be important to know. So the systems it accepts 99 | take an input of `Entity`. 100 | 101 | ```rust 102 | use bevy::prelude::*; 103 | use bevy_input_sequence::prelude::*; 104 | 105 | /// Define an event. 106 | #[derive(Event, Clone, Debug)] 107 | struct MyEvent(Entity); 108 | 109 | /// Add event as an key sequence. 110 | fn main() { 111 | App::new() 112 | .add_plugins(MinimalPlugins) 113 | .add_plugins(InputSequencePlugin::default()) 114 | .add_event::() 115 | .add_systems(Startup, setup) 116 | .update(); // Normally you'd run it here. 117 | } 118 | 119 | fn setup(mut commands: Commands) { 120 | commands.queue( 121 | ButtonSequence::new(action::send_event_with_input(|gamepad| MyEvent(gamepad)), 122 | [GamepadButton::North, 123 | GamepadButton::East, 124 | GamepadButton::South, 125 | GamepadButton::West]) 126 | ); 127 | } 128 | 129 | fn check_events(mut events: EventReader) { 130 | for event in events.read() { 131 | info!("got event {event:?}"); 132 | } 133 | } 134 | ``` 135 | 136 | ## Trigger an Event on Key Sequence 137 | 138 | You can also trigger an event with `action::trigger()` or `action::trigger_targets()`. 139 | 140 | ```rust 141 | use bevy::prelude::*; 142 | use bevy_input_sequence::prelude::*; 143 | 144 | /// Define an event. 145 | #[derive(Event, Clone, Debug)] 146 | struct MyEvent; 147 | 148 | /// Add event as an key sequence. 149 | fn main() { 150 | App::new() 151 | .add_plugins(MinimalPlugins) 152 | .add_plugins(InputSequencePlugin::default()) 153 | .add_event::() 154 | .add_systems(Startup, setup) 155 | .add_observer(check_trigger) 156 | .update(); // Normally you'd run it here. 157 | } 158 | 159 | fn setup(mut commands: Commands) { 160 | commands.queue( 161 | KeySequence::new(action::trigger(MyEvent), 162 | keyseq! { Ctrl-E Alt-L Super-M }) 163 | ); 164 | } 165 | 166 | fn check_trigger(mut trigger: Trigger) { 167 | info!("got event {:?}", trigger.event()); 168 | } 169 | ``` 170 | 171 | ## KeySequence Creation Patterns 172 | 173 | `KeySequence::new` now returns `KeySequenceBuilder`, which implements `Command`. 174 | Therefore, you need to call `Commands::queue` instead of `Commands::spawn`. 175 | 176 | ```rust 177 | use bevy::prelude::*; 178 | use bevy_input_sequence::prelude::*; 179 | 180 | #[derive(Event, Clone)] 181 | struct MyEvent; 182 | 183 | fn create_key_sequence(mut commands: Commands) { 184 | commands.queue(KeySequence::new( 185 | action::send_event(bevy::app::AppExit::default()), 186 | keyseq! { Ctrl-E L M } 187 | )); 188 | } 189 | 190 | fn create_key_sequence_and_add_it_to_an_entity(mut commands: Commands) { 191 | let id = commands.spawn_empty().id(); 192 | commands.entity(id).queue(KeySequence::new( 193 | action::send_event(MyEvent), 194 | keyseq! { Ctrl-E L M } 195 | )); 196 | // OR 197 | commands.spawn_empty().queue(KeySequence::new( 198 | action::send_event(MyEvent), 199 | keyseq! { Ctrl-E L M } 200 | )); 201 | } 202 | ``` 203 | 204 | ### Advanced Creation 205 | 206 | The `KeySequenceBuilder` requires a `&mut World` to build it. You can build it 207 | yourself like so: 208 | 209 | ```rust 210 | use bevy::prelude::*; 211 | use bevy_input_sequence::prelude::*; 212 | 213 | fn create_key_sequence_within_command(mut commands: Commands) { 214 | commands.queue(|world: &mut World| { 215 | let builder = KeySequence::new( 216 | move || { info!("got it"); }, 217 | keyseq! { Ctrl-E L M } 218 | ); 219 | let key_sequence: KeySequence = builder.build(world); 220 | // And then put it somewhere? It ought to go as a component. 221 | }); 222 | } 223 | ``` 224 | 225 | # Runnable Examples 226 | 227 | ## keycode 228 | 229 | The `keycode` example recognizes the key sequences `W D S A` and `W A S D` and 230 | fires a distinct event. 231 | 232 | ``` sh 233 | cargo run --example keycode 234 | ``` 235 | 236 | ## keymod 237 | 238 | The `keymod` example recognizes `Ctrl-W Ctrl-D Ctrl-S Ctrl-A` and fires an event. 239 | 240 | ``` sh 241 | cargo run --example keymod 242 | ``` 243 | 244 | ## gamepad_button 245 | 246 | The `gamepad_button` example recognizes gamepad buttons `North East South West` 247 | or `Y B A X` on an Xbox controller and fires an event. 248 | 249 | ``` sh 250 | cargo run --example gamepad_button 251 | ``` 252 | 253 | ## multiple_input 254 | 255 | The `multiple_input` example recognizes gamepad buttons `North East South West`, 256 | or `Y B A X` on an Xbox controller, or `W D S A` on a keyboard and fires an 257 | event. 258 | 259 | ``` sh 260 | cargo run --example multiple_input 261 | ``` 262 | 263 | Note: Either `W D S A` will be recognized from the keyboard, or `Y B A X` will 264 | be recognized from the controller. But a mixed sequence like `W D A X` will not 265 | currently be recognized. If this should be done and how exactly one should do it 266 | are under consideration. Please open an issue or PR if you have thoughts on this. 267 | 268 | ## only_if 269 | 270 | The `only_if` example recognizes `Space` and fires an event if it's in game 271 | mode. The `Escape` key toggles the app between menu and game mode. It does this 272 | by only sending the `Space` event if it's in game mode. 273 | 274 | ``` sh 275 | cargo run --example only_if 276 | ``` 277 | 278 | ## run_if 279 | 280 | The `run_if` has the same behavior as `only_if` but achieves it differently. It 281 | places the `InputSequencePlugin` systems in a system set that is configured to 282 | only run in game mode. Because of this the `Escape` key which toggles between 283 | game and menu mode cannot be a `KeySequence`. 284 | 285 | ``` sh 286 | cargo run --example run_if 287 | ``` 288 | 289 | # Compatibility 290 | 291 | | bevy-input-sequence | bevy | 292 | |---------------------|------| 293 | | 0.8 | 0.16 | 294 | | 0.7 | 0.15 | 295 | | 0.5 ~ 0.6 | 0.14 | 296 | | 0.3 ~ 0.4 | 0.13 | 297 | | 0.2 | 0.12 | 298 | | 0.1 | 0.11 | 299 | 300 | # License 301 | 302 | This crate is licensed under the MIT License or the Apache License 2.0. 303 | -------------------------------------------------------------------------------- /LICENSE-APACHE2: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /src/plugin.rs: -------------------------------------------------------------------------------- 1 | use 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 | }; 22 | use std::collections::{HashMap, VecDeque}; 23 | 24 | use crate::{ 25 | cache::{ButtonSequenceCache, KeySequenceCache}, 26 | chord::{is_modifier, KeyChordQueue}, 27 | frame_time::FrameTime, 28 | input_sequence::{ButtonSequence, InputSequence, KeySequence}, 29 | KeyChord, Modifiers, 30 | }; 31 | use trie_rs::inc_search::{Answer, IncSearch}; 32 | 33 | /// ButtonInput sequence plugin. 34 | pub struct InputSequencePlugin { 35 | #[allow(clippy::type_complexity)] 36 | schedules: Vec<(Interned, Option>)>, 37 | match_key: Option, 38 | match_button: Option, 39 | } 40 | 41 | impl 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 | 51 | impl Plugin for InputSequencePlugin { 52 | fn build(&self, app: &mut App) { 53 | if self 54 | .match_key 55 | .unwrap_or(app.world().get_resource::>().is_some()) 56 | { 57 | app 58 | .register_type::>() 59 | // .register_type::>() 60 | ; 61 | // Add key sequence. 62 | app.init_resource::(); 63 | app.init_resource::(); 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::>() 97 | // .is_some(), 98 | ) { 99 | // app 100 | // .register_type::>>() 101 | // // .register_type::>() 102 | // ; 103 | // Add button sequences. 104 | app.init_resource::(); 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 | 136 | impl 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` 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` exists. 170 | pub fn match_button(mut self, yes: bool) -> Self { 171 | self.match_button = Some(yes); 172 | self 173 | } 174 | } 175 | 176 | fn detect_key_additions( 177 | sequences: Query<&InputSequence, Added>>, 178 | mut cache: ResMut, 179 | ) { 180 | if sequences.iter().next().is_some() { 181 | cache.reset(); 182 | } 183 | } 184 | 185 | #[allow(clippy::type_complexity)] 186 | fn detect_button_additions( 187 | sequences: Query< 188 | &InputSequence>, 189 | Added>>, 190 | >, 191 | mut cache: ResMut, 192 | ) { 193 | if sequences.iter().next().is_some() { 194 | cache.reset(); 195 | } 196 | } 197 | 198 | fn detect_key_removals( 199 | mut cache: ResMut, 200 | mut removals: RemovedComponents>, 201 | ) { 202 | if removals.read().next().is_some() { 203 | cache.reset(); 204 | } 205 | } 206 | 207 | fn detect_button_removals( 208 | mut cache: ResMut, 209 | mut removals: RemovedComponents>>, 210 | ) { 211 | if removals.read().next().is_some() { 212 | cache.reset(); 213 | } 214 | } 215 | 216 | #[allow(clippy::too_many_arguments)] 217 | fn button_sequence_matcher( 218 | sequences: Query<&ButtonSequence>, 219 | time: Res