Skip to content

Commit d8796ae

Browse files
Polish and improve docs for bevy_input_focus (#16887)
# Objective `bevy_input_focus` needs some love before we ship it to users. There's a few missing helper methods, the docs could be improved, and `AutoFocus` should be more generally available. ## Solution The changes here are broken down by commit, and should generally be uncontroversial. The ones to focus on during review are: - Make navigate take a & InputFocus argument: this makes the intended pattern clearer to users - Remove TabGroup requirement from `AutoFocus`: I want auto-focusing even with gamepad-style focus navigation! - Handle case where tab group is None more gracefully: I think we can try harder to provide something usable, and shouldn't just fail to navigate ## Testing The `tab_navigation` example continues to work.
1 parent 20049d4 commit d8796ae

File tree

3 files changed

+110
-60
lines changed

3 files changed

+110
-60
lines changed
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
//! Contains the [`AutoFocus`] component and related machinery.
2+
3+
use bevy_ecs::{component::ComponentId, prelude::*, world::DeferredWorld};
4+
5+
use crate::SetInputFocus;
6+
7+
/// Indicates that this widget should automatically receive [`InputFocus`](crate::InputFocus).
8+
///
9+
/// This can be useful for things like dialog boxes, the first text input in a form,
10+
/// or the first button in a game menu.
11+
///
12+
/// The focus is swapped when this component is added
13+
/// or an entity with this component is spawned.
14+
#[derive(Debug, Default, Component, Copy, Clone)]
15+
#[component(on_add = on_auto_focus_added)]
16+
pub struct AutoFocus;
17+
18+
fn on_auto_focus_added(mut world: DeferredWorld, entity: Entity, _: ComponentId) {
19+
world.set_input_focus(entity);
20+
}

crates/bevy_input_focus/src/lib.rs

Lines changed: 62 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,20 @@
88
//! Keyboard focus system for Bevy.
99
//!
1010
//! This crate provides a system for managing input focus in Bevy applications, including:
11-
//! * A resource for tracking which entity has input focus.
12-
//! * Methods for getting and setting input focus.
13-
//! * Event definitions for triggering bubble-able keyboard input events to the focused entity.
14-
//! * A system for dispatching keyboard input events to the focused entity.
11+
//! * [`InputFocus`], a resource for tracking which entity has input focus.
12+
//! * Methods for getting and setting input focus via [`SetInputFocus`], [`InputFocus`] and [`IsFocusedHelper`].
13+
//! * A generic [`FocusedInput`] event for input events which bubble up from the focused entity.
1514
//!
16-
//! This crate does *not* provide any integration with UI widgets, or provide functions for
17-
//! tab navigation or gamepad-based focus navigation, as those are typically application-specific.
15+
//! This crate does *not* provide any integration with UI widgets: this is the responsibility of the widget crate,
16+
//! which should depend on [`bevy_input_focus`](crate).
1817
1918
pub mod tab_navigation;
2019

20+
// This module is too small / specific to be exported by the crate,
21+
// but it's nice to have it separate for code organization.
22+
mod autofocus;
23+
pub use autofocus::*;
24+
2125
use bevy_app::{App, Plugin, PreUpdate, Startup};
2226
use bevy_ecs::{
2327
prelude::*, query::QueryData, system::SystemParam, traversal::Traversal, world::DeferredWorld,
@@ -29,11 +33,36 @@ use core::fmt::Debug;
2933

3034
/// Resource representing which entity has input focus, if any. Keyboard events will be
3135
/// dispatched to the current focus entity, or to the primary window if no entity has focus.
32-
#[derive(Clone, Debug, Resource)]
36+
#[derive(Clone, Debug, Default, Resource)]
3337
pub struct InputFocus(pub Option<Entity>);
3438

35-
/// Resource representing whether the input focus indicator should be visible. It's up to the
36-
/// current focus navigation system to set this resource. For a desktop/web style of user interface
39+
impl InputFocus {
40+
/// Create a new [`InputFocus`] resource with the given entity.
41+
///
42+
/// This is mostly useful for tests.
43+
pub const fn from_entity(entity: Entity) -> Self {
44+
Self(Some(entity))
45+
}
46+
47+
/// Set the entity with input focus.
48+
pub const fn set(&mut self, entity: Entity) {
49+
self.0 = Some(entity);
50+
}
51+
52+
/// Returns the entity with input focus, if any.
53+
pub const fn get(&self) -> Option<Entity> {
54+
self.0
55+
}
56+
57+
/// Clears input focus.
58+
pub const fn clear(&mut self) {
59+
self.0 = None;
60+
}
61+
}
62+
63+
/// Resource representing whether the input focus indicator should be visible on UI elements.
64+
///
65+
/// It's up to the current focus navigation system to set this resource. For a desktop/web style of user interface
3766
/// this would be set to true when the user presses the tab key, and set to false when the user
3867
/// clicks on a different element.
3968
#[derive(Clone, Debug, Resource)]
@@ -43,6 +72,8 @@ pub struct InputFocusVisible(pub bool);
4372
///
4473
/// These methods are equivalent to modifying the [`InputFocus`] resource directly,
4574
/// but only take effect when commands are applied.
75+
///
76+
/// See [`IsFocused`] for methods to check if an entity has focus.
4677
pub trait SetInputFocus {
4778
/// Set input focus to the given entity.
4879
///
@@ -151,8 +182,10 @@ impl<E: Event + Clone> Traversal<FocusedInput<E>> for WindowTraversal {
151182
}
152183
}
153184

154-
/// Plugin which registers the system for dispatching keyboard events based on focus and
155-
/// hover state.
185+
/// Plugin which sets up systems for dispatching bubbling keyboard and gamepad button events to the focused entity.
186+
///
187+
/// To add bubbling to your own input events, add the [`dispatch_focused_input::<MyEvent>`](dispatch_focused_input) system to your app,
188+
/// as described in the docs for [`FocusedInput`].
156189
pub struct InputDispatchPlugin;
157190

158191
impl Plugin for InputDispatchPlugin {
@@ -198,19 +231,19 @@ pub fn dispatch_focused_input<E: Event + Clone>(
198231
mut commands: Commands,
199232
) {
200233
if let Ok(window) = windows.get_single() {
201-
// If an element has keyboard focus, then dispatch the key event to that element.
202-
if let Some(focus_elt) = focus.0 {
234+
// If an element has keyboard focus, then dispatch the input event to that element.
235+
if let Some(focused_entity) = focus.0 {
203236
for ev in key_events.read() {
204237
commands.trigger_targets(
205238
FocusedInput {
206239
input: ev.clone(),
207240
window,
208241
},
209-
focus_elt,
242+
focused_entity,
210243
);
211244
}
212245
} else {
213-
// If no element has input focus, then dispatch the key event to the primary window.
246+
// If no element has input focus, then dispatch the input event to the primary window.
214247
// There should be only one primary window.
215248
for ev in key_events.read() {
216249
commands.trigger_targets(
@@ -225,27 +258,36 @@ pub fn dispatch_focused_input<E: Event + Clone>(
225258
}
226259
}
227260

228-
/// Trait which defines methods to check if an entity currently has focus. This is implemented
229-
/// for [`World`] and [`IsFocusedHelper`].
261+
/// Trait which defines methods to check if an entity currently has focus.
262+
///
263+
/// This is implemented for [`World`] and [`IsFocusedHelper`].
230264
/// [`DeferredWorld`] indirectly implements it through [`Deref`].
231265
///
266+
/// For use within systems, use [`IsFocusedHelper`].
267+
///
268+
/// See [`SetInputFocus`] for methods to set and clear input focus.
269+
///
232270
/// [`Deref`]: std::ops::Deref
233271
pub trait IsFocused {
234272
/// Returns true if the given entity has input focus.
235273
fn is_focused(&self, entity: Entity) -> bool;
236274

237275
/// Returns true if the given entity or any of its descendants has input focus.
276+
///
277+
/// Note that for unusual layouts, the focus may not be within the entity's visual bounds.
238278
fn is_focus_within(&self, entity: Entity) -> bool;
239279

240-
/// Returns true if the given entity has input focus and the focus indicator is visible.
280+
/// Returns true if the given entity has input focus and the focus indicator should be visible.
241281
fn is_focus_visible(&self, entity: Entity) -> bool;
242282

243283
/// Returns true if the given entity, or any descendant, has input focus and the focus
244-
/// indicator is visible.
284+
/// indicator should be visible.
245285
fn is_focus_within_visible(&self, entity: Entity) -> bool;
246286
}
247287

248-
/// System param that helps get information about the current focused entity.
288+
/// A system param that helps get information about the current focused entity.
289+
///
290+
/// When working with the entire [`World`], consider using the [`IsFocused`] instead.
249291
#[derive(SystemParam)]
250292
pub struct IsFocusedHelper<'w, 's> {
251293
parent_query: Query<'w, 's, &'static Parent>,

crates/bevy_input_focus/src/tab_navigation.rs

Lines changed: 28 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -9,31 +9,27 @@
99
//! * An index < 0 means that the entity is not focusable via sequential navigation, but
1010
//! can still be focused via direct selection.
1111
//!
12-
//! Tabbable entities must be descendants of a `TabGroup` entity, which is a component that
12+
//! Tabbable entities must be descendants of a [`TabGroup`] entity, which is a component that
1313
//! marks a tree of entities as containing tabbable elements. The order of tab groups
14-
//! is determined by the `order` field, with lower orders being tabbed first. Modal tab groups
14+
//! is determined by the [`TabGroup::order`] field, with lower orders being tabbed first. Modal tab groups
1515
//! are used for ui elements that should only tab within themselves, such as modal dialog boxes.
1616
//!
17-
//! There are several different ways to use this module. To enable automatic tabbing, add the
18-
//! `TabNavigationPlugin` to your app. (Make sure you also have `InputDispatchPlugin` installed).
17+
//! To enable automatic tabbing, add the
18+
//! [`TabNavigationPlugin`] and [`InputDispatchPlugin`](crate::InputDispatchPlugin) to your app.
1919
//! This will install a keyboard event observer on the primary window which automatically handles
2020
//! tab navigation for you.
2121
//!
22-
//! Alternatively, if you want to have more control over tab navigation, or are using an event
23-
//! mapping framework such as LWIM, you can use the `TabNavigation` helper object directly instead.
24-
//! This object can be injected into your systems, and provides a `navigate` method which can be
22+
//! Alternatively, if you want to have more control over tab navigation, or are using an input-action-mapping framework,
23+
//! you can use the [`TabNavigation`] system parameter directly instead.
24+
//! This object can be injected into your systems, and provides a [`navigate`](`TabNavigation::navigate`) method which can be
2525
//! used to navigate between focusable entities.
26-
//!
27-
//! This module also provides `AutoFocus`, a component which can be added to an entity to
28-
//! automatically focus it when it is added to the world.
2926
use bevy_app::{App, Plugin, Startup};
3027
use bevy_ecs::{
31-
component::{Component, ComponentId},
28+
component::Component,
3229
entity::Entity,
3330
observer::Trigger,
3431
query::{With, Without},
3532
system::{Commands, Query, Res, ResMut, SystemParam},
36-
world::DeferredWorld,
3733
};
3834
use bevy_hierarchy::{Children, HierarchyQueryExt, Parent};
3935
use bevy_input::{
@@ -43,7 +39,7 @@ use bevy_input::{
4339
use bevy_utils::tracing::warn;
4440
use bevy_window::PrimaryWindow;
4541

46-
use crate::{FocusedInput, InputFocus, InputFocusVisible, SetInputFocus};
42+
use crate::{FocusedInput, InputFocus, InputFocusVisible};
4743

4844
/// A component which indicates that an entity wants to participate in tab navigation.
4945
///
@@ -52,10 +48,6 @@ use crate::{FocusedInput, InputFocus, InputFocusVisible, SetInputFocus};
5248
#[derive(Debug, Default, Component, Copy, Clone)]
5349
pub struct TabIndex(pub i32);
5450

55-
/// Indicates that this widget should automatically receive focus when it's added.
56-
#[derive(Debug, Default, Component, Copy, Clone)]
57-
pub struct AutoFocus;
58-
5951
/// A component used to mark a tree of entities as containing tabbable elements.
6052
#[derive(Debug, Default, Component, Copy, Clone)]
6153
pub struct TabGroup {
@@ -87,7 +79,9 @@ impl TabGroup {
8779
}
8880
}
8981

90-
/// Navigation action for tabbing.
82+
/// A navigation action for tabbing.
83+
///
84+
/// These values are consumed by the [`TabNavigation`] system param.
9185
pub enum NavAction {
9286
/// Navigate to the next focusable entity, wrapping around to the beginning if at the end.
9387
Next,
@@ -120,6 +114,8 @@ pub struct TabNavigation<'w, 's> {
120114
impl TabNavigation<'_, '_> {
121115
/// Navigate to the next focusable entity.
122116
///
117+
/// Focusable entities are determined by the presence of the [`TabIndex`] component.
118+
///
123119
/// Arguments:
124120
/// * `focus`: The current focus entity, or `None` if no entity has focus.
125121
/// * `action`: Whether to select the next, previous, first, or last focusable entity.
@@ -128,7 +124,7 @@ impl TabNavigation<'_, '_> {
128124
/// or last focusable entity, depending on the direction of navigation. For example, if
129125
/// `action` is `Next` and no focusable entities are found, then this function will return
130126
/// the first focusable entity.
131-
pub fn navigate(&self, focus: Option<Entity>, action: NavAction) -> Option<Entity> {
127+
pub fn navigate(&self, focus: &InputFocus, action: NavAction) -> Option<Entity> {
132128
// If there are no tab groups, then there are no focusable entities.
133129
if self.tabgroup_query.is_empty() {
134130
warn!("No tab groups found");
@@ -137,7 +133,7 @@ impl TabNavigation<'_, '_> {
137133

138134
// Start by identifying which tab group we are in. Mainly what we want to know is if
139135
// we're in a modal group.
140-
let tabgroup = focus.and_then(|focus_ent| {
136+
let tabgroup = focus.0.and_then(|focus_ent| {
141137
self.parent_query
142138
.iter_ancestors(focus_ent)
143139
.find_map(|entity| {
@@ -148,9 +144,8 @@ impl TabNavigation<'_, '_> {
148144
})
149145
});
150146

151-
if focus.is_some() && tabgroup.is_none() {
152-
warn!("No tab group found for focus entity");
153-
return None;
147+
if focus.0.is_some() && tabgroup.is_none() {
148+
warn!("No tab group found for focus entity. Users will not be able to navigate back to this entity.");
154149
}
155150

156151
self.navigate_in_group(tabgroup, focus, action)
@@ -159,7 +154,7 @@ impl TabNavigation<'_, '_> {
159154
fn navigate_in_group(
160155
&self,
161156
tabgroup: Option<(Entity, &TabGroup)>,
162-
focus: Option<Entity>,
157+
focus: &InputFocus,
163158
action: NavAction,
164159
) -> Option<Entity> {
165160
// List of all focusable entities found.
@@ -201,7 +196,7 @@ impl TabNavigation<'_, '_> {
201196
// Stable sort by tabindex
202197
focusable.sort_by(compare_tab_indices);
203198

204-
let index = focusable.iter().position(|e| Some(e.0) == focus);
199+
let index = focusable.iter().position(|e| Some(e.0) == focus.0);
205200
let count = focusable.len();
206201
let next = match (index, action) {
207202
(Some(idx), NavAction::Next) => (idx + 1).rem_euclid(count),
@@ -247,15 +242,12 @@ fn compare_tab_indices(a: &(Entity, TabIndex), b: &(Entity, TabIndex)) -> core::
247242
a.1 .0.cmp(&b.1 .0)
248243
}
249244

250-
/// Plugin for handling keyboard input.
245+
/// Plugin for navigating between focusable entities using keyboard input.
251246
pub struct TabNavigationPlugin;
252247

253248
impl Plugin for TabNavigationPlugin {
254249
fn build(&self, app: &mut App) {
255250
app.add_systems(Startup, setup_tab_navigation);
256-
app.world_mut()
257-
.register_component_hooks::<AutoFocus>()
258-
.on_add(on_auto_focus_added);
259251
}
260252
}
261253

@@ -283,7 +275,7 @@ pub fn handle_tab_navigation(
283275
&& !key_event.repeat
284276
{
285277
let next = nav.navigate(
286-
focus.0,
278+
&focus,
287279
if keys.pressed(KeyCode::ShiftLeft) || keys.pressed(KeyCode::ShiftRight) {
288280
NavAction::Previous
289281
} else {
@@ -298,12 +290,6 @@ pub fn handle_tab_navigation(
298290
}
299291
}
300292

301-
fn on_auto_focus_added(mut world: DeferredWorld, entity: Entity, _: ComponentId) {
302-
if world.entity(entity).contains::<TabIndex>() {
303-
world.set_input_focus(entity);
304-
}
305-
}
306-
307293
#[cfg(test)]
308294
mod tests {
309295
use bevy_ecs::system::SystemState;
@@ -326,16 +312,18 @@ mod tests {
326312
assert_eq!(tab_navigation.tabgroup_query.iter().count(), 1);
327313
assert_eq!(tab_navigation.tabindex_query.iter().count(), 2);
328314

329-
let next_entity = tab_navigation.navigate(Some(tab_entity_1), NavAction::Next);
315+
let next_entity =
316+
tab_navigation.navigate(&InputFocus::from_entity(tab_entity_1), NavAction::Next);
330317
assert_eq!(next_entity, Some(tab_entity_2));
331318

332-
let prev_entity = tab_navigation.navigate(Some(tab_entity_2), NavAction::Previous);
319+
let prev_entity =
320+
tab_navigation.navigate(&InputFocus::from_entity(tab_entity_2), NavAction::Previous);
333321
assert_eq!(prev_entity, Some(tab_entity_1));
334322

335-
let first_entity = tab_navigation.navigate(None, NavAction::First);
323+
let first_entity = tab_navigation.navigate(&InputFocus::default(), NavAction::First);
336324
assert_eq!(first_entity, Some(tab_entity_1));
337325

338-
let last_entity = tab_navigation.navigate(None, NavAction::Last);
326+
let last_entity = tab_navigation.navigate(&InputFocus::default(), NavAction::Last);
339327
assert_eq!(last_entity, Some(tab_entity_2));
340328
}
341329
}

0 commit comments

Comments
 (0)