Skip to content

Commit 9fdddf7

Browse files
authored
Core Checkbox (#19665)
# Objective This is part of the "core widgets" effort: #19236. ## Solution This adds the "core checkbox" widget type. ## Testing Tested using examples core_widgets and core_widgets_observers. Note to reviewers: I reorganized the code in the examples, so the diffs are large because of code moves.
1 parent 35166d9 commit 9fdddf7

File tree

7 files changed

+877
-283
lines changed

7 files changed

+877
-283
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
}

crates/bevy_ui/src/interaction_states.rs

Lines changed: 24 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
use bevy_a11y::AccessibilityNode;
33
use bevy_ecs::{
44
component::Component,
5-
lifecycle::{Add, Insert, Remove},
5+
lifecycle::{Add, Remove},
66
observer::On,
77
world::DeferredWorld,
88
};
@@ -40,21 +40,17 @@ pub(crate) fn on_remove_disabled(
4040
#[derive(Component, Default, Debug)]
4141
pub struct Pressed;
4242

43-
/// Component that indicates whether a checkbox or radio button is in a checked state.
43+
/// Component that indicates that a widget can be checked.
4444
#[derive(Component, Default, Debug)]
45-
#[component(immutable)]
46-
pub struct Checked(pub bool);
45+
pub struct Checkable;
4746

48-
impl Checked {
49-
/// Returns whether the checkbox or radio button is currently checked.
50-
pub fn get(&self) -> bool {
51-
self.0
52-
}
53-
}
47+
/// Component that indicates whether a checkbox or radio button is in a checked state.
48+
#[derive(Component, Default, Debug)]
49+
pub struct Checked;
5450

55-
pub(crate) fn on_insert_is_checked(trigger: On<Insert, Checked>, mut world: DeferredWorld) {
51+
pub(crate) fn on_add_checkable(trigger: On<Add, Checked>, mut world: DeferredWorld) {
5652
let mut entity = world.entity_mut(trigger.target());
57-
let checked = entity.get::<Checked>().unwrap().get();
53+
let checked = entity.get::<Checked>().is_some();
5854
if let Some(mut accessibility) = entity.get_mut::<AccessibilityNode>() {
5955
accessibility.set_toggled(match checked {
6056
true => accesskit::Toggled::True,
@@ -63,7 +59,22 @@ pub(crate) fn on_insert_is_checked(trigger: On<Insert, Checked>, mut world: Defe
6359
}
6460
}
6561

66-
pub(crate) fn on_remove_is_checked(trigger: On<Remove, Checked>, mut world: DeferredWorld) {
62+
pub(crate) fn on_remove_checkable(trigger: On<Add, Checked>, mut world: DeferredWorld) {
63+
// Remove the 'toggled' attribute entirely.
64+
let mut entity = world.entity_mut(trigger.target());
65+
if let Some(mut accessibility) = entity.get_mut::<AccessibilityNode>() {
66+
accessibility.clear_toggled();
67+
}
68+
}
69+
70+
pub(crate) fn on_add_checked(trigger: On<Add, Checked>, mut world: DeferredWorld) {
71+
let mut entity = world.entity_mut(trigger.target());
72+
if let Some(mut accessibility) = entity.get_mut::<AccessibilityNode>() {
73+
accessibility.set_toggled(accesskit::Toggled::True);
74+
}
75+
}
76+
77+
pub(crate) fn on_remove_checked(trigger: On<Remove, Checked>, mut world: DeferredWorld) {
6778
let mut entity = world.entity_mut(trigger.target());
6879
if let Some(mut accessibility) = entity.get_mut::<AccessibilityNode>() {
6980
accessibility.set_toggled(accesskit::Toggled::False);

crates/bevy_ui/src/lib.rs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ mod ui_node;
3939
pub use focus::*;
4040
pub use geometry::*;
4141
pub use gradients::*;
42-
pub use interaction_states::{Checked, InteractionDisabled, Pressed};
42+
pub use interaction_states::{Checkable, Checked, InteractionDisabled, Pressed};
4343
pub use layout::*;
4444
pub use measurement::*;
4545
pub use render::*;
@@ -323,8 +323,10 @@ fn build_text_interop(app: &mut App) {
323323

324324
app.add_observer(interaction_states::on_add_disabled)
325325
.add_observer(interaction_states::on_remove_disabled)
326-
.add_observer(interaction_states::on_insert_is_checked)
327-
.add_observer(interaction_states::on_remove_is_checked);
326+
.add_observer(interaction_states::on_add_checkable)
327+
.add_observer(interaction_states::on_remove_checkable)
328+
.add_observer(interaction_states::on_add_checked)
329+
.add_observer(interaction_states::on_remove_checked);
328330

329331
app.configure_sets(
330332
PostUpdate,

0 commit comments

Comments
 (0)