Generalize bubbling focus input events to other kinds of input (#16876)

# 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.
This commit is contained in:
Alice Cecile 2024-12-17 17:04:50 -08:00 committed by GitHub
parent a4b89d0d5e
commit b9123e74b6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 49 additions and 27 deletions

View File

@ -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;
@ -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;
@ -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.
@ -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>,
@ -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,
@ -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,
},
@ -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,
},
@ -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()) {

View File

@ -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.
///
@ -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>,