-
-
Notifications
You must be signed in to change notification settings - Fork 3.6k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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
- Loading branch information
Showing
4 changed files
with
276 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
# Bevy Input Focus | ||
|
||
[![License](https://img.shields.io/badge/license-MIT%2FApache-blue.svg)](https://github.com/bevyengine/bevy#license) | ||
[![Crates.io](https://img.shields.io/crates/v/bevy_input_focus.svg)](https://crates.io/crates/bevy_input_focus) | ||
[![Downloads](https://img.shields.io/crates/d/bevy_input_focus.svg)](https://crates.io/crates/bevy_input_focus) | ||
[![Docs](https://docs.rs/bevy_input_focus/badge.svg)](https://docs.rs/bevy_input_focus/latest/bevy_input_focus/) | ||
[![Discord](https://img.shields.io/discord/691052431525675048.svg?label=&logo=discord&logoColor=ffffff&color=7389D8&labelColor=6A7EC2)](https://discord.gg/bevy) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -44,6 +44,7 @@ crates=( | |
bevy_gizmos | ||
bevy_text | ||
bevy_a11y | ||
bevy_input_focus | ||
bevy_ui | ||
bevy_winit | ||
bevy_dev_tools | ||
|