1#![cfg_attr(docsrs, feature(doc_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#[cfg(feature = "std")]
21extern crate std;
22
23extern crate alloc;
24
25pub mod directional_navigation;
26pub mod tab_navigation;
27
28mod autofocus;
31pub use autofocus::*;
32
33#[cfg(any(feature = "keyboard", feature = "gamepad", feature = "mouse"))]
34use bevy_app::PreUpdate;
35use bevy_app::{App, Plugin, PostStartup};
36use bevy_ecs::{
37 entity::Entities, prelude::*, query::QueryData, system::SystemParam, traversal::Traversal,
38};
39#[cfg(feature = "gamepad")]
40use bevy_input::gamepad::GamepadButtonChangedEvent;
41#[cfg(feature = "keyboard")]
42use bevy_input::keyboard::KeyboardInput;
43#[cfg(feature = "mouse")]
44use bevy_input::mouse::MouseWheel;
45use bevy_window::{PrimaryWindow, Window};
46use core::fmt::Debug;
47
48#[cfg(feature = "bevy_reflect")]
49use bevy_reflect::{prelude::*, Reflect};
50
51#[derive(Clone, Debug, Default, Resource)]
89#[cfg_attr(
90 feature = "bevy_reflect",
91 derive(Reflect),
92 reflect(Debug, Default, Resource, Clone)
93)]
94pub struct InputFocus(pub Option<Entity>);
95
96impl InputFocus {
97 pub const fn from_entity(entity: Entity) -> Self {
101 Self(Some(entity))
102 }
103
104 pub const fn set(&mut self, entity: Entity) {
106 self.0 = Some(entity);
107 }
108
109 pub const fn get(&self) -> Option<Entity> {
111 self.0
112 }
113
114 pub const fn clear(&mut self) {
116 self.0 = None;
117 }
118}
119
120#[derive(Clone, Debug, Resource, Default)]
135#[cfg_attr(
136 feature = "bevy_reflect",
137 derive(Reflect),
138 reflect(Debug, Resource, Clone)
139)]
140pub struct InputFocusVisible(pub bool);
141
142#[derive(EntityEvent, Clone, Debug, Component)]
150#[entity_event(propagate = WindowTraversal, auto_propagate)]
151#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Component, Clone))]
152pub struct FocusedInput<M: Message + Clone> {
153 #[event_target]
155 pub focused_entity: Entity,
156 pub input: M,
158 window: Entity,
160}
161
162#[derive(Clone, EntityEvent)]
165#[entity_event(propagate = WindowTraversal, auto_propagate)]
166pub struct AcquireFocus {
167 #[event_target]
169 pub focused_entity: Entity,
170 window: Entity,
172}
173
174#[derive(QueryData)]
175pub struct WindowTraversal {
177 child_of: Option<&'static ChildOf>,
178 window: Option<&'static Window>,
179}
180
181impl<M: Message + Clone> Traversal<FocusedInput<M>> for WindowTraversal {
182 fn traverse(item: Self::Item<'_, '_>, event: &FocusedInput<M>) -> Option<Entity> {
183 let WindowTraversalItem { child_of, window } = item;
184
185 if let Some(child_of) = child_of {
187 return Some(child_of.parent());
188 };
189
190 if window.is_none() {
192 return Some(event.window);
193 }
194
195 None
196 }
197}
198
199impl Traversal<AcquireFocus> for WindowTraversal {
200 fn traverse(item: Self::Item<'_, '_>, event: &AcquireFocus) -> Option<Entity> {
201 let WindowTraversalItem { child_of, window } = item;
202
203 if let Some(child_of) = child_of {
205 return Some(child_of.parent());
206 };
207
208 if window.is_none() {
210 return Some(event.window);
211 }
212
213 None
214 }
215}
216
217pub struct InputDispatchPlugin;
222
223impl Plugin for InputDispatchPlugin {
224 fn build(&self, app: &mut App) {
225 app.add_systems(PostStartup, set_initial_focus)
226 .init_resource::<InputFocus>()
227 .init_resource::<InputFocusVisible>();
228
229 #[cfg(any(feature = "keyboard", feature = "gamepad", feature = "mouse"))]
230 app.add_systems(
231 PreUpdate,
232 (
233 #[cfg(feature = "keyboard")]
234 dispatch_focused_input::<KeyboardInput>,
235 #[cfg(feature = "gamepad")]
236 dispatch_focused_input::<GamepadButtonChangedEvent>,
237 #[cfg(feature = "mouse")]
238 dispatch_focused_input::<MouseWheel>,
239 )
240 .in_set(InputFocusSystems::Dispatch),
241 );
242 }
243}
244
245#[derive(SystemSet, Debug, PartialEq, Eq, Hash, Clone)]
249pub enum InputFocusSystems {
250 Dispatch,
252}
253
254pub fn set_initial_focus(
256 mut input_focus: ResMut<InputFocus>,
257 window: Single<Entity, With<PrimaryWindow>>,
258) {
259 if input_focus.0.is_none() {
260 input_focus.0 = Some(*window);
261 }
262}
263
264pub fn dispatch_focused_input<M: Message + Clone>(
270 mut input_reader: MessageReader<M>,
271 mut focus: ResMut<InputFocus>,
272 windows: Query<Entity, With<PrimaryWindow>>,
273 entities: &Entities,
274 mut commands: Commands,
275) {
276 if let Ok(window) = windows.single() {
277 if let Some(focused_entity) = focus.0 {
279 if entities.contains(focused_entity) {
281 for ev in input_reader.read() {
282 commands.trigger(FocusedInput {
283 focused_entity,
284 input: ev.clone(),
285 window,
286 });
287 }
288 } else {
289 focus.0 = None;
291 for ev in input_reader.read() {
292 commands.trigger(FocusedInput {
293 focused_entity: window,
294 input: ev.clone(),
295 window,
296 });
297 }
298 }
299 } else {
300 for ev in input_reader.read() {
303 commands.trigger(FocusedInput {
304 focused_entity: window,
305 input: ev.clone(),
306 window,
307 });
308 }
309 }
310 }
311}
312
313pub trait IsFocused {
324 fn is_focused(&self, entity: Entity) -> bool;
326
327 fn is_focus_within(&self, entity: Entity) -> bool;
331
332 fn is_focus_visible(&self, entity: Entity) -> bool;
334
335 fn is_focus_within_visible(&self, entity: Entity) -> bool;
338}
339
340#[derive(SystemParam)]
344pub struct IsFocusedHelper<'w, 's> {
345 parent_query: Query<'w, 's, &'static ChildOf>,
346 input_focus: Option<Res<'w, InputFocus>>,
347 input_focus_visible: Option<Res<'w, InputFocusVisible>>,
348}
349
350impl IsFocused for IsFocusedHelper<'_, '_> {
351 fn is_focused(&self, entity: Entity) -> bool {
352 self.input_focus
353 .as_deref()
354 .and_then(|f| f.0)
355 .is_some_and(|e| e == entity)
356 }
357
358 fn is_focus_within(&self, entity: Entity) -> bool {
359 let Some(focus) = self.input_focus.as_deref().and_then(|f| f.0) else {
360 return false;
361 };
362 if focus == entity {
363 return true;
364 }
365 self.parent_query.iter_ancestors(focus).any(|e| e == entity)
366 }
367
368 fn is_focus_visible(&self, entity: Entity) -> bool {
369 self.input_focus_visible.as_deref().is_some_and(|vis| vis.0) && self.is_focused(entity)
370 }
371
372 fn is_focus_within_visible(&self, entity: Entity) -> bool {
373 self.input_focus_visible.as_deref().is_some_and(|vis| vis.0) && self.is_focus_within(entity)
374 }
375}
376
377impl IsFocused for World {
378 fn is_focused(&self, entity: Entity) -> bool {
379 self.get_resource::<InputFocus>()
380 .and_then(|f| f.0)
381 .is_some_and(|f| f == entity)
382 }
383
384 fn is_focus_within(&self, entity: Entity) -> bool {
385 let Some(focus) = self.get_resource::<InputFocus>().and_then(|f| f.0) else {
386 return false;
387 };
388 let mut e = focus;
389 loop {
390 if e == entity {
391 return true;
392 }
393 if let Some(parent) = self.entity(e).get::<ChildOf>().map(ChildOf::parent) {
394 e = parent;
395 } else {
396 return false;
397 }
398 }
399 }
400
401 fn is_focus_visible(&self, entity: Entity) -> bool {
402 self.get_resource::<InputFocusVisible>()
403 .is_some_and(|vis| vis.0)
404 && self.is_focused(entity)
405 }
406
407 fn is_focus_within_visible(&self, entity: Entity) -> bool {
408 self.get_resource::<InputFocusVisible>()
409 .is_some_and(|vis| vis.0)
410 && self.is_focus_within(entity)
411 }
412}
413
414#[cfg(test)]
415mod tests {
416 use super::*;
417
418 use alloc::string::String;
419 use bevy_app::Startup;
420 use bevy_ecs::{observer::On, system::RunSystemOnce, world::DeferredWorld};
421 use bevy_input::{
422 keyboard::{Key, KeyCode},
423 ButtonState, InputPlugin,
424 };
425
426 #[derive(Component, Default)]
427 struct GatherKeyboardEvents(String);
428
429 fn gather_keyboard_events(
430 event: On<FocusedInput<KeyboardInput>>,
431 mut query: Query<&mut GatherKeyboardEvents>,
432 ) {
433 if let Ok(mut gather) = query.get_mut(event.focused_entity) {
434 if let Key::Character(c) = &event.input.logical_key {
435 gather.0.push_str(c.as_str());
436 }
437 }
438 }
439
440 fn key_a_message() -> KeyboardInput {
441 KeyboardInput {
442 key_code: KeyCode::KeyA,
443 logical_key: Key::Character("A".into()),
444 state: ButtonState::Pressed,
445 text: Some("A".into()),
446 repeat: false,
447 window: Entity::PLACEHOLDER,
448 }
449 }
450
451 #[test]
452 fn test_no_panics_if_resource_missing() {
453 let mut app = App::new();
454 let entity = app.world_mut().spawn_empty().id();
457
458 assert!(!app.world().is_focused(entity));
459
460 app.world_mut()
461 .run_system_once(move |helper: IsFocusedHelper| {
462 assert!(!helper.is_focused(entity));
463 assert!(!helper.is_focus_within(entity));
464 assert!(!helper.is_focus_visible(entity));
465 assert!(!helper.is_focus_within_visible(entity));
466 })
467 .unwrap();
468
469 app.world_mut()
470 .run_system_once(move |world: DeferredWorld| {
471 assert!(!world.is_focused(entity));
472 assert!(!world.is_focus_within(entity));
473 assert!(!world.is_focus_visible(entity));
474 assert!(!world.is_focus_within_visible(entity));
475 })
476 .unwrap();
477 }
478
479 #[test]
480 fn initial_focus_unset_if_no_primary_window() {
481 let mut app = App::new();
482 app.add_plugins((InputPlugin, InputDispatchPlugin));
483
484 app.update();
485
486 assert_eq!(app.world().resource::<InputFocus>().0, None);
487 }
488
489 #[test]
490 fn initial_focus_set_to_primary_window() {
491 let mut app = App::new();
492 app.add_plugins((InputPlugin, InputDispatchPlugin));
493
494 let entity_window = app
495 .world_mut()
496 .spawn((Window::default(), PrimaryWindow))
497 .id();
498 app.update();
499
500 assert_eq!(app.world().resource::<InputFocus>().0, Some(entity_window));
501 }
502
503 #[test]
504 fn initial_focus_not_overridden() {
505 let mut app = App::new();
506 app.add_plugins((InputPlugin, InputDispatchPlugin));
507
508 app.world_mut().spawn((Window::default(), PrimaryWindow));
509
510 app.add_systems(Startup, |mut commands: Commands| {
511 commands.spawn(AutoFocus);
512 });
513
514 app.update();
515
516 let autofocus_entity = app
517 .world_mut()
518 .query_filtered::<Entity, With<AutoFocus>>()
519 .single(app.world())
520 .unwrap();
521
522 assert_eq!(
523 app.world().resource::<InputFocus>().0,
524 Some(autofocus_entity)
525 );
526 }
527
528 #[test]
529 fn test_keyboard_events() {
530 fn get_gathered(app: &App, entity: Entity) -> &str {
531 app.world()
532 .entity(entity)
533 .get::<GatherKeyboardEvents>()
534 .unwrap()
535 .0
536 .as_str()
537 }
538
539 let mut app = App::new();
540
541 app.add_plugins((InputPlugin, InputDispatchPlugin))
542 .add_observer(gather_keyboard_events);
543
544 app.world_mut().spawn((Window::default(), PrimaryWindow));
545
546 app.update();
548
549 let entity_a = app
550 .world_mut()
551 .spawn((GatherKeyboardEvents::default(), AutoFocus))
552 .id();
553
554 let child_of_b = app
555 .world_mut()
556 .spawn((GatherKeyboardEvents::default(),))
557 .id();
558
559 let entity_b = app
560 .world_mut()
561 .spawn((GatherKeyboardEvents::default(),))
562 .add_child(child_of_b)
563 .id();
564
565 assert!(app.world().is_focused(entity_a));
566 assert!(!app.world().is_focused(entity_b));
567 assert!(!app.world().is_focused(child_of_b));
568 assert!(!app.world().is_focus_visible(entity_a));
569 assert!(!app.world().is_focus_visible(entity_b));
570 assert!(!app.world().is_focus_visible(child_of_b));
571
572 app.world_mut().write_message(key_a_message());
574 app.update();
575
576 assert_eq!(get_gathered(&app, entity_a), "A");
577 assert_eq!(get_gathered(&app, entity_b), "");
578 assert_eq!(get_gathered(&app, child_of_b), "");
579
580 app.world_mut().insert_resource(InputFocus(None));
581
582 assert!(!app.world().is_focused(entity_a));
583 assert!(!app.world().is_focus_visible(entity_a));
584
585 app.world_mut().write_message(key_a_message());
587 app.update();
588
589 assert_eq!(get_gathered(&app, entity_a), "A");
590 assert_eq!(get_gathered(&app, entity_b), "");
591 assert_eq!(get_gathered(&app, child_of_b), "");
592
593 app.world_mut()
594 .insert_resource(InputFocus::from_entity(entity_b));
595 assert!(app.world().is_focused(entity_b));
596 assert!(!app.world().is_focused(child_of_b));
597
598 app.world_mut()
599 .run_system_once(move |mut input_focus: ResMut<InputFocus>| {
600 input_focus.set(child_of_b);
601 })
602 .unwrap();
603 assert!(app.world().is_focus_within(entity_b));
604
605 app.world_mut()
607 .write_message_batch(core::iter::repeat_n(key_a_message(), 4));
608 app.update();
609
610 assert_eq!(get_gathered(&app, entity_a), "A");
611 assert_eq!(get_gathered(&app, entity_b), "AAAA");
612 assert_eq!(get_gathered(&app, child_of_b), "AAAA");
613
614 app.world_mut().resource_mut::<InputFocusVisible>().0 = true;
615
616 app.world_mut()
617 .run_system_once(move |helper: IsFocusedHelper| {
618 assert!(!helper.is_focused(entity_a));
619 assert!(!helper.is_focus_within(entity_a));
620 assert!(!helper.is_focus_visible(entity_a));
621 assert!(!helper.is_focus_within_visible(entity_a));
622
623 assert!(!helper.is_focused(entity_b));
624 assert!(helper.is_focus_within(entity_b));
625 assert!(!helper.is_focus_visible(entity_b));
626 assert!(helper.is_focus_within_visible(entity_b));
627
628 assert!(helper.is_focused(child_of_b));
629 assert!(helper.is_focus_within(child_of_b));
630 assert!(helper.is_focus_visible(child_of_b));
631 assert!(helper.is_focus_within_visible(child_of_b));
632 })
633 .unwrap();
634
635 app.world_mut()
636 .run_system_once(move |world: DeferredWorld| {
637 assert!(!world.is_focused(entity_a));
638 assert!(!world.is_focus_within(entity_a));
639 assert!(!world.is_focus_visible(entity_a));
640 assert!(!world.is_focus_within_visible(entity_a));
641
642 assert!(!world.is_focused(entity_b));
643 assert!(world.is_focus_within(entity_b));
644 assert!(!world.is_focus_visible(entity_b));
645 assert!(world.is_focus_within_visible(entity_b));
646
647 assert!(world.is_focused(child_of_b));
648 assert!(world.is_focus_within(child_of_b));
649 assert!(world.is_focus_visible(child_of_b));
650 assert!(world.is_focus_within_visible(child_of_b));
651 })
652 .unwrap();
653 }
654
655 #[test]
656 fn dispatch_clears_focus_when_focused_entity_despawned() {
657 let mut app = App::new();
658 app.add_plugins((InputPlugin, InputDispatchPlugin));
659
660 app.world_mut().spawn((Window::default(), PrimaryWindow));
661 app.update();
662
663 let entity = app.world_mut().spawn_empty().id();
664 app.world_mut()
665 .insert_resource(InputFocus::from_entity(entity));
666 app.world_mut().entity_mut(entity).despawn();
667
668 assert_eq!(app.world().resource::<InputFocus>().0, Some(entity));
669
670 app.world_mut().write_message(key_a_message());
672 app.update();
673
674 assert_eq!(app.world().resource::<InputFocus>().0, None);
675 }
676}