Skip to content

Commit 9e73ec7

Browse files
Merge branch 'main' into observer_trigger_refactor
2 parents ea87a3f + 84f21f7 commit 9e73ec7

File tree

16 files changed

+1401
-519
lines changed

16 files changed

+1401
-519
lines changed
Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
use accesskit::Role;
2+
use bevy_a11y::AccessibilityNode;
3+
use bevy_app::{App, Plugin};
4+
use bevy_ecs::event::{EntityEvent, Event};
5+
use bevy_ecs::query::{Has, Without};
6+
use bevy_ecs::system::{In, ResMut};
7+
use bevy_ecs::{
8+
component::Component,
9+
observer::On,
10+
system::{Commands, Query, SystemId},
11+
};
12+
use bevy_input::keyboard::{KeyCode, KeyboardInput};
13+
use bevy_input::ButtonState;
14+
use bevy_input_focus::{FocusedInput, InputFocus, InputFocusVisible};
15+
use bevy_picking::events::{Click, Pointer};
16+
use bevy_ui::{Checkable, Checked, InteractionDisabled};
17+
18+
/// Headless widget implementation for checkboxes. The [`Checked`] component represents the current
19+
/// state of the checkbox. The `on_change` field is an optional system id that will be run when the
20+
/// checkbox is clicked, or when the `Enter` or `Space` key is pressed while the checkbox is
21+
/// focused. If the `on_change` field is `None`, then instead of calling a callback, the checkbox
22+
/// will update its own [`Checked`] state directly.
23+
///
24+
/// # Toggle switches
25+
///
26+
/// The [`CoreCheckbox`] component can be used to implement other kinds of toggle widgets. If you
27+
/// are going to do a toggle switch, you should override the [`AccessibilityNode`] component with
28+
/// the `Switch` role instead of the `Checkbox` role.
29+
#[derive(Component, Debug, Default)]
30+
#[require(AccessibilityNode(accesskit::Node::new(Role::CheckBox)), Checkable)]
31+
pub struct CoreCheckbox {
32+
/// One-shot system that is run when the checkbox state needs to be changed.
33+
pub on_change: Option<SystemId<In<bool>>>,
34+
}
35+
36+
fn checkbox_on_key_input(
37+
mut ev: On<FocusedInput<KeyboardInput>>,
38+
q_checkbox: Query<(&CoreCheckbox, Has<Checked>), Without<InteractionDisabled>>,
39+
mut commands: Commands,
40+
) {
41+
if let Ok((checkbox, is_checked)) = q_checkbox.get(ev.target()) {
42+
let event = &ev.event().input;
43+
if event.state == ButtonState::Pressed
44+
&& !event.repeat
45+
&& (event.key_code == KeyCode::Enter || event.key_code == KeyCode::Space)
46+
{
47+
ev.propagate(false);
48+
set_checkbox_state(&mut commands, ev.target(), checkbox, !is_checked);
49+
}
50+
}
51+
}
52+
53+
fn checkbox_on_pointer_click(
54+
mut ev: On<Pointer<Click>>,
55+
q_checkbox: Query<(&CoreCheckbox, Has<Checked>, Has<InteractionDisabled>)>,
56+
focus: Option<ResMut<InputFocus>>,
57+
focus_visible: Option<ResMut<InputFocusVisible>>,
58+
mut commands: Commands,
59+
) {
60+
if let Ok((checkbox, is_checked, disabled)) = q_checkbox.get(ev.target()) {
61+
// Clicking on a button makes it the focused input,
62+
// and hides the focus ring if it was visible.
63+
if let Some(mut focus) = focus {
64+
focus.0 = Some(ev.target());
65+
}
66+
if let Some(mut focus_visible) = focus_visible {
67+
focus_visible.0 = false;
68+
}
69+
70+
ev.propagate(false);
71+
if !disabled {
72+
set_checkbox_state(&mut commands, ev.target(), checkbox, !is_checked);
73+
}
74+
}
75+
}
76+
77+
/// Event which can be triggered on a checkbox to set the checked state. This can be used to control
78+
/// the checkbox via gamepad buttons or other inputs.
79+
///
80+
/// # Example:
81+
///
82+
/// ```
83+
/// use bevy_ecs::system::Commands;
84+
/// use bevy_core_widgets::{CoreCheckbox, SetChecked};
85+
///
86+
/// fn setup(mut commands: Commands) {
87+
/// // Create a checkbox
88+
/// let checkbox = commands.spawn((
89+
/// CoreCheckbox::default(),
90+
/// )).id();
91+
///
92+
/// // Set to checked
93+
/// commands.trigger_targets(SetChecked(true), checkbox);
94+
/// }
95+
/// ```
96+
#[derive(Event, EntityEvent)]
97+
pub struct SetChecked(pub bool);
98+
99+
/// Event which can be triggered on a checkbox to toggle the checked state. This can be used to
100+
/// control the checkbox via gamepad buttons or other inputs.
101+
///
102+
/// # Example:
103+
///
104+
/// ```
105+
/// use bevy_ecs::system::Commands;
106+
/// use bevy_core_widgets::{CoreCheckbox, ToggleChecked};
107+
///
108+
/// fn setup(mut commands: Commands) {
109+
/// // Create a checkbox
110+
/// let checkbox = commands.spawn((
111+
/// CoreCheckbox::default(),
112+
/// )).id();
113+
///
114+
/// // Set to checked
115+
/// commands.trigger_targets(ToggleChecked, checkbox);
116+
/// }
117+
/// ```
118+
#[derive(Event, EntityEvent)]
119+
pub struct ToggleChecked;
120+
121+
fn checkbox_on_set_checked(
122+
mut ev: On<SetChecked>,
123+
q_checkbox: Query<(&CoreCheckbox, Has<Checked>, Has<InteractionDisabled>)>,
124+
mut commands: Commands,
125+
) {
126+
if let Ok((checkbox, is_checked, disabled)) = q_checkbox.get(ev.target()) {
127+
ev.propagate(false);
128+
if disabled {
129+
return;
130+
}
131+
132+
let will_be_checked = ev.event().0;
133+
if will_be_checked != is_checked {
134+
set_checkbox_state(&mut commands, ev.target(), checkbox, will_be_checked);
135+
}
136+
}
137+
}
138+
139+
fn checkbox_on_toggle_checked(
140+
mut ev: On<ToggleChecked>,
141+
q_checkbox: Query<(&CoreCheckbox, Has<Checked>, Has<InteractionDisabled>)>,
142+
mut commands: Commands,
143+
) {
144+
if let Ok((checkbox, is_checked, disabled)) = q_checkbox.get(ev.target()) {
145+
ev.propagate(false);
146+
if disabled {
147+
return;
148+
}
149+
150+
set_checkbox_state(&mut commands, ev.target(), checkbox, !is_checked);
151+
}
152+
}
153+
154+
fn set_checkbox_state(
155+
commands: &mut Commands,
156+
entity: impl Into<bevy_ecs::entity::Entity>,
157+
checkbox: &CoreCheckbox,
158+
new_state: bool,
159+
) {
160+
if let Some(on_change) = checkbox.on_change {
161+
commands.run_system_with(on_change, new_state);
162+
} else if new_state {
163+
commands.entity(entity.into()).insert(Checked);
164+
} else {
165+
commands.entity(entity.into()).remove::<Checked>();
166+
}
167+
}
168+
169+
/// Plugin that adds the observers for the [`CoreCheckbox`] widget.
170+
pub struct CoreCheckboxPlugin;
171+
172+
impl Plugin for CoreCheckboxPlugin {
173+
fn build(&self, app: &mut App) {
174+
app.add_observer(checkbox_on_key_input)
175+
.add_observer(checkbox_on_pointer_click)
176+
.add_observer(checkbox_on_set_checked)
177+
.add_observer(checkbox_on_toggle_checked);
178+
}
179+
}

crates/bevy_core_widgets/src/lib.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,13 @@
1515
// the widget level, like `SliderValue`, should not have the `Core` prefix.
1616

1717
mod core_button;
18+
mod core_checkbox;
1819
mod core_slider;
1920

2021
use bevy_app::{App, Plugin};
2122

2223
pub use core_button::{CoreButton, CoreButtonPlugin};
24+
pub use core_checkbox::{CoreCheckbox, CoreCheckboxPlugin, SetChecked, ToggleChecked};
2325
pub use core_slider::{
2426
CoreSlider, CoreSliderDragState, CoreSliderPlugin, CoreSliderThumb, SetSliderValue,
2527
SliderRange, SliderStep, SliderValue, TrackClick,
@@ -31,6 +33,6 @@ pub struct CoreWidgetsPlugin;
3133

3234
impl Plugin for CoreWidgetsPlugin {
3335
fn build(&self, app: &mut App) {
34-
app.add_plugins((CoreButtonPlugin, CoreSliderPlugin));
36+
app.add_plugins((CoreButtonPlugin, CoreCheckboxPlugin, CoreSliderPlugin));
3537
}
3638
}

0 commit comments

Comments
 (0)