From 3e2e619157631053d0c05fade2745a252b8ffcdd Mon Sep 17 00:00:00 2001 From: Hennadii Chernyshchyk Date: Mon, 28 Oct 2024 21:03:16 +0200 Subject: [PATCH] Add `BlockBy::events_only` --- CHANGELOG.md | 1 + examples/all_conditions.rs | 8 +- src/input_context/context_instance.rs | 4 +- src/input_context/input_condition.rs | 11 ++- src/input_context/input_condition/block_by.rs | 27 +++++- src/input_context/trigger_tracker.rs | 16 +++- tests/condition_kind.rs | 85 +++++++++++++++++++ tests/state_and_value_merge.rs | 44 ++++++++++ 8 files changed, 186 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cf1bf97..eb197ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Logging for binding. - `ActionsData::insert_action`. +- `BlockBy::events` to block only events. Could be used for chords to avoid triggering required actions. ### Changed diff --git a/examples/all_conditions.rs b/examples/all_conditions.rs index fc2e981..9b53116 100644 --- a/examples/all_conditions.rs +++ b/examples/all_conditions.rs @@ -61,8 +61,12 @@ impl InputContext for DummyContext { ctx.bind::() .with(TapAction::KEY) .with_condition(Tap::new(0.5)); - ctx.bind::().with(ChordMember1::KEY); - ctx.bind::().with(ChordMember2::KEY); + ctx.bind::() + .with(ChordMember1::KEY) + .with_condition(BlockBy::::events_only()); // Don't trigger the action when the chord is active. + ctx.bind::() + .with(ChordMember2::KEY) + .with_condition(BlockBy::::events_only()); ctx.bind::() .with_condition(Chord::::default()) .with_condition(Chord::::default()); diff --git a/src/input_context/context_instance.rs b/src/input_context/context_instance.rs index 8747127..1c02b2d 100644 --- a/src/input_context/context_instance.rs +++ b/src/input_context/context_instance.rs @@ -306,7 +306,9 @@ impl ActionBind { let value = tracker.value().convert(self.dim); action.update_time(time); - action.trigger_events(commands, entities, state, value); + if !tracker.events_blocked() { + action.trigger_events(commands, entities, state, value); + } action.set_state(state); } } diff --git a/src/input_context/input_condition.rs b/src/input_context/input_condition.rs index cde3a17..645cd42 100644 --- a/src/input_context/input_condition.rs +++ b/src/input_context/input_condition.rs @@ -49,6 +49,8 @@ pub trait InputCondition: Sync + Send + Debug + 'static { /// If no conditions are provided, the state will be set to [`ActionState::Fired`] /// on any non-zero value, functioning similarly to a [`Down`](down::Down) condition /// with a zero actuation threshold. +/// +/// For details about how actions are combined, see [`ContextInstance`](super::context_instance::ContextInstance). pub enum ConditionKind { /// The most significant [`ActionState`] from all explicit conditions will be the /// resulting state. @@ -59,8 +61,13 @@ pub enum ConditionKind { /// Otherwise, the most significant state will be capped at [`ActionState::Ongoing`]. Implicit, /// If any blocking condition fails to return [`ActionState::Fired`], - /// it will override the state with [`ActionState::None`]. + /// it will override the state with [`ActionState::None`] or block the events. /// /// Doesn't contribute to the state on its own. - Blocker, + Blocker { + /// Block only events instead of overriding the state. + /// + /// Other actions will be able to see the action state in [`ActionsData`]. + events_only: bool, + }, } diff --git a/src/input_context/input_condition/block_by.rs b/src/input_context/input_condition/block_by.rs index 2ec5beb..0befc66 100644 --- a/src/input_context/input_condition/block_by.rs +++ b/src/input_context/input_condition/block_by.rs @@ -9,18 +9,37 @@ use crate::{ }; /// Requires another action to not be triggered within the same context. -/// -/// Could be used for chords to avoid triggering required actions. #[derive(Debug)] pub struct BlockBy { /// Action that blocks this condition when active. marker: PhantomData, + + /// Wheter to block the state or only the events. + /// + /// By default set to false. + pub events_only: bool, +} + +impl BlockBy { + /// Block only events. + /// + /// For details, see [`ConditionKind::Blocker::events_only`]. + /// + /// This can be used for chords to avoid triggering required actions. + /// Otherwise, the chord will register the release and cancel itself. + pub fn events_only() -> Self { + Self { + marker: PhantomData, + events_only: true, + } + } } impl Default for BlockBy { fn default() -> Self { Self { marker: PhantomData, + events_only: false, } } } @@ -55,7 +74,9 @@ impl InputCondition for BlockBy { } fn kind(&self) -> ConditionKind { - ConditionKind::Blocker + ConditionKind::Blocker { + events_only: self.events_only, + } } } diff --git a/src/input_context/trigger_tracker.rs b/src/input_context/trigger_tracker.rs index 826d553..6b38373 100644 --- a/src/input_context/trigger_tracker.rs +++ b/src/input_context/trigger_tracker.rs @@ -20,6 +20,7 @@ pub(super) struct TriggerTracker { found_implicit: bool, all_implicits_fired: bool, blocked: bool, + events_blocked: bool, } impl TriggerTracker { @@ -33,6 +34,7 @@ impl TriggerTracker { found_implicit: false, all_implicits_fired: true, blocked: false, + events_blocked: false, } } @@ -74,8 +76,13 @@ impl TriggerTracker { self.all_implicits_fired &= state == ActionState::Fired; self.found_active |= state != ActionState::None; } - ConditionKind::Blocker => { - self.blocked = state == ActionState::None; + ConditionKind::Blocker { events_only } => { + let blocked = state == ActionState::None; + if events_only { + self.events_blocked = blocked; + } else { + self.blocked = blocked; + } } } } @@ -107,6 +114,10 @@ impl TriggerTracker { self.value } + pub(super) fn events_blocked(&self) -> bool { + self.events_blocked + } + /// Merges input-level tracker into an action-level tracker. pub(super) fn merge(&mut self, other: Self, accumulation: Accumulation) { match self.state().cmp(&other.state()) { @@ -137,6 +148,7 @@ impl TriggerTracker { self.found_implicit |= other.found_implicit; self.all_implicits_fired &= other.all_implicits_fired; self.blocked |= other.blocked; + self.events_blocked |= other.events_blocked; } Ordering::Greater => (), } diff --git a/tests/condition_kind.rs b/tests/condition_kind.rs index f15aa75..49b79f2 100644 --- a/tests/condition_kind.rs +++ b/tests/condition_kind.rs @@ -202,6 +202,80 @@ fn blocker() { assert_eq!(event.state, ActionState::Fired); } +#[test] +fn events_blocker() { + let mut app = App::new(); + app.add_plugins(( + MinimalPlugins, + InputPlugin, + EnhancedInputPlugin, + ActionRecorderPlugin, + )) + .add_input_context::() + .record_action::() + .record_action::(); + + let entity = app.world_mut().spawn(DummyContext).id(); + + app.update(); + + let recorded = app.world().resource::(); + + let events = recorded.get::(entity).unwrap(); + assert!(events.is_empty()); + + let events = recorded.get::(entity).unwrap(); + assert!(events.is_empty()); + + let mut keys = app.world_mut().resource_mut::>(); + keys.press(ReleaseAction::KEY); + keys.press(EventsBlocker::KEY); + + app.update(); + + let recorded = app.world().resource::(); + + let events = recorded.get::(entity).unwrap(); + let event = events.last().unwrap(); + assert_eq!(event.value, true.into()); + assert_eq!(event.state, ActionState::Ongoing); + + let events = recorded.get::(entity).unwrap(); + let event = events.last().unwrap(); + assert_eq!(event.value, true.into()); + assert_eq!(event.state, ActionState::Fired); + + app.world_mut() + .resource_mut::>() + .release(ReleaseAction::KEY); + + app.update(); + + let recorded = app.world().resource::(); + + let events = recorded.get::(entity).unwrap(); + let event = events.last().unwrap(); + assert_eq!(event.value, false.into()); + assert_eq!(event.state, ActionState::Fired); + + let events = recorded.get::(entity).unwrap(); + assert!(events.is_empty()); + + app.update(); + + let recorded = app.world().resource::(); + + let events = recorded.get::(entity).unwrap(); + let event = events.last().unwrap(); + assert_eq!(event.value, false.into()); + assert_eq!(event.state, ActionState::None); + + let events = recorded.get::(entity).unwrap(); + let event = events.last().unwrap(); + assert_eq!(event.value, true.into()); + assert_eq!(event.state, ActionState::Fired); +} + #[derive(Debug, Component)] struct DummyContext; @@ -220,6 +294,9 @@ impl InputContext for DummyContext { ctx.bind::() .with(Blocker::KEY) .with_condition(BlockBy::::default()); + ctx.bind::() + .with(EventsBlocker::KEY) + .with_condition(BlockBy::::events_only()); ctx } @@ -252,3 +329,11 @@ struct Blocker; impl Blocker { const KEY: KeyCode = KeyCode::KeyD; } + +#[derive(Debug, InputAction)] +#[input_action(dim = Bool)] +struct EventsBlocker; + +impl EventsBlocker { + const KEY: KeyCode = KeyCode::KeyE; +} diff --git a/tests/state_and_value_merge.rs b/tests/state_and_value_merge.rs index c358b3d..fd3bddf 100644 --- a/tests/state_and_value_merge.rs +++ b/tests/state_and_value_merge.rs @@ -84,6 +84,16 @@ fn input_level() { ActionState::None, "if a blocker condition fails, it should override other conditions" ); + + let mut keys = app.world_mut().resource_mut::>(); + keys.release(Blocker::KEY); + keys.press(EventsBlocker::KEY); + + app.update(); + + let recorded = app.world().resource::(); + let events = recorded.get::(entity).unwrap(); + assert!(events.is_empty()); } #[test] @@ -165,6 +175,16 @@ fn action_level() { ActionState::None, "if a blocker condition fails, it should override other conditions" ); + + let mut keys = app.world_mut().resource_mut::>(); + keys.release(Blocker::KEY); + keys.press(EventsBlocker::KEY); + + app.update(); + + let recorded = app.world().resource::(); + let events = recorded.get::(entity).unwrap(); + assert!(events.is_empty()); } #[test] @@ -246,6 +266,16 @@ fn both_levels() { ActionState::None, "if a blocker condition fails, it should override other conditions" ); + + let mut keys = app.world_mut().resource_mut::>(); + keys.release(Blocker::KEY); + keys.press(EventsBlocker::KEY); + + app.update(); + + let recorded = app.world().resource::(); + let events = recorded.get::(entity).unwrap(); + assert!(events.is_empty()); } #[derive(Debug, Component)] @@ -259,17 +289,20 @@ impl InputContext for DummyContext { let release = Release::default(); let chord = Chord::::default(); let block_by = BlockBy::::default(); + let block_events_by = BlockBy::::events_only(); let swizzle_axis = SwizzleAxis::YXZ; let negate = Negate::default(); let scale = Scale::splat(2.0); ctx.bind::().with(ChordMember::KEY); ctx.bind::().with(Blocker::KEY); + ctx.bind::().with(EventsBlocker::KEY); ctx.bind::() .with( InputBind::new(InputLevel::KEY1) .with_condition(chord) .with_condition(block_by) + .with_condition(block_events_by) .with_condition(down) .with_condition(release) .with_modifier(swizzle_axis) @@ -279,6 +312,7 @@ impl InputContext for DummyContext { InputBind::new(InputLevel::KEY2) .with_condition(chord) .with_condition(block_by) + .with_condition(block_events_by) .with_condition(down) .with_condition(release) .with_modifier(swizzle_axis) @@ -291,6 +325,7 @@ impl InputContext for DummyContext { .with_condition(release) .with_condition(chord) .with_condition(block_by) + .with_condition(block_events_by) .with_modifier(swizzle_axis) .with_modifier(negate) .with_modifier(scale); @@ -308,6 +343,7 @@ impl InputContext for DummyContext { .with_condition(release) .with_condition(chord) .with_condition(block_by) + .with_condition(block_events_by) .with_modifier(swizzle_axis); ctx @@ -356,3 +392,11 @@ struct Blocker; impl Blocker { const KEY: KeyCode = KeyCode::KeyH; } + +#[derive(Debug, InputAction)] +#[input_action(dim = Bool)] +struct EventsBlocker; + +impl EventsBlocker { + const KEY: KeyCode = KeyCode::KeyI; +}