Skip to content

Commit

Permalink
Tab navigation framework for bevy_input_focus. (#16795)
Browse files Browse the repository at this point in the history
# Objective

This PR continues the work of `bevy_input_focus` by adding a pluggable
tab navigation framework.

As part of this work, `FocusKeyboardEvent` now propagates to the window
after exhausting all ancestors.

## Testing

Unit tests and manual tests.

---------

Co-authored-by: Alice Cecile <[email protected]>
  • Loading branch information
viridia and alice-i-cecile authored Dec 16, 2024
1 parent bf3692a commit 5c67cfc
Show file tree
Hide file tree
Showing 6 changed files with 636 additions and 15 deletions.
11 changes: 11 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3947,3 +3947,14 @@ doc-scrape-examples = true

[package.metadata.example.testbed_ui_layout_rounding]
hidden = true

[[example]]
name = "tab_navigation"
path = "examples/ui/tab_navigation.rs"
doc-scrape-examples = true

[package.metadata.example.tab_navigation]
name = "Tab Navigation"
description = "Demonstration of Tab Navigation"
category = "UI (User Interface)"
wasm = true
1 change: 1 addition & 0 deletions crates/bevy_input_focus/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ bevy_app = { path = "../bevy_app", version = "0.15.0-dev", default-features = fa
bevy_ecs = { path = "../bevy_ecs", version = "0.15.0-dev", default-features = false }
bevy_input = { path = "../bevy_input", version = "0.15.0-dev", default-features = false }
bevy_hierarchy = { path = "../bevy_hierarchy", version = "0.15.0-dev", default-features = false }
bevy_utils = { path = "../bevy_utils", version = "0.15.0-dev", default-features = false }
bevy_window = { path = "../bevy_window", version = "0.15.0-dev", default-features = false }

[dev-dependencies]
Expand Down
82 changes: 67 additions & 15 deletions crates/bevy_input_focus/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,22 @@
//! This crate does *not* provide any integration with UI widgets, or provide functions for
//! tab navigation or gamepad-based focus navigation, as those are typically application-specific.
pub mod tab_navigation;

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

/// Resource representing which entity has input focus, if any. Keyboard events will be
/// dispatched to the current focus entity, or to the primary window if no entity has focus.
Expand Down Expand Up @@ -102,14 +106,43 @@ impl SetInputFocus for Commands<'_, '_> {
/// input focus entity, if any. If no entity has input focus, then the event is dispatched to
/// the main window.
#[derive(Clone, Debug, Component)]
pub struct FocusKeyboardInput(pub KeyboardInput);
pub struct FocusKeyboardInput {
/// The keyboard input event.
pub input: KeyboardInput,
window: Entity,
}

impl Event for FocusKeyboardInput {
type Traversal = &'static Parent;
type Traversal = WindowTraversal;

const AUTO_PROPAGATE: bool = true;
}

#[derive(QueryData)]
/// These are for accessing components defined on the targeted entity
pub struct WindowTraversal {
parent: Option<&'static Parent>,
window: Option<&'static Window>,
}

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

// Send event to parent, if it has one.
if let Some(parent) = parent {
return Some(parent.get());
};

// Otherwise, send it to the window entity (unless this is a window entity).
if window.is_none() {
return Some(event.window);
}

None
}
}

/// Plugin which registers the system for dispatching keyboard events based on focus and
/// hover state.
pub struct InputDispatchPlugin;
Expand All @@ -130,17 +163,29 @@ fn dispatch_keyboard_input(
windows: Query<Entity, With<PrimaryWindow>>,
mut commands: Commands,
) {
// If an element has keyboard focus, then dispatch the key event to that element.
if let Some(focus_elt) = focus.0 {
for ev in key_events.read() {
commands.trigger_targets(FocusKeyboardInput(ev.clone()), focus_elt);
}
} else {
// If no element has input focus, then dispatch the key event to the primary window.
// There should be only one primary window.
if let Ok(window) = windows.get_single() {
if let Ok(window) = windows.get_single() {
// If an element has keyboard focus, then dispatch the key event to that element.
if let Some(focus_elt) = focus.0 {
for ev in key_events.read() {
commands.trigger_targets(FocusKeyboardInput(ev.clone()), window);
commands.trigger_targets(
FocusKeyboardInput {
input: ev.clone(),
window,
},
focus_elt,
);
}
} else {
// If no element has input focus, then dispatch the key event to the primary window.
// There should be only one primary window.
for ev in key_events.read() {
commands.trigger_targets(
FocusKeyboardInput {
input: ev.clone(),
window,
},
window,
);
}
}
}
Expand Down Expand Up @@ -248,6 +293,7 @@ mod tests {
keyboard::{Key, KeyCode},
ButtonState, InputPlugin,
};
use bevy_window::WindowResolution;
use smol_str::SmolStr;

#[derive(Component)]
Expand All @@ -266,7 +312,7 @@ mod tests {
mut query: Query<&mut GatherKeyboardEvents>,
) {
if let Ok(mut gather) = query.get_mut(trigger.target()) {
if let Key::Character(c) = &trigger.0.logical_key {
if let Key::Character(c) = &trigger.input.logical_key {
gather.0.push_str(c.as_str());
}
}
Expand Down Expand Up @@ -324,6 +370,12 @@ mod tests {
app.add_plugins((InputPlugin, InputDispatchPlugin))
.add_observer(gather_keyboard_events);

let window = Window {
resolution: WindowResolution::new(800., 600.),
..Default::default()
};
app.world_mut().spawn((window, PrimaryWindow));

let entity_a = app
.world_mut()
.spawn((GatherKeyboardEvents::default(), SetFocusOnAdd))
Expand Down
Loading

0 comments on commit 5c67cfc

Please sign in to comment.