Fixing AutoFocus
not working if it's spawn before Startup (#19618)
# Objective - `AutoFocus` component does seem to work with the `set_initial_focus` that was running in `Startup`. - If an element is spawn during `PreStartup` or during `OnEnter(SomeState)` that happens before `Startup`, the focus is overridden by `set_initial_focus` which sets the focus to the primary window. ## Solution - `set_initial_focus` only sets the focus to the `PrimaryWindow` if no other focus is set. - *Note*: `cargo test --package bevy_input_focus` was not working, so some changes are related to that. ## Testing - `cargo test --package bevy_input_focus`: OK - `cargo run --package ci`: OK
This commit is contained in:
parent
ce3db843bc
commit
4da420cc4c
@ -73,9 +73,6 @@ bevy_reflect = { path = "../bevy_reflect", version = "0.16.0-dev", features = [
|
|||||||
thiserror = { version = "2", default-features = false }
|
thiserror = { version = "2", default-features = false }
|
||||||
log = { version = "0.4", default-features = false }
|
log = { version = "0.4", default-features = false }
|
||||||
|
|
||||||
[dev-dependencies]
|
|
||||||
smol_str = "0.2"
|
|
||||||
|
|
||||||
[lints]
|
[lints]
|
||||||
workspace = true
|
workspace = true
|
||||||
|
|
||||||
|
@ -30,7 +30,7 @@ pub mod tab_navigation;
|
|||||||
mod autofocus;
|
mod autofocus;
|
||||||
pub use autofocus::*;
|
pub use autofocus::*;
|
||||||
|
|
||||||
use bevy_app::{App, Plugin, PreUpdate, Startup};
|
use bevy_app::{App, Plugin, PostStartup, PreUpdate};
|
||||||
use bevy_ecs::{prelude::*, query::QueryData, system::SystemParam, traversal::Traversal};
|
use bevy_ecs::{prelude::*, query::QueryData, system::SystemParam, traversal::Traversal};
|
||||||
use bevy_input::{gamepad::GamepadButtonChangedEvent, keyboard::KeyboardInput, mouse::MouseWheel};
|
use bevy_input::{gamepad::GamepadButtonChangedEvent, keyboard::KeyboardInput, mouse::MouseWheel};
|
||||||
use bevy_window::{PrimaryWindow, Window};
|
use bevy_window::{PrimaryWindow, Window};
|
||||||
@ -185,7 +185,7 @@ pub struct InputDispatchPlugin;
|
|||||||
|
|
||||||
impl Plugin for InputDispatchPlugin {
|
impl Plugin for InputDispatchPlugin {
|
||||||
fn build(&self, app: &mut App) {
|
fn build(&self, app: &mut App) {
|
||||||
app.add_systems(Startup, set_initial_focus)
|
app.add_systems(PostStartup, set_initial_focus)
|
||||||
.init_resource::<InputFocus>()
|
.init_resource::<InputFocus>()
|
||||||
.init_resource::<InputFocusVisible>()
|
.init_resource::<InputFocusVisible>()
|
||||||
.add_systems(
|
.add_systems(
|
||||||
@ -218,12 +218,14 @@ pub enum InputFocusSystems {
|
|||||||
#[deprecated(since = "0.17.0", note = "Renamed to `InputFocusSystems`.")]
|
#[deprecated(since = "0.17.0", note = "Renamed to `InputFocusSystems`.")]
|
||||||
pub type InputFocusSet = InputFocusSystems;
|
pub type InputFocusSet = InputFocusSystems;
|
||||||
|
|
||||||
/// Sets the initial focus to the primary window, if any.
|
/// If no entity is focused, sets the focus to the primary window, if any.
|
||||||
pub fn set_initial_focus(
|
pub fn set_initial_focus(
|
||||||
mut input_focus: ResMut<InputFocus>,
|
mut input_focus: ResMut<InputFocus>,
|
||||||
window: Single<Entity, With<PrimaryWindow>>,
|
window: Single<Entity, With<PrimaryWindow>>,
|
||||||
) {
|
) {
|
||||||
input_focus.0 = Some(*window);
|
if input_focus.0.is_none() {
|
||||||
|
input_focus.0 = Some(*window);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// System which dispatches bubbled input events to the focused entity, or to the primary window
|
/// System which dispatches bubbled input events to the focused entity, or to the primary window
|
||||||
@ -368,24 +370,12 @@ mod tests {
|
|||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
use alloc::string::String;
|
use alloc::string::String;
|
||||||
use bevy_ecs::{
|
use bevy_app::Startup;
|
||||||
lifecycle::HookContext, observer::On, system::RunSystemOnce, world::DeferredWorld,
|
use bevy_ecs::{observer::On, system::RunSystemOnce, world::DeferredWorld};
|
||||||
};
|
|
||||||
use bevy_input::{
|
use bevy_input::{
|
||||||
keyboard::{Key, KeyCode},
|
keyboard::{Key, KeyCode},
|
||||||
ButtonState, InputPlugin,
|
ButtonState, InputPlugin,
|
||||||
};
|
};
|
||||||
use bevy_window::WindowResolution;
|
|
||||||
use smol_str::SmolStr;
|
|
||||||
|
|
||||||
#[derive(Component)]
|
|
||||||
#[component(on_add = set_focus_on_add)]
|
|
||||||
struct SetFocusOnAdd;
|
|
||||||
|
|
||||||
fn set_focus_on_add(mut world: DeferredWorld, HookContext { entity, .. }: HookContext) {
|
|
||||||
let mut input_focus = world.resource_mut::<InputFocus>();
|
|
||||||
input_focus.set(entity);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Component, Default)]
|
#[derive(Component, Default)]
|
||||||
struct GatherKeyboardEvents(String);
|
struct GatherKeyboardEvents(String);
|
||||||
@ -401,14 +391,16 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const KEY_A_EVENT: KeyboardInput = KeyboardInput {
|
fn key_a_event() -> KeyboardInput {
|
||||||
key_code: KeyCode::KeyA,
|
KeyboardInput {
|
||||||
logical_key: Key::Character(SmolStr::new_static("A")),
|
key_code: KeyCode::KeyA,
|
||||||
state: ButtonState::Pressed,
|
logical_key: Key::Character("A".into()),
|
||||||
text: Some(SmolStr::new_static("A")),
|
state: ButtonState::Pressed,
|
||||||
repeat: false,
|
text: Some("A".into()),
|
||||||
window: Entity::PLACEHOLDER,
|
repeat: false,
|
||||||
};
|
window: Entity::PLACEHOLDER,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_no_panics_if_resource_missing() {
|
fn test_no_panics_if_resource_missing() {
|
||||||
@ -438,6 +430,55 @@ mod tests {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn initial_focus_unset_if_no_primary_window() {
|
||||||
|
let mut app = App::new();
|
||||||
|
app.add_plugins((InputPlugin, InputDispatchPlugin));
|
||||||
|
|
||||||
|
app.update();
|
||||||
|
|
||||||
|
assert_eq!(app.world().resource::<InputFocus>().0, None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn initial_focus_set_to_primary_window() {
|
||||||
|
let mut app = App::new();
|
||||||
|
app.add_plugins((InputPlugin, InputDispatchPlugin));
|
||||||
|
|
||||||
|
let entity_window = app
|
||||||
|
.world_mut()
|
||||||
|
.spawn((Window::default(), PrimaryWindow))
|
||||||
|
.id();
|
||||||
|
app.update();
|
||||||
|
|
||||||
|
assert_eq!(app.world().resource::<InputFocus>().0, Some(entity_window));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn initial_focus_not_overridden() {
|
||||||
|
let mut app = App::new();
|
||||||
|
app.add_plugins((InputPlugin, InputDispatchPlugin));
|
||||||
|
|
||||||
|
app.world_mut().spawn((Window::default(), PrimaryWindow));
|
||||||
|
|
||||||
|
app.add_systems(Startup, |mut commands: Commands| {
|
||||||
|
commands.spawn(AutoFocus);
|
||||||
|
});
|
||||||
|
|
||||||
|
app.update();
|
||||||
|
|
||||||
|
let autofocus_entity = app
|
||||||
|
.world_mut()
|
||||||
|
.query_filtered::<Entity, With<AutoFocus>>()
|
||||||
|
.single(app.world())
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
app.world().resource::<InputFocus>().0,
|
||||||
|
Some(autofocus_entity)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_keyboard_events() {
|
fn test_keyboard_events() {
|
||||||
fn get_gathered(app: &App, entity: Entity) -> &str {
|
fn get_gathered(app: &App, entity: Entity) -> &str {
|
||||||
@ -454,18 +495,14 @@ mod tests {
|
|||||||
app.add_plugins((InputPlugin, InputDispatchPlugin))
|
app.add_plugins((InputPlugin, InputDispatchPlugin))
|
||||||
.add_observer(gather_keyboard_events);
|
.add_observer(gather_keyboard_events);
|
||||||
|
|
||||||
let window = Window {
|
app.world_mut().spawn((Window::default(), PrimaryWindow));
|
||||||
resolution: WindowResolution::new(800., 600.),
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
app.world_mut().spawn((window, PrimaryWindow));
|
|
||||||
|
|
||||||
// Run the world for a single frame to set up the initial focus
|
// Run the world for a single frame to set up the initial focus
|
||||||
app.update();
|
app.update();
|
||||||
|
|
||||||
let entity_a = app
|
let entity_a = app
|
||||||
.world_mut()
|
.world_mut()
|
||||||
.spawn((GatherKeyboardEvents::default(), SetFocusOnAdd))
|
.spawn((GatherKeyboardEvents::default(), AutoFocus))
|
||||||
.id();
|
.id();
|
||||||
|
|
||||||
let child_of_b = app
|
let child_of_b = app
|
||||||
@ -487,7 +524,7 @@ mod tests {
|
|||||||
assert!(!app.world().is_focus_visible(child_of_b));
|
assert!(!app.world().is_focus_visible(child_of_b));
|
||||||
|
|
||||||
// entity_a should receive this event
|
// entity_a should receive this event
|
||||||
app.world_mut().send_event(KEY_A_EVENT);
|
app.world_mut().send_event(key_a_event());
|
||||||
app.update();
|
app.update();
|
||||||
|
|
||||||
assert_eq!(get_gathered(&app, entity_a), "A");
|
assert_eq!(get_gathered(&app, entity_a), "A");
|
||||||
@ -500,7 +537,7 @@ mod tests {
|
|||||||
assert!(!app.world().is_focus_visible(entity_a));
|
assert!(!app.world().is_focus_visible(entity_a));
|
||||||
|
|
||||||
// This event should be lost
|
// This event should be lost
|
||||||
app.world_mut().send_event(KEY_A_EVENT);
|
app.world_mut().send_event(key_a_event());
|
||||||
app.update();
|
app.update();
|
||||||
|
|
||||||
assert_eq!(get_gathered(&app, entity_a), "A");
|
assert_eq!(get_gathered(&app, entity_a), "A");
|
||||||
@ -520,7 +557,8 @@ mod tests {
|
|||||||
assert!(app.world().is_focus_within(entity_b));
|
assert!(app.world().is_focus_within(entity_b));
|
||||||
|
|
||||||
// These events should be received by entity_b and child_of_b
|
// These events should be received by entity_b and child_of_b
|
||||||
app.world_mut().send_event_batch([KEY_A_EVENT; 4]);
|
app.world_mut()
|
||||||
|
.send_event_batch(core::iter::repeat_n(key_a_event(), 4));
|
||||||
app.update();
|
app.update();
|
||||||
|
|
||||||
assert_eq!(get_gathered(&app, entity_a), "A");
|
assert_eq!(get_gathered(&app, entity_a), "A");
|
||||||
|
Loading…
Reference in New Issue
Block a user