Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
bevyengine
GitHub Repository: bevyengine/bevy
Path: blob/main/crates/bevy_input_focus/src/lib.rs
6595 views
1
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
2
#![forbid(unsafe_code)]
3
#![doc(
4
html_logo_url = "https://bevy.org/assets/icon.png",
5
html_favicon_url = "https://bevy.org/assets/icon.png"
6
)]
7
#![no_std]
8
9
//! A UI-centric focus system for Bevy.
10
//!
11
//! This crate provides a system for managing input focus in Bevy applications, including:
12
//! * [`InputFocus`], a resource for tracking which entity has input focus.
13
//! * Methods for getting and setting input focus via [`InputFocus`] and [`IsFocusedHelper`].
14
//! * A generic [`FocusedInput`] event for input events which bubble up from the focused entity.
15
//! * Various navigation frameworks for moving input focus between entities based on user input, such as [`tab_navigation`] and [`directional_navigation`].
16
//!
17
//! This crate does *not* provide any integration with UI widgets: this is the responsibility of the widget crate,
18
//! which should depend on [`bevy_input_focus`](crate).
19
20
#[cfg(feature = "std")]
21
extern crate std;
22
23
extern crate alloc;
24
25
pub mod directional_navigation;
26
pub mod tab_navigation;
27
28
// This module is too small / specific to be exported by the crate,
29
// but it's nice to have it separate for code organization.
30
mod autofocus;
31
pub use autofocus::*;
32
33
use bevy_app::{App, Plugin, PostStartup, PreUpdate};
34
use bevy_ecs::{
35
entity::Entities, prelude::*, query::QueryData, system::SystemParam, traversal::Traversal,
36
};
37
use bevy_input::{gamepad::GamepadButtonChangedEvent, keyboard::KeyboardInput, mouse::MouseWheel};
38
use bevy_window::{PrimaryWindow, Window};
39
use core::fmt::Debug;
40
41
#[cfg(feature = "bevy_reflect")]
42
use bevy_reflect::{prelude::*, Reflect};
43
44
/// Resource representing which entity has input focus, if any. Input events (other than pointer-like inputs) will be
45
/// dispatched to the current focus entity, or to the primary window if no entity has focus.
46
///
47
/// Changing the input focus is as easy as modifying this resource.
48
///
49
/// # Examples
50
///
51
/// From within a system:
52
///
53
/// ```rust
54
/// use bevy_ecs::prelude::*;
55
/// use bevy_input_focus::InputFocus;
56
///
57
/// fn clear_focus(mut input_focus: ResMut<InputFocus>) {
58
/// input_focus.clear();
59
/// }
60
/// ```
61
///
62
/// With exclusive (or deferred) world access:
63
///
64
/// ```rust
65
/// use bevy_ecs::prelude::*;
66
/// use bevy_input_focus::InputFocus;
67
///
68
/// fn set_focus_from_world(world: &mut World) {
69
/// let entity = world.spawn_empty().id();
70
///
71
/// // Fetch the resource from the world
72
/// let mut input_focus = world.resource_mut::<InputFocus>();
73
/// // Then mutate it!
74
/// input_focus.set(entity);
75
///
76
/// // Or you can just insert a fresh copy of the resource
77
/// // which will overwrite the existing one.
78
/// world.insert_resource(InputFocus::from_entity(entity));
79
/// }
80
/// ```
81
#[derive(Clone, Debug, Default, Resource)]
82
#[cfg_attr(
83
feature = "bevy_reflect",
84
derive(Reflect),
85
reflect(Debug, Default, Resource, Clone)
86
)]
87
pub struct InputFocus(pub Option<Entity>);
88
89
impl InputFocus {
90
/// Create a new [`InputFocus`] resource with the given entity.
91
///
92
/// This is mostly useful for tests.
93
pub const fn from_entity(entity: Entity) -> Self {
94
Self(Some(entity))
95
}
96
97
/// Set the entity with input focus.
98
pub const fn set(&mut self, entity: Entity) {
99
self.0 = Some(entity);
100
}
101
102
/// Returns the entity with input focus, if any.
103
pub const fn get(&self) -> Option<Entity> {
104
self.0
105
}
106
107
/// Clears input focus.
108
pub const fn clear(&mut self) {
109
self.0 = None;
110
}
111
}
112
113
/// Resource representing whether the input focus indicator should be visible on UI elements.
114
///
115
/// Note that this resource is not used by [`bevy_input_focus`](crate) itself, but is provided for
116
/// convenience to UI widgets or frameworks that want to display a focus indicator.
117
/// [`InputFocus`] may still be `Some` even if the focus indicator is not visible.
118
///
119
/// The value of this resource should be set by your focus navigation solution.
120
/// For a desktop/web style of user interface this would be set to true when the user presses the tab key,
121
/// and set to false when the user clicks on a different element.
122
/// By contrast, a console-style UI intended to be navigated with a gamepad may always have the focus indicator visible.
123
///
124
/// To easily access information about whether focus indicators should be shown for a given entity, use the [`IsFocused`] trait.
125
///
126
/// By default, this resource is set to `false`.
127
#[derive(Clone, Debug, Resource, Default)]
128
#[cfg_attr(
129
feature = "bevy_reflect",
130
derive(Reflect),
131
reflect(Debug, Resource, Clone)
132
)]
133
pub struct InputFocusVisible(pub bool);
134
135
/// A bubble-able user input event that starts at the currently focused entity.
136
///
137
/// This event is normally dispatched to the current input focus entity, if any.
138
/// If no entity has input focus, then the event is dispatched to the main window.
139
///
140
/// To set up your own bubbling input event, add the [`dispatch_focused_input::<MyEvent>`](dispatch_focused_input) system to your app,
141
/// in the [`InputFocusSystems::Dispatch`] system set during [`PreUpdate`].
142
#[derive(EntityEvent, Clone, Debug, Component)]
143
#[entity_event(traversal = WindowTraversal, auto_propagate)]
144
#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Component, Clone))]
145
pub struct FocusedInput<E: BufferedEvent + Clone> {
146
/// The underlying input event.
147
pub input: E,
148
/// The primary window entity.
149
window: Entity,
150
}
151
152
/// An event which is used to set input focus. Trigger this on an entity, and it will bubble
153
/// until it finds a focusable entity, and then set focus to it.
154
#[derive(Clone, EntityEvent)]
155
#[entity_event(traversal = WindowTraversal, auto_propagate)]
156
pub struct AcquireFocus {
157
/// The primary window entity.
158
window: Entity,
159
}
160
161
#[derive(QueryData)]
162
/// These are for accessing components defined on the targeted entity
163
pub struct WindowTraversal {
164
child_of: Option<&'static ChildOf>,
165
window: Option<&'static Window>,
166
}
167
168
impl<E: BufferedEvent + Clone> Traversal<FocusedInput<E>> for WindowTraversal {
169
fn traverse(item: Self::Item<'_, '_>, event: &FocusedInput<E>) -> Option<Entity> {
170
let WindowTraversalItem { child_of, window } = item;
171
172
// Send event to parent, if it has one.
173
if let Some(child_of) = child_of {
174
return Some(child_of.parent());
175
};
176
177
// Otherwise, send it to the window entity (unless this is a window entity).
178
if window.is_none() {
179
return Some(event.window);
180
}
181
182
None
183
}
184
}
185
186
impl Traversal<AcquireFocus> for WindowTraversal {
187
fn traverse(item: Self::Item<'_, '_>, event: &AcquireFocus) -> Option<Entity> {
188
let WindowTraversalItem { child_of, window } = item;
189
190
// Send event to parent, if it has one.
191
if let Some(child_of) = child_of {
192
return Some(child_of.parent());
193
};
194
195
// Otherwise, send it to the window entity (unless this is a window entity).
196
if window.is_none() {
197
return Some(event.window);
198
}
199
200
None
201
}
202
}
203
204
/// Plugin which sets up systems for dispatching bubbling keyboard and gamepad button events to the focused entity.
205
///
206
/// To add bubbling to your own input events, add the [`dispatch_focused_input::<MyEvent>`](dispatch_focused_input) system to your app,
207
/// as described in the docs for [`FocusedInput`].
208
pub struct InputDispatchPlugin;
209
210
impl Plugin for InputDispatchPlugin {
211
fn build(&self, app: &mut App) {
212
app.add_systems(PostStartup, set_initial_focus)
213
.init_resource::<InputFocus>()
214
.init_resource::<InputFocusVisible>()
215
.add_systems(
216
PreUpdate,
217
(
218
dispatch_focused_input::<KeyboardInput>,
219
dispatch_focused_input::<GamepadButtonChangedEvent>,
220
dispatch_focused_input::<MouseWheel>,
221
)
222
.in_set(InputFocusSystems::Dispatch),
223
);
224
}
225
}
226
227
/// System sets for [`bevy_input_focus`](crate).
228
///
229
/// These systems run in the [`PreUpdate`] schedule.
230
#[derive(SystemSet, Debug, PartialEq, Eq, Hash, Clone)]
231
pub enum InputFocusSystems {
232
/// System which dispatches bubbled input events to the focused entity, or to the primary window.
233
Dispatch,
234
}
235
236
/// Deprecated alias for [`InputFocusSystems`].
237
#[deprecated(since = "0.17.0", note = "Renamed to `InputFocusSystems`.")]
238
pub type InputFocusSet = InputFocusSystems;
239
240
/// If no entity is focused, sets the focus to the primary window, if any.
241
pub fn set_initial_focus(
242
mut input_focus: ResMut<InputFocus>,
243
window: Single<Entity, With<PrimaryWindow>>,
244
) {
245
if input_focus.0.is_none() {
246
input_focus.0 = Some(*window);
247
}
248
}
249
250
/// System which dispatches bubbled input events to the focused entity, or to the primary window
251
/// if no entity has focus.
252
///
253
/// If the currently focused entity no longer exists (has been despawned), this system will
254
/// automatically clear the focus and dispatch events to the primary window instead.
255
pub fn dispatch_focused_input<E: BufferedEvent + Clone>(
256
mut key_events: EventReader<E>,
257
mut focus: ResMut<InputFocus>,
258
windows: Query<Entity, With<PrimaryWindow>>,
259
entities: &Entities,
260
mut commands: Commands,
261
) {
262
if let Ok(window) = windows.single() {
263
// If an element has keyboard focus, then dispatch the input event to that element.
264
if let Some(focused_entity) = focus.0 {
265
// Check if the focused entity is still alive
266
if entities.contains(focused_entity) {
267
for ev in key_events.read() {
268
commands.trigger_targets(
269
FocusedInput {
270
input: ev.clone(),
271
window,
272
},
273
focused_entity,
274
);
275
}
276
} else {
277
// If the focused entity no longer exists, clear focus and dispatch to window
278
focus.0 = None;
279
for ev in key_events.read() {
280
commands.trigger_targets(
281
FocusedInput {
282
input: ev.clone(),
283
window,
284
},
285
window,
286
);
287
}
288
}
289
} else {
290
// If no element has input focus, then dispatch the input event to the primary window.
291
// There should be only one primary window.
292
for ev in key_events.read() {
293
commands.trigger_targets(
294
FocusedInput {
295
input: ev.clone(),
296
window,
297
},
298
window,
299
);
300
}
301
}
302
}
303
}
304
305
/// Trait which defines methods to check if an entity currently has focus.
306
///
307
/// This is implemented for [`World`] and [`IsFocusedHelper`].
308
/// [`DeferredWorld`](bevy_ecs::world::DeferredWorld) indirectly implements it through [`Deref`].
309
///
310
/// For use within systems, use [`IsFocusedHelper`].
311
///
312
/// Modify the [`InputFocus`] resource to change the focused entity.
313
///
314
/// [`Deref`]: std::ops::Deref
315
pub trait IsFocused {
316
/// Returns true if the given entity has input focus.
317
fn is_focused(&self, entity: Entity) -> bool;
318
319
/// Returns true if the given entity or any of its descendants has input focus.
320
///
321
/// Note that for unusual layouts, the focus may not be within the entity's visual bounds.
322
fn is_focus_within(&self, entity: Entity) -> bool;
323
324
/// Returns true if the given entity has input focus and the focus indicator should be visible.
325
fn is_focus_visible(&self, entity: Entity) -> bool;
326
327
/// Returns true if the given entity, or any descendant, has input focus and the focus
328
/// indicator should be visible.
329
fn is_focus_within_visible(&self, entity: Entity) -> bool;
330
}
331
332
/// A system param that helps get information about the current focused entity.
333
///
334
/// When working with the entire [`World`], consider using the [`IsFocused`] instead.
335
#[derive(SystemParam)]
336
pub struct IsFocusedHelper<'w, 's> {
337
parent_query: Query<'w, 's, &'static ChildOf>,
338
input_focus: Option<Res<'w, InputFocus>>,
339
input_focus_visible: Option<Res<'w, InputFocusVisible>>,
340
}
341
342
impl IsFocused for IsFocusedHelper<'_, '_> {
343
fn is_focused(&self, entity: Entity) -> bool {
344
self.input_focus
345
.as_deref()
346
.and_then(|f| f.0)
347
.is_some_and(|e| e == entity)
348
}
349
350
fn is_focus_within(&self, entity: Entity) -> bool {
351
let Some(focus) = self.input_focus.as_deref().and_then(|f| f.0) else {
352
return false;
353
};
354
if focus == entity {
355
return true;
356
}
357
self.parent_query.iter_ancestors(focus).any(|e| e == entity)
358
}
359
360
fn is_focus_visible(&self, entity: Entity) -> bool {
361
self.input_focus_visible.as_deref().is_some_and(|vis| vis.0) && self.is_focused(entity)
362
}
363
364
fn is_focus_within_visible(&self, entity: Entity) -> bool {
365
self.input_focus_visible.as_deref().is_some_and(|vis| vis.0) && self.is_focus_within(entity)
366
}
367
}
368
369
impl IsFocused for World {
370
fn is_focused(&self, entity: Entity) -> bool {
371
self.get_resource::<InputFocus>()
372
.and_then(|f| f.0)
373
.is_some_and(|f| f == entity)
374
}
375
376
fn is_focus_within(&self, entity: Entity) -> bool {
377
let Some(focus) = self.get_resource::<InputFocus>().and_then(|f| f.0) else {
378
return false;
379
};
380
let mut e = focus;
381
loop {
382
if e == entity {
383
return true;
384
}
385
if let Some(parent) = self.entity(e).get::<ChildOf>().map(ChildOf::parent) {
386
e = parent;
387
} else {
388
return false;
389
}
390
}
391
}
392
393
fn is_focus_visible(&self, entity: Entity) -> bool {
394
self.get_resource::<InputFocusVisible>()
395
.is_some_and(|vis| vis.0)
396
&& self.is_focused(entity)
397
}
398
399
fn is_focus_within_visible(&self, entity: Entity) -> bool {
400
self.get_resource::<InputFocusVisible>()
401
.is_some_and(|vis| vis.0)
402
&& self.is_focus_within(entity)
403
}
404
}
405
406
#[cfg(test)]
407
mod tests {
408
use super::*;
409
410
use alloc::string::String;
411
use bevy_app::Startup;
412
use bevy_ecs::{observer::On, system::RunSystemOnce, world::DeferredWorld};
413
use bevy_input::{
414
keyboard::{Key, KeyCode},
415
ButtonState, InputPlugin,
416
};
417
418
#[derive(Component, Default)]
419
struct GatherKeyboardEvents(String);
420
421
fn gather_keyboard_events(
422
event: On<FocusedInput<KeyboardInput>>,
423
mut query: Query<&mut GatherKeyboardEvents>,
424
) {
425
if let Ok(mut gather) = query.get_mut(event.entity()) {
426
if let Key::Character(c) = &event.input.logical_key {
427
gather.0.push_str(c.as_str());
428
}
429
}
430
}
431
432
fn key_a_event() -> KeyboardInput {
433
KeyboardInput {
434
key_code: KeyCode::KeyA,
435
logical_key: Key::Character("A".into()),
436
state: ButtonState::Pressed,
437
text: Some("A".into()),
438
repeat: false,
439
window: Entity::PLACEHOLDER,
440
}
441
}
442
443
#[test]
444
fn test_no_panics_if_resource_missing() {
445
let mut app = App::new();
446
// Note that we do not insert InputFocus here!
447
448
let entity = app.world_mut().spawn_empty().id();
449
450
assert!(!app.world().is_focused(entity));
451
452
app.world_mut()
453
.run_system_once(move |helper: IsFocusedHelper| {
454
assert!(!helper.is_focused(entity));
455
assert!(!helper.is_focus_within(entity));
456
assert!(!helper.is_focus_visible(entity));
457
assert!(!helper.is_focus_within_visible(entity));
458
})
459
.unwrap();
460
461
app.world_mut()
462
.run_system_once(move |world: DeferredWorld| {
463
assert!(!world.is_focused(entity));
464
assert!(!world.is_focus_within(entity));
465
assert!(!world.is_focus_visible(entity));
466
assert!(!world.is_focus_within_visible(entity));
467
})
468
.unwrap();
469
}
470
471
#[test]
472
fn initial_focus_unset_if_no_primary_window() {
473
let mut app = App::new();
474
app.add_plugins((InputPlugin, InputDispatchPlugin));
475
476
app.update();
477
478
assert_eq!(app.world().resource::<InputFocus>().0, None);
479
}
480
481
#[test]
482
fn initial_focus_set_to_primary_window() {
483
let mut app = App::new();
484
app.add_plugins((InputPlugin, InputDispatchPlugin));
485
486
let entity_window = app
487
.world_mut()
488
.spawn((Window::default(), PrimaryWindow))
489
.id();
490
app.update();
491
492
assert_eq!(app.world().resource::<InputFocus>().0, Some(entity_window));
493
}
494
495
#[test]
496
fn initial_focus_not_overridden() {
497
let mut app = App::new();
498
app.add_plugins((InputPlugin, InputDispatchPlugin));
499
500
app.world_mut().spawn((Window::default(), PrimaryWindow));
501
502
app.add_systems(Startup, |mut commands: Commands| {
503
commands.spawn(AutoFocus);
504
});
505
506
app.update();
507
508
let autofocus_entity = app
509
.world_mut()
510
.query_filtered::<Entity, With<AutoFocus>>()
511
.single(app.world())
512
.unwrap();
513
514
assert_eq!(
515
app.world().resource::<InputFocus>().0,
516
Some(autofocus_entity)
517
);
518
}
519
520
#[test]
521
fn test_keyboard_events() {
522
fn get_gathered(app: &App, entity: Entity) -> &str {
523
app.world()
524
.entity(entity)
525
.get::<GatherKeyboardEvents>()
526
.unwrap()
527
.0
528
.as_str()
529
}
530
531
let mut app = App::new();
532
533
app.add_plugins((InputPlugin, InputDispatchPlugin))
534
.add_observer(gather_keyboard_events);
535
536
app.world_mut().spawn((Window::default(), PrimaryWindow));
537
538
// Run the world for a single frame to set up the initial focus
539
app.update();
540
541
let entity_a = app
542
.world_mut()
543
.spawn((GatherKeyboardEvents::default(), AutoFocus))
544
.id();
545
546
let child_of_b = app
547
.world_mut()
548
.spawn((GatherKeyboardEvents::default(),))
549
.id();
550
551
let entity_b = app
552
.world_mut()
553
.spawn((GatherKeyboardEvents::default(),))
554
.add_child(child_of_b)
555
.id();
556
557
assert!(app.world().is_focused(entity_a));
558
assert!(!app.world().is_focused(entity_b));
559
assert!(!app.world().is_focused(child_of_b));
560
assert!(!app.world().is_focus_visible(entity_a));
561
assert!(!app.world().is_focus_visible(entity_b));
562
assert!(!app.world().is_focus_visible(child_of_b));
563
564
// entity_a should receive this event
565
app.world_mut().write_event(key_a_event());
566
app.update();
567
568
assert_eq!(get_gathered(&app, entity_a), "A");
569
assert_eq!(get_gathered(&app, entity_b), "");
570
assert_eq!(get_gathered(&app, child_of_b), "");
571
572
app.world_mut().insert_resource(InputFocus(None));
573
574
assert!(!app.world().is_focused(entity_a));
575
assert!(!app.world().is_focus_visible(entity_a));
576
577
// This event should be lost
578
app.world_mut().write_event(key_a_event());
579
app.update();
580
581
assert_eq!(get_gathered(&app, entity_a), "A");
582
assert_eq!(get_gathered(&app, entity_b), "");
583
assert_eq!(get_gathered(&app, child_of_b), "");
584
585
app.world_mut()
586
.insert_resource(InputFocus::from_entity(entity_b));
587
assert!(app.world().is_focused(entity_b));
588
assert!(!app.world().is_focused(child_of_b));
589
590
app.world_mut()
591
.run_system_once(move |mut input_focus: ResMut<InputFocus>| {
592
input_focus.set(child_of_b);
593
})
594
.unwrap();
595
assert!(app.world().is_focus_within(entity_b));
596
597
// These events should be received by entity_b and child_of_b
598
app.world_mut()
599
.write_event_batch(core::iter::repeat_n(key_a_event(), 4));
600
app.update();
601
602
assert_eq!(get_gathered(&app, entity_a), "A");
603
assert_eq!(get_gathered(&app, entity_b), "AAAA");
604
assert_eq!(get_gathered(&app, child_of_b), "AAAA");
605
606
app.world_mut().resource_mut::<InputFocusVisible>().0 = true;
607
608
app.world_mut()
609
.run_system_once(move |helper: IsFocusedHelper| {
610
assert!(!helper.is_focused(entity_a));
611
assert!(!helper.is_focus_within(entity_a));
612
assert!(!helper.is_focus_visible(entity_a));
613
assert!(!helper.is_focus_within_visible(entity_a));
614
615
assert!(!helper.is_focused(entity_b));
616
assert!(helper.is_focus_within(entity_b));
617
assert!(!helper.is_focus_visible(entity_b));
618
assert!(helper.is_focus_within_visible(entity_b));
619
620
assert!(helper.is_focused(child_of_b));
621
assert!(helper.is_focus_within(child_of_b));
622
assert!(helper.is_focus_visible(child_of_b));
623
assert!(helper.is_focus_within_visible(child_of_b));
624
})
625
.unwrap();
626
627
app.world_mut()
628
.run_system_once(move |world: DeferredWorld| {
629
assert!(!world.is_focused(entity_a));
630
assert!(!world.is_focus_within(entity_a));
631
assert!(!world.is_focus_visible(entity_a));
632
assert!(!world.is_focus_within_visible(entity_a));
633
634
assert!(!world.is_focused(entity_b));
635
assert!(world.is_focus_within(entity_b));
636
assert!(!world.is_focus_visible(entity_b));
637
assert!(world.is_focus_within_visible(entity_b));
638
639
assert!(world.is_focused(child_of_b));
640
assert!(world.is_focus_within(child_of_b));
641
assert!(world.is_focus_visible(child_of_b));
642
assert!(world.is_focus_within_visible(child_of_b));
643
})
644
.unwrap();
645
}
646
647
#[test]
648
fn dispatch_clears_focus_when_focused_entity_despawned() {
649
let mut app = App::new();
650
app.add_plugins((InputPlugin, InputDispatchPlugin));
651
652
app.world_mut().spawn((Window::default(), PrimaryWindow));
653
app.update();
654
655
let entity = app.world_mut().spawn_empty().id();
656
app.world_mut()
657
.insert_resource(InputFocus::from_entity(entity));
658
app.world_mut().entity_mut(entity).despawn();
659
660
assert_eq!(app.world().resource::<InputFocus>().0, Some(entity));
661
662
// Send input event - this should clear focus instead of panicking
663
app.world_mut().write_event(key_a_event());
664
app.update();
665
666
assert_eq!(app.world().resource::<InputFocus>().0, None);
667
}
668
}
669
670