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_attr(
18 feature = "bevy_picking",
19 doc = "* [`pointer_focus`], which focuses (or blurs) entities in response to pointer clicks, independently of any navigation framework (requires the `bevy_picking` feature)."
20)]
21#[cfg(feature = "std")]
26extern crate std;
27
28extern crate alloc;
29
30pub mod directional_navigation;
31pub mod navigator;
32#[cfg(feature = "bevy_picking")]
33pub mod pointer_focus;
34pub mod tab_navigation;
35
36mod autofocus;
39pub use autofocus::*;
40
41mod gained_and_lost;
42pub use gained_and_lost::*;
43
44use alloc::vec;
45use alloc::vec::Vec;
46#[cfg(any(feature = "keyboard", feature = "gamepad", feature = "mouse"))]
47use bevy_app::PreUpdate;
48use bevy_app::{App, Plugin, PostStartup, PostUpdate};
49use bevy_ecs::{
50 entity::Entities, prelude::*, query::QueryData, system::SystemParam, traversal::Traversal,
51};
52#[cfg(feature = "gamepad")]
53use bevy_input::gamepad::GamepadButtonChangedEvent;
54#[cfg(feature = "keyboard")]
55use bevy_input::keyboard::KeyboardInput;
56#[cfg(feature = "mouse")]
57use bevy_input::mouse::MouseWheel;
58use bevy_window::{PrimaryWindow, Window};
59use core::fmt::Debug;
60
61#[cfg(feature = "bevy_reflect")]
62use bevy_reflect::{prelude::*, Reflect};
63
64#[derive(Clone, Debug, Default, Resource, PartialEq)]
104#[cfg_attr(
105 feature = "bevy_reflect",
106 derive(Reflect),
107 reflect(Debug, Default, Resource, Clone)
108)]
109pub struct InputFocus {
110 current_focus: Option<Entity>,
112 recorded_changes: Vec<Option<(Entity, FocusCause)>>,
116 original_focus: Option<Entity>,
120}
121
122impl InputFocus {
123 pub fn from_entity(entity: Entity) -> Self {
132 Self {
133 current_focus: Some(entity),
134 recorded_changes: vec![Some((entity, FocusCause::Navigated))],
135 original_focus: None,
136 }
137 }
138
139 pub fn set(&mut self, entity: Entity, cause: FocusCause) {
146 self.current_focus = Some(entity);
147 self.recorded_changes.push(Some((entity, cause)));
148 }
149
150 pub const fn get(&self) -> Option<Entity> {
152 self.current_focus
153 }
154
155 pub fn clear(&mut self) {
157 self.current_focus = None;
158 self.recorded_changes.push(None);
159 }
160}
161
162#[derive(Clone, Debug, Resource, Default)]
177#[cfg_attr(
178 feature = "bevy_reflect",
179 derive(Reflect),
180 reflect(Debug, Resource, Clone)
181)]
182pub struct InputFocusVisible(pub bool);
183
184#[derive(EntityEvent, Clone, Debug, Component)]
192#[entity_event(propagate = WindowTraversal, auto_propagate)]
193#[cfg_attr(
194 feature = "bevy_reflect",
195 derive(Reflect),
196 reflect(Event, Component, Clone)
197)]
198pub struct FocusedInput<M: Message + Clone> {
199 #[event_target]
201 pub focused_entity: Entity,
202 pub input: M,
204 window: Entity,
206}
207
208impl<M: Message + Clone> FocusedInput<M> {
209 pub fn new(focused_entity: Entity, input: M, window: Entity) -> Self {
211 Self {
212 focused_entity,
213 input,
214 window,
215 }
216 }
217}
218
219#[derive(EntityEvent, Debug, Clone)]
222#[entity_event(propagate = WindowTraversal, auto_propagate)]
223#[cfg_attr(
224 feature = "bevy_reflect",
225 derive(Reflect),
226 reflect(Event, Clone, Debug)
227)]
228pub struct AcquireFocus {
229 #[event_target]
231 pub focused_entity: Entity,
232 pub window: Entity,
234}
235
236#[derive(QueryData)]
237pub struct WindowTraversal {
239 child_of: Option<&'static ChildOf>,
240 window: Option<&'static Window>,
241}
242
243impl<M: Message + Clone> Traversal<FocusedInput<M>> for WindowTraversal {
244 fn traverse(item: Self::Item<'_, '_>, event: &FocusedInput<M>) -> Option<Entity> {
245 let WindowTraversalItem { child_of, window } = item;
246
247 if let Some(child_of) = child_of {
249 return Some(child_of.parent());
250 };
251
252 if window.is_none() {
254 return Some(event.window);
255 }
256
257 None
258 }
259}
260
261impl Traversal<AcquireFocus> for WindowTraversal {
262 fn traverse(item: Self::Item<'_, '_>, event: &AcquireFocus) -> Option<Entity> {
263 let WindowTraversalItem { child_of, window } = item;
264
265 if let Some(child_of) = child_of {
267 return Some(child_of.parent());
268 };
269
270 if window.is_none() {
272 return Some(event.window);
273 }
274
275 None
276 }
277}
278
279pub fn on_window_acquire_focus_clear(
300 mut acquire_focus: On<AcquireFocus>,
301 windows: Query<(), With<Window>>,
302 mut focus: ResMut<InputFocus>,
303) {
304 if windows.contains(acquire_focus.focused_entity) {
305 acquire_focus.propagate(false);
307 if focus.get().is_some() {
309 focus.clear();
310 }
311 }
312}
313
314#[derive(Default)]
320pub struct InputFocusPlugin;
321
322impl Plugin for InputFocusPlugin {
323 fn build(&self, app: &mut App) {
324 app.add_systems(PostStartup, set_initial_focus)
325 .init_resource::<InputFocus>()
326 .init_resource::<InputFocusVisible>()
327 .add_observer(on_window_acquire_focus_clear)
328 .add_systems(
329 PostUpdate,
330 process_recorded_focus_changes.in_set(InputFocusSystems::FocusChangeEvents),
331 );
332 }
333}
334
335#[derive(Default)]
340pub struct InputDispatchPlugin;
341
342impl Plugin for InputDispatchPlugin {
343 fn build(&self, app: &mut App) {
344 #[cfg(not(any(feature = "keyboard", feature = "gamepad", feature = "mouse")))]
345 let _ = app;
346 #[cfg(any(feature = "keyboard", feature = "gamepad", feature = "mouse"))]
347 app.add_systems(
348 PreUpdate,
349 (
350 #[cfg(feature = "keyboard")]
351 dispatch_focused_input::<KeyboardInput>,
352 #[cfg(feature = "gamepad")]
353 dispatch_focused_input::<GamepadButtonChangedEvent>,
354 #[cfg(feature = "mouse")]
355 dispatch_focused_input::<MouseWheel>,
356 )
357 .chain()
358 .in_set(InputFocusSystems::Dispatch)
359 .after(bevy_input::InputSystems),
360 );
361 }
362}
363
364#[derive(SystemSet, Debug, PartialEq, Eq, Hash, Clone)]
368pub enum InputFocusSystems {
369 Dispatch,
373 FocusChangeEvents,
377}
378
379pub fn set_initial_focus(
381 mut input_focus: ResMut<InputFocus>,
382 window: Single<Entity, With<PrimaryWindow>>,
383) {
384 if input_focus.get().is_none() {
385 input_focus.set(*window, FocusCause::Navigated);
386 }
387}
388
389pub fn dispatch_focused_input<M: Message + Clone>(
395 mut input_reader: MessageReader<M>,
396 mut focus: ResMut<InputFocus>,
397 windows: Query<Entity, With<PrimaryWindow>>,
398 entities: &Entities,
399 mut commands: Commands,
400) {
401 if let Ok(window) = windows.single() {
402 if let Some(focused_entity) = focus.get() {
404 if entities.contains(focused_entity) {
406 for ev in input_reader.read() {
407 commands.trigger(FocusedInput {
408 focused_entity,
409 input: ev.clone(),
410 window,
411 });
412 }
413 } else {
414 focus.clear();
416 for ev in input_reader.read() {
417 commands.trigger(FocusedInput {
418 focused_entity: window,
419 input: ev.clone(),
420 window,
421 });
422 }
423 }
424 } else {
425 for ev in input_reader.read() {
428 commands.trigger(FocusedInput {
429 focused_entity: window,
430 input: ev.clone(),
431 window,
432 });
433 }
434 }
435 }
436}
437
438pub trait IsFocused {
449 fn is_focused(&self, entity: Entity) -> bool;
451
452 fn is_focus_within(&self, entity: Entity) -> bool;
456
457 fn is_focus_visible(&self, entity: Entity) -> bool;
459
460 fn is_focus_within_visible(&self, entity: Entity) -> bool;
463}
464
465#[derive(SystemParam)]
469pub struct IsFocusedHelper<'w, 's> {
470 parent_query: Query<'w, 's, &'static ChildOf>,
471 input_focus: Option<Res<'w, InputFocus>>,
472 input_focus_visible: Option<Res<'w, InputFocusVisible>>,
473}
474
475impl IsFocused for IsFocusedHelper<'_, '_> {
476 fn is_focused(&self, entity: Entity) -> bool {
477 self.input_focus
478 .as_deref()
479 .and_then(InputFocus::get)
480 .is_some_and(|e| e == entity)
481 }
482
483 fn is_focus_within(&self, entity: Entity) -> bool {
484 let Some(focus) = self.input_focus.as_deref().and_then(InputFocus::get) else {
485 return false;
486 };
487 if focus == entity {
488 return true;
489 }
490 self.parent_query.iter_ancestors(focus).any(|e| e == entity)
491 }
492
493 fn is_focus_visible(&self, entity: Entity) -> bool {
494 self.input_focus_visible.as_deref().is_some_and(|vis| vis.0) && self.is_focused(entity)
495 }
496
497 fn is_focus_within_visible(&self, entity: Entity) -> bool {
498 self.input_focus_visible.as_deref().is_some_and(|vis| vis.0) && self.is_focus_within(entity)
499 }
500}
501
502impl IsFocused for World {
503 fn is_focused(&self, entity: Entity) -> bool {
504 self.get_resource::<InputFocus>()
505 .and_then(InputFocus::get)
506 .is_some_and(|f| f == entity)
507 }
508
509 fn is_focus_within(&self, entity: Entity) -> bool {
510 let Some(focus) = self.get_resource::<InputFocus>().and_then(InputFocus::get) else {
511 return false;
512 };
513 let mut e = focus;
514 loop {
515 if e == entity {
516 return true;
517 }
518 if let Some(parent) = self.entity(e).get::<ChildOf>().map(ChildOf::parent) {
519 e = parent;
520 } else {
521 return false;
522 }
523 }
524 }
525
526 fn is_focus_visible(&self, entity: Entity) -> bool {
527 self.get_resource::<InputFocusVisible>()
528 .is_some_and(|vis| vis.0)
529 && self.is_focused(entity)
530 }
531
532 fn is_focus_within_visible(&self, entity: Entity) -> bool {
533 self.get_resource::<InputFocusVisible>()
534 .is_some_and(|vis| vis.0)
535 && self.is_focus_within(entity)
536 }
537}
538
539#[cfg(test)]
540mod tests {
541 use super::*;
542
543 use alloc::string::String;
544 use bevy_app::Startup;
545 use bevy_ecs::{observer::On, system::RunSystemOnce, world::DeferredWorld};
546 use bevy_input::{
547 keyboard::{Key, KeyCode},
548 ButtonState, InputPlugin,
549 };
550
551 #[derive(Component, Default)]
552 struct GatherKeyboardEvents(String);
553
554 fn gather_keyboard_events(
555 event: On<FocusedInput<KeyboardInput>>,
556 mut query: Query<&mut GatherKeyboardEvents>,
557 ) {
558 if let Ok(mut gather) = query.get_mut(event.focused_entity)
559 && let Key::Character(c) = &event.input.logical_key
560 {
561 gather.0.push_str(c.as_str());
562 }
563 }
564
565 fn key_a_message(window: Entity) -> KeyboardInput {
566 KeyboardInput {
567 key_code: KeyCode::KeyA,
568 logical_key: Key::Character("A".into()),
569 state: ButtonState::Pressed,
570 text: Some("A".into()),
571 repeat: false,
572 window,
573 }
574 }
575
576 #[test]
577 fn test_no_panics_if_resource_missing() {
578 let mut app = App::new();
579 let entity = app.world_mut().spawn_empty().id();
582
583 assert!(!app.world().is_focused(entity));
584
585 app.world_mut()
586 .run_system_once(move |helper: IsFocusedHelper| {
587 assert!(!helper.is_focused(entity));
588 assert!(!helper.is_focus_within(entity));
589 assert!(!helper.is_focus_visible(entity));
590 assert!(!helper.is_focus_within_visible(entity));
591 })
592 .unwrap();
593
594 app.world_mut()
595 .run_system_once(move |world: DeferredWorld| {
596 assert!(!world.is_focused(entity));
597 assert!(!world.is_focus_within(entity));
598 assert!(!world.is_focus_visible(entity));
599 assert!(!world.is_focus_within_visible(entity));
600 })
601 .unwrap();
602 }
603
604 #[test]
605 fn initial_focus_unset_if_no_primary_window() {
606 let mut app = App::new();
607 app.add_plugins((InputPlugin, InputFocusPlugin));
608
609 app.update();
610
611 assert_eq!(app.world().resource::<InputFocus>().get(), None);
612 }
613
614 #[test]
615 fn initial_focus_set_to_primary_window() {
616 let mut app = App::new();
617 app.add_plugins((InputPlugin, InputFocusPlugin));
618
619 let entity_window = app
620 .world_mut()
621 .spawn((Window::default(), PrimaryWindow))
622 .id();
623 app.update();
624
625 assert_eq!(
626 app.world().resource::<InputFocus>().get(),
627 Some(entity_window)
628 );
629 }
630
631 #[test]
632 fn initial_focus_not_overridden() {
633 let mut app = App::new();
634 app.add_plugins((InputPlugin, InputFocusPlugin));
635
636 app.world_mut().spawn((Window::default(), PrimaryWindow));
637
638 app.add_systems(Startup, |mut commands: Commands| {
639 commands.spawn(AutoFocus);
640 });
641
642 app.update();
643
644 let autofocus_entity = app
645 .world_mut()
646 .query_filtered::<Entity, With<AutoFocus>>()
647 .single(app.world())
648 .unwrap();
649
650 assert_eq!(
651 app.world().resource::<InputFocus>().get(),
652 Some(autofocus_entity)
653 );
654 }
655
656 #[test]
657 fn test_keyboard_events() {
658 fn get_gathered(app: &App, entity: Entity) -> &str {
659 app.world()
660 .entity(entity)
661 .get::<GatherKeyboardEvents>()
662 .unwrap()
663 .0
664 .as_str()
665 }
666
667 let mut app = App::new();
668
669 app.add_plugins((InputPlugin, InputFocusPlugin, InputDispatchPlugin))
670 .add_observer(gather_keyboard_events);
671
672 let window = app
673 .world_mut()
674 .spawn((Window::default(), PrimaryWindow))
675 .id();
676
677 app.update();
679
680 let entity_a = app
681 .world_mut()
682 .spawn((GatherKeyboardEvents::default(), AutoFocus))
683 .id();
684
685 let child_of_b = app
686 .world_mut()
687 .spawn((GatherKeyboardEvents::default(),))
688 .id();
689
690 let entity_b = app
691 .world_mut()
692 .spawn((GatherKeyboardEvents::default(),))
693 .add_child(child_of_b)
694 .id();
695
696 assert!(app.world().is_focused(entity_a));
697 assert!(!app.world().is_focused(entity_b));
698 assert!(!app.world().is_focused(child_of_b));
699 assert!(!app.world().is_focus_visible(entity_a));
700 assert!(!app.world().is_focus_visible(entity_b));
701 assert!(!app.world().is_focus_visible(child_of_b));
702
703 app.world_mut().write_message(key_a_message(window));
705 app.update();
706
707 assert_eq!(get_gathered(&app, entity_a), "A");
708 assert_eq!(get_gathered(&app, entity_b), "");
709 assert_eq!(get_gathered(&app, child_of_b), "");
710
711 app.world_mut().insert_resource(InputFocus::default());
712
713 assert!(!app.world().is_focused(entity_a));
714 assert!(!app.world().is_focus_visible(entity_a));
715
716 app.world_mut().write_message(key_a_message(window));
718 app.update();
719
720 assert_eq!(get_gathered(&app, entity_a), "A");
721 assert_eq!(get_gathered(&app, entity_b), "");
722 assert_eq!(get_gathered(&app, child_of_b), "");
723
724 app.world_mut()
725 .insert_resource(InputFocus::from_entity(entity_b));
726 assert!(app.world().is_focused(entity_b));
727 assert!(!app.world().is_focused(child_of_b));
728
729 app.world_mut()
730 .run_system_once(move |mut input_focus: ResMut<InputFocus>| {
731 input_focus.set(child_of_b, FocusCause::Navigated);
732 })
733 .unwrap();
734 assert!(app.world().is_focus_within(entity_b));
735
736 app.world_mut()
738 .write_message_batch(core::iter::repeat_n(key_a_message(window), 4));
739 app.update();
740
741 assert_eq!(get_gathered(&app, entity_a), "A");
742 assert_eq!(get_gathered(&app, entity_b), "AAAA");
743 assert_eq!(get_gathered(&app, child_of_b), "AAAA");
744
745 app.world_mut().resource_mut::<InputFocusVisible>().0 = true;
746
747 app.world_mut()
748 .run_system_once(move |helper: IsFocusedHelper| {
749 assert!(!helper.is_focused(entity_a));
750 assert!(!helper.is_focus_within(entity_a));
751 assert!(!helper.is_focus_visible(entity_a));
752 assert!(!helper.is_focus_within_visible(entity_a));
753
754 assert!(!helper.is_focused(entity_b));
755 assert!(helper.is_focus_within(entity_b));
756 assert!(!helper.is_focus_visible(entity_b));
757 assert!(helper.is_focus_within_visible(entity_b));
758
759 assert!(helper.is_focused(child_of_b));
760 assert!(helper.is_focus_within(child_of_b));
761 assert!(helper.is_focus_visible(child_of_b));
762 assert!(helper.is_focus_within_visible(child_of_b));
763 })
764 .unwrap();
765
766 app.world_mut()
767 .run_system_once(move |world: DeferredWorld| {
768 assert!(!world.is_focused(entity_a));
769 assert!(!world.is_focus_within(entity_a));
770 assert!(!world.is_focus_visible(entity_a));
771 assert!(!world.is_focus_within_visible(entity_a));
772
773 assert!(!world.is_focused(entity_b));
774 assert!(world.is_focus_within(entity_b));
775 assert!(!world.is_focus_visible(entity_b));
776 assert!(world.is_focus_within_visible(entity_b));
777
778 assert!(world.is_focused(child_of_b));
779 assert!(world.is_focus_within(child_of_b));
780 assert!(world.is_focus_visible(child_of_b));
781 assert!(world.is_focus_within_visible(child_of_b));
782 })
783 .unwrap();
784 }
785
786 #[test]
787 fn dispatch_clears_focus_when_focused_entity_despawned() {
788 let mut app = App::new();
789 app.add_plugins((InputPlugin, InputFocusPlugin, InputDispatchPlugin));
790
791 let window = app
792 .world_mut()
793 .spawn((Window::default(), PrimaryWindow))
794 .id();
795 app.update();
796
797 let entity = app.world_mut().spawn_empty().id();
798 app.world_mut()
799 .insert_resource(InputFocus::from_entity(entity));
800 app.world_mut().entity_mut(entity).despawn();
801
802 assert_eq!(app.world().resource::<InputFocus>().get(), Some(entity));
803
804 app.world_mut().write_message(key_a_message(window));
806 app.update();
807
808 assert_eq!(app.world().resource::<InputFocus>().get(), None);
809 }
810
811 fn acquire_focus_app() -> (App, Entity) {
813 let mut app = App::new();
814 app.add_plugins((InputPlugin, InputFocusPlugin));
815 let window = app
816 .world_mut()
817 .spawn((Window::default(), PrimaryWindow))
818 .id();
819 app.update();
821 (app, window)
822 }
823
824 #[test]
825 fn acquire_focus_clears_focus_when_reaching_window() {
826 let (mut app, window) = acquire_focus_app();
827
828 let previously_focused = app.world_mut().spawn_empty().id();
830 app.world_mut()
831 .insert_resource(InputFocus::from_entity(previously_focused));
832
833 let non_focusable = app.world_mut().spawn(ChildOf(window)).id();
838 app.world_mut().trigger(AcquireFocus {
839 focused_entity: non_focusable,
840 window,
841 });
842 app.update();
843
844 assert_eq!(app.world().resource::<InputFocus>().get(), None);
845 }
846}