
# Objective Now that #13432 has been merged, it's important we update our reflected types to properly opt into this feature. If we do not, then this could cause issues for users downstream who want to make use of reflection-based cloning. ## Solution This PR is broken into 4 commits: 1. Add `#[reflect(Clone)]` on all types marked `#[reflect(opaque)]` that are also `Clone`. This is mandatory as these types would otherwise cause the cloning operation to fail for any type that contains it at any depth. 2. Update the reflection example to suggest adding `#[reflect(Clone)]` on opaque types. 3. Add `#[reflect(clone)]` attributes on all fields marked `#[reflect(ignore)]` that are also `Clone`. This prevents the ignored field from causing the cloning operation to fail. Note that some of the types that contain these fields are also `Clone`, and thus can be marked `#[reflect(Clone)]`. This makes the `#[reflect(clone)]` attribute redundant. However, I think it's safer to keep it marked in the case that the `Clone` impl/derive is ever removed. I'm open to removing them, though, if people disagree. 4. Finally, I added `#[reflect(Clone)]` on all types that are also `Clone`. While not strictly necessary, it enables us to reduce the generated output since we can just call `Clone::clone` directly instead of calling `PartialReflect::reflect_clone` on each variant/field. It also means we benefit from any optimizations or customizations made in the `Clone` impl, including directly dereferencing `Copy` values and increasing reference counters. Along with that change I also took the liberty of adding any missing registrations that I saw could be applied to the type as well, such as `Default`, `PartialEq`, and `Hash`. There were hundreds of these to edit, though, so it's possible I missed quite a few. That last commit is **_massive_**. There were nearly 700 types to update. So it's recommended to review the first three before moving onto that last one. Additionally, I can break the last commit off into its own PR or into smaller PRs, but I figured this would be the easiest way of doing it (and in a timely manner since I unfortunately don't have as much time as I used to for code contributions). ## Testing You can test locally with a `cargo check`: ``` cargo check --workspace --all-features ```
567 lines
19 KiB
Rust
567 lines
19 KiB
Rust
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
|
|
#![forbid(unsafe_code)]
|
|
#![doc(
|
|
html_logo_url = "https://bevyengine.org/assets/icon.png",
|
|
html_favicon_url = "https://bevyengine.org/assets/icon.png"
|
|
)]
|
|
#![no_std]
|
|
|
|
//! A UI-centric focus system for Bevy.
|
|
//!
|
|
//! 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.
|
|
//! * 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,
|
|
//! which should depend on [`bevy_input_focus`](crate).
|
|
|
|
#[cfg(feature = "std")]
|
|
extern crate std;
|
|
|
|
extern crate alloc;
|
|
|
|
pub mod directional_navigation;
|
|
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.
|
|
mod autofocus;
|
|
pub use autofocus::*;
|
|
|
|
use bevy_app::{App, Plugin, PreUpdate, Startup};
|
|
use bevy_ecs::{prelude::*, query::QueryData, system::SystemParam, traversal::Traversal};
|
|
use bevy_input::{gamepad::GamepadButtonChangedEvent, keyboard::KeyboardInput, mouse::MouseWheel};
|
|
use bevy_window::{PrimaryWindow, Window};
|
|
use core::fmt::Debug;
|
|
|
|
#[cfg(feature = "bevy_reflect")]
|
|
use bevy_reflect::{prelude::*, Reflect};
|
|
|
|
/// Resource representing which entity has input focus, if any. Input events (other than pointer-like inputs) will be
|
|
/// dispatched to the current focus entity, or to the primary window if no entity has focus.
|
|
///
|
|
/// Changing the input focus is as easy as modifying this resource.
|
|
///
|
|
/// # Examples
|
|
///
|
|
/// From within a system:
|
|
///
|
|
/// ```rust
|
|
/// use bevy_ecs::prelude::*;
|
|
/// use bevy_input_focus::InputFocus;
|
|
///
|
|
/// fn clear_focus(mut input_focus: ResMut<InputFocus>) {
|
|
/// input_focus.clear();
|
|
/// }
|
|
/// ```
|
|
///
|
|
/// With exclusive (or deferred) world access:
|
|
///
|
|
/// ```rust
|
|
/// use bevy_ecs::prelude::*;
|
|
/// use bevy_input_focus::InputFocus;
|
|
///
|
|
/// fn set_focus_from_world(world: &mut World) {
|
|
/// let entity = world.spawn_empty().id();
|
|
///
|
|
/// // Fetch the resource from the world
|
|
/// let mut input_focus = world.resource_mut::<InputFocus>();
|
|
/// // Then mutate it!
|
|
/// input_focus.set(entity);
|
|
///
|
|
/// // Or you can just insert a fresh copy of the resource
|
|
/// // which will overwrite the existing one.
|
|
/// world.insert_resource(InputFocus::from_entity(entity));
|
|
/// }
|
|
/// ```
|
|
#[derive(Clone, Debug, Default, Resource)]
|
|
#[cfg_attr(
|
|
feature = "bevy_reflect",
|
|
derive(Reflect),
|
|
reflect(Debug, Default, Resource, Clone)
|
|
)]
|
|
pub struct InputFocus(pub Option<Entity>);
|
|
|
|
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))
|
|
}
|
|
|
|
/// Set the entity with input focus.
|
|
pub const fn set(&mut self, entity: Entity) {
|
|
self.0 = Some(entity);
|
|
}
|
|
|
|
/// Returns the entity with input focus, if any.
|
|
pub const fn get(&self) -> Option<Entity> {
|
|
self.0
|
|
}
|
|
|
|
/// Clears input focus.
|
|
pub const fn clear(&mut self) {
|
|
self.0 = None;
|
|
}
|
|
}
|
|
|
|
/// Resource representing whether the input focus indicator should be visible on UI elements.
|
|
///
|
|
/// Note that this resource is not used by [`bevy_input_focus`](crate) itself, but is provided for
|
|
/// convenience to UI widgets or frameworks that want to display a focus indicator.
|
|
/// [`InputFocus`] may still be `Some` even if the focus indicator is not visible.
|
|
///
|
|
/// The value of this resource should be set by your focus navigation solution.
|
|
/// For a desktop/web style of user interface this would be set to true when the user presses the tab key,
|
|
/// and set to false when the user clicks on a different element.
|
|
/// By contrast, a console-style UI intended to be navigated with a gamepad may always have the focus indicator visible.
|
|
///
|
|
/// To easily access information about whether focus indicators should be shown for a given entity, use the [`IsFocused`] trait.
|
|
///
|
|
/// By default, this resource is set to `false`.
|
|
#[derive(Clone, Debug, Resource, Default)]
|
|
#[cfg_attr(
|
|
feature = "bevy_reflect",
|
|
derive(Reflect),
|
|
reflect(Debug, Resource, Clone)
|
|
)]
|
|
pub struct InputFocusVisible(pub bool);
|
|
|
|
/// A bubble-able user input event that starts at the currently focused entity.
|
|
///
|
|
/// This event is normally dispatched to the current input focus entity, if any.
|
|
/// If no entity has input focus, then the event is dispatched to the main window.
|
|
///
|
|
/// To set up your own bubbling input event, add the [`dispatch_focused_input::<MyEvent>`](dispatch_focused_input) system to your app,
|
|
/// in the [`InputFocusSet::Dispatch`] system set during [`PreUpdate`].
|
|
#[derive(Clone, Debug, Component)]
|
|
#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Component, Clone))]
|
|
pub struct FocusedInput<E: Event + Clone> {
|
|
/// The underlying input event.
|
|
pub input: E,
|
|
/// The primary window entity.
|
|
window: Entity,
|
|
}
|
|
|
|
impl<E: Event + Clone> Event for FocusedInput<E> {
|
|
type Traversal = WindowTraversal;
|
|
|
|
const AUTO_PROPAGATE: bool = true;
|
|
}
|
|
|
|
#[derive(QueryData)]
|
|
/// These are for accessing components defined on the targeted entity
|
|
pub struct WindowTraversal {
|
|
child_of: Option<&'static ChildOf>,
|
|
window: Option<&'static Window>,
|
|
}
|
|
|
|
impl<E: Event + Clone> Traversal<FocusedInput<E>> for WindowTraversal {
|
|
fn traverse(item: Self::Item<'_>, event: &FocusedInput<E>) -> Option<Entity> {
|
|
let WindowTraversalItem { child_of, window } = item;
|
|
|
|
// Send event to parent, if it has one.
|
|
if let Some(child_of) = child_of {
|
|
return Some(child_of.parent);
|
|
};
|
|
|
|
// Otherwise, send it to the window entity (unless this is a window entity).
|
|
if window.is_none() {
|
|
return Some(event.window);
|
|
}
|
|
|
|
None
|
|
}
|
|
}
|
|
|
|
/// 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::<MyEvent>`](dispatch_focused_input) system to your app,
|
|
/// as described in the docs for [`FocusedInput`].
|
|
pub struct InputDispatchPlugin;
|
|
|
|
impl Plugin for InputDispatchPlugin {
|
|
fn build(&self, app: &mut App) {
|
|
app.add_systems(Startup, set_initial_focus)
|
|
.init_resource::<InputFocus>()
|
|
.init_resource::<InputFocusVisible>()
|
|
.add_systems(
|
|
PreUpdate,
|
|
(
|
|
dispatch_focused_input::<KeyboardInput>,
|
|
dispatch_focused_input::<GamepadButtonChangedEvent>,
|
|
dispatch_focused_input::<MouseWheel>,
|
|
)
|
|
.in_set(InputFocusSet::Dispatch),
|
|
);
|
|
|
|
#[cfg(feature = "bevy_reflect")]
|
|
app.register_type::<AutoFocus>()
|
|
.register_type::<InputFocus>()
|
|
.register_type::<InputFocusVisible>();
|
|
}
|
|
}
|
|
|
|
/// System sets for [`bevy_input_focus`](crate).
|
|
///
|
|
/// These systems run in the [`PreUpdate`] schedule.
|
|
#[derive(SystemSet, Debug, PartialEq, Eq, Hash, Clone)]
|
|
pub enum InputFocusSet {
|
|
/// System which dispatches bubbled input events to the focused entity, or to the primary window.
|
|
Dispatch,
|
|
}
|
|
|
|
/// Sets the initial focus to the primary window, if any.
|
|
pub fn set_initial_focus(
|
|
mut input_focus: ResMut<InputFocus>,
|
|
window: Single<Entity, With<PrimaryWindow>>,
|
|
) {
|
|
input_focus.0 = Some(*window);
|
|
}
|
|
|
|
/// System which dispatches bubbled input events to the focused entity, or to the primary window
|
|
/// if no entity has focus.
|
|
pub fn dispatch_focused_input<E: Event + Clone>(
|
|
mut key_events: EventReader<E>,
|
|
focus: Res<InputFocus>,
|
|
windows: Query<Entity, With<PrimaryWindow>>,
|
|
mut commands: Commands,
|
|
) {
|
|
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 {
|
|
for ev in key_events.read() {
|
|
commands.trigger_targets(
|
|
FocusedInput {
|
|
input: ev.clone(),
|
|
window,
|
|
},
|
|
focused_entity,
|
|
);
|
|
}
|
|
} else {
|
|
// If no element has input focus, then dispatch the input event to the primary window.
|
|
// There should be only one primary window.
|
|
for ev in key_events.read() {
|
|
commands.trigger_targets(
|
|
FocusedInput {
|
|
input: ev.clone(),
|
|
window,
|
|
},
|
|
window,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Trait which defines methods to check if an entity currently has focus.
|
|
///
|
|
/// This is implemented for [`World`] and [`IsFocusedHelper`].
|
|
/// [`DeferredWorld`](bevy_ecs::world::DeferredWorld) indirectly implements it through [`Deref`].
|
|
///
|
|
/// For use within systems, use [`IsFocusedHelper`].
|
|
///
|
|
/// Modify the [`InputFocus`] resource to change the focused entity.
|
|
///
|
|
/// [`Deref`]: std::ops::Deref
|
|
pub trait IsFocused {
|
|
/// Returns true if the given entity has input focus.
|
|
fn is_focused(&self, entity: Entity) -> bool;
|
|
|
|
/// Returns true if the given entity or any of its descendants has input focus.
|
|
///
|
|
/// Note that for unusual layouts, the focus may not be within the entity's visual bounds.
|
|
fn is_focus_within(&self, entity: Entity) -> bool;
|
|
|
|
/// Returns true if the given entity has input focus and the focus indicator should be visible.
|
|
fn is_focus_visible(&self, entity: Entity) -> bool;
|
|
|
|
/// Returns true if the given entity, or any descendant, has input focus and the focus
|
|
/// indicator should be visible.
|
|
fn is_focus_within_visible(&self, entity: Entity) -> bool;
|
|
}
|
|
|
|
/// A system param that helps get information about the current focused entity.
|
|
///
|
|
/// When working with the entire [`World`], consider using the [`IsFocused`] instead.
|
|
#[derive(SystemParam)]
|
|
pub struct IsFocusedHelper<'w, 's> {
|
|
parent_query: Query<'w, 's, &'static ChildOf>,
|
|
input_focus: Option<Res<'w, InputFocus>>,
|
|
input_focus_visible: Option<Res<'w, InputFocusVisible>>,
|
|
}
|
|
|
|
impl IsFocused for IsFocusedHelper<'_, '_> {
|
|
fn is_focused(&self, entity: Entity) -> bool {
|
|
self.input_focus
|
|
.as_deref()
|
|
.and_then(|f| f.0)
|
|
.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 {
|
|
return false;
|
|
};
|
|
if focus == entity {
|
|
return true;
|
|
}
|
|
self.parent_query.iter_ancestors(focus).any(|e| e == entity)
|
|
}
|
|
|
|
fn is_focus_visible(&self, entity: Entity) -> bool {
|
|
self.input_focus_visible.as_deref().is_some_and(|vis| vis.0) && self.is_focused(entity)
|
|
}
|
|
|
|
fn is_focus_within_visible(&self, entity: Entity) -> bool {
|
|
self.input_focus_visible.as_deref().is_some_and(|vis| vis.0) && self.is_focus_within(entity)
|
|
}
|
|
}
|
|
|
|
impl IsFocused for World {
|
|
fn is_focused(&self, entity: Entity) -> bool {
|
|
self.get_resource::<InputFocus>()
|
|
.and_then(|f| f.0)
|
|
.is_some_and(|f| f == entity)
|
|
}
|
|
|
|
fn is_focus_within(&self, entity: Entity) -> bool {
|
|
let Some(focus) = self.get_resource::<InputFocus>().and_then(|f| f.0) else {
|
|
return false;
|
|
};
|
|
let mut e = focus;
|
|
loop {
|
|
if e == entity {
|
|
return true;
|
|
}
|
|
if let Some(parent) = self.entity(e).get::<ChildOf>().map(|c| c.parent) {
|
|
e = parent;
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
fn is_focus_visible(&self, entity: Entity) -> bool {
|
|
self.get_resource::<InputFocusVisible>()
|
|
.is_some_and(|vis| vis.0)
|
|
&& self.is_focused(entity)
|
|
}
|
|
|
|
fn is_focus_within_visible(&self, entity: Entity) -> bool {
|
|
self.get_resource::<InputFocusVisible>()
|
|
.is_some_and(|vis| vis.0)
|
|
&& self.is_focus_within(entity)
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
use alloc::string::String;
|
|
use bevy_ecs::{
|
|
component::HookContext, observer::Trigger, system::RunSystemOnce, world::DeferredWorld,
|
|
};
|
|
use bevy_input::{
|
|
keyboard::{Key, KeyCode},
|
|
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)]
|
|
struct GatherKeyboardEvents(String);
|
|
|
|
fn gather_keyboard_events(
|
|
trigger: Trigger<FocusedInput<KeyboardInput>>,
|
|
mut query: Query<&mut GatherKeyboardEvents>,
|
|
) {
|
|
if let Ok(mut gather) = query.get_mut(trigger.target()) {
|
|
if let Key::Character(c) = &trigger.input.logical_key {
|
|
gather.0.push_str(c.as_str());
|
|
}
|
|
}
|
|
}
|
|
|
|
const KEY_A_EVENT: KeyboardInput = KeyboardInput {
|
|
key_code: KeyCode::KeyA,
|
|
logical_key: Key::Character(SmolStr::new_static("A")),
|
|
state: ButtonState::Pressed,
|
|
text: Some(SmolStr::new_static("A")),
|
|
repeat: false,
|
|
window: Entity::PLACEHOLDER,
|
|
};
|
|
|
|
#[test]
|
|
fn test_no_panics_if_resource_missing() {
|
|
let mut app = App::new();
|
|
// Note that we do not insert InputFocus here!
|
|
|
|
let entity = app.world_mut().spawn_empty().id();
|
|
|
|
assert!(!app.world().is_focused(entity));
|
|
|
|
app.world_mut()
|
|
.run_system_once(move |helper: IsFocusedHelper| {
|
|
assert!(!helper.is_focused(entity));
|
|
assert!(!helper.is_focus_within(entity));
|
|
assert!(!helper.is_focus_visible(entity));
|
|
assert!(!helper.is_focus_within_visible(entity));
|
|
})
|
|
.unwrap();
|
|
|
|
app.world_mut()
|
|
.run_system_once(move |world: DeferredWorld| {
|
|
assert!(!world.is_focused(entity));
|
|
assert!(!world.is_focus_within(entity));
|
|
assert!(!world.is_focus_visible(entity));
|
|
assert!(!world.is_focus_within_visible(entity));
|
|
})
|
|
.unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn test_keyboard_events() {
|
|
fn get_gathered(app: &App, entity: Entity) -> &str {
|
|
app.world()
|
|
.entity(entity)
|
|
.get::<GatherKeyboardEvents>()
|
|
.unwrap()
|
|
.0
|
|
.as_str()
|
|
}
|
|
|
|
let mut app = App::new();
|
|
|
|
app.add_plugins((InputPlugin, InputDispatchPlugin))
|
|
.add_observer(gather_keyboard_events);
|
|
|
|
let window = Window {
|
|
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
|
|
app.update();
|
|
|
|
let entity_a = app
|
|
.world_mut()
|
|
.spawn((GatherKeyboardEvents::default(), SetFocusOnAdd))
|
|
.id();
|
|
|
|
let child_of_b = app
|
|
.world_mut()
|
|
.spawn((GatherKeyboardEvents::default(),))
|
|
.id();
|
|
|
|
let entity_b = app
|
|
.world_mut()
|
|
.spawn((GatherKeyboardEvents::default(),))
|
|
.add_child(child_of_b)
|
|
.id();
|
|
|
|
assert!(app.world().is_focused(entity_a));
|
|
assert!(!app.world().is_focused(entity_b));
|
|
assert!(!app.world().is_focused(child_of_b));
|
|
assert!(!app.world().is_focus_visible(entity_a));
|
|
assert!(!app.world().is_focus_visible(entity_b));
|
|
assert!(!app.world().is_focus_visible(child_of_b));
|
|
|
|
// entity_a should receive this event
|
|
app.world_mut().send_event(KEY_A_EVENT);
|
|
app.update();
|
|
|
|
assert_eq!(get_gathered(&app, entity_a), "A");
|
|
assert_eq!(get_gathered(&app, entity_b), "");
|
|
assert_eq!(get_gathered(&app, child_of_b), "");
|
|
|
|
app.world_mut().insert_resource(InputFocus(None));
|
|
|
|
assert!(!app.world().is_focused(entity_a));
|
|
assert!(!app.world().is_focus_visible(entity_a));
|
|
|
|
// This event should be lost
|
|
app.world_mut().send_event(KEY_A_EVENT);
|
|
app.update();
|
|
|
|
assert_eq!(get_gathered(&app, entity_a), "A");
|
|
assert_eq!(get_gathered(&app, entity_b), "");
|
|
assert_eq!(get_gathered(&app, child_of_b), "");
|
|
|
|
app.world_mut()
|
|
.insert_resource(InputFocus::from_entity(entity_b));
|
|
assert!(app.world().is_focused(entity_b));
|
|
assert!(!app.world().is_focused(child_of_b));
|
|
|
|
app.world_mut()
|
|
.run_system_once(move |mut input_focus: ResMut<InputFocus>| {
|
|
input_focus.set(child_of_b);
|
|
})
|
|
.unwrap();
|
|
assert!(app.world().is_focus_within(entity_b));
|
|
|
|
// These events should be received by entity_b and child_of_b
|
|
app.world_mut().send_event_batch([KEY_A_EVENT; 4]);
|
|
app.update();
|
|
|
|
assert_eq!(get_gathered(&app, entity_a), "A");
|
|
assert_eq!(get_gathered(&app, entity_b), "AAAA");
|
|
assert_eq!(get_gathered(&app, child_of_b), "AAAA");
|
|
|
|
app.world_mut().resource_mut::<InputFocusVisible>().0 = true;
|
|
|
|
app.world_mut()
|
|
.run_system_once(move |helper: IsFocusedHelper| {
|
|
assert!(!helper.is_focused(entity_a));
|
|
assert!(!helper.is_focus_within(entity_a));
|
|
assert!(!helper.is_focus_visible(entity_a));
|
|
assert!(!helper.is_focus_within_visible(entity_a));
|
|
|
|
assert!(!helper.is_focused(entity_b));
|
|
assert!(helper.is_focus_within(entity_b));
|
|
assert!(!helper.is_focus_visible(entity_b));
|
|
assert!(helper.is_focus_within_visible(entity_b));
|
|
|
|
assert!(helper.is_focused(child_of_b));
|
|
assert!(helper.is_focus_within(child_of_b));
|
|
assert!(helper.is_focus_visible(child_of_b));
|
|
assert!(helper.is_focus_within_visible(child_of_b));
|
|
})
|
|
.unwrap();
|
|
|
|
app.world_mut()
|
|
.run_system_once(move |world: DeferredWorld| {
|
|
assert!(!world.is_focused(entity_a));
|
|
assert!(!world.is_focus_within(entity_a));
|
|
assert!(!world.is_focus_visible(entity_a));
|
|
assert!(!world.is_focus_within_visible(entity_a));
|
|
|
|
assert!(!world.is_focused(entity_b));
|
|
assert!(world.is_focus_within(entity_b));
|
|
assert!(!world.is_focus_visible(entity_b));
|
|
assert!(world.is_focus_within_visible(entity_b));
|
|
|
|
assert!(world.is_focused(child_of_b));
|
|
assert!(world.is_focus_within(child_of_b));
|
|
assert!(world.is_focus_visible(child_of_b));
|
|
assert!(world.is_focus_within_visible(child_of_b));
|
|
})
|
|
.unwrap();
|
|
}
|
|
}
|