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
33use bevy_app::{App, Plugin, PostStartup, PreUpdate};
34use bevy_ecs::{
35 entity::Entities, prelude::*, query::QueryData, system::SystemParam, traversal::Traversal,
36};
37use bevy_input::{gamepad::GamepadButtonChangedEvent, keyboard::KeyboardInput, mouse::MouseWheel};
38use bevy_window::{PrimaryWindow, Window};
39use core::fmt::Debug;
40
41#[cfg(feature = "bevy_reflect")]
42use bevy_reflect::{prelude::*, Reflect};
43
44#[derive(Clone, Debug, Default, Resource)]
82#[cfg_attr(
83 feature = "bevy_reflect",
84 derive(Reflect),
85 reflect(Debug, Default, Resource, Clone)
86)]
87pub struct InputFocus(pub Option<Entity>);
88
89impl InputFocus {
90 pub const fn from_entity(entity: Entity) -> Self {
94 Self(Some(entity))
95 }
96
97 pub const fn set(&mut self, entity: Entity) {
99 self.0 = Some(entity);
100 }
101
102 pub const fn get(&self) -> Option<Entity> {
104 self.0
105 }
106
107 pub const fn clear(&mut self) {
109 self.0 = None;
110 }
111}
112
113#[derive(Clone, Debug, Resource, Default)]
128#[cfg_attr(
129 feature = "bevy_reflect",
130 derive(Reflect),
131 reflect(Debug, Resource, Clone)
132)]
133pub struct InputFocusVisible(pub bool);
134
135#[derive(EntityEvent, Clone, Debug, Component)]
143#[entity_event(propagate = WindowTraversal, auto_propagate)]
144#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Component, Clone))]
145pub struct FocusedInput<M: Message + Clone> {
146 #[event_target]
148 pub focused_entity: Entity,
149 pub input: M,
151 window: Entity,
153}
154
155#[derive(Clone, EntityEvent)]
158#[entity_event(propagate = WindowTraversal, auto_propagate)]
159pub struct AcquireFocus {
160 #[event_target]
162 pub focused_entity: Entity,
163 window: Entity,
165}
166
167#[derive(QueryData)]
168pub struct WindowTraversal {
170 child_of: Option<&'static ChildOf>,
171 window: Option<&'static Window>,
172}
173
174impl<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 if let Some(child_of) = child_of {
180 return Some(child_of.parent());
181 };
182
183 if window.is_none() {
185 return Some(event.window);
186 }
187
188 None
189 }
190}
191
192impl Traversal<AcquireFocus> for WindowTraversal {
193 fn traverse(item: Self::Item<'_, '_>, event: &AcquireFocus) -> Option<Entity> {
194 let WindowTraversalItem { child_of, window } = item;
195
196 if let Some(child_of) = child_of {
198 return Some(child_of.parent());
199 };
200
201 if window.is_none() {
203 return Some(event.window);
204 }
205
206 None
207 }
208}
209
210pub struct InputDispatchPlugin;
215
216impl 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#[derive(SystemSet, Debug, PartialEq, Eq, Hash, Clone)]
237pub enum InputFocusSystems {
238 Dispatch,
240}
241
242#[deprecated(since = "0.17.0", note = "Renamed to `InputFocusSystems`.")]
244pub type InputFocusSet = InputFocusSystems;
245
246pub 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
256pub 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 let Some(focused_entity) = focus.0 {
271 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 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 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
305pub trait IsFocused {
316 fn is_focused(&self, entity: Entity) -> bool;
318
319 fn is_focus_within(&self, entity: Entity) -> bool;
323
324 fn is_focus_visible(&self, entity: Entity) -> bool;
326
327 fn is_focus_within_visible(&self, entity: Entity) -> bool;
330}
331
332#[derive(SystemParam)]
336pub 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
342impl 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
369impl 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)]
407mod 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 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 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 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 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 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 app.world_mut().write_message(key_a_message());
664 app.update();
665
666 assert_eq!(app.world().resource::<InputFocus>().0, None);
667 }
668}