Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
bevyengine
GitHub Repository: bevyengine/bevy
Path: blob/main/crates/bevy_input_focus/src/lib.rs
6849 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(propagate = WindowTraversal, auto_propagate)]
144
#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Component, Clone))]
145
pub struct FocusedInput<M: Message + Clone> {
146
/// The entity that has received focused input.
147
#[event_target]
148
pub focused_entity: Entity,
149
/// The underlying input message.
150
pub input: M,
151
/// The primary window entity.
152
window: Entity,
153
}
154
155
/// An event which is used to set input focus. Trigger this on an entity, and it will bubble
156
/// until it finds a focusable entity, and then set focus to it.
157
#[derive(Clone, EntityEvent)]
158
#[entity_event(propagate = WindowTraversal, auto_propagate)]
159
pub struct AcquireFocus {
160
/// The entity that has acquired focus.
161
#[event_target]
162
pub focused_entity: Entity,
163
/// The primary window entity.
164
window: Entity,
165
}
166
167
#[derive(QueryData)]
168
/// These are for accessing components defined on the targeted entity
169
pub struct WindowTraversal {
170
child_of: Option<&'static ChildOf>,
171
window: Option<&'static Window>,
172
}
173
174
impl<M: Message + Clone> Traversal<FocusedInput<M>> for WindowTraversal {
175
fn traverse(item: Self::Item<'_, '_>, event: &FocusedInput<M>) -> Option<Entity> {
176
let WindowTraversalItem { child_of, window } = item;
177
178
// Send event to parent, if it has one.
179
if let Some(child_of) = child_of {
180
return Some(child_of.parent());
181
};
182
183
// Otherwise, send it to the window entity (unless this is a window entity).
184
if window.is_none() {
185
return Some(event.window);
186
}
187
188
None
189
}
190
}
191
192
impl Traversal<AcquireFocus> for WindowTraversal {
193
fn traverse(item: Self::Item<'_, '_>, event: &AcquireFocus) -> Option<Entity> {
194
let WindowTraversalItem { child_of, window } = item;
195
196
// Send event to parent, if it has one.
197
if let Some(child_of) = child_of {
198
return Some(child_of.parent());
199
};
200
201
// Otherwise, send it to the window entity (unless this is a window entity).
202
if window.is_none() {
203
return Some(event.window);
204
}
205
206
None
207
}
208
}
209
210
/// Plugin which sets up systems for dispatching bubbling keyboard and gamepad button events to the focused entity.
211
///
212
/// To add bubbling to your own input events, add the [`dispatch_focused_input::<MyEvent>`](dispatch_focused_input) system to your app,
213
/// as described in the docs for [`FocusedInput`].
214
pub struct InputDispatchPlugin;
215
216
impl Plugin for InputDispatchPlugin {
217
fn build(&self, app: &mut App) {
218
app.add_systems(PostStartup, set_initial_focus)
219
.init_resource::<InputFocus>()
220
.init_resource::<InputFocusVisible>()
221
.add_systems(
222
PreUpdate,
223
(
224
dispatch_focused_input::<KeyboardInput>,
225
dispatch_focused_input::<GamepadButtonChangedEvent>,
226
dispatch_focused_input::<MouseWheel>,
227
)
228
.in_set(InputFocusSystems::Dispatch),
229
);
230
}
231
}
232
233
/// System sets for [`bevy_input_focus`](crate).
234
///
235
/// These systems run in the [`PreUpdate`] schedule.
236
#[derive(SystemSet, Debug, PartialEq, Eq, Hash, Clone)]
237
pub enum InputFocusSystems {
238
/// System which dispatches bubbled input events to the focused entity, or to the primary window.
239
Dispatch,
240
}
241
242
/// Deprecated alias for [`InputFocusSystems`].
243
#[deprecated(since = "0.17.0", note = "Renamed to `InputFocusSystems`.")]
244
pub type InputFocusSet = InputFocusSystems;
245
246
/// If no entity is focused, sets the focus to the primary window, if any.
247
pub fn set_initial_focus(
248
mut input_focus: ResMut<InputFocus>,
249
window: Single<Entity, With<PrimaryWindow>>,
250
) {
251
if input_focus.0.is_none() {
252
input_focus.0 = Some(*window);
253
}
254
}
255
256
/// System which dispatches bubbled input events to the focused entity, or to the primary window
257
/// if no entity has focus.
258
///
259
/// If the currently focused entity no longer exists (has been despawned), this system will
260
/// automatically clear the focus and dispatch events to the primary window instead.
261
pub fn dispatch_focused_input<M: Message + Clone>(
262
mut input_reader: MessageReader<M>,
263
mut focus: ResMut<InputFocus>,
264
windows: Query<Entity, With<PrimaryWindow>>,
265
entities: &Entities,
266
mut commands: Commands,
267
) {
268
if let Ok(window) = windows.single() {
269
// If an element has keyboard focus, then dispatch the input event to that element.
270
if let Some(focused_entity) = focus.0 {
271
// Check if the focused entity is still alive
272
if entities.contains(focused_entity) {
273
for ev in input_reader.read() {
274
commands.trigger(FocusedInput {
275
focused_entity,
276
input: ev.clone(),
277
window,
278
});
279
}
280
} else {
281
// If the focused entity no longer exists, clear focus and dispatch to window
282
focus.0 = None;
283
for ev in input_reader.read() {
284
commands.trigger(FocusedInput {
285
focused_entity: window,
286
input: ev.clone(),
287
window,
288
});
289
}
290
}
291
} else {
292
// If no element has input focus, then dispatch the input event to the primary window.
293
// There should be only one primary window.
294
for ev in input_reader.read() {
295
commands.trigger(FocusedInput {
296
focused_entity: window,
297
input: ev.clone(),
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.focused_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_message() -> 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_message(key_a_message());
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_message(key_a_message());
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_message_batch(core::iter::repeat_n(key_a_message(), 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_message(key_a_message());
664
app.update();
665
666
assert_eq!(app.world().resource::<InputFocus>().0, None);
667
}
668
}
669
670