Skip to content

Commit 7b6c5f4

Browse files
authored
Change core widgets to use callback enum instead of option (#19855)
# Objective Because we want to be able to support more notification options in the future (in addition to just using registered one-shot systems), the `Option<SystemId>` notifications have been changed to a new enum, `Callback`. @alice-i-cecile
1 parent c6ba3d3 commit 7b6c5f4

File tree

12 files changed

+217
-95
lines changed

12 files changed

+217
-95
lines changed
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
use bevy_ecs::system::{Commands, SystemId, SystemInput};
2+
use bevy_ecs::world::{DeferredWorld, World};
3+
4+
/// A callback defines how we want to be notified when a widget changes state. Unlike an event
5+
/// or observer, callbacks are intended for "point-to-point" communication that cuts across the
6+
/// hierarchy of entities. Callbacks can be created in advance of the entity they are attached
7+
/// to, and can be passed around as parameters.
8+
///
9+
/// Example:
10+
/// ```
11+
/// use bevy_app::App;
12+
/// use bevy_core_widgets::{Callback, Notify};
13+
/// use bevy_ecs::system::{Commands, IntoSystem};
14+
///
15+
/// let mut app = App::new();
16+
///
17+
/// // Register a one-shot system
18+
/// fn my_callback_system() {
19+
/// println!("Callback executed!");
20+
/// }
21+
///
22+
/// let system_id = app.world_mut().register_system(my_callback_system);
23+
///
24+
/// // Wrap system in a callback
25+
/// let callback = Callback::System(system_id);
26+
///
27+
/// // Later, when we want to execute the callback:
28+
/// app.world_mut().commands().notify(&callback);
29+
/// ```
30+
#[derive(Default, Debug)]
31+
pub enum Callback<I: SystemInput = ()> {
32+
/// Invoke a one-shot system
33+
System(SystemId<I>),
34+
/// Ignore this notification
35+
#[default]
36+
Ignore,
37+
}
38+
39+
/// Trait used to invoke a [`Callback`], unifying the API across callers.
40+
pub trait Notify {
41+
/// Invoke the callback with no arguments.
42+
fn notify(&mut self, callback: &Callback<()>);
43+
44+
/// Invoke the callback with one argument.
45+
fn notify_with<I>(&mut self, callback: &Callback<I>, input: I::Inner<'static>)
46+
where
47+
I: SystemInput<Inner<'static>: Send> + 'static;
48+
}
49+
50+
impl<'w, 's> Notify for Commands<'w, 's> {
51+
fn notify(&mut self, callback: &Callback<()>) {
52+
match callback {
53+
Callback::System(system_id) => self.run_system(*system_id),
54+
Callback::Ignore => (),
55+
}
56+
}
57+
58+
fn notify_with<I>(&mut self, callback: &Callback<I>, input: I::Inner<'static>)
59+
where
60+
I: SystemInput<Inner<'static>: Send> + 'static,
61+
{
62+
match callback {
63+
Callback::System(system_id) => self.run_system_with(*system_id, input),
64+
Callback::Ignore => (),
65+
}
66+
}
67+
}
68+
69+
impl Notify for World {
70+
fn notify(&mut self, callback: &Callback<()>) {
71+
match callback {
72+
Callback::System(system_id) => {
73+
let _ = self.run_system(*system_id);
74+
}
75+
Callback::Ignore => (),
76+
}
77+
}
78+
79+
fn notify_with<I>(&mut self, callback: &Callback<I>, input: I::Inner<'static>)
80+
where
81+
I: SystemInput<Inner<'static>: Send> + 'static,
82+
{
83+
match callback {
84+
Callback::System(system_id) => {
85+
let _ = self.run_system_with(*system_id, input);
86+
}
87+
Callback::Ignore => (),
88+
}
89+
}
90+
}
91+
92+
impl Notify for DeferredWorld<'_> {
93+
fn notify(&mut self, callback: &Callback<()>) {
94+
match callback {
95+
Callback::System(system_id) => {
96+
self.commands().run_system(*system_id);
97+
}
98+
Callback::Ignore => (),
99+
}
100+
}
101+
102+
fn notify_with<I>(&mut self, callback: &Callback<I>, input: I::Inner<'static>)
103+
where
104+
I: SystemInput<Inner<'static>: Send> + 'static,
105+
{
106+
match callback {
107+
Callback::System(system_id) => {
108+
self.commands().run_system_with(*system_id, input);
109+
}
110+
Callback::Ignore => (),
111+
}
112+
}
113+
}

crates/bevy_core_widgets/src/core_button.rs

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,24 +7,25 @@ use bevy_ecs::{
77
entity::Entity,
88
observer::On,
99
query::With,
10-
system::{Commands, Query, SystemId},
10+
system::{Commands, Query},
1111
};
1212
use bevy_input::keyboard::{KeyCode, KeyboardInput};
1313
use bevy_input::ButtonState;
1414
use bevy_input_focus::FocusedInput;
1515
use bevy_picking::events::{Cancel, Click, DragEnd, Pointer, Press, Release};
1616
use bevy_ui::{InteractionDisabled, Pressed};
1717

18+
use crate::{Callback, Notify};
19+
1820
/// Headless button widget. This widget maintains a "pressed" state, which is used to
1921
/// indicate whether the button is currently being pressed by the user. It emits a `ButtonClicked`
2022
/// event when the button is un-pressed.
2123
#[derive(Component, Default, Debug)]
2224
#[require(AccessibilityNode(accesskit::Node::new(Role::Button)))]
2325
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>,
26+
/// Callback to invoke when the button is clicked, or when the `Enter` or `Space` key
27+
/// is pressed while the button is focused.
28+
pub on_activate: Callback,
2829
}
2930

3031
fn button_on_key_event(
@@ -39,10 +40,8 @@ fn button_on_key_event(
3940
&& event.state == ButtonState::Pressed
4041
&& (event.key_code == KeyCode::Enter || event.key_code == KeyCode::Space)
4142
{
42-
if let Some(on_click) = bstate.on_click {
43-
trigger.propagate(false);
44-
commands.run_system(on_click);
45-
}
43+
trigger.propagate(false);
44+
commands.notify(&bstate.on_activate);
4645
}
4746
}
4847
}
@@ -56,9 +55,7 @@ fn button_on_pointer_click(
5655
if let Ok((bstate, pressed, disabled)) = q_state.get_mut(trigger.target()) {
5756
trigger.propagate(false);
5857
if pressed && !disabled {
59-
if let Some(on_click) = bstate.on_click {
60-
commands.run_system(on_click);
61-
}
58+
commands.notify(&bstate.on_activate);
6259
}
6360
}
6461
}

crates/bevy_core_widgets/src/core_checkbox.rs

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,19 +7,21 @@ use bevy_ecs::system::{In, ResMut};
77
use bevy_ecs::{
88
component::Component,
99
observer::On,
10-
system::{Commands, Query, SystemId},
10+
system::{Commands, Query},
1111
};
1212
use bevy_input::keyboard::{KeyCode, KeyboardInput};
1313
use bevy_input::ButtonState;
1414
use bevy_input_focus::{FocusedInput, InputFocus, InputFocusVisible};
1515
use bevy_picking::events::{Click, Pointer};
1616
use bevy_ui::{Checkable, Checked, InteractionDisabled};
1717

18+
use crate::{Callback, Notify as _};
19+
1820
/// Headless widget implementation for checkboxes. The [`Checked`] component represents the current
1921
/// state of the checkbox. The `on_change` field is an optional system id that will be run when the
2022
/// 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+
/// focused. If the `on_change` field is `Callback::Ignore`, then instead of calling a callback, the
24+
/// checkbox will update its own [`Checked`] state directly.
2325
///
2426
/// # Toggle switches
2527
///
@@ -29,8 +31,10 @@ use bevy_ui::{Checkable, Checked, InteractionDisabled};
2931
#[derive(Component, Debug, Default)]
3032
#[require(AccessibilityNode(accesskit::Node::new(Role::CheckBox)), Checkable)]
3133
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+
/// One-shot system that is run when the checkbox state needs to be changed. If this value is
35+
/// `Callback::Ignore`, then the checkbox will update it's own internal [`Checked`] state
36+
/// without notification.
37+
pub on_change: Callback<In<bool>>,
3438
}
3539

3640
fn checkbox_on_key_input(
@@ -157,8 +161,8 @@ fn set_checkbox_state(
157161
checkbox: &CoreCheckbox,
158162
new_state: bool,
159163
) {
160-
if let Some(on_change) = checkbox.on_change {
161-
commands.run_system_with(on_change, new_state);
164+
if !matches!(checkbox.on_change, Callback::Ignore) {
165+
commands.notify_with(&checkbox.on_change, new_state);
162166
} else if new_state {
163167
commands.entity(entity.into()).insert(Checked);
164168
} else {

crates/bevy_core_widgets/src/core_radio.rs

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,16 @@ use bevy_ecs::{
99
entity::Entity,
1010
observer::On,
1111
query::With,
12-
system::{Commands, Query, SystemId},
12+
system::{Commands, Query},
1313
};
1414
use bevy_input::keyboard::{KeyCode, KeyboardInput};
1515
use bevy_input::ButtonState;
1616
use bevy_input_focus::FocusedInput;
1717
use bevy_picking::events::{Click, Pointer};
1818
use bevy_ui::{Checkable, Checked, InteractionDisabled};
1919

20+
use crate::{Callback, Notify};
21+
2022
/// Headless widget implementation for a "radio button group". This component is used to group
2123
/// multiple [`CoreRadio`] components together, allowing them to behave as a single unit. It
2224
/// implements the tab navigation logic and keyboard shortcuts for radio buttons.
@@ -36,7 +38,7 @@ use bevy_ui::{Checkable, Checked, InteractionDisabled};
3638
#[require(AccessibilityNode(accesskit::Node::new(Role::RadioGroup)))]
3739
pub struct CoreRadioGroup {
3840
/// Callback which is called when the selected radio button changes.
39-
pub on_change: Option<SystemId<In<Entity>>>,
41+
pub on_change: Callback<In<Entity>>,
4042
}
4143

4244
/// Headless widget implementation for radio buttons. These should be enclosed within a
@@ -131,9 +133,7 @@ fn radio_group_on_key_input(
131133
let (next_id, _) = radio_buttons[next_index];
132134

133135
// Trigger the on_change event for the newly checked radio button
134-
if let Some(on_change) = on_change {
135-
commands.run_system_with(*on_change, next_id);
136-
}
136+
commands.notify_with(on_change, next_id);
137137
}
138138
}
139139
}
@@ -196,9 +196,7 @@ fn radio_group_on_button_click(
196196
}
197197

198198
// Trigger the on_change event for the newly checked radio button
199-
if let Some(on_change) = on_change {
200-
commands.run_system_with(*on_change, radio_id);
201-
}
199+
commands.notify_with(on_change, radio_id);
202200
}
203201
}
204202

crates/bevy_core_widgets/src/core_slider.rs

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ use bevy_ecs::{
1313
component::Component,
1414
observer::On,
1515
query::With,
16-
system::{Commands, Query, SystemId},
16+
system::{Commands, Query},
1717
};
1818
use bevy_input::keyboard::{KeyCode, KeyboardInput};
1919
use bevy_input::ButtonState;
@@ -22,6 +22,8 @@ use bevy_log::warn_once;
2222
use bevy_picking::events::{Drag, DragEnd, DragStart, Pointer, Press};
2323
use bevy_ui::{ComputedNode, ComputedNodeTarget, InteractionDisabled, UiGlobalTransform, UiScale};
2424

25+
use crate::{Callback, Notify};
26+
2527
/// Defines how the slider should behave when you click on the track (not the thumb).
2628
#[derive(Debug, Default, PartialEq, Clone, Copy)]
2729
pub enum TrackClick {
@@ -72,8 +74,9 @@ pub enum TrackClick {
7274
)]
7375
pub struct CoreSlider {
7476
/// Callback which is called when the slider is dragged or the value is changed via other user
75-
/// interaction. If this value is `None`, then the slider will self-update.
76-
pub on_change: Option<SystemId<In<f32>>>,
77+
/// interaction. If this value is `Callback::Ignore`, then the slider will update it's own
78+
/// internal [`SliderValue`] state without notification.
79+
pub on_change: Callback<In<f32>>,
7780
/// Set the track-clicking behavior for this slider.
7881
pub track_click: TrackClick,
7982
// TODO: Think about whether we want a "vertical" option.
@@ -257,12 +260,12 @@ pub(crate) fn slider_on_pointer_down(
257260
TrackClick::Snap => click_val,
258261
});
259262

260-
if let Some(on_change) = slider.on_change {
261-
commands.run_system_with(on_change, new_value);
262-
} else {
263+
if matches!(slider.on_change, Callback::Ignore) {
263264
commands
264265
.entity(trigger.target())
265266
.insert(SliderValue(new_value));
267+
} else {
268+
commands.notify_with(&slider.on_change, new_value);
266269
}
267270
}
268271
}
@@ -322,12 +325,12 @@ pub(crate) fn slider_on_drag(
322325
range.start() + span * 0.5
323326
};
324327

325-
if let Some(on_change) = slider.on_change {
326-
commands.run_system_with(on_change, new_value);
327-
} else {
328+
if matches!(slider.on_change, Callback::Ignore) {
328329
commands
329330
.entity(trigger.target())
330331
.insert(SliderValue(new_value));
332+
} else {
333+
commands.notify_with(&slider.on_change, new_value);
331334
}
332335
}
333336
}
@@ -369,12 +372,12 @@ fn slider_on_key_input(
369372
}
370373
};
371374
trigger.propagate(false);
372-
if let Some(on_change) = slider.on_change {
373-
commands.run_system_with(on_change, new_value);
374-
} else {
375+
if matches!(slider.on_change, Callback::Ignore) {
375376
commands
376377
.entity(trigger.target())
377378
.insert(SliderValue(new_value));
379+
} else {
380+
commands.notify_with(&slider.on_change, new_value);
378381
}
379382
}
380383
}
@@ -461,12 +464,12 @@ fn slider_on_set_value(
461464
range.clamp(value.0 + *delta * step.map(|s| s.0).unwrap_or_default())
462465
}
463466
};
464-
if let Some(on_change) = slider.on_change {
465-
commands.run_system_with(on_change, new_value);
466-
} else {
467+
if matches!(slider.on_change, Callback::Ignore) {
467468
commands
468469
.entity(trigger.target())
469470
.insert(SliderValue(new_value));
471+
} else {
472+
commands.notify_with(&slider.on_change, new_value);
470473
}
471474
}
472475
}

crates/bevy_core_widgets/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
// styled/opinionated widgets that use them. Components which are directly exposed to users above
1515
// the widget level, like `SliderValue`, should not have the `Core` prefix.
1616

17+
mod callback;
1718
mod core_button;
1819
mod core_checkbox;
1920
mod core_radio;
@@ -22,6 +23,7 @@ mod core_slider;
2223

2324
use bevy_app::{App, Plugin};
2425

26+
pub use callback::{Callback, Notify};
2527
pub use core_button::{CoreButton, CoreButtonPlugin};
2628
pub use core_checkbox::{CoreCheckbox, CoreCheckboxPlugin, SetChecked, ToggleChecked};
2729
pub use core_radio::{CoreRadio, CoreRadioGroup, CoreRadioGroupPlugin};

0 commit comments

Comments
 (0)