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")]
22extern crate std;
23
24extern crate alloc;
25
26pub mod directional_navigation;
27pub mod navigator;
28pub mod tab_navigation;
29
30mod autofocus;
33pub use autofocus::*;
34
35mod gained_and_lost;
36pub use gained_and_lost::*;
37
38use alloc::vec;
39use alloc::vec::Vec;
40#[cfg(any(feature = "keyboard", feature = "gamepad", feature = "mouse"))]
41use bevy_app::PreUpdate;
42use bevy_app::{App, Plugin, PostStartup, PostUpdate};
43use bevy_ecs::{
44 entity::Entities, prelude::*, query::QueryData, system::SystemParam, traversal::Traversal,
45};
46#[cfg(feature = "gamepad")]
47use bevy_input::gamepad::GamepadButtonChangedEvent;
48#[cfg(feature = "keyboard")]
49use bevy_input::keyboard::KeyboardInput;
50#[cfg(feature = "mouse")]
51use bevy_input::mouse::MouseWheel;
52use bevy_window::{PrimaryWindow, Window};
53use core::fmt::Debug;
54
55#[cfg(feature = "bevy_reflect")]
56use bevy_reflect::{prelude::*, Reflect};
57
58#[derive(Clone, Debug, Default, Resource, PartialEq)]
98#[cfg_attr(
99 feature = "bevy_reflect",
100 derive(Reflect),
101 reflect(Debug, Default, Resource, Clone)
102)]
103pub struct InputFocus {
104 current_focus: Option<Entity>,
106 recorded_changes: Vec<Option<(Entity, FocusCause)>>,
110 original_focus: Option<Entity>,
114}
115
116impl InputFocus {
117 pub fn from_entity(entity: Entity) -> Self {
126 Self {
127 current_focus: Some(entity),
128 recorded_changes: vec![Some((entity, FocusCause::Navigated))],
129 original_focus: None,
130 }
131 }
132
133 pub fn set(&mut self, entity: Entity, cause: FocusCause) {
140 self.current_focus = Some(entity);
141 self.recorded_changes.push(Some((entity, cause)));
142 }
143
144 pub const fn get(&self) -> Option<Entity> {
146 self.current_focus
147 }
148
149 pub fn clear(&mut self) {
151 self.current_focus = None;
152 self.recorded_changes.push(None);
153 }
154}
155
156#[derive(Clone, Debug, Resource, Default)]
171#[cfg_attr(
172 feature = "bevy_reflect",
173 derive(Reflect),
174 reflect(Debug, Resource, Clone)
175)]
176pub struct InputFocusVisible(pub bool);
177
178#[derive(EntityEvent, Clone, Debug, Component)]
186#[entity_event(propagate = WindowTraversal, auto_propagate)]
187#[cfg_attr(
188 feature = "bevy_reflect",
189 derive(Reflect),
190 reflect(Event, Component, Clone)
191)]
192pub struct FocusedInput<M: Message + Clone> {
193 #[event_target]
195 pub focused_entity: Entity,
196 pub input: M,
198 window: Entity,
200}
201
202#[derive(EntityEvent, Debug, Clone)]
205#[entity_event(propagate = WindowTraversal, auto_propagate)]
206#[cfg_attr(
207 feature = "bevy_reflect",
208 derive(Reflect),
209 reflect(Event, Clone, Debug)
210)]
211pub struct AcquireFocus {
212 #[event_target]
214 pub focused_entity: Entity,
215 pub window: Entity,
217}
218
219#[derive(QueryData)]
220pub struct WindowTraversal {
222 child_of: Option<&'static ChildOf>,
223 window: Option<&'static Window>,
224}
225
226impl<M: Message + Clone> Traversal<FocusedInput<M>> for WindowTraversal {
227 fn traverse(item: Self::Item<'_, '_>, event: &FocusedInput<M>) -> Option<Entity> {
228 let WindowTraversalItem { child_of, window } = item;
229
230 if let Some(child_of) = child_of {
232 return Some(child_of.parent());
233 };
234
235 if window.is_none() {
237 return Some(event.window);
238 }
239
240 None
241 }
242}
243
244impl Traversal<AcquireFocus> for WindowTraversal {
245 fn traverse(item: Self::Item<'_, '_>, event: &AcquireFocus) -> Option<Entity> {
246 let WindowTraversalItem { child_of, window } = item;
247
248 if let Some(child_of) = child_of {
250 return Some(child_of.parent());
251 };
252
253 if window.is_none() {
255 return Some(event.window);
256 }
257
258 None
259 }
260}
261
262#[derive(Default)]
268pub struct InputFocusPlugin;
269
270impl Plugin for InputFocusPlugin {
271 fn build(&self, app: &mut App) {
272 app.add_systems(PostStartup, set_initial_focus)
273 .init_resource::<InputFocus>()
274 .init_resource::<InputFocusVisible>()
275 .add_systems(
276 PostUpdate,
277 process_recorded_focus_changes.in_set(InputFocusSystems::FocusChangeEvents),
278 );
279 }
280}
281
282#[derive(Default)]
287pub struct InputDispatchPlugin;
288
289impl Plugin for InputDispatchPlugin {
290 fn build(&self, app: &mut App) {
291 #[cfg(not(any(feature = "keyboard", feature = "gamepad", feature = "mouse")))]
292 let _ = app;
293 #[cfg(any(feature = "keyboard", feature = "gamepad", feature = "mouse"))]
294 app.add_systems(
295 PreUpdate,
296 (
297 #[cfg(feature = "keyboard")]
298 dispatch_focused_input::<KeyboardInput>,
299 #[cfg(feature = "gamepad")]
300 dispatch_focused_input::<GamepadButtonChangedEvent>,
301 #[cfg(feature = "mouse")]
302 dispatch_focused_input::<MouseWheel>,
303 )
304 .chain()
305 .in_set(InputFocusSystems::Dispatch)
306 .after(bevy_input::InputSystems),
307 );
308 }
309}
310
311#[derive(SystemSet, Debug, PartialEq, Eq, Hash, Clone)]
315pub enum InputFocusSystems {
316 Dispatch,
320 FocusChangeEvents,
324}
325
326pub fn set_initial_focus(
328 mut input_focus: ResMut<InputFocus>,
329 window: Single<Entity, With<PrimaryWindow>>,
330) {
331 if input_focus.get().is_none() {
332 input_focus.set(*window, FocusCause::Navigated);
333 }
334}
335
336pub fn dispatch_focused_input<M: Message + Clone>(
342 mut input_reader: MessageReader<M>,
343 mut focus: ResMut<InputFocus>,
344 windows: Query<Entity, With<PrimaryWindow>>,
345 entities: &Entities,
346 mut commands: Commands,
347) {
348 if let Ok(window) = windows.single() {
349 if let Some(focused_entity) = focus.get() {
351 if entities.contains(focused_entity) {
353 for ev in input_reader.read() {
354 commands.trigger(FocusedInput {
355 focused_entity,
356 input: ev.clone(),
357 window,
358 });
359 }
360 } else {
361 focus.clear();
363 for ev in input_reader.read() {
364 commands.trigger(FocusedInput {
365 focused_entity: window,
366 input: ev.clone(),
367 window,
368 });
369 }
370 }
371 } else {
372 for ev in input_reader.read() {
375 commands.trigger(FocusedInput {
376 focused_entity: window,
377 input: ev.clone(),
378 window,
379 });
380 }
381 }
382 }
383}
384
385pub trait IsFocused {
396 fn is_focused(&self, entity: Entity) -> bool;
398
399 fn is_focus_within(&self, entity: Entity) -> bool;
403
404 fn is_focus_visible(&self, entity: Entity) -> bool;
406
407 fn is_focus_within_visible(&self, entity: Entity) -> bool;
410}
411
412#[derive(SystemParam)]
416pub struct IsFocusedHelper<'w, 's> {
417 parent_query: Query<'w, 's, &'static ChildOf>,
418 input_focus: Option<Res<'w, InputFocus>>,
419 input_focus_visible: Option<Res<'w, InputFocusVisible>>,
420}
421
422impl IsFocused for IsFocusedHelper<'_, '_> {
423 fn is_focused(&self, entity: Entity) -> bool {
424 self.input_focus
425 .as_deref()
426 .and_then(InputFocus::get)
427 .is_some_and(|e| e == entity)
428 }
429
430 fn is_focus_within(&self, entity: Entity) -> bool {
431 let Some(focus) = self.input_focus.as_deref().and_then(InputFocus::get) else {
432 return false;
433 };
434 if focus == entity {
435 return true;
436 }
437 self.parent_query.iter_ancestors(focus).any(|e| e == entity)
438 }
439
440 fn is_focus_visible(&self, entity: Entity) -> bool {
441 self.input_focus_visible.as_deref().is_some_and(|vis| vis.0) && self.is_focused(entity)
442 }
443
444 fn is_focus_within_visible(&self, entity: Entity) -> bool {
445 self.input_focus_visible.as_deref().is_some_and(|vis| vis.0) && self.is_focus_within(entity)
446 }
447}
448
449impl IsFocused for World {
450 fn is_focused(&self, entity: Entity) -> bool {
451 self.get_resource::<InputFocus>()
452 .and_then(InputFocus::get)
453 .is_some_and(|f| f == entity)
454 }
455
456 fn is_focus_within(&self, entity: Entity) -> bool {
457 let Some(focus) = self.get_resource::<InputFocus>().and_then(InputFocus::get) else {
458 return false;
459 };
460 let mut e = focus;
461 loop {
462 if e == entity {
463 return true;
464 }
465 if let Some(parent) = self.entity(e).get::<ChildOf>().map(ChildOf::parent) {
466 e = parent;
467 } else {
468 return false;
469 }
470 }
471 }
472
473 fn is_focus_visible(&self, entity: Entity) -> bool {
474 self.get_resource::<InputFocusVisible>()
475 .is_some_and(|vis| vis.0)
476 && self.is_focused(entity)
477 }
478
479 fn is_focus_within_visible(&self, entity: Entity) -> bool {
480 self.get_resource::<InputFocusVisible>()
481 .is_some_and(|vis| vis.0)
482 && self.is_focus_within(entity)
483 }
484}
485
486#[cfg(test)]
487mod tests {
488 use super::*;
489
490 use alloc::string::String;
491 use bevy_app::Startup;
492 use bevy_ecs::{observer::On, system::RunSystemOnce, world::DeferredWorld};
493 use bevy_input::{
494 keyboard::{Key, KeyCode},
495 ButtonState, InputPlugin,
496 };
497
498 #[derive(Component, Default)]
499 struct GatherKeyboardEvents(String);
500
501 fn gather_keyboard_events(
502 event: On<FocusedInput<KeyboardInput>>,
503 mut query: Query<&mut GatherKeyboardEvents>,
504 ) {
505 if let Ok(mut gather) = query.get_mut(event.focused_entity)
506 && let Key::Character(c) = &event.input.logical_key
507 {
508 gather.0.push_str(c.as_str());
509 }
510 }
511
512 fn key_a_message() -> KeyboardInput {
513 KeyboardInput {
514 key_code: KeyCode::KeyA,
515 logical_key: Key::Character("A".into()),
516 state: ButtonState::Pressed,
517 text: Some("A".into()),
518 repeat: false,
519 window: Entity::PLACEHOLDER,
520 }
521 }
522
523 #[test]
524 fn test_no_panics_if_resource_missing() {
525 let mut app = App::new();
526 let entity = app.world_mut().spawn_empty().id();
529
530 assert!(!app.world().is_focused(entity));
531
532 app.world_mut()
533 .run_system_once(move |helper: IsFocusedHelper| {
534 assert!(!helper.is_focused(entity));
535 assert!(!helper.is_focus_within(entity));
536 assert!(!helper.is_focus_visible(entity));
537 assert!(!helper.is_focus_within_visible(entity));
538 })
539 .unwrap();
540
541 app.world_mut()
542 .run_system_once(move |world: DeferredWorld| {
543 assert!(!world.is_focused(entity));
544 assert!(!world.is_focus_within(entity));
545 assert!(!world.is_focus_visible(entity));
546 assert!(!world.is_focus_within_visible(entity));
547 })
548 .unwrap();
549 }
550
551 #[test]
552 fn initial_focus_unset_if_no_primary_window() {
553 let mut app = App::new();
554 app.add_plugins((InputPlugin, InputFocusPlugin));
555
556 app.update();
557
558 assert_eq!(app.world().resource::<InputFocus>().get(), None);
559 }
560
561 #[test]
562 fn initial_focus_set_to_primary_window() {
563 let mut app = App::new();
564 app.add_plugins((InputPlugin, InputFocusPlugin));
565
566 let entity_window = app
567 .world_mut()
568 .spawn((Window::default(), PrimaryWindow))
569 .id();
570 app.update();
571
572 assert_eq!(
573 app.world().resource::<InputFocus>().get(),
574 Some(entity_window)
575 );
576 }
577
578 #[test]
579 fn initial_focus_not_overridden() {
580 let mut app = App::new();
581 app.add_plugins((InputPlugin, InputFocusPlugin));
582
583 app.world_mut().spawn((Window::default(), PrimaryWindow));
584
585 app.add_systems(Startup, |mut commands: Commands| {
586 commands.spawn(AutoFocus);
587 });
588
589 app.update();
590
591 let autofocus_entity = app
592 .world_mut()
593 .query_filtered::<Entity, With<AutoFocus>>()
594 .single(app.world())
595 .unwrap();
596
597 assert_eq!(
598 app.world().resource::<InputFocus>().get(),
599 Some(autofocus_entity)
600 );
601 }
602
603 #[test]
604 fn test_keyboard_events() {
605 fn get_gathered(app: &App, entity: Entity) -> &str {
606 app.world()
607 .entity(entity)
608 .get::<GatherKeyboardEvents>()
609 .unwrap()
610 .0
611 .as_str()
612 }
613
614 let mut app = App::new();
615
616 app.add_plugins((InputPlugin, InputFocusPlugin, InputDispatchPlugin))
617 .add_observer(gather_keyboard_events);
618
619 app.world_mut().spawn((Window::default(), PrimaryWindow));
620
621 app.update();
623
624 let entity_a = app
625 .world_mut()
626 .spawn((GatherKeyboardEvents::default(), AutoFocus))
627 .id();
628
629 let child_of_b = app
630 .world_mut()
631 .spawn((GatherKeyboardEvents::default(),))
632 .id();
633
634 let entity_b = app
635 .world_mut()
636 .spawn((GatherKeyboardEvents::default(),))
637 .add_child(child_of_b)
638 .id();
639
640 assert!(app.world().is_focused(entity_a));
641 assert!(!app.world().is_focused(entity_b));
642 assert!(!app.world().is_focused(child_of_b));
643 assert!(!app.world().is_focus_visible(entity_a));
644 assert!(!app.world().is_focus_visible(entity_b));
645 assert!(!app.world().is_focus_visible(child_of_b));
646
647 app.world_mut().write_message(key_a_message());
649 app.update();
650
651 assert_eq!(get_gathered(&app, entity_a), "A");
652 assert_eq!(get_gathered(&app, entity_b), "");
653 assert_eq!(get_gathered(&app, child_of_b), "");
654
655 app.world_mut().insert_resource(InputFocus::default());
656
657 assert!(!app.world().is_focused(entity_a));
658 assert!(!app.world().is_focus_visible(entity_a));
659
660 app.world_mut().write_message(key_a_message());
662 app.update();
663
664 assert_eq!(get_gathered(&app, entity_a), "A");
665 assert_eq!(get_gathered(&app, entity_b), "");
666 assert_eq!(get_gathered(&app, child_of_b), "");
667
668 app.world_mut()
669 .insert_resource(InputFocus::from_entity(entity_b));
670 assert!(app.world().is_focused(entity_b));
671 assert!(!app.world().is_focused(child_of_b));
672
673 app.world_mut()
674 .run_system_once(move |mut input_focus: ResMut<InputFocus>| {
675 input_focus.set(child_of_b, FocusCause::Navigated);
676 })
677 .unwrap();
678 assert!(app.world().is_focus_within(entity_b));
679
680 app.world_mut()
682 .write_message_batch(core::iter::repeat_n(key_a_message(), 4));
683 app.update();
684
685 assert_eq!(get_gathered(&app, entity_a), "A");
686 assert_eq!(get_gathered(&app, entity_b), "AAAA");
687 assert_eq!(get_gathered(&app, child_of_b), "AAAA");
688
689 app.world_mut().resource_mut::<InputFocusVisible>().0 = true;
690
691 app.world_mut()
692 .run_system_once(move |helper: IsFocusedHelper| {
693 assert!(!helper.is_focused(entity_a));
694 assert!(!helper.is_focus_within(entity_a));
695 assert!(!helper.is_focus_visible(entity_a));
696 assert!(!helper.is_focus_within_visible(entity_a));
697
698 assert!(!helper.is_focused(entity_b));
699 assert!(helper.is_focus_within(entity_b));
700 assert!(!helper.is_focus_visible(entity_b));
701 assert!(helper.is_focus_within_visible(entity_b));
702
703 assert!(helper.is_focused(child_of_b));
704 assert!(helper.is_focus_within(child_of_b));
705 assert!(helper.is_focus_visible(child_of_b));
706 assert!(helper.is_focus_within_visible(child_of_b));
707 })
708 .unwrap();
709
710 app.world_mut()
711 .run_system_once(move |world: DeferredWorld| {
712 assert!(!world.is_focused(entity_a));
713 assert!(!world.is_focus_within(entity_a));
714 assert!(!world.is_focus_visible(entity_a));
715 assert!(!world.is_focus_within_visible(entity_a));
716
717 assert!(!world.is_focused(entity_b));
718 assert!(world.is_focus_within(entity_b));
719 assert!(!world.is_focus_visible(entity_b));
720 assert!(world.is_focus_within_visible(entity_b));
721
722 assert!(world.is_focused(child_of_b));
723 assert!(world.is_focus_within(child_of_b));
724 assert!(world.is_focus_visible(child_of_b));
725 assert!(world.is_focus_within_visible(child_of_b));
726 })
727 .unwrap();
728 }
729
730 #[test]
731 fn dispatch_clears_focus_when_focused_entity_despawned() {
732 let mut app = App::new();
733 app.add_plugins((InputPlugin, InputFocusPlugin, InputDispatchPlugin));
734
735 app.world_mut().spawn((Window::default(), PrimaryWindow));
736 app.update();
737
738 let entity = app.world_mut().spawn_empty().id();
739 app.world_mut()
740 .insert_resource(InputFocus::from_entity(entity));
741 app.world_mut().entity_mut(entity).despawn();
742
743 assert_eq!(app.world().resource::<InputFocus>().get(), Some(entity));
744
745 app.world_mut().write_message(key_a_message());
747 app.update();
748
749 assert_eq!(app.world().resource::<InputFocus>().get(), None);
750 }
751}