1#![cfg_attr(docsrs, feature(doc_auto_cfg))]
2#![forbid(unsafe_code)]
3#![doc(
4 html_logo_url = "https://bevyengine.org/assets/icon.png",
5 html_favicon_url = "https://bevyengine.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, PreUpdate, Startup};
34use bevy_ecs::{prelude::*, query::QueryData, system::SystemParam, traversal::Traversal};
35use bevy_input::{gamepad::GamepadButtonChangedEvent, keyboard::KeyboardInput, mouse::MouseWheel};
36use bevy_window::{PrimaryWindow, Window};
37use core::fmt::Debug;
38
39#[cfg(feature = "bevy_reflect")]
40use bevy_reflect::{prelude::*, Reflect};
41
42#[derive(Clone, Debug, Default, Resource)]
80#[cfg_attr(
81 feature = "bevy_reflect",
82 derive(Reflect),
83 reflect(Debug, Default, Resource, Clone)
84)]
85pub struct InputFocus(pub Option<Entity>);
86
87impl InputFocus {
88 pub const fn from_entity(entity: Entity) -> Self {
92 Self(Some(entity))
93 }
94
95 pub const fn set(&mut self, entity: Entity) {
97 self.0 = Some(entity);
98 }
99
100 pub const fn get(&self) -> Option<Entity> {
102 self.0
103 }
104
105 pub const fn clear(&mut self) {
107 self.0 = None;
108 }
109}
110
111#[derive(Clone, Debug, Resource, Default)]
126#[cfg_attr(
127 feature = "bevy_reflect",
128 derive(Reflect),
129 reflect(Debug, Resource, Clone)
130)]
131pub struct InputFocusVisible(pub bool);
132
133#[derive(Clone, Debug, Component)]
141#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Component, Clone))]
142pub struct FocusedInput<E: Event + Clone> {
143 pub input: E,
145 window: Entity,
147}
148
149impl<E: Event + Clone> Event for FocusedInput<E> {
150 type Traversal = WindowTraversal;
151
152 const AUTO_PROPAGATE: bool = true;
153}
154
155#[derive(QueryData)]
156pub struct WindowTraversal {
158 child_of: Option<&'static ChildOf>,
159 window: Option<&'static Window>,
160}
161
162impl<E: Event + Clone> Traversal<FocusedInput<E>> for WindowTraversal {
163 fn traverse(item: Self::Item<'_>, event: &FocusedInput<E>) -> Option<Entity> {
164 let WindowTraversalItem { child_of, window } = item;
165
166 if let Some(child_of) = child_of {
168 return Some(child_of.parent());
169 };
170
171 if window.is_none() {
173 return Some(event.window);
174 }
175
176 None
177 }
178}
179
180pub struct InputDispatchPlugin;
185
186impl Plugin for InputDispatchPlugin {
187 fn build(&self, app: &mut App) {
188 app.add_systems(Startup, set_initial_focus)
189 .init_resource::<InputFocus>()
190 .init_resource::<InputFocusVisible>()
191 .add_systems(
192 PreUpdate,
193 (
194 dispatch_focused_input::<KeyboardInput>,
195 dispatch_focused_input::<GamepadButtonChangedEvent>,
196 dispatch_focused_input::<MouseWheel>,
197 )
198 .in_set(InputFocusSet::Dispatch),
199 );
200
201 #[cfg(feature = "bevy_reflect")]
202 app.register_type::<AutoFocus>()
203 .register_type::<InputFocus>()
204 .register_type::<InputFocusVisible>();
205 }
206}
207
208#[derive(SystemSet, Debug, PartialEq, Eq, Hash, Clone)]
212pub enum InputFocusSet {
213 Dispatch,
215}
216
217pub fn set_initial_focus(
219 mut input_focus: ResMut<InputFocus>,
220 window: Single<Entity, With<PrimaryWindow>>,
221) {
222 input_focus.0 = Some(*window);
223}
224
225pub fn dispatch_focused_input<E: Event + Clone>(
228 mut key_events: EventReader<E>,
229 focus: Res<InputFocus>,
230 windows: Query<Entity, With<PrimaryWindow>>,
231 mut commands: Commands,
232) {
233 if let Ok(window) = windows.single() {
234 if let Some(focused_entity) = focus.0 {
236 for ev in key_events.read() {
237 commands.trigger_targets(
238 FocusedInput {
239 input: ev.clone(),
240 window,
241 },
242 focused_entity,
243 );
244 }
245 } else {
246 for ev in key_events.read() {
249 commands.trigger_targets(
250 FocusedInput {
251 input: ev.clone(),
252 window,
253 },
254 window,
255 );
256 }
257 }
258 }
259}
260
261pub trait IsFocused {
272 fn is_focused(&self, entity: Entity) -> bool;
274
275 fn is_focus_within(&self, entity: Entity) -> bool;
279
280 fn is_focus_visible(&self, entity: Entity) -> bool;
282
283 fn is_focus_within_visible(&self, entity: Entity) -> bool;
286}
287
288#[derive(SystemParam)]
292pub struct IsFocusedHelper<'w, 's> {
293 parent_query: Query<'w, 's, &'static ChildOf>,
294 input_focus: Option<Res<'w, InputFocus>>,
295 input_focus_visible: Option<Res<'w, InputFocusVisible>>,
296}
297
298impl IsFocused for IsFocusedHelper<'_, '_> {
299 fn is_focused(&self, entity: Entity) -> bool {
300 self.input_focus
301 .as_deref()
302 .and_then(|f| f.0)
303 .is_some_and(|e| e == entity)
304 }
305
306 fn is_focus_within(&self, entity: Entity) -> bool {
307 let Some(focus) = self.input_focus.as_deref().and_then(|f| f.0) else {
308 return false;
309 };
310 if focus == entity {
311 return true;
312 }
313 self.parent_query.iter_ancestors(focus).any(|e| e == entity)
314 }
315
316 fn is_focus_visible(&self, entity: Entity) -> bool {
317 self.input_focus_visible.as_deref().is_some_and(|vis| vis.0) && self.is_focused(entity)
318 }
319
320 fn is_focus_within_visible(&self, entity: Entity) -> bool {
321 self.input_focus_visible.as_deref().is_some_and(|vis| vis.0) && self.is_focus_within(entity)
322 }
323}
324
325impl IsFocused for World {
326 fn is_focused(&self, entity: Entity) -> bool {
327 self.get_resource::<InputFocus>()
328 .and_then(|f| f.0)
329 .is_some_and(|f| f == entity)
330 }
331
332 fn is_focus_within(&self, entity: Entity) -> bool {
333 let Some(focus) = self.get_resource::<InputFocus>().and_then(|f| f.0) else {
334 return false;
335 };
336 let mut e = focus;
337 loop {
338 if e == entity {
339 return true;
340 }
341 if let Some(parent) = self.entity(e).get::<ChildOf>().map(ChildOf::parent) {
342 e = parent;
343 } else {
344 return false;
345 }
346 }
347 }
348
349 fn is_focus_visible(&self, entity: Entity) -> bool {
350 self.get_resource::<InputFocusVisible>()
351 .is_some_and(|vis| vis.0)
352 && self.is_focused(entity)
353 }
354
355 fn is_focus_within_visible(&self, entity: Entity) -> bool {
356 self.get_resource::<InputFocusVisible>()
357 .is_some_and(|vis| vis.0)
358 && self.is_focus_within(entity)
359 }
360}
361
362#[cfg(test)]
363mod tests {
364 use super::*;
365
366 use alloc::string::String;
367 use bevy_ecs::{
368 component::HookContext, observer::Trigger, system::RunSystemOnce, world::DeferredWorld,
369 };
370 use bevy_input::{
371 keyboard::{Key, KeyCode},
372 ButtonState, InputPlugin,
373 };
374 use bevy_window::WindowResolution;
375 use smol_str::SmolStr;
376
377 #[derive(Component)]
378 #[component(on_add = set_focus_on_add)]
379 struct SetFocusOnAdd;
380
381 fn set_focus_on_add(mut world: DeferredWorld, HookContext { entity, .. }: HookContext) {
382 let mut input_focus = world.resource_mut::<InputFocus>();
383 input_focus.set(entity);
384 }
385
386 #[derive(Component, Default)]
387 struct GatherKeyboardEvents(String);
388
389 fn gather_keyboard_events(
390 trigger: Trigger<FocusedInput<KeyboardInput>>,
391 mut query: Query<&mut GatherKeyboardEvents>,
392 ) {
393 if let Ok(mut gather) = query.get_mut(trigger.target()) {
394 if let Key::Character(c) = &trigger.input.logical_key {
395 gather.0.push_str(c.as_str());
396 }
397 }
398 }
399
400 const KEY_A_EVENT: KeyboardInput = KeyboardInput {
401 key_code: KeyCode::KeyA,
402 logical_key: Key::Character(SmolStr::new_static("A")),
403 state: ButtonState::Pressed,
404 text: Some(SmolStr::new_static("A")),
405 repeat: false,
406 window: Entity::PLACEHOLDER,
407 };
408
409 #[test]
410 fn test_no_panics_if_resource_missing() {
411 let mut app = App::new();
412 let entity = app.world_mut().spawn_empty().id();
415
416 assert!(!app.world().is_focused(entity));
417
418 app.world_mut()
419 .run_system_once(move |helper: IsFocusedHelper| {
420 assert!(!helper.is_focused(entity));
421 assert!(!helper.is_focus_within(entity));
422 assert!(!helper.is_focus_visible(entity));
423 assert!(!helper.is_focus_within_visible(entity));
424 })
425 .unwrap();
426
427 app.world_mut()
428 .run_system_once(move |world: DeferredWorld| {
429 assert!(!world.is_focused(entity));
430 assert!(!world.is_focus_within(entity));
431 assert!(!world.is_focus_visible(entity));
432 assert!(!world.is_focus_within_visible(entity));
433 })
434 .unwrap();
435 }
436
437 #[test]
438 fn test_keyboard_events() {
439 fn get_gathered(app: &App, entity: Entity) -> &str {
440 app.world()
441 .entity(entity)
442 .get::<GatherKeyboardEvents>()
443 .unwrap()
444 .0
445 .as_str()
446 }
447
448 let mut app = App::new();
449
450 app.add_plugins((InputPlugin, InputDispatchPlugin))
451 .add_observer(gather_keyboard_events);
452
453 let window = Window {
454 resolution: WindowResolution::new(800., 600.),
455 ..Default::default()
456 };
457 app.world_mut().spawn((window, PrimaryWindow));
458
459 app.update();
461
462 let entity_a = app
463 .world_mut()
464 .spawn((GatherKeyboardEvents::default(), SetFocusOnAdd))
465 .id();
466
467 let child_of_b = app
468 .world_mut()
469 .spawn((GatherKeyboardEvents::default(),))
470 .id();
471
472 let entity_b = app
473 .world_mut()
474 .spawn((GatherKeyboardEvents::default(),))
475 .add_child(child_of_b)
476 .id();
477
478 assert!(app.world().is_focused(entity_a));
479 assert!(!app.world().is_focused(entity_b));
480 assert!(!app.world().is_focused(child_of_b));
481 assert!(!app.world().is_focus_visible(entity_a));
482 assert!(!app.world().is_focus_visible(entity_b));
483 assert!(!app.world().is_focus_visible(child_of_b));
484
485 app.world_mut().send_event(KEY_A_EVENT);
487 app.update();
488
489 assert_eq!(get_gathered(&app, entity_a), "A");
490 assert_eq!(get_gathered(&app, entity_b), "");
491 assert_eq!(get_gathered(&app, child_of_b), "");
492
493 app.world_mut().insert_resource(InputFocus(None));
494
495 assert!(!app.world().is_focused(entity_a));
496 assert!(!app.world().is_focus_visible(entity_a));
497
498 app.world_mut().send_event(KEY_A_EVENT);
500 app.update();
501
502 assert_eq!(get_gathered(&app, entity_a), "A");
503 assert_eq!(get_gathered(&app, entity_b), "");
504 assert_eq!(get_gathered(&app, child_of_b), "");
505
506 app.world_mut()
507 .insert_resource(InputFocus::from_entity(entity_b));
508 assert!(app.world().is_focused(entity_b));
509 assert!(!app.world().is_focused(child_of_b));
510
511 app.world_mut()
512 .run_system_once(move |mut input_focus: ResMut<InputFocus>| {
513 input_focus.set(child_of_b);
514 })
515 .unwrap();
516 assert!(app.world().is_focus_within(entity_b));
517
518 app.world_mut().send_event_batch([KEY_A_EVENT; 4]);
520 app.update();
521
522 assert_eq!(get_gathered(&app, entity_a), "A");
523 assert_eq!(get_gathered(&app, entity_b), "AAAA");
524 assert_eq!(get_gathered(&app, child_of_b), "AAAA");
525
526 app.world_mut().resource_mut::<InputFocusVisible>().0 = true;
527
528 app.world_mut()
529 .run_system_once(move |helper: IsFocusedHelper| {
530 assert!(!helper.is_focused(entity_a));
531 assert!(!helper.is_focus_within(entity_a));
532 assert!(!helper.is_focus_visible(entity_a));
533 assert!(!helper.is_focus_within_visible(entity_a));
534
535 assert!(!helper.is_focused(entity_b));
536 assert!(helper.is_focus_within(entity_b));
537 assert!(!helper.is_focus_visible(entity_b));
538 assert!(helper.is_focus_within_visible(entity_b));
539
540 assert!(helper.is_focused(child_of_b));
541 assert!(helper.is_focus_within(child_of_b));
542 assert!(helper.is_focus_visible(child_of_b));
543 assert!(helper.is_focus_within_visible(child_of_b));
544 })
545 .unwrap();
546
547 app.world_mut()
548 .run_system_once(move |world: DeferredWorld| {
549 assert!(!world.is_focused(entity_a));
550 assert!(!world.is_focus_within(entity_a));
551 assert!(!world.is_focus_visible(entity_a));
552 assert!(!world.is_focus_within_visible(entity_a));
553
554 assert!(!world.is_focused(entity_b));
555 assert!(world.is_focus_within(entity_b));
556 assert!(!world.is_focus_visible(entity_b));
557 assert!(world.is_focus_within_visible(entity_b));
558
559 assert!(world.is_focused(child_of_b));
560 assert!(world.is_focus_within(child_of_b));
561 assert!(world.is_focus_visible(child_of_b));
562 assert!(world.is_focus_within_visible(child_of_b));
563 })
564 .unwrap();
565 }
566}