mirror of
https://github.com/bevyengine/bevy
synced 2025-03-07 08:47:24 +00:00
Add "bevy_input_focus" crate. (#15611)
# Objective Define a framework for handling keyboard focus and bubbled keyboard events, as discussed in #15374. ## Solution Introduces a new crate, `bevy_input_focus`. This crate provides: * A resource for tracking which entity has keyboard focus. * Methods for getting and setting keyboard focus. * Event definitions for triggering bubble-able keyboard input events to the focused entity. * A system for dispatching keyboard input events to the focused entity. This crate does *not* provide any integration with UI widgets, or provide functions for tab navigation or gamepad-based focus navigation, as those are typically application-specific. ## Testing Most of the code has been copied from a different project, one that has been well tested. However, most of what's in this module consists of type definitions, with relatively small amounts of executable code. That being said, I expect that there will be substantial bikeshedding on the design, and I would prefer to hold off writing tests until after things have settled. I think that an example would be appropriate, however I'm waiting on a few other pending changes to Bevy before doing so. In particular, I can see a simple example with four buttons, with focus navigation between them, and which can be triggered by the keyboard. @alice-i-cecile
This commit is contained in:
parent
a35811d088
commit
ea33fc04ab
4 changed files with 276 additions and 0 deletions
25
crates/bevy_input_focus/Cargo.toml
Normal file
25
crates/bevy_input_focus/Cargo.toml
Normal file
|
@ -0,0 +1,25 @@
|
|||
[package]
|
||||
name = "bevy_input_focus"
|
||||
version = "0.15.0-dev"
|
||||
edition = "2021"
|
||||
description = "Keyboard focus management"
|
||||
homepage = "https://bevyengine.org"
|
||||
repository = "https://github.com/bevyengine/bevy"
|
||||
license = "MIT OR Apache-2.0"
|
||||
keywords = ["bevy", "color"]
|
||||
rust-version = "1.76.0"
|
||||
|
||||
[dependencies]
|
||||
bevy_app = { path = "../bevy_app", version = "0.15.0-dev", default-features = false }
|
||||
bevy_ecs = { path = "../bevy_ecs", version = "0.15.0-dev", default-features = false }
|
||||
bevy_input = { path = "../bevy_input", version = "0.15.0-dev", default-features = false }
|
||||
bevy_hierarchy = { path = "../bevy_hierarchy", version = "0.15.0-dev", default-features = false }
|
||||
bevy_utils = { path = "../bevy_utils", version = "0.15.0-dev", default-features = false }
|
||||
bevy_window = { path = "../bevy_window", version = "0.15.0-dev", default-features = false }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
rustdoc-args = ["-Zunstable-options", "--generate-link-to-definition"]
|
||||
all-features = true
|
7
crates/bevy_input_focus/README.md
Normal file
7
crates/bevy_input_focus/README.md
Normal file
|
@ -0,0 +1,7 @@
|
|||
# Bevy Input Focus
|
||||
|
||||
[](https://github.com/bevyengine/bevy#license)
|
||||
[](https://crates.io/crates/bevy_input_focus)
|
||||
[](https://crates.io/crates/bevy_input_focus)
|
||||
[](https://docs.rs/bevy_input_focus/latest/bevy_input_focus/)
|
||||
[](https://discord.gg/bevy)
|
243
crates/bevy_input_focus/src/lib.rs
Normal file
243
crates/bevy_input_focus/src/lib.rs
Normal file
|
@ -0,0 +1,243 @@
|
|||
#![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"
|
||||
)]
|
||||
|
||||
//! Keyboard focus system for Bevy.
|
||||
//!
|
||||
//! This crate provides a system for managing input focus in Bevy applications, including:
|
||||
//! * A resource for tracking which entity has input focus.
|
||||
//! * Methods for getting and setting input focus.
|
||||
//! * Event definitions for triggering bubble-able keyboard input events to the focused entity.
|
||||
//! * A system for dispatching keyboard input events to the focused entity.
|
||||
//!
|
||||
//! This crate does *not* provide any integration with UI widgets, or provide functions for
|
||||
//! tab navigation or gamepad-based focus navigation, as those are typically application-specific.
|
||||
|
||||
use bevy_app::{App, Plugin, Update};
|
||||
use bevy_ecs::{
|
||||
component::Component,
|
||||
entity::Entity,
|
||||
event::{Event, EventReader},
|
||||
query::With,
|
||||
system::{Commands, Query, Res, Resource},
|
||||
world::{Command, DeferredWorld, World},
|
||||
};
|
||||
use bevy_hierarchy::Parent;
|
||||
use bevy_input::keyboard::KeyboardInput;
|
||||
use bevy_window::PrimaryWindow;
|
||||
|
||||
/// Resource representing which entity has input focus, if any. Keyboard events will be
|
||||
/// dispatched to the current focus entity, or to the primary window if no entity has focus.
|
||||
#[derive(Clone, Debug, Resource)]
|
||||
pub struct InputFocus(pub Option<Entity>);
|
||||
|
||||
/// Resource representing whether the input focus indicator should be visible. It's up to the
|
||||
/// current focus navigation system to set this resource. 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.
|
||||
#[derive(Clone, Debug, Resource)]
|
||||
pub struct InputFocusVisible(pub bool);
|
||||
|
||||
/// Helper functions for [`World`] and [`DeferredWorld`] to set and clear input focus.
|
||||
pub trait SetInputFocus {
|
||||
/// Set input focus to the given entity.
|
||||
fn set_input_focus(&mut self, entity: Entity);
|
||||
/// Clear input focus.
|
||||
fn clear_input_focus(&mut self);
|
||||
}
|
||||
|
||||
impl SetInputFocus for World {
|
||||
fn set_input_focus(&mut self, entity: Entity) {
|
||||
if let Some(mut focus) = self.get_resource_mut::<InputFocus>() {
|
||||
focus.0 = Some(entity);
|
||||
}
|
||||
}
|
||||
|
||||
fn clear_input_focus(&mut self) {
|
||||
if let Some(mut focus) = self.get_resource_mut::<InputFocus>() {
|
||||
focus.0 = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'w> SetInputFocus for DeferredWorld<'w> {
|
||||
fn set_input_focus(&mut self, entity: Entity) {
|
||||
if let Some(mut focus) = self.get_resource_mut::<InputFocus>() {
|
||||
focus.0 = Some(entity);
|
||||
}
|
||||
}
|
||||
|
||||
fn clear_input_focus(&mut self) {
|
||||
if let Some(mut focus) = self.get_resource_mut::<InputFocus>() {
|
||||
focus.0 = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Command to set input focus to the given entity.
|
||||
pub struct SetFocusCommand(Option<Entity>);
|
||||
|
||||
impl Command for SetFocusCommand {
|
||||
fn apply(self, world: &mut World) {
|
||||
if let Some(mut focus) = world.get_resource_mut::<InputFocus>() {
|
||||
focus.0 = self.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A bubble-able event for keyboard input. 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.
|
||||
#[derive(Clone, Debug, Component)]
|
||||
pub struct FocusKeyboardInput(pub KeyboardInput);
|
||||
|
||||
impl Event for FocusKeyboardInput {
|
||||
type Traversal = &'static Parent;
|
||||
|
||||
const AUTO_PROPAGATE: bool = true;
|
||||
}
|
||||
|
||||
/// Plugin which registers the system for dispatching keyboard events based on focus and
|
||||
/// hover state.
|
||||
pub struct InputDispatchPlugin;
|
||||
|
||||
impl Plugin for InputDispatchPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.insert_resource(InputFocus(None))
|
||||
.add_systems(Update, dispatch_keyboard_input);
|
||||
}
|
||||
}
|
||||
|
||||
/// System which dispatches keyboard input events to the focused entity, or to the primary window
|
||||
/// if no entity has focus.
|
||||
fn dispatch_keyboard_input(
|
||||
mut key_events: EventReader<KeyboardInput>,
|
||||
focus: Res<InputFocus>,
|
||||
windows: Query<Entity, With<PrimaryWindow>>,
|
||||
mut commands: Commands,
|
||||
) {
|
||||
// If an element has keyboard focus, then dispatch the key event to that element.
|
||||
if let Some(focus_elt) = focus.0 {
|
||||
for ev in key_events.read() {
|
||||
commands.trigger_targets(FocusKeyboardInput(ev.clone()), focus_elt);
|
||||
}
|
||||
} else {
|
||||
// If no element has input focus, then dispatch the key event to the primary window.
|
||||
// There should be only one primary window.
|
||||
if let Ok(window) = windows.get_single() {
|
||||
for ev in key_events.read() {
|
||||
commands.trigger_targets(FocusKeyboardInput(ev.clone()), window);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Trait which defines methods to check if an entity currently has focus. This is implemented
|
||||
/// for both [`World`] and [`DeferredWorld`].
|
||||
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.
|
||||
fn is_focus_within(&self, entity: Entity) -> bool;
|
||||
|
||||
/// Returns true if the given entity has input focus and the focus indicator is visible.
|
||||
fn is_focus_visible(&self, entity: Entity) -> bool;
|
||||
|
||||
/// Returns true if the given entity, or any descenant, has input focus and the focus
|
||||
/// indicator is visible.
|
||||
fn is_focus_within_visible(&self, entity: Entity) -> bool;
|
||||
}
|
||||
|
||||
impl IsFocused for DeferredWorld<'_> {
|
||||
fn is_focused(&self, entity: Entity) -> bool {
|
||||
self.get_resource::<InputFocus>()
|
||||
.map(|f| f.0)
|
||||
.unwrap_or_default()
|
||||
.map(|f| f == entity)
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn is_focus_within(&self, entity: Entity) -> bool {
|
||||
let Some(focus_resource) = self.get_resource::<InputFocus>() else {
|
||||
return false;
|
||||
};
|
||||
let Some(focus) = focus_resource.0 else {
|
||||
return false;
|
||||
};
|
||||
let mut e = entity;
|
||||
loop {
|
||||
if e == focus {
|
||||
return true;
|
||||
}
|
||||
if let Some(parent) = self.entity(e).get::<Parent>() {
|
||||
e = parent.get();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn is_focus_visible(&self, entity: Entity) -> bool {
|
||||
self.get_resource::<InputFocusVisible>()
|
||||
.map(|vis| vis.0)
|
||||
.unwrap_or_default()
|
||||
&& self.is_focused(entity)
|
||||
}
|
||||
|
||||
fn is_focus_within_visible(&self, entity: Entity) -> bool {
|
||||
self.get_resource::<InputFocusVisible>()
|
||||
.map(|vis| vis.0)
|
||||
.unwrap_or_default()
|
||||
&& self.is_focus_within(entity)
|
||||
}
|
||||
}
|
||||
|
||||
impl IsFocused for World {
|
||||
fn is_focused(&self, entity: Entity) -> bool {
|
||||
self.get_resource::<InputFocus>()
|
||||
.map(|f| f.0)
|
||||
.unwrap_or_default()
|
||||
.map(|f| f == entity)
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn is_focus_within(&self, entity: Entity) -> bool {
|
||||
let Some(focus_resource) = self.get_resource::<InputFocus>() else {
|
||||
return false;
|
||||
};
|
||||
let Some(focus) = focus_resource.0 else {
|
||||
return false;
|
||||
};
|
||||
let mut e = entity;
|
||||
loop {
|
||||
if e == focus {
|
||||
return true;
|
||||
}
|
||||
if let Some(parent) = self.entity(e).get::<Parent>() {
|
||||
e = parent.get();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn is_focus_visible(&self, entity: Entity) -> bool {
|
||||
self.get_resource::<InputFocusVisible>()
|
||||
.map(|vis| vis.0)
|
||||
.unwrap_or_default()
|
||||
&& self.is_focused(entity)
|
||||
}
|
||||
|
||||
fn is_focus_within_visible(&self, entity: Entity) -> bool {
|
||||
self.get_resource::<InputFocusVisible>()
|
||||
.map(|vis| vis.0)
|
||||
.unwrap_or_default()
|
||||
&& self.is_focus_within(entity)
|
||||
}
|
||||
}
|
|
@ -44,6 +44,7 @@ crates=(
|
|||
bevy_gizmos
|
||||
bevy_text
|
||||
bevy_a11y
|
||||
bevy_input_focus
|
||||
bevy_ui
|
||||
bevy_winit
|
||||
bevy_dev_tools
|
||||
|
|
Loading…
Add table
Reference in a new issue