Skip to content

Commit

Permalink
Generalize bubbling focus input events to other kinds of input (#16876)
Browse files Browse the repository at this point in the history
# Objective

The new `bevy_input_focus` crates has a tool to bubble input events up
the entity hierarchy, ending with the window, based on the currently
focused entity. Right now though, this only works for keyboard events!

Both `bevy_ui` buttons and `bevy_egui` should hook into this system
(primarily for contextual hotkeys), and we would like to drive
`leafwing_input_manager` via these events, to help resolve longstanding
pain around "absorbing" / "consuming" inputs based on focus. In order to
make that work properly though, we need gamepad support!

## Solution

The logic backing this has been changed to be generic for any cloneable
event types, and the machinery to make use of this externally has been
made `pub`.

Within the engine itself, I've added support for gamepad button and
scroll events, but nothing else. Mouse button / touch bubbling is
handled via bevy_picking, and mouse / gamepad motion doesn't really make
sense to bubble.

## Testing

The `tab_navigation` example continues to work, and CI is green.

## Future Work

I would like to add more complex UI examples to stress test this, but
not here please.

We should take advantage of the bubbled mouse scrolling when defining
scrolled widgets.
  • Loading branch information
alice-i-cecile authored Dec 18, 2024
1 parent a4b89d0 commit b9123e7
Show file tree
Hide file tree
Showing 2 changed files with 49 additions and 27 deletions.
64 changes: 40 additions & 24 deletions crates/bevy_input_focus/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,10 @@ pub mod tab_navigation;

use bevy_app::{App, Plugin, PreUpdate, Startup};
use bevy_ecs::{
component::Component,
entity::Entity,
event::{Event, EventReader},
query::{QueryData, With},
system::{Commands, Query, Res, ResMut, Resource, Single, SystemParam},
traversal::Traversal,
world::{Command, DeferredWorld, World},
prelude::*, query::QueryData, system::SystemParam, traversal::Traversal, world::DeferredWorld,
};
use bevy_hierarchy::{HierarchyQueryExt, Parent};
use bevy_input::keyboard::KeyboardInput;
use bevy_input::{gamepad::GamepadButtonChangedEvent, keyboard::KeyboardInput, mouse::MouseWheel};
use bevy_window::{PrimaryWindow, Window};
use core::fmt::Debug;

Expand Down Expand Up @@ -111,17 +105,22 @@ impl SetInputFocus for Commands<'_, '_> {
}
}

