bevy_ui_widgets/
checkbox.rs

1use accesskit::Role;
2use bevy_a11y::AccessibilityNode;
3use bevy_app::{App, Plugin};
4use bevy_ecs::event::EntityEvent;
5use bevy_ecs::query::{Has, With, Without};
6use bevy_ecs::system::ResMut;
7use bevy_ecs::{
8    component::Component,
9    observer::On,
10    system::{Commands, Query},
11};
12use bevy_input::keyboard::{KeyCode, KeyboardInput};
13use bevy_input::ButtonState;
14use bevy_input_focus::{FocusedInput, InputFocus, InputFocusVisible};
15use bevy_picking::events::{Click, Pointer};
16use bevy_ui::{Checkable, Checked, InteractionDisabled};
17
18use crate::ValueChange;
19use bevy_ecs::entity::Entity;
20
21/// Headless widget implementation for checkboxes. The [`Checked`] component represents the current
22/// state of the checkbox. The widget will emit a [`ValueChange<bool>`] event when clicked, or when
23/// the `Enter` or `Space` key is pressed while the checkbox is focused.
24///
25/// Add the [`checkbox_self_update`] observer watching the entity with this component to automatically add and remove the [`Checked`] component.
26///
27/// # Toggle switches
28///
29/// The [`Checkbox`] component can be used to implement other kinds of toggle widgets. If you
30/// are going to do a toggle switch, you should override the [`AccessibilityNode`] component with
31/// the `Switch` role instead of the `Checkbox` role.
32#[derive(Component, Debug, Default)]
33#[require(AccessibilityNode(accesskit::Node::new(Role::CheckBox)), Checkable)]
34pub struct Checkbox;
35
36fn checkbox_on_key_input(
37    mut ev: On<FocusedInput<KeyboardInput>>,
38    q_checkbox: Query<Has<Checked>, (With<Checkbox>, Without<InteractionDisabled>)>,
39    mut commands: Commands,
40) {
41    if let Ok(is_checked) = q_checkbox.get(ev.focused_entity) {
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            commands.trigger(ValueChange {
49                source: ev.focused_entity,
50                value: !is_checked,
51            });
52        }
53    }
54}
55
56fn checkbox_on_pointer_click(
57    mut click: On<Pointer<Click>>,
58    q_checkbox: Query<(Has<Checked>, Has<InteractionDisabled>), With<Checkbox>>,
59    focus: Option<ResMut<InputFocus>>,
60    focus_visible: Option<ResMut<InputFocusVisible>>,
61    mut commands: Commands,
62) {
63    if let Ok((is_checked, disabled)) = q_checkbox.get(click.entity) {
64        // Clicking on a button makes it the focused input,
65        // and hides the focus ring if it was visible.
66        if let Some(mut focus) = focus {
67            focus.0 = Some(click.entity);
68        }
69        if let Some(mut focus_visible) = focus_visible {
70            focus_visible.0 = false;
71        }
72
73        click.propagate(false);
74        if !disabled {
75            commands.trigger(ValueChange {
76                source: click.entity,
77                value: !is_checked,
78            });
79        }
80    }
81}
82
83/// Event which can be triggered on a checkbox to set the checked state. This can be used to control
84/// the checkbox via gamepad buttons or other inputs.
85///
86/// # Example:
87///
88/// ```
89/// use bevy_ecs::system::Commands;
90/// use bevy_ui_widgets::{Checkbox, SetChecked};
91///
92/// fn setup(mut commands: Commands) {
93///     // Create a checkbox
94///     let entity = commands.spawn((
95///         Checkbox::default(),
96///     )).id();
97///
98///     // Set to checked
99///     commands.trigger(SetChecked { entity, checked: true});
100/// }
101/// ```
102#[derive(EntityEvent)]
103pub struct SetChecked {
104    /// The [`Checkbox`] entity to set the "checked" state on.
105    pub entity: Entity,
106    /// Sets the `checked` state to `true` or `false`.
107    pub checked: bool,
108}
109
110/// Event which can be triggered on a checkbox to toggle the checked state. This can be used to
111/// control the checkbox via gamepad buttons or other inputs.
112///
113/// # Example:
114///
115/// ```
116/// use bevy_ecs::system::Commands;
117/// use bevy_ui_widgets::{Checkbox, ToggleChecked};
118///
119/// fn setup(mut commands: Commands) {
120///     // Create a checkbox
121///     let entity = commands.spawn((
122///         Checkbox::default(),
123///     )).id();
124///
125///     // Set to checked
126///     commands.trigger(ToggleChecked { entity });
127/// }
128/// ```
129#[derive(EntityEvent)]
130pub struct ToggleChecked {
131    /// The [`Entity`] of the toggled [`Checkbox`]
132    pub entity: Entity,
133}
134
135fn checkbox_on_set_checked(
136    set_checked: On<SetChecked>,
137    q_checkbox: Query<(Has<Checked>, Has<InteractionDisabled>), With<Checkbox>>,
138    mut commands: Commands,
139) {
140    if let Ok((is_checked, disabled)) = q_checkbox.get(set_checked.entity) {
141        if disabled {
142            return;
143        }
144
145        let will_be_checked = set_checked.checked;
146        if will_be_checked != is_checked {
147            commands.trigger(ValueChange {
148                source: set_checked.entity,
149                value: will_be_checked,
150            });
151        }
152    }
153}
154
155fn checkbox_on_toggle_checked(
156    toggle_checked: On<ToggleChecked>,
157    q_checkbox: Query<(Has<Checked>, Has<InteractionDisabled>), With<Checkbox>>,
158    mut commands: Commands,
159) {
160    if let Ok((is_checked, disabled)) = q_checkbox.get(toggle_checked.entity) {
161        if disabled {
162            return;
163        }
164
165        commands.trigger(ValueChange {
166            source: toggle_checked.entity,
167            value: !is_checked,
168        });
169    }
170}
171
172/// Plugin that adds the observers for the [`Checkbox`] widget.
173pub struct CheckboxPlugin;
174
175impl Plugin for CheckboxPlugin {
176    fn build(&self, app: &mut App) {
177        app.add_observer(checkbox_on_key_input)
178            .add_observer(checkbox_on_pointer_click)
179            .add_observer(checkbox_on_set_checked)
180            .add_observer(checkbox_on_toggle_checked);
181    }
182}
183
184/// Observer function which updates the checkbox value in response to a [`ValueChange`] event.
185/// This can be used to make the checkbox automatically update its own state when clicked,
186/// as opposed to managing the checkbox state externally.
187pub fn checkbox_self_update(value_change: On<ValueChange<bool>>, mut commands: Commands) {
188    if value_change.value {
189        commands.entity(value_change.source).insert(Checked);
190    } else {
191        commands.entity(value_change.source).remove::<Checked>();
192    }
193}