Skip to content

Commit 57ddae1

Browse files
Core button widget (#19366)
# Objective Part of #19236 ## Solution Adds a new `bevy_core_widgets` crate containing headless widget implementations. This PR adds a single `CoreButton` widget, more widgets to be added later once this is approved. ## Testing There's an example, ui/core_widgets. --------- Co-authored-by: Alice Cecile <[email protected]>
1 parent c549b9e commit 57ddae1

File tree

16 files changed

+1242
-2
lines changed

16 files changed

+1242
-2
lines changed

Cargo.toml

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,7 @@ default = [
134134
"bevy_audio",
135135
"bevy_color",
136136
"bevy_core_pipeline",
137+
"bevy_core_widgets",
137138
"bevy_anti_aliasing",
138139
"bevy_gilrs",
139140
"bevy_gizmos",
@@ -292,6 +293,9 @@ bevy_log = ["bevy_internal/bevy_log"]
292293
# Enable input focus subsystem
293294
bevy_input_focus = ["bevy_internal/bevy_input_focus"]
294295

296+
# Headless widget collection for Bevy UI.
297+
bevy_core_widgets = ["bevy_internal/bevy_core_widgets"]
298+
295299
# Enable passthrough loading for SPIR-V shaders (Only supported on Vulkan, shader capabilities and extensions must agree with the platform implementation)
296300
spirv_shader_passthrough = ["bevy_internal/spirv_shader_passthrough"]
297301

@@ -4438,3 +4442,25 @@ name = "Hotpatching Systems"
44384442
description = "Demonstrates how to hotpatch systems"
44394443
category = "ECS (Entity Component System)"
44404444
wasm = false
4445+
4446+
[[example]]
4447+
name = "core_widgets"
4448+
path = "examples/ui/core_widgets.rs"
4449+
doc-scrape-examples = true
4450+
4451+
[package.metadata.example.core_widgets]
4452+
name = "Core Widgets"
4453+
description = "Demonstrates use of core (headless) widgets in Bevy UI"
4454+
category = "UI (User Interface)"
4455+
wasm = true
4456+
4457+
[[example]]
4458+
name = "core_widgets_observers"
4459+
path = "examples/ui/core_widgets_observers.rs"
4460+
doc-scrape-examples = true
4461+
4462+
[package.metadata.example.core_widgets_observers]
4463+
name = "Core Widgets (w/Observers)"
4464+
description = "Demonstrates use of core (headless) widgets in Bevy UI, with Observers"
4465+
category = "UI (User Interface)"
4466+
wasm = true

crates/bevy_core_widgets/Cargo.toml

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
[package]
2+
name = "bevy_core_widgets"
3+
version = "0.16.0-dev"
4+
edition = "2024"
5+
description = "Unstyled common widgets for B Bevy Engine"
6+
homepage = "https://bevyengine.org"
7+
repository = "https://github.com/bevyengine/bevy"
8+
license = "MIT OR Apache-2.0"
9+
keywords = ["bevy"]
10+
11+
[dependencies]
12+
# bevy
13+
bevy_app = { path = "../bevy_app", version = "0.16.0-dev" }
14+
bevy_a11y = { path = "../bevy_a11y", version = "0.16.0-dev" }
15+
bevy_ecs = { path = "../bevy_ecs", version = "0.16.0-dev" }
16+
bevy_input = { path = "../bevy_input", version = "0.16.0-dev" }
17+
bevy_input_focus = { path = "../bevy_input_focus", version = "0.16.0-dev" }
18+
bevy_picking = { path = "../bevy_picking", version = "0.16.0-dev" }
19+
bevy_ui = { path = "../bevy_ui", version = "0.16.0-dev" }
20+
21+
# other
22+
accesskit = "0.19"
23+
24+
[features]
25+
default = []
26+
27+
[lints]
28+
workspace = true
29+
30+
[package.metadata.docs.rs]
31+
rustdoc-args = ["-Zunstable-options", "--generate-link-to-definition"]
32+
all-features = true
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
use accesskit::Role;
2+
use bevy_a11y::AccessibilityNode;
3+
use bevy_app::{App, Plugin};
4+
use bevy_ecs::query::Has;
5+
use bevy_ecs::system::ResMut;
6+
use bevy_ecs::{
7+
component::Component,
8+
entity::Entity,
9+
observer::Trigger,
10+
query::With,
11+
system::{Commands, Query, SystemId},
12+
};
13+
use bevy_input::keyboard::{KeyCode, KeyboardInput};
14+
use bevy_input_focus::{FocusedInput, InputFocus, InputFocusVisible};
15+
use bevy_picking::events::{Cancel, Click, DragEnd, Pointer, Pressed, Released};
16+
use bevy_ui::{Depressed, InteractionDisabled};
17+
18+
/// Headless button widget. This widget maintains a "pressed" state, which is used to
19+
/// indicate whether the button is currently being pressed by the user. It emits a `ButtonClicked`
20+
/// event when the button is un-pressed.
21+
#[derive(Component, Debug)]
22+
#[require(AccessibilityNode(accesskit::Node::new(Role::Button)))]
23+
pub struct CoreButton {
24+
/// Optional system to run when the button is clicked, or when the Enter or Space key
25+
/// is pressed while the button is focused. If this field is `None`, the button will
26+
/// emit a `ButtonClicked` event when clicked.
27+
pub on_click: Option<SystemId>,
28+
}
29+
30+
fn button_on_key_event(
31+
mut trigger: Trigger<FocusedInput<KeyboardInput>>,
32+
q_state: Query<(&CoreButton, Has<InteractionDisabled>)>,
33+
mut commands: Commands,
34+
) {
35+
if let Ok((bstate, disabled)) = q_state.get(trigger.target().unwrap()) {
36+
if !disabled {
37+
let event = &trigger.event().input;
38+
if !event.repeat
39+
&& (event.key_code == KeyCode::Enter || event.key_code == KeyCode::Space)
40+
{
41+
if let Some(on_click) = bstate.on_click {
42+
trigger.propagate(false);
43+
commands.run_system(on_click);
44+
}
45+
}
46+
}
47+
}
48+
}
49+
50+
fn button_on_pointer_click(
51+
mut trigger: Trigger<Pointer<Click>>,
52+
mut q_state: Query<(&CoreButton, Has<Depressed>, Has<InteractionDisabled>)>,
53+
mut commands: Commands,
54+
) {
55+
if let Ok((bstate, pressed, disabled)) = q_state.get_mut(trigger.target().unwrap()) {
56+
trigger.propagate(false);
57+
if pressed && !disabled {
58+
if let Some(on_click) = bstate.on_click {
59+
commands.run_system(on_click);
60+
}
61+
}
62+
}
63+
}
64+
65+
fn button_on_pointer_down(
66+
mut trigger: Trigger<Pointer<Pressed>>,
67+
mut q_state: Query<(Entity, Has<InteractionDisabled>, Has<Depressed>), With<CoreButton>>,
68+
focus: Option<ResMut<InputFocus>>,
69+
focus_visible: Option<ResMut<InputFocusVisible>>,
70+
mut commands: Commands,
71+
) {
72+
if let Ok((button, disabled, depressed)) = q_state.get_mut(trigger.target().unwrap()) {
73+
trigger.propagate(false);
74+
if !disabled {
75+
if !depressed {
76+
commands.entity(button).insert(Depressed);
77+
}
78+
// Clicking on a button makes it the focused input,
79+
// and hides the focus ring if it was visible.
80+
if let Some(mut focus) = focus {
81+
focus.0 = trigger.target();
82+
}
83+
if let Some(mut focus_visible) = focus_visible {
84+
focus_visible.0 = false;
85+
}
86+
}
87+
}
88+
}
89+
90+
fn button_on_pointer_up(
91+
mut trigger: Trigger<Pointer<Released>>,
92+
mut q_state: Query<(Entity, Has<InteractionDisabled>, Has<Depressed>), With<CoreButton>>,
93+
mut commands: Commands,
94+
) {
95+
if let Ok((button, disabled, depressed)) = q_state.get_mut(trigger.target().unwrap()) {
96+
trigger.propagate(false);
97+
if !disabled && depressed {
98+
commands.entity(button).remove::<Depressed>();
99+
}
100+
}
101+
}
102+
103+
fn button_on_pointer_drag_end(
104+
mut trigger: Trigger<Pointer<DragEnd>>,
105+
mut q_state: Query<(Entity, Has<InteractionDisabled>, Has<Depressed>), With<CoreButton>>,
106+
mut commands: Commands,
107+
) {
108+
if let Ok((button, disabled, depressed)) = q_state.get_mut(trigger.target().unwrap()) {
109+
trigger.propagate(false);
110+
if !disabled && depressed {
111+
commands.entity(button).remove::<Depressed>();
112+
}
113+
}
114+
}
115+
116+
fn button_on_pointer_cancel(
117+
mut trigger: Trigger<Pointer<Cancel>>,
118+
mut q_state: Query<(Entity, Has<InteractionDisabled>, Has<Depressed>), With<CoreButton>>,
119+
mut commands: Commands,
120+
) {
121+
if let Ok((button, disabled, depressed)) = q_state.get_mut(trigger.target().unwrap()) {
122+
trigger.propagate(false);
123+
if !disabled && depressed {
124+
commands.entity(button).remove::<Depressed>();
125+
}
126+
}
127+
}
128+
129+
/// Plugin that adds the observers for the [`CoreButton`] widget.
130+
pub struct CoreButtonPlugin;
131+
132+
impl Plugin for CoreButtonPlugin {
133+
fn build(&self, app: &mut App) {
134+
app.add_observer(button_on_key_event)
135+
.add_observer(button_on_pointer_down)
136+
.add_observer(button_on_pointer_up)
137+
.add_observer(button_on_pointer_click)
138+
.add_observer(button_on_pointer_drag_end)
139+
.add_observer(button_on_pointer_cancel);
140+
}
141+
}

crates/bevy_core_widgets/src/lib.rs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
//! This crate provides a set of core widgets for Bevy UI, such as buttons, checkboxes, and sliders.
2+
//! These widgets have no inherent styling, it's the responsibility of the user to add styling
3+
//! appropriate for their game or application.
4+
//!
5+
//! # State Management
6+
//!
7+
//! Most of the widgets use external state management: this means that the widgets do not
8+
//! automatically update their own internal state, but instead rely on the app to update the widget
9+
//! state (as well as any other related game state) in response to a change event emitted by the
10+
//! widget. The primary motivation for this is to avoid two-way data binding in scenarios where the
11+
//! user interface is showing a live view of dynamic data coming from deeper within the game engine.
12+
13+
mod core_button;
14+
15+
use bevy_app::{App, Plugin};
16+
17+
pub use core_button::{CoreButton, CoreButtonPlugin};
18+
19+
/// A plugin that registers the observers for all of the core widgets. If you don't want to
20+
/// use all of the widgets, you can import the individual widget plugins instead.
21+
pub struct CoreWidgetsPlugin;
22+
23+
impl Plugin for CoreWidgetsPlugin {
24+
fn build(&self, app: &mut App) {
25+
app.add_plugins(CoreButtonPlugin);
26+
}
27+
}

crates/bevy_internal/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -395,6 +395,7 @@ bevy_color = { path = "../bevy_color", optional = true, version = "0.16.0-dev",
395395
"bevy_reflect",
396396
] }
397397
bevy_core_pipeline = { path = "../bevy_core_pipeline", optional = true, version = "0.16.0-dev" }
398+
bevy_core_widgets = { path = "../bevy_core_widgets", optional = true, version = "0.16.0-dev" }
398399
bevy_anti_aliasing = { path = "../bevy_anti_aliasing", optional = true, version = "0.16.0-dev" }
399400
bevy_dev_tools = { path = "../bevy_dev_tools", optional = true, version = "0.16.0-dev" }
400401
bevy_gilrs = { path = "../bevy_gilrs", optional = true, version = "0.16.0-dev" }

crates/bevy_internal/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ pub use bevy_audio as audio;
2929
pub use bevy_color as color;
3030
#[cfg(feature = "bevy_core_pipeline")]
3131
pub use bevy_core_pipeline as core_pipeline;
32+
#[cfg(feature = "bevy_core_widgets")]
33+
pub use bevy_core_widgets as core_widgets;
3234
#[cfg(feature = "bevy_dev_tools")]
3335
pub use bevy_dev_tools as dev_tools;
3436
pub use bevy_diagnostic as diagnostic;

0 commit comments

Comments
 (0)