/// A bubble-able event for keyboard input. This event is normally dispatched to the current
/// input focus entity, if any. If no entity has input focus, then the event is dispatched to
/// the main window.
/// A bubble-able user input event that starts at the currently focused entity.
///
/// This event is normally dispatched to the current input focus entity, if any.
/// If no entity has input focus, then the event is dispatched to the main window.
///
/// To set up your own bubbling input event, add the [`dispatch_focused_input::<MyEvent>`](dispatch_focused_input) system to your app,
/// in the [`InputFocusSet::Dispatch`] system set during [`PreUpdate`].
#[derive(Clone, Debug, Component)]
pub struct FocusKeyboardInput {
/// The keyboard input event.
pub input: KeyboardInput,
pub struct FocusedInput<E: Event + Clone> {
/// The underlying input event.
pub input: E,
/// The primary window entity.
window: Entity,
}

impl Event for FocusKeyboardInput {
impl<E: Event + Clone> Event for FocusedInput<E> {
type Traversal = WindowTraversal;

const AUTO_PROPAGATE: bool = true;
Expand All @@ -134,8 +133,8 @@ pub struct WindowTraversal {
window: Option<&'static Window>,
}

impl Traversal<FocusKeyboardInput> for WindowTraversal {
fn traverse(item: Self::Item<'_>, event: &FocusKeyboardInput) -> Option<Entity> {
impl<E: Event + Clone> Traversal<FocusedInput<E>> for WindowTraversal {
fn traverse(item: Self::Item<'_>, event: &FocusedInput<E>) -> Option<Entity> {
let WindowTraversalItem { parent, window } = item;

// Send event to parent, if it has one.
Expand All @@ -161,10 +160,27 @@ impl Plugin for InputDispatchPlugin {
app.add_systems(Startup, set_initial_focus)
.insert_resource(InputFocus(None))
.insert_resource(InputFocusVisible(false))
.add_systems(PreUpdate, dispatch_keyboard_input);
.add_systems(
PreUpdate,
(
dispatch_focused_input::<KeyboardInput>,
dispatch_focused_input::<GamepadButtonChangedEvent>,
dispatch_focused_input::<MouseWheel>,
)
.in_set(InputFocusSet::Dispatch),
);
}
}

/// System sets for [`bevy_input_focus`](crate).
///
/// These systems run in the [`PreUpdate`] schedule.
#[derive(SystemSet, Debug, PartialEq, Eq, Hash, Clone)]
pub enum InputFocusSet {
/// System which dispatches bubbled input events to the focused entity, or to the primary window.
Dispatch,
}

/// Sets the initial focus to the primary window, if any.
pub fn set_initial_focus(
mut input_focus: ResMut<InputFocus>,
Expand All @@ -173,10 +189,10 @@ pub fn set_initial_focus(
input_focus.0 = Some(*window);
}

/// System which dispatches keyboard input events to the focused entity, or to the primary window
/// System which dispatches bubbled input events to the focused entity, or to the primary window
/// if no entity has focus.
fn dispatch_keyboard_input(
mut key_events: EventReader<KeyboardInput>,
pub fn dispatch_focused_input<E: Event + Clone>(
mut key_events: EventReader<E>,
focus: Res<InputFocus>,
windows: Query<Entity, With<PrimaryWindow>>,
mut commands: Commands,
Expand All @@ -186,7 +202,7 @@ fn dispatch_keyboard_input(
if let Some(focus_elt) = focus.0 {
for ev in key_events.read() {
commands.trigger_targets(
FocusKeyboardInput {
FocusedInput {
input: ev.clone(),
window,
},
Expand All @@ -198,7 +214,7 @@ fn dispatch_keyboard_input(
// There should be only one primary window.
for ev in key_events.read() {
commands.trigger_targets(
FocusKeyboardInput {
FocusedInput {
input: ev.clone(),
window,
},
Expand Down Expand Up @@ -326,7 +342,7 @@ mod tests {
struct GatherKeyboardEvents(String);

fn gather_keyboard_events(
trigger: Trigger<FocusKeyboardInput>,
trigger: Trigger<FocusedInput<KeyboardInput>>,
mut query: Query<&mut GatherKeyboardEvents>,
) {
if let Ok(mut gather) = query.get_mut(trigger.target()) {
Expand Down
12 changes: 9 additions & 3 deletions crates/bevy_input_focus/src/tab_navigation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,14 @@ use bevy_ecs::{
world::DeferredWorld,
};
use bevy_hierarchy::{Children, HierarchyQueryExt, Parent};
use bevy_input::{keyboard::KeyCode, ButtonInput, ButtonState};
use bevy_input::{
keyboard::{KeyCode, KeyboardInput},
ButtonInput, ButtonState,
};
use bevy_utils::tracing::warn;
use bevy_window::PrimaryWindow;

use crate::{FocusKeyboardInput, InputFocus, InputFocusVisible, SetInputFocus};
use crate::{FocusedInput, InputFocus, InputFocusVisible, SetInputFocus};

/// A component which indicates that an entity wants to participate in tab navigation.
///
Expand Down Expand Up @@ -263,8 +266,11 @@ fn setup_tab_navigation(mut commands: Commands, window: Query<Entity, With<Prima
}

/// Observer function which handles tab navigation.
///
/// This observer responds to [`KeyCode::Tab`] events and Shift+Tab events,
/// cycling through focusable entities in the order determined by their tab index.
pub fn handle_tab_navigation(
mut trigger: Trigger<FocusKeyboardInput>,
mut trigger: Trigger<FocusedInput<KeyboardInput>>,
nav: TabNavigation,
mut focus: ResMut<InputFocus>,
mut visible: ResMut<InputFocusVisible>,
Expand Down

0 comments on commit b9123e7

Please sign in to comment.