9
9
//! * An index < 0 means that the entity is not focusable via sequential navigation, but
10
10
//! can still be focused via direct selection.
11
11
//!
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
13
13
//! 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
15
15
//! are used for ui elements that should only tab within themselves, such as modal dialog boxes.
16
16
//!
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 .
19
19
//! This will install a keyboard event observer on the primary window which automatically handles
20
20
//! tab navigation for you.
21
21
//!
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
25
25
//! 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.
29
26
use bevy_app:: { App , Plugin , Startup } ;
30
27
use bevy_ecs:: {
31
- component:: { Component , ComponentId } ,
28
+ component:: Component ,
32
29
entity:: Entity ,
33
30
observer:: Trigger ,
34
31
query:: { With , Without } ,
35
32
system:: { Commands , Query , Res , ResMut , SystemParam } ,
36
- world:: DeferredWorld ,
37
33
} ;
38
34
use bevy_hierarchy:: { Children , HierarchyQueryExt , Parent } ;
39
35
use bevy_input:: {
@@ -43,7 +39,7 @@ use bevy_input::{
43
39
use bevy_utils:: tracing:: warn;
44
40
use bevy_window:: PrimaryWindow ;
45
41
46
- use crate :: { FocusedInput , InputFocus , InputFocusVisible , SetInputFocus } ;
42
+ use crate :: { FocusedInput , InputFocus , InputFocusVisible } ;
47
43
48
44
/// A component which indicates that an entity wants to participate in tab navigation.
49
45
///
@@ -52,10 +48,6 @@ use crate::{FocusedInput, InputFocus, InputFocusVisible, SetInputFocus};
52
48
#[ derive( Debug , Default , Component , Copy , Clone ) ]
53
49
pub struct TabIndex ( pub i32 ) ;
54
50
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
-
59
51
/// A component used to mark a tree of entities as containing tabbable elements.
60
52
#[ derive( Debug , Default , Component , Copy , Clone ) ]
61
53
pub struct TabGroup {
@@ -87,7 +79,9 @@ impl TabGroup {
87
79
}
88
80
}
89
81
90
- /// Navigation action for tabbing.
82
+ /// A navigation action for tabbing.
83
+ ///
84
+ /// These values are consumed by the [`TabNavigation`] system param.
91
85
pub enum NavAction {
92
86
/// Navigate to the next focusable entity, wrapping around to the beginning if at the end.
93
87
Next ,
@@ -120,6 +114,8 @@ pub struct TabNavigation<'w, 's> {
120
114
impl TabNavigation < ' _ , ' _ > {
121
115
/// Navigate to the next focusable entity.
122
116
///
117
+ /// Focusable entities are determined by the presence of the [`TabIndex`] component.
118
+ ///
123
119
/// Arguments:
124
120
/// * `focus`: The current focus entity, or `None` if no entity has focus.
125
121
/// * `action`: Whether to select the next, previous, first, or last focusable entity.
@@ -128,7 +124,7 @@ impl TabNavigation<'_, '_> {
128
124
/// or last focusable entity, depending on the direction of navigation. For example, if
129
125
/// `action` is `Next` and no focusable entities are found, then this function will return
130
126
/// 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 > {
132
128
// If there are no tab groups, then there are no focusable entities.
133
129
if self . tabgroup_query . is_empty ( ) {
134
130
warn ! ( "No tab groups found" ) ;
@@ -137,7 +133,7 @@ impl TabNavigation<'_, '_> {
137
133
138
134
// Start by identifying which tab group we are in. Mainly what we want to know is if
139
135
// we're in a modal group.
140
- let tabgroup = focus. and_then ( |focus_ent| {
136
+ let tabgroup = focus. 0 . and_then ( |focus_ent| {
141
137
self . parent_query
142
138
. iter_ancestors ( focus_ent)
143
139
. find_map ( |entity| {
@@ -148,9 +144,8 @@ impl TabNavigation<'_, '_> {
148
144
} )
149
145
} ) ;
150
146
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." ) ;
154
149
}
155
150
156
151
self . navigate_in_group ( tabgroup, focus, action)
@@ -159,7 +154,7 @@ impl TabNavigation<'_, '_> {
159
154
fn navigate_in_group (
160
155
& self ,
161
156
tabgroup : Option < ( Entity , & TabGroup ) > ,
162
- focus : Option < Entity > ,
157
+ focus : & InputFocus ,
163
158
action : NavAction ,
164
159
) -> Option < Entity > {
165
160
// List of all focusable entities found.
@@ -201,7 +196,7 @@ impl TabNavigation<'_, '_> {
201
196
// Stable sort by tabindex
202
197
focusable. sort_by ( compare_tab_indices) ;
203
198
204
- let index = focusable. iter ( ) . position ( |e| Some ( e. 0 ) == focus) ;
199
+ let index = focusable. iter ( ) . position ( |e| Some ( e. 0 ) == focus. 0 ) ;
205
200
let count = focusable. len ( ) ;
206
201
let next = match ( index, action) {
207
202
( Some ( idx) , NavAction :: Next ) => ( idx + 1 ) . rem_euclid ( count) ,
@@ -247,15 +242,12 @@ fn compare_tab_indices(a: &(Entity, TabIndex), b: &(Entity, TabIndex)) -> core::
247
242
a. 1 . 0 . cmp ( & b. 1 . 0 )
248
243
}
249
244
250
- /// Plugin for handling keyboard input.
245
+ /// Plugin for navigating between focusable entities using keyboard input.
251
246
pub struct TabNavigationPlugin ;
252
247
253
248
impl Plugin for TabNavigationPlugin {
254
249
fn build ( & self , app : & mut App ) {
255
250
app. add_systems ( Startup , setup_tab_navigation) ;
256
- app. world_mut ( )
257
- . register_component_hooks :: < AutoFocus > ( )
258
- . on_add ( on_auto_focus_added) ;
259
251
}
260
252
}
261
253
@@ -283,7 +275,7 @@ pub fn handle_tab_navigation(
283
275
&& !key_event. repeat
284
276
{
285
277
let next = nav. navigate (
286
- focus. 0 ,
278
+ & focus,
287
279
if keys. pressed ( KeyCode :: ShiftLeft ) || keys. pressed ( KeyCode :: ShiftRight ) {
288
280
NavAction :: Previous
289
281
} else {
@@ -298,12 +290,6 @@ pub fn handle_tab_navigation(
298
290
}
299
291
}
300
292
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
-
307
293
#[ cfg( test) ]
308
294
mod tests {
309
295
use bevy_ecs:: system:: SystemState ;
@@ -326,16 +312,18 @@ mod tests {
326
312
assert_eq ! ( tab_navigation. tabgroup_query. iter( ) . count( ) , 1 ) ;
327
313
assert_eq ! ( tab_navigation. tabindex_query. iter( ) . count( ) , 2 ) ;
328
314
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 ) ;
330
317
assert_eq ! ( next_entity, Some ( tab_entity_2) ) ;
331
318
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 ) ;
333
321
assert_eq ! ( prev_entity, Some ( tab_entity_1) ) ;
334
322
335
- let first_entity = tab_navigation. navigate ( None , NavAction :: First ) ;
323
+ let first_entity = tab_navigation. navigate ( & InputFocus :: default ( ) , NavAction :: First ) ;
336
324
assert_eq ! ( first_entity, Some ( tab_entity_1) ) ;
337
325
338
- let last_entity = tab_navigation. navigate ( None , NavAction :: Last ) ;
326
+ let last_entity = tab_navigation. navigate ( & InputFocus :: default ( ) , NavAction :: Last ) ;
339
327
assert_eq ! ( last_entity, Some ( tab_entity_2) ) ;
340
328
}
341
329
}
0 commit comments