diff --git a/_release-content/migration-guides/input_focus_setting_getting.md b/_release-content/migration-guides/input_focus_setting_getting.md new file mode 100644 index 0000000000000..128e0d4f67ee1 --- /dev/null +++ b/_release-content/migration-guides/input_focus_setting_getting.md @@ -0,0 +1,27 @@ +--- +title: "`InputFocus` fields are no longer public" +pull_requests: [23723] +--- + +The `.0` field on `InputFocus` is no longer public. +Use the getter and setters methods instead. + +Before: + +```rust +let focused_entity = input_focus.0; +input_focus.0 = Some(entity); +input_focus.0 = None; +``` + +After: + +```rust +let focused_entity = input_focus.get(); +input_focus.set(entity); +input_focus.clear(); +``` + +Additionally, the core setup of `InputFocus` and related resources now occurs in `InputFocusPlugin`, +rather than `InputDispatchPlugin`. +This is part of `DefaultPlugins`, so most users will not need to make any changes. diff --git a/crates/bevy_feathers/src/controls/text_input.rs b/crates/bevy_feathers/src/controls/text_input.rs index dbbb0cd2e205b..547fe6ea5326a 100644 --- a/crates/bevy_feathers/src/controls/text_input.rs +++ b/crates/bevy_feathers/src/controls/text_input.rs @@ -146,7 +146,7 @@ fn update_text_input_focus( // We're not using FocusIndicator here because (a) the focus ring is inset rather than // an outline, and (b) we want to detect focus on a descendant rather than an ancestor. if focus.is_changed() { - let focus_parent = focus.0.and_then(|focus_ent| { + let focus_parent = focus.get().and_then(|focus_ent| { if focus_visible.0 && q_inputs.contains(focus_ent) { parents .iter_ancestors(focus_ent) diff --git a/crates/bevy_feathers/src/focus.rs b/crates/bevy_feathers/src/focus.rs index 122efd10df122..828eb98eda1c6 100644 --- a/crates/bevy_feathers/src/focus.rs +++ b/crates/bevy_feathers/src/focus.rs @@ -37,7 +37,7 @@ fn manage_focus_indicators( } let mut visited = HashSet::::with_capacity(q_indicators.count()); - if let Some(focus) = input_focus.0 + if let Some(focus) = input_focus.get() && input_focus_visible.0 { for entity in q_children diff --git a/crates/bevy_input_focus/src/directional_navigation.rs b/crates/bevy_input_focus/src/directional_navigation.rs index d94e1e8154713..5586dfe0c29f1 100644 --- a/crates/bevy_input_focus/src/directional_navigation.rs +++ b/crates/bevy_input_focus/src/directional_navigation.rs @@ -411,7 +411,7 @@ impl<'w> DirectionalNavigation<'w> { &mut self, direction: CompassOctant, ) -> Result { - if let Some(current_focus) = self.focus.0 { + if let Some(current_focus) = self.focus.get() { // Respect manual edges first match self.map.get_neighbor(current_focus, direction) { NavNeighbor::Auto => Err(DirectionalNavigationError::NoNeighborInDirection { diff --git a/crates/bevy_input_focus/src/gained_and_lost.rs b/crates/bevy_input_focus/src/gained_and_lost.rs new file mode 100644 index 0000000000000..8ad2b987eed1e --- /dev/null +++ b/crates/bevy_input_focus/src/gained_and_lost.rs @@ -0,0 +1,300 @@ +//! Contains [`FocusGained`] and [`FocusLost`] events, +//! as well as [`process_recorded_focus_changes`] to send them when the focused entity changes. + +use super::InputFocus; +use bevy_ecs::prelude::*; + +/// An [`EntityEvent`] that is sent when an entity gains [`InputFocus`]. +/// +/// This event bubbles up the entity hierarchy, so if a child entity gains focus, its parents will also receive this event. +#[derive(EntityEvent, Debug, Clone)] +#[entity_event(auto_propagate)] +pub struct FocusGained { + /// The entity that gained focus. + pub entity: Entity, +} + +/// An [`EntityEvent`] that is sent when an entity loses [`InputFocus`]. +/// +/// This event bubbles up the entity hierarchy, so if a child entity loses focus, its parents will also receive this event. +#[derive(EntityEvent, Debug, Clone)] +#[entity_event(auto_propagate)] +pub struct FocusLost { + /// The entity that lost focus. + pub entity: Entity, +} + +/// Reads the recorded focus changes from the [`InputFocus`] resource and sends the appropriate [`FocusGained`] and [`FocusLost`] events. +/// +/// This system is part of [`InputFocusPlugin`](super::InputFocusPlugin). +pub fn process_recorded_focus_changes(mut focus: ResMut, mut commands: Commands) { + // We need to track the previous focus as we go, + // so we can send the correct FocusLost events when focus changes. + let mut previous_focus = focus.original_focus; + for change in focus.recorded_changes.drain(..) { + match change { + Some(new_focus) => { + if let Some(old_focus) = previous_focus { + commands.trigger(FocusLost { entity: old_focus }); + } + commands.trigger(FocusGained { entity: new_focus }); + previous_focus = Some(new_focus); + } + None => { + if let Some(old_focus) = previous_focus { + commands.trigger(FocusLost { entity: old_focus }); + } + previous_focus = None; + } + } + } + + focus.original_focus = focus.current_focus; +} + +#[cfg(test)] +mod tests { + use super::*; + use alloc::vec; + use alloc::vec::Vec; + use bevy_app::App; + use bevy_ecs::observer::On; + use bevy_input::InputPlugin; + + /// Tracks the sequence of [`FocusGained`] and [`FocusLost`] events for assertions. + #[derive(Debug, Clone, PartialEq)] + enum FocusEvent { + Gained(Entity), + Lost(Entity), + } + + #[derive(Resource, Default)] + struct FocusEventLog(Vec); + + fn setup_app() -> App { + let mut app = App::new(); + app.add_plugins((InputPlugin, super::super::InputFocusPlugin)); + app.init_resource::(); + + app.add_observer(|trigger: On, mut log: ResMut| { + log.0.push(FocusEvent::Gained(trigger.entity)); + }); + app.add_observer(|trigger: On, mut log: ResMut| { + log.0.push(FocusEvent::Lost(trigger.entity)); + }); + + // Run once to finish startup + app.update(); + + app + } + + // Convenience method to extract and clear the log values for assertions + fn take_log(app: &mut App) -> Vec { + core::mem::take(&mut app.world_mut().resource_mut::().0) + } + + #[test] + fn no_changes_no_events() { + let mut app = setup_app(); + + app.update(); + assert!(take_log(&mut app).is_empty()); + } + + #[test] + fn gain_focus_from_none() { + let mut app = setup_app(); + + let entity = app.world_mut().spawn_empty().id(); + app.world_mut().resource_mut::().set(entity); + app.update(); + + assert_eq!(take_log(&mut app), vec![FocusEvent::Gained(entity)]); + } + + #[test] + fn lose_focus_to_none() { + let mut app = setup_app(); + let entity = app.world_mut().spawn_empty().id(); + + // Establish initial focus. + app.world_mut().resource_mut::().set(entity); + app.update(); + take_log(&mut app); + + app.world_mut().resource_mut::().clear(); + app.update(); + + assert_eq!(take_log(&mut app), vec![FocusEvent::Lost(entity)]); + } + + #[test] + fn switch_focus_between_entities() { + let mut app = setup_app(); + let a = app.world_mut().spawn_empty().id(); + let b = app.world_mut().spawn_empty().id(); + + app.world_mut().resource_mut::().set(a); + app.update(); + take_log(&mut app); + + app.world_mut().resource_mut::().set(b); + app.update(); + + assert_eq!( + take_log(&mut app), + vec![FocusEvent::Lost(a), FocusEvent::Gained(b)] + ); + } + + #[test] + fn multiple_changes_in_single_frame() { + let mut app = setup_app(); + take_log(&mut app); + + let a = app.world_mut().spawn_empty().id(); + let b = app.world_mut().spawn_empty().id(); + let c = app.world_mut().spawn_empty().id(); + + let mut focus = app.world_mut().resource_mut::(); + focus.set(a); + focus.set(b); + focus.clear(); + focus.set(c); + + app.update(); + + assert_eq!( + take_log(&mut app), + vec![ + FocusEvent::Gained(a), + FocusEvent::Lost(a), + FocusEvent::Gained(b), + FocusEvent::Lost(b), + FocusEvent::Gained(c), + ] + ); + } + + #[test] + fn set_focus_to_same_entity() { + let mut app = setup_app(); + let entity = app.world_mut().spawn_empty().id(); + + app.world_mut().resource_mut::().set(entity); + app.update(); + take_log(&mut app); + + // Setting focus to the already-focused entity still records a change. + app.world_mut().resource_mut::().set(entity); + app.update(); + + assert_eq!( + take_log(&mut app), + vec![FocusEvent::Lost(entity), FocusEvent::Gained(entity)] + ); + } + + #[test] + fn clear_when_already_none() { + let mut app = setup_app(); + take_log(&mut app); + + app.world_mut().resource_mut::().clear(); + app.update(); + + // No entity was focused, so no FocusLost should fire. + assert!(take_log(&mut app).is_empty()); + } + + #[test] + fn double_clear() { + let mut app = setup_app(); + let entity = app.world_mut().spawn_empty().id(); + + app.world_mut().resource_mut::().set(entity); + app.update(); + take_log(&mut app); + + // Clear twice — only one FocusLost should fire (the second clear has no previous focus). + let mut focus = app.world_mut().resource_mut::(); + focus.clear(); + focus.clear(); + app.update(); + + assert_eq!(take_log(&mut app), vec![FocusEvent::Lost(entity)]); + } + + #[test] + fn events_propagate_to_parent() { + let mut app = setup_app(); + take_log(&mut app); + + let child = app.world_mut().spawn_empty().id(); + let parent = app.world_mut().spawn_empty().add_child(child).id(); + + app.world_mut().resource_mut::().set(child); + app.update(); + + // The event fires on the child, then bubbles to the parent. + let log = take_log(&mut app); + assert!( + log.contains(&FocusEvent::Gained(child)), + "child should receive FocusGained" + ); + assert!( + log.contains(&FocusEvent::Gained(parent)), + "parent should receive FocusGained via propagation" + ); + + app.world_mut().resource_mut::().clear(); + app.update(); + + let log = take_log(&mut app); + assert!( + log.contains(&FocusEvent::Lost(child)), + "child should receive FocusLost" + ); + assert!( + log.contains(&FocusEvent::Lost(parent)), + "parent should receive FocusLost via propagation" + ); + } + + #[test] + fn focus_lost_on_despawned_entity() { + let mut app = setup_app(); + let entity = app.world_mut().spawn_empty().id(); + + app.world_mut().resource_mut::().set(entity); + app.update(); + take_log(&mut app); + + // Record a focus change away from the entity, then despawn it before processing. + app.world_mut().resource_mut::().clear(); + app.world_mut().entity_mut(entity).despawn(); + app.update(); + + // FocusLost should still fire (and not panic). + let log = take_log(&mut app); + assert_eq!(log, vec![FocusEvent::Lost(entity)]); + } + + #[test] + fn from_entity_fires_gained_event() { + let mut app = setup_app(); + take_log(&mut app); + + let entity = app.world_mut().spawn_empty().id(); + app.world_mut() + .insert_resource(InputFocus::from_entity(entity)); + app.update(); + + let log = take_log(&mut app); + assert!( + log.contains(&FocusEvent::Gained(entity)), + "from_entity should record a change that fires FocusGained" + ); + } +} diff --git a/crates/bevy_input_focus/src/lib.rs b/crates/bevy_input_focus/src/lib.rs index 772d36eda4e5c..09d4b3932e27f 100644 --- a/crates/bevy_input_focus/src/lib.rs +++ b/crates/bevy_input_focus/src/lib.rs @@ -11,7 +11,8 @@ //! This crate provides a system for managing input focus in Bevy applications, including: //! * [`InputFocus`], a resource for tracking which entity has input focus. //! * Methods for getting and setting input focus via [`InputFocus`] and [`IsFocusedHelper`]. -//! * A generic [`FocusedInput`] event for input events which bubble up from the focused entity. +//! * Events for when entities gain or lose focus: [`FocusGained`] and [`FocusLost`]. +//! * A generic [`FocusedInput`] event to send input events which bubble up from the focused entity. //! * Various navigation frameworks for moving input focus between entities based on user input, such as [`tab_navigation`] and [`directional_navigation`]. //! //! This crate does *not* provide any integration with UI widgets: this is the responsibility of the widget crate, @@ -26,14 +27,19 @@ pub mod directional_navigation; pub mod navigator; pub mod tab_navigation; -// This module is too small / specific to be exported by the crate, -// but it's nice to have it separate for code organization. +// These modules are too small / specific to be exported by the crate, +// but it's nice to have them separate for code organization. mod autofocus; pub use autofocus::*; +mod gained_and_lost; +pub use gained_and_lost::*; + +use alloc::vec; +use alloc::vec::Vec; #[cfg(any(feature = "keyboard", feature = "gamepad", feature = "mouse"))] use bevy_app::PreUpdate; -use bevy_app::{App, Plugin, PostStartup}; +use bevy_app::{App, Plugin, PostStartup, PostUpdate}; use bevy_ecs::{ entity::Entities, prelude::*, query::QueryData, system::SystemParam, traversal::Traversal, }; @@ -55,6 +61,8 @@ use bevy_reflect::{prelude::*, Reflect}; /// /// Changing the input focus is as easy as modifying this resource. /// +/// To detect when an entity gains or loses focus, listen for the [`FocusGained`] and [`FocusLost`] events. +/// /// # Examples /// /// From within a system: @@ -93,29 +101,56 @@ use bevy_reflect::{prelude::*, Reflect}; derive(Reflect), reflect(Debug, Default, Resource, Clone) )] -pub struct InputFocus(pub Option); +pub struct InputFocus { + /// The entity that currently has input focus, if any. + current_focus: Option, + /// The set of input focus changes that have been recorded since the last time [`FocusGained`] and [`FocusLost`] events were sent. + /// + /// These are stored in a first-in-first-out manner, so the most recent change is at the end of the vector. + recorded_changes: Vec>, + /// The entity that had input focus at the time of the last sent [`FocusGained`] or [`FocusLost`] event, if any. + /// + /// This is used to determine which events to send when processing recorded focus changes. + original_focus: Option, +} impl InputFocus { /// Create a new [`InputFocus`] resource with the given entity. /// /// This is mostly useful for tests. - pub const fn from_entity(entity: Entity) -> Self { - Self(Some(entity)) + /// + /// WARNING: this will clear any buffered focus changes, + /// so it may cause missed [`FocusGained`] and [`FocusLost`] events. + /// + /// Prefer the [`set`](InputFocus::set) method for normal use, which will preserve buffered changes. + pub fn from_entity(entity: Entity) -> Self { + Self { + current_focus: Some(entity), + recorded_changes: vec![Some(entity)], + original_focus: None, + } } /// Set the entity with input focus. - pub const fn set(&mut self, entity: Entity) { - self.0 = Some(entity); + /// + /// When spawning entities, you may want to use the [`AutoFocus`] component instead, + /// which will automatically set focus to the entity when it is spawned. + /// + /// This is particularly useful when working with bsn! scenes, where spawning may be delayed. + pub fn set(&mut self, entity: Entity) { + self.current_focus = Some(entity); + self.recorded_changes.push(Some(entity)); } /// Returns the entity with input focus, if any. pub const fn get(&self) -> Option { - self.0 + self.current_focus } /// Clears input focus. - pub const fn clear(&mut self) { - self.0 = None; + pub fn clear(&mut self) { + self.current_focus = None; + self.recorded_changes.push(None); } } @@ -163,7 +198,7 @@ pub struct FocusedInput { /// An event which is used to set input focus. Trigger this on an entity, and it will bubble /// until it finds a focusable entity, and then set focus to it. -#[derive(Clone, EntityEvent)] +#[derive(EntityEvent, Debug, Clone)] #[entity_event(propagate = WindowTraversal, auto_propagate)] pub struct AcquireFocus { /// The entity that has acquired focus. @@ -216,6 +251,26 @@ impl Traversal for WindowTraversal { } } +/// Plugin which sets up the core input focus system. +/// +/// This includes the[`InputFocus`] and [`InputFocusVisible`] resources, +/// [`set_initial_focus`] system to initialize the focus to the primary window, if any, at startup, +/// and [`process_recorded_focus_changes`] to send [`FocusGained`] and [`FocusLost`] events when the focused entity changes. +#[derive(Default)] +pub struct InputFocusPlugin; + +impl Plugin for InputFocusPlugin { + fn build(&self, app: &mut App) { + app.add_systems(PostStartup, set_initial_focus) + .init_resource::() + .init_resource::() + .add_systems( + PostUpdate, + process_recorded_focus_changes.in_set(InputFocusSystems::FocusChangeEvents), + ); + } +} + /// Plugin which sets up systems for dispatching bubbling keyboard and gamepad button events to the focused entity. /// /// To add bubbling to your own input events, add the [`dispatch_focused_input::`](dispatch_focused_input) system to your app, @@ -225,10 +280,6 @@ pub struct InputDispatchPlugin; impl Plugin for InputDispatchPlugin { fn build(&self, app: &mut App) { - app.add_systems(PostStartup, set_initial_focus) - .init_resource::() - .init_resource::(); - #[cfg(any(feature = "keyboard", feature = "gamepad", feature = "mouse"))] app.add_systems( PreUpdate, @@ -253,7 +304,13 @@ impl Plugin for InputDispatchPlugin { #[derive(SystemSet, Debug, PartialEq, Eq, Hash, Clone)] pub enum InputFocusSystems { /// System which dispatches bubbled input events to the focused entity, or to the primary window. + /// + /// Occurs in the [`PreUpdate`] schedule, after [`InputSystems`]. Dispatch, + /// System which processes recorded focus changes and sends the appropriate [`FocusGained`] and [`FocusLost`] events. + /// + /// Occurs in the [`PostUpdate`] schedule. + FocusChangeEvents, } /// If no entity is focused, sets the focus to the primary window, if any. @@ -261,8 +318,8 @@ pub fn set_initial_focus( mut input_focus: ResMut, window: Single>, ) { - if input_focus.0.is_none() { - input_focus.0 = Some(*window); + if input_focus.get().is_none() { + input_focus.set(*window); } } @@ -280,7 +337,7 @@ pub fn dispatch_focused_input( ) { if let Ok(window) = windows.single() { // If an element has keyboard focus, then dispatch the input event to that element. - if let Some(focused_entity) = focus.0 { + if let Some(focused_entity) = focus.get() { // Check if the focused entity is still alive if entities.contains(focused_entity) { for ev in input_reader.read() { @@ -292,7 +349,7 @@ pub fn dispatch_focused_input( } } else { // If the focused entity no longer exists, clear focus and dispatch to window - focus.0 = None; + focus.clear(); for ev in input_reader.read() { commands.trigger(FocusedInput { focused_entity: window, @@ -356,12 +413,12 @@ impl IsFocused for IsFocusedHelper<'_, '_> { fn is_focused(&self, entity: Entity) -> bool { self.input_focus .as_deref() - .and_then(|f| f.0) + .and_then(InputFocus::get) .is_some_and(|e| e == entity) } fn is_focus_within(&self, entity: Entity) -> bool { - let Some(focus) = self.input_focus.as_deref().and_then(|f| f.0) else { + let Some(focus) = self.input_focus.as_deref().and_then(InputFocus::get) else { return false; }; if focus == entity { @@ -382,12 +439,12 @@ impl IsFocused for IsFocusedHelper<'_, '_> { impl IsFocused for World { fn is_focused(&self, entity: Entity) -> bool { self.get_resource::() - .and_then(|f| f.0) + .and_then(InputFocus::get) .is_some_and(|f| f == entity) } fn is_focus_within(&self, entity: Entity) -> bool { - let Some(focus) = self.get_resource::().and_then(|f| f.0) else { + let Some(focus) = self.get_resource::().and_then(InputFocus::get) else { return false; }; let mut e = focus; @@ -484,17 +541,17 @@ mod tests { #[test] fn initial_focus_unset_if_no_primary_window() { let mut app = App::new(); - app.add_plugins((InputPlugin, InputDispatchPlugin)); + app.add_plugins((InputPlugin, InputFocusPlugin)); app.update(); - assert_eq!(app.world().resource::().0, None); + assert_eq!(app.world().resource::().get(), None); } #[test] fn initial_focus_set_to_primary_window() { let mut app = App::new(); - app.add_plugins((InputPlugin, InputDispatchPlugin)); + app.add_plugins((InputPlugin, InputFocusPlugin)); let entity_window = app .world_mut() @@ -502,13 +559,16 @@ mod tests { .id(); app.update(); - assert_eq!(app.world().resource::().0, Some(entity_window)); + assert_eq!( + app.world().resource::().get(), + Some(entity_window) + ); } #[test] fn initial_focus_not_overridden() { let mut app = App::new(); - app.add_plugins((InputPlugin, InputDispatchPlugin)); + app.add_plugins((InputPlugin, InputFocusPlugin)); app.world_mut().spawn((Window::default(), PrimaryWindow)); @@ -525,7 +585,7 @@ mod tests { .unwrap(); assert_eq!( - app.world().resource::().0, + app.world().resource::().get(), Some(autofocus_entity) ); } @@ -543,7 +603,7 @@ mod tests { let mut app = App::new(); - app.add_plugins((InputPlugin, InputDispatchPlugin)) + app.add_plugins((InputPlugin, InputFocusPlugin, InputDispatchPlugin)) .add_observer(gather_keyboard_events); app.world_mut().spawn((Window::default(), PrimaryWindow)); @@ -582,7 +642,7 @@ mod tests { assert_eq!(get_gathered(&app, entity_b), ""); assert_eq!(get_gathered(&app, child_of_b), ""); - app.world_mut().insert_resource(InputFocus(None)); + app.world_mut().insert_resource(InputFocus::default()); assert!(!app.world().is_focused(entity_a)); assert!(!app.world().is_focus_visible(entity_a)); @@ -660,7 +720,7 @@ mod tests { #[test] fn dispatch_clears_focus_when_focused_entity_despawned() { let mut app = App::new(); - app.add_plugins((InputPlugin, InputDispatchPlugin)); + app.add_plugins((InputPlugin, InputFocusPlugin, InputDispatchPlugin)); app.world_mut().spawn((Window::default(), PrimaryWindow)); app.update(); @@ -670,12 +730,12 @@ mod tests { .insert_resource(InputFocus::from_entity(entity)); app.world_mut().entity_mut(entity).despawn(); - assert_eq!(app.world().resource::().0, Some(entity)); + assert_eq!(app.world().resource::().get(), Some(entity)); // Send input event - this should clear focus instead of panicking app.world_mut().write_message(key_a_message()); app.update(); - assert_eq!(app.world().resource::().0, None); + assert_eq!(app.world().resource::().get(), None); } } diff --git a/crates/bevy_input_focus/src/tab_navigation.rs b/crates/bevy_input_focus/src/tab_navigation.rs index 4ee868efdda38..60602a2449b41 100644 --- a/crates/bevy_input_focus/src/tab_navigation.rs +++ b/crates/bevy_input_focus/src/tab_navigation.rs @@ -187,7 +187,7 @@ impl TabNavigation<'_, '_> { // Start by identifying which tab group we are in. Mainly what we want to know is if // we're in a modal group. - let tabgroup = focus.0.and_then(|focus_ent| { + let tabgroup = focus.get().and_then(|focus_ent| { self.parent_query .iter_ancestors(focus_ent) .find_map(|entity| { @@ -198,7 +198,7 @@ impl TabNavigation<'_, '_> { }) }); - self.navigate_internal(focus.0, action, tabgroup) + self.navigate_internal(focus.get(), action, tabgroup) } /// Initialize focus to a focusable child of a container, either the first or last @@ -358,14 +358,14 @@ pub(crate) fn acquire_focus( // Stop and focus it acquire_focus.propagate(false); // Don't mutate unless we need to, for change detection - if focus.0 != Some(acquire_focus.focused_entity) { - focus.0 = Some(acquire_focus.focused_entity); + if focus.get() != Some(acquire_focus.focused_entity) { + focus.set(acquire_focus.focused_entity); } } else if windows.contains(acquire_focus.focused_entity) { // Stop and clear focus acquire_focus.propagate(false); // Don't mutate unless we need to, for change detection - if focus.0.is_some() { + if focus.get().is_some() { focus.clear(); } } diff --git a/crates/bevy_internal/src/default_plugins.rs b/crates/bevy_internal/src/default_plugins.rs index dbb73c6d3b031..08f23df3c665a 100644 --- a/crates/bevy_internal/src/default_plugins.rs +++ b/crates/bevy_internal/src/default_plugins.rs @@ -13,6 +13,8 @@ plugin_group! { bevy_diagnostic:::DiagnosticsPlugin, bevy_input:::InputPlugin, #[cfg(feature = "bevy_input_focus")] + bevy_input_focus:::InputFocusPlugin, + #[cfg(feature = "bevy_input_focus")] bevy_input_focus:::InputDispatchPlugin, #[custom(cfg(not(feature = "bevy_window")))] bevy_app:::ScheduleRunnerPlugin, diff --git a/crates/bevy_ui/src/auto_directional_navigation.rs b/crates/bevy_ui/src/auto_directional_navigation.rs index ed7e71097b360..cc4710dc77c2a 100644 --- a/crates/bevy_ui/src/auto_directional_navigation.rs +++ b/crates/bevy_ui/src/auto_directional_navigation.rs @@ -150,7 +150,7 @@ pub struct AutoDirectionalNavigator<'w, 's> { impl<'w, 's> AutoDirectionalNavigator<'w, 's> { /// Returns the current input focus pub fn input_focus(&mut self) -> Option { - self.manual_directional_navigation.focus.0 + self.manual_directional_navigation.focus.get() } /// Tries to find the neighbor in a given direction from the given entity. Assumes the entity is valid. diff --git a/crates/bevy_ui/src/widget/text_editable.rs b/crates/bevy_ui/src/widget/text_editable.rs index fe7773c1786cc..22fb02909cf37 100644 --- a/crates/bevy_ui/src/widget/text_editable.rs +++ b/crates/bevy_ui/src/widget/text_editable.rs @@ -293,7 +293,7 @@ pub fn editable_text_system( .cursor_geometry(editable_text.cursor_width * font_size); if let Some(input_focus) = input_focus.as_ref() - && Some(entity) == input_focus.0 + && Some(entity) == input_focus.get() { if input_focus.is_changed() || editable_text.text_edited diff --git a/crates/bevy_ui_widgets/src/checkbox.rs b/crates/bevy_ui_widgets/src/checkbox.rs index e11232ebc9f36..dfe7da658fc2f 100644 --- a/crates/bevy_ui_widgets/src/checkbox.rs +++ b/crates/bevy_ui_widgets/src/checkbox.rs @@ -64,7 +64,7 @@ fn checkbox_on_pointer_click( // Clicking on a button makes it the focused input, // and hides the focus ring if it was visible. if let Some(mut focus) = focus { - focus.0 = Some(click.entity); + focus.set(click.entity); } if let Some(mut focus_visible) = focus_visible { focus_visible.0 = false; diff --git a/crates/bevy_ui_widgets/src/menu.rs b/crates/bevy_ui_widgets/src/menu.rs index 4a16a399937a8..2469333aa7e08 100644 --- a/crates/bevy_ui_widgets/src/menu.rs +++ b/crates/bevy_ui_widgets/src/menu.rs @@ -125,7 +125,7 @@ fn menu_acquire_focus( match tab_navigation.initialize(menu, NavAction::First) { Ok(next) => { commands.entity(menu).remove::(); - focus.0 = Some(next); + focus.set(next); } Err(e) => { warn!( @@ -154,7 +154,7 @@ fn menu_on_lose_focus( for menu in q_menus.iter() { // TODO: Change this logic when we support submenus. Don't want to send multiple close // events. Perhaps what we can do is add `MenuLostFocus` to the whole stack. - let contains_focus = match focus.0 { + let contains_focus = match focus.get() { Some(focus_ent) => { focus_ent == menu || q_parent.iter_ancestors(focus_ent).any(|ent| ent == menu) } @@ -228,37 +228,61 @@ fn menu_on_key_event( // Focus the adjacent item in the up direction KeyCode::ArrowUp if menu.layout == MenuLayout::Column => { ev.propagate(false); - focus.0 = tab_navigation.navigate(&focus, NavAction::Previous).ok(); + if let Ok(next) = tab_navigation.navigate(&focus, NavAction::Previous) { + focus.set(next); + } else { + focus.clear(); + } } // Focus the adjacent item in the down direction KeyCode::ArrowDown if menu.layout == MenuLayout::Column => { ev.propagate(false); - focus.0 = tab_navigation.navigate(&focus, NavAction::Next).ok(); + if let Ok(next) = tab_navigation.navigate(&focus, NavAction::Next) { + focus.set(next); + } else { + focus.clear(); + } } // Focus the adjacent item in the left direction KeyCode::ArrowLeft if menu.layout == MenuLayout::Row => { ev.propagate(false); - focus.0 = tab_navigation.navigate(&focus, NavAction::Previous).ok(); + if let Ok(next) = tab_navigation.navigate(&focus, NavAction::Previous) { + focus.set(next); + } else { + focus.clear(); + } } // Focus the adjacent item in the right direction KeyCode::ArrowRight if menu.layout == MenuLayout::Row => { ev.propagate(false); - focus.0 = tab_navigation.navigate(&focus, NavAction::Next).ok(); + if let Ok(next) = tab_navigation.navigate(&focus, NavAction::Next) { + focus.set(next); + } else { + focus.clear(); + } } // Focus the first item KeyCode::Home => { ev.propagate(false); - focus.0 = tab_navigation.navigate(&focus, NavAction::First).ok(); + if let Ok(next) = tab_navigation.navigate(&focus, NavAction::First) { + focus.set(next); + } else { + focus.clear(); + } } // Focus the last item KeyCode::End => { ev.propagate(false); - focus.0 = tab_navigation.navigate(&focus, NavAction::Last).ok(); + if let Ok(next) = tab_navigation.navigate(&focus, NavAction::Last) { + focus.set(next); + } else { + focus.clear(); + } } _ => (), diff --git a/crates/bevy_winit/src/accessibility.rs b/crates/bevy_winit/src/accessibility.rs index f75a6b4e981c2..57bfc15280a12 100644 --- a/crates/bevy_winit/src/accessibility.rs +++ b/crates/bevy_winit/src/accessibility.rs @@ -215,7 +215,7 @@ fn update_accessibility_nodes( if focus.is_changed() || !nodes.is_empty() { // Don't panic if the focused entity does not currently exist // It's probably waiting to be spawned - if let Some(focused_entity) = focus.0 + if let Some(focused_entity) = focus.get() && !node_entities.contains(focused_entity) { return; @@ -268,7 +268,7 @@ fn update_adapter( nodes: to_update, tree: None, tree_id: TreeId::ROOT, - focus: NodeId(focus.0.unwrap_or(primary_window_id).to_bits()), + focus: NodeId(focus.get().unwrap_or(primary_window_id).to_bits()), } } diff --git a/examples/ui/navigation/directional_navigation.rs b/examples/ui/navigation/directional_navigation.rs index 1412e6e160737..78c125dc5a1aa 100644 --- a/examples/ui/navigation/directional_navigation.rs +++ b/examples/ui/navigation/directional_navigation.rs @@ -370,7 +370,7 @@ fn update_focus_display( mut display_query: Query<&mut Text, With>, ) { if let Ok(mut text) = display_query.single_mut() { - if let Some(focused_entity) = input_focus.0 { + if let Some(focused_entity) = input_focus.get() { if let Ok(name) = button_query.get(focused_entity) { **text = format!("Focused: {}", name); } else { @@ -428,7 +428,7 @@ fn highlight_focused_element( mut query: Query<(Entity, &mut BorderColor)>, ) { for (entity, mut border_color) in query.iter_mut() { - if input_focus.0 == Some(entity) && input_focus_visible.0 { + if input_focus.get() == Some(entity) && input_focus_visible.0 { *border_color = BorderColor::all(FOCUSED_BORDER); } else { *border_color = BorderColor::DEFAULT; @@ -444,7 +444,7 @@ fn interact_with_focused_button( if action_state .pressed_actions .contains(&DirectionalNavigationAction::Select) - && let Some(focused_entity) = input_focus.0 + && let Some(focused_entity) = input_focus.get() { commands.trigger(Pointer::new( PointerId::Mouse, diff --git a/examples/ui/navigation/directional_navigation_overrides.rs b/examples/ui/navigation/directional_navigation_overrides.rs index ed5ac605dbc57..f0f39e86a037a 100644 --- a/examples/ui/navigation/directional_navigation_overrides.rs +++ b/examples/ui/navigation/directional_navigation_overrides.rs @@ -763,7 +763,7 @@ fn update_focus_display( mut display_query: Query<&mut Text, With>, ) { if let Ok(mut text) = display_query.single_mut() { - if let Some(focused_entity) = input_focus.0 { + if let Some(focused_entity) = input_focus.get() { if let Ok(name) = button_query.get(focused_entity) { **text = format!("Focused: {}", name); } else { @@ -821,7 +821,7 @@ fn highlight_focused_element( mut query: Query<(Entity, &mut BorderColor, &Page)>, ) { for (entity, mut border_color, page) in query.iter_mut() { - if input_focus.0 == Some(entity) && input_focus_visible.0 { + if input_focus.get() == Some(entity) && input_focus_visible.0 { *border_color = BorderColor::all(FOCUSED_BORDER_COLORS[page.0]); } else { *border_color = BorderColor::DEFAULT; @@ -837,7 +837,7 @@ fn interact_with_focused_button( if action_state .pressed_actions .contains(&DirectionalNavigationAction::Select) - && let Some(focused_entity) = input_focus.0 + && let Some(focused_entity) = input_focus.get() { commands.trigger(Pointer::new( PointerId::Mouse, diff --git a/examples/ui/widgets/feathers.rs b/examples/ui/widgets/feathers.rs index b2c800f75ed17..07a400415a647 100644 --- a/examples/ui/widgets/feathers.rs +++ b/examples/ui/widgets/feathers.rs @@ -464,7 +464,7 @@ fn update_colors( // Only update the hex input field when it's not focused, otherwise it interferes // with typing. let (input_ent, mut editable_text) = q_text_input.into_inner(); - if Some(input_ent) != focus.0 { + if Some(input_ent) != focus.get() { editable_text.queue_edit(TextEdit::SelectAll); editable_text.queue_edit(TextEdit::Insert(colors.rgb_color.to_hex().into())); } diff --git a/examples/ui/widgets/standard_widgets.rs b/examples/ui/widgets/standard_widgets.rs index 1696b23f224e8..a640f4afae6fa 100644 --- a/examples/ui/widgets/standard_widgets.rs +++ b/examples/ui/widgets/standard_widgets.rs @@ -815,7 +815,7 @@ fn on_menu_event( } } MenuAction::FocusRoot => { - focus.0 = Some(anchor); + focus.set(anchor); } } } diff --git a/examples/ui/widgets/tab_navigation.rs b/examples/ui/widgets/tab_navigation.rs index 29897630ac6fb..9563133c8eb7b 100644 --- a/examples/ui/widgets/tab_navigation.rs +++ b/examples/ui/widgets/tab_navigation.rs @@ -52,7 +52,7 @@ fn focus_system( ) { if focus.is_changed() { for button in query.iter_mut() { - if focus.0 == Some(button) { + if focus.get() == Some(button) { commands.entity(button).insert(Outline { color: Color::WHITE, width: px(2), @@ -81,7 +81,7 @@ fn setup(mut commands: Commands) { }) .observe( |mut event: On>, mut focus: ResMut| { - focus.0 = None; + focus.clear(); event.propagate(false); }, ) @@ -139,7 +139,7 @@ fn setup(mut commands: Commands) { .observe( |mut click: On>, mut focus: ResMut| { - focus.0 = Some(click.entity); + focus.set(click.entity); click.propagate(false); }, );