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
This commit is contained in:
Talin 2025-06-30 20:23:38 -07:00 committed by GitHub
parent c6ba3d31cf
commit 7b6c5f4431
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 217 additions and 95 deletions

View File

@ -0,0 +1,113 @@
use bevy_ecs::system::{Commands, SystemId, SystemInput};
use bevy_ecs::world::{DeferredWorld, World};
/// A callback defines how we want to be notified when a widget changes state. Unlike an event
/// or observer, callbacks are intended for "point-to-point" communication that cuts across the
/// hierarchy of entities. Callbacks can be created in advance of the entity they are attached
/// to, and can be passed around as parameters.
///
/// Example:
/// ```
/// use bevy_app::App;
/// use bevy_core_widgets::{Callback, Notify};
/// use bevy_ecs::system::{Commands, IntoSystem};
///
/// let mut app = App::new();
///
/// // Register a one-shot system
/// fn my_callback_system() {
/// println!("Callback executed!");
/// }
///
/// let system_id = app.world_mut().register_system(my_callback_system);
///
/// // Wrap system in a callback
/// let callback = Callback::System(system_id);
///
/// // Later, when we want to execute the callback:
/// app.world_mut().commands().notify(&callback);
/// ```
#[derive(Default, Debug)]
pub enum Callback<I: SystemInput = ()> {
/// Invoke a one-shot system
System(SystemId<I>),
/// Ignore this notification
#[default]
Ignore,
}
/// Trait used to invoke a [`Callback`], unifying the API across callers.
pub trait Notify {
/// Invoke the callback with no arguments.
fn notify(&mut self, callback: &Callback<()>);
/// Invoke the callback with one argument.
fn notify_with<I>(&mut self, callback: &Callback<I>, input: I::Inner<'static>)
where
I: SystemInput<Inner<'static>: Send> + 'static;
}
impl<'w, 's> Notify for Commands<'w, 's> {
fn notify(&mut self, callback: &Callback<()>) {
match callback {
Callback::System(system_id) => self.run_system(*system_id),
Callback::Ignore => (),
}
}
fn notify_with<I>(&mut self, callback: &Callback<I>, input: I::Inner<'static>)
where
I: SystemInput<Inner<'static>: Send> + 'static,
{
match callback {
Callback::System(system_id) => self.run_system_with(*system_id, input),
Callback::Ignore => (),
}
}
}
impl Notify for World {
fn notify(&mut self, callback: &Callback<()>) {
match callback {
Callback::System(system_id) => {
let _ = self.run_system(*system_id);
}
Callback::Ignore => (),
}
}
fn notify_with<I>(&mut self, callback: &Callback<I>, input: I::Inner<'static>)
where
I: SystemInput<Inner<'static>: Send> + 'static,
{
match callback {
Callback::System(system_id) => {
let _ = self.run_system_with(*system_id, input);
}
Callback::Ignore => (),
}
}
}
impl Notify for DeferredWorld<'_> {
fn notify(&mut self, callback: &Callback<()>) {
match callback {
Callback::System(system_id) => {
self.commands().run_system(*system_id);
}
Callback::Ignore => (),
}
}
fn notify_with<I>(&mut self, callback: &Callback<I>, input: I::Inner<'static>)
where
I: SystemInput<Inner<'static>: Send> + 'static,
{
match callback {
Callback::System(system_id) => {
self.commands().run_system_with(*system_id, input);
}
Callback::Ignore => (),
}
}
}

View File

@ -7,7 +7,7 @@ use bevy_ecs::{
entity::Entity,
observer::On,
query::With,
system::{Commands, Query, SystemId},
system::{Commands, Query},
};
use bevy_input::keyboard::{KeyCode, KeyboardInput};
use bevy_input::ButtonState;
@ -15,16 +15,17 @@ use bevy_input_focus::FocusedInput;
use bevy_picking::events::{Cancel, Click, DragEnd, Pointer, Press, Release};
use bevy_ui::{InteractionDisabled, Pressed};
use crate::{Callback, Notify};
/// Headless button widget. This widget maintains a "pressed" state, which is used to
/// indicate whether the button is currently being pressed by the user. It emits a `ButtonClicked`
/// event when the button is un-pressed.
#[derive(Component, Default, Debug)]
#[require(AccessibilityNode(accesskit::Node::new(Role::Button)))]
pub struct CoreButton {
/// Optional system to run when the button is clicked, or when the Enter or Space key
/// is pressed while the button is focused. If this field is `None`, the button will
/// emit a `ButtonClicked` event when clicked.
pub on_click: Option<SystemId>,
/// Callback to invoke when the button is clicked, or when the `Enter` or `Space` key
/// is pressed while the button is focused.
pub on_activate: Callback,
}
fn button_on_key_event(
@ -39,10 +40,8 @@ fn button_on_key_event(
&& event.state == ButtonState::Pressed
&& (event.key_code == KeyCode::Enter || event.key_code == KeyCode::Space)
{
if let Some(on_click) = bstate.on_click {
trigger.propagate(false);
commands.run_system(on_click);
}
commands.notify(&bstate.on_activate);
}
}
}
@ -56,9 +55,7 @@ fn button_on_pointer_click(
if let Ok((bstate, pressed, disabled)) = q_state.get_mut(trigger.target()) {
trigger.propagate(false);
if pressed && !disabled {
if let Some(on_click) = bstate.on_click {
commands.run_system(on_click);
}
commands.notify(&bstate.on_activate);
}
}
}

View File

@ -7,7 +7,7 @@ use bevy_ecs::system::{In, ResMut};
use bevy_ecs::{
component::Component,
observer::On,
system::{Commands, Query, SystemId},
system::{Commands, Query},
};
use bevy_input::keyboard::{KeyCode, KeyboardInput};
use bevy_input::ButtonState;
@ -15,11 +15,13 @@ use bevy_input_focus::{FocusedInput, InputFocus, InputFocusVisible};
use bevy_picking::events::{Click, Pointer};
use bevy_ui::{Checkable, Checked, InteractionDisabled};
use crate::{Callback, Notify as _};
/// Headless widget implementation for checkboxes. The [`Checked`] component represents the current
/// state of the checkbox. The `on_change` field is an optional system id that will be run when the
/// checkbox is clicked, or when the `Enter` or `Space` key is pressed while the checkbox is
/// focused. If the `on_change` field is `None`, then instead of calling a callback, the checkbox
/// will update its own [`Checked`] state directly.
/// focused. If the `on_change` field is `Callback::Ignore`, then instead of calling a callback, the
/// checkbox will update its own [`Checked`] state directly.
///
/// # Toggle switches
///
@ -29,8 +31,10 @@ use bevy_ui::{Checkable, Checked, InteractionDisabled};
#[derive(Component, Debug, Default)]
#[require(AccessibilityNode(accesskit::Node::new(Role::CheckBox)), Checkable)]
pub struct CoreCheckbox {
/// One-shot system that is run when the checkbox state needs to be changed.
pub on_change: Option<SystemId<In<bool>>>,
/// One-shot system that is run when the checkbox state needs to be changed. If this value is
/// `Callback::Ignore`, then the checkbox will update it's own internal [`Checked`] state
/// without notification.
pub on_change: Callback<In<bool>>,
}
fn checkbox_on_key_input(
@ -157,8 +161,8 @@ fn set_checkbox_state(
checkbox: &CoreCheckbox,
new_state: bool,
) {
if let Some(on_change) = checkbox.on_change {
commands.run_system_with(on_change, new_state);
if !matches!(checkbox.on_change, Callback::Ignore) {
commands.notify_with(&checkbox.on_change, new_state);
} else if new_state {
commands.entity(entity.into()).insert(Checked);
} else {

View File

@ -9,7 +9,7 @@ use bevy_ecs::{
entity::Entity,
observer::On,
query::With,
system::{Commands, Query, SystemId},
system::{Commands, Query},
};
use bevy_input::keyboard::{KeyCode, KeyboardInput};
use bevy_input::ButtonState;
@ -17,6 +17,8 @@ use bevy_input_focus::FocusedInput;
use bevy_picking::events::{Click, Pointer};
use bevy_ui::{Checkable, Checked, InteractionDisabled};
use crate::{Callback, Notify};
/// Headless widget implementation for a "radio button group". This component is used to group
/// multiple [`CoreRadio`] components together, allowing them to behave as a single unit. It
/// implements the tab navigation logic and keyboard shortcuts for radio buttons.
@ -36,7 +38,7 @@ use bevy_ui::{Checkable, Checked, InteractionDisabled};
#[require(AccessibilityNode(accesskit::Node::new(Role::RadioGroup)))]
pub struct CoreRadioGroup {
/// Callback which is called when the selected radio button changes.
pub on_change: Option<SystemId<In<Entity>>>,
pub on_change: Callback<In<Entity>>,
}
/// Headless widget implementation for radio buttons. These should be enclosed within a
@ -131,9 +133,7 @@ fn radio_group_on_key_input(
let (next_id, _) = radio_buttons[next_index];
// Trigger the on_change event for the newly checked radio button
if let Some(on_change) = on_change {
commands.run_system_with(*on_change, next_id);
}
commands.notify_with(on_change, next_id);
}
}
}
@ -196,9 +196,7 @@ fn radio_group_on_button_click(
}
// Trigger the on_change event for the newly checked radio button
if let Some(on_change) = on_change {
commands.run_system_with(*on_change, radio_id);
}
commands.notify_with(on_change, radio_id);
}
}

View File

@ -13,7 +13,7 @@ use bevy_ecs::{
component::Component,
observer::On,
query::With,
system::{Commands, Query, SystemId},
system::{Commands, Query},
};
use bevy_input::keyboard::{KeyCode, KeyboardInput};
use bevy_input::ButtonState;
@ -22,6 +22,8 @@ use bevy_log::warn_once;
use bevy_picking::events::{Drag, DragEnd, DragStart, Pointer, Press};
use bevy_ui::{ComputedNode, ComputedNodeTarget, InteractionDisabled, UiGlobalTransform, UiScale};
use crate::{Callback, Notify};
/// Defines how the slider should behave when you click on the track (not the thumb).
#[derive(Debug, Default, PartialEq, Clone, Copy)]
pub enum TrackClick {
@ -72,8 +74,9 @@ pub enum TrackClick {
)]
pub struct CoreSlider {
/// Callback which is called when the slider is dragged or the value is changed via other user
/// interaction. If this value is `None`, then the slider will self-update.
pub on_change: Option<SystemId<In<f32>>>,
/// interaction. If this value is `Callback::Ignore`, then the slider will update it's own
/// internal [`SliderValue`] state without notification.
pub on_change: Callback<In<f32>>,
/// Set the track-clicking behavior for this slider.
pub track_click: TrackClick,
// TODO: Think about whether we want a "vertical" option.
@ -257,12 +260,12 @@ pub(crate) fn slider_on_pointer_down(
TrackClick::Snap => click_val,
});
if let Some(on_change) = slider.on_change {
commands.run_system_with(on_change, new_value);
} else {
if matches!(slider.on_change, Callback::Ignore) {
commands
.entity(trigger.target())
.insert(SliderValue(new_value));
} else {
commands.notify_with(&slider.on_change, new_value);
}
}
}
@ -322,12 +325,12 @@ pub(crate) fn slider_on_drag(
range.start() + span * 0.5
};
if let Some(on_change) = slider.on_change {
commands.run_system_with(on_change, new_value);
} else {
if matches!(slider.on_change, Callback::Ignore) {
commands
.entity(trigger.target())
.insert(SliderValue(new_value));
} else {
commands.notify_with(&slider.on_change, new_value);
}
}
}
@ -369,12 +372,12 @@ fn slider_on_key_input(
}
};
trigger.propagate(false);
if let Some(on_change) = slider.on_change {
commands.run_system_with(on_change, new_value);
} else {
if matches!(slider.on_change, Callback::Ignore) {
commands
.entity(trigger.target())
.insert(SliderValue(new_value));
} else {
commands.notify_with(&slider.on_change, new_value);
}
}
}
@ -461,12 +464,12 @@ fn slider_on_set_value(
range.clamp(value.0 + *delta * step.map(|s| s.0).unwrap_or_default())
}
};
if let Some(on_change) = slider.on_change {
commands.run_system_with(on_change, new_value);
} else {
if matches!(slider.on_change, Callback::Ignore) {
commands
.entity(trigger.target())
.insert(SliderValue(new_value));
} else {
commands.notify_with(&slider.on_change, new_value);
}
}
}

View File

@ -14,6 +14,7 @@
// styled/opinionated widgets that use them. Components which are directly exposed to users above
// the widget level, like `SliderValue`, should not have the `Core` prefix.
mod callback;
mod core_button;
mod core_checkbox;
mod core_radio;
@ -22,6 +23,7 @@ mod core_slider;
use bevy_app::{App, Plugin};
pub use callback::{Callback, Notify};
pub use core_button::{CoreButton, CoreButtonPlugin};
pub use core_checkbox::{CoreCheckbox, CoreCheckboxPlugin, SetChecked, ToggleChecked};
pub use core_radio::{CoreRadio, CoreRadioGroup, CoreRadioGroupPlugin};

View File

@ -1,5 +1,5 @@
use bevy_app::{Plugin, PreUpdate};
use bevy_core_widgets::CoreButton;
use bevy_core_widgets::{Callback, CoreButton};
use bevy_ecs::{
bundle::Bundle,
component::Component,
@ -9,7 +9,7 @@ use bevy_ecs::{
query::{Added, Changed, Has, Or},
schedule::IntoScheduleConfigs,
spawn::{SpawnRelated, SpawnableList},
system::{Commands, Query, SystemId},
system::{Commands, Query},
};
use bevy_input_focus::tab_navigation::TabIndex;
use bevy_picking::{hover::Hovered, PickingSystems};
@ -38,14 +38,14 @@ pub enum ButtonVariant {
}
/// Parameters for the button template, passed to [`button`] function.
#[derive(Default, Clone)]
#[derive(Default)]
pub struct ButtonProps {
/// Color variant for the button.
pub variant: ButtonVariant,
/// Rounded corners options
pub corners: RoundedCorners,
/// Click handler
pub on_click: Option<SystemId>,
pub on_click: Callback,
}
/// Template function to spawn a button.
@ -69,7 +69,7 @@ pub fn button<C: SpawnableList<ChildOf> + Send + Sync + 'static, B: Bundle>(
..Default::default()
},
CoreButton {
on_click: props.on_click,
on_activate: props.on_click,
},
props.variant,
Hovered::default(),

View File

@ -2,7 +2,7 @@ use core::f32::consts::PI;
use bevy_app::{Plugin, PreUpdate};
use bevy_color::Color;
use bevy_core_widgets::{CoreSlider, SliderRange, SliderValue, TrackClick};
use bevy_core_widgets::{Callback, CoreSlider, SliderRange, SliderValue, TrackClick};
use bevy_ecs::{
bundle::Bundle,
children,
@ -13,7 +13,7 @@ use bevy_ecs::{
query::{Added, Changed, Has, Or, Spawned, With},
schedule::IntoScheduleConfigs,
spawn::SpawnRelated,
system::{In, Query, Res, SystemId},
system::{In, Query, Res},
};
use bevy_input_focus::tab_navigation::TabIndex;
use bevy_picking::PickingSystems;
@ -34,7 +34,6 @@ use crate::{
};
/// Slider template properties, passed to [`slider`] function.
#[derive(Clone)]
pub struct SliderProps {
/// Slider current value
pub value: f32,
@ -43,7 +42,7 @@ pub struct SliderProps {
/// Slider maximum value
pub max: f32,
/// On-change handler
pub on_change: Option<SystemId<In<f32>>>,
pub on_change: Callback<In<f32>>,
}
impl Default for SliderProps {
@ -52,7 +51,7 @@ impl Default for SliderProps {
value: 0.0,
min: 0.0,
max: 1.0,
on_change: None,
on_change: Callback::Ignore,
}
}
}

View File

@ -3,10 +3,10 @@
use bevy::{
color::palettes::basic::*,
core_widgets::{
CoreButton, CoreCheckbox, CoreRadio, CoreRadioGroup, CoreSlider, CoreSliderDragState,
CoreSliderThumb, CoreWidgetsPlugin, SliderRange, SliderValue, TrackClick,
Callback, CoreButton, CoreCheckbox, CoreRadio, CoreRadioGroup, CoreSlider,
CoreSliderDragState, CoreSliderThumb, CoreWidgetsPlugin, SliderRange, SliderValue,
TrackClick,
},
ecs::system::SystemId,
input_focus::{
tab_navigation::{TabGroup, TabIndex, TabNavigationPlugin},
InputDispatchPlugin,
@ -146,17 +146,17 @@ fn setup(mut commands: Commands, assets: Res<AssetServer>) {
commands.spawn(Camera2d);
commands.spawn(demo_root(
&assets,
on_click,
on_change_value,
on_change_radio,
Callback::System(on_click),
Callback::System(on_change_value),
Callback::System(on_change_radio),
));
}
fn demo_root(
asset_server: &AssetServer,
on_click: SystemId,
on_change_value: SystemId<In<f32>>,
on_change_radio: SystemId<In<Entity>>,
on_click: Callback,
on_change_value: Callback<In<f32>>,
on_change_radio: Callback<In<Entity>>,
) -> impl Bundle {
(
Node {
@ -172,15 +172,15 @@ fn demo_root(
TabGroup::default(),
children![
button(asset_server, on_click),
slider(0.0, 100.0, 50.0, Some(on_change_value)),
checkbox(asset_server, "Checkbox", None),
radio_group(asset_server, Some(on_change_radio)),
slider(0.0, 100.0, 50.0, on_change_value),
checkbox(asset_server, "Checkbox", Callback::Ignore),
radio_group(asset_server, on_change_radio),
Text::new("Press 'D' to toggle widget disabled states"),
],
)
}
fn button(asset_server: &AssetServer, on_click: SystemId) -> impl Bundle {
fn button(asset_server: &AssetServer, on_click: Callback) -> impl Bundle {
(
Node {
width: Val::Px(150.0),
@ -192,7 +192,7 @@ fn button(asset_server: &AssetServer, on_click: SystemId) -> impl Bundle {
},
DemoButton,
CoreButton {
on_click: Some(on_click),
on_activate: on_click,
},
Hovered::default(),
TabIndex(0),
@ -323,7 +323,7 @@ fn set_button_style(
}
/// Create a demo slider
fn slider(min: f32, max: f32, value: f32, on_change: Option<SystemId<In<f32>>>) -> impl Bundle {
fn slider(min: f32, max: f32, value: f32, on_change: Callback<In<f32>>) -> impl Bundle {
(
Node {
display: Display::Flex,
@ -468,7 +468,7 @@ fn thumb_color(disabled: bool, hovered: bool) -> Color {
fn checkbox(
asset_server: &AssetServer,
caption: &str,
on_change: Option<SystemId<In<bool>>>,
on_change: Callback<In<bool>>,
) -> impl Bundle {
(
Node {
@ -661,7 +661,7 @@ fn set_checkbox_or_radio_style(
}
/// Create a demo radio group
fn radio_group(asset_server: &AssetServer, on_change: Option<SystemId<In<Entity>>>) -> impl Bundle {
fn radio_group(asset_server: &AssetServer, on_change: Callback<In<Entity>>) -> impl Bundle {
(
Node {
display: Display::Flex,

View File

@ -3,8 +3,8 @@
use bevy::{
color::palettes::basic::*,
core_widgets::{
CoreButton, CoreCheckbox, CoreSlider, CoreSliderThumb, CoreWidgetsPlugin, SliderRange,
SliderValue,
Callback, CoreButton, CoreCheckbox, CoreSlider, CoreSliderThumb, CoreWidgetsPlugin,
SliderRange, SliderValue,
},
ecs::system::SystemId,
input_focus::{
@ -120,15 +120,15 @@ fn demo_root(
},
TabGroup::default(),
children![
button(asset_server, on_click),
slider(0.0, 100.0, 50.0, Some(on_change_value)),
checkbox(asset_server, "Checkbox", None),
button(asset_server, Callback::System(on_click)),
slider(0.0, 100.0, 50.0, Callback::System(on_change_value)),
checkbox(asset_server, "Checkbox", Callback::Ignore),
Text::new("Press 'D' to toggle widget disabled states"),
],
)
}
fn button(asset_server: &AssetServer, on_click: SystemId) -> impl Bundle {
fn button(asset_server: &AssetServer, on_click: Callback) -> impl Bundle {
(
Node {
width: Val::Px(150.0),
@ -140,7 +140,7 @@ fn button(asset_server: &AssetServer, on_click: SystemId) -> impl Bundle {
},
DemoButton,
CoreButton {
on_click: Some(on_click),
on_activate: on_click,
},
Hovered::default(),
TabIndex(0),
@ -351,7 +351,7 @@ fn set_button_style(
}
/// Create a demo slider
fn slider(min: f32, max: f32, value: f32, on_change: Option<SystemId<In<f32>>>) -> impl Bundle {
fn slider(min: f32, max: f32, value: f32, on_change: Callback<In<f32>>) -> impl Bundle {
(
Node {
display: Display::Flex,
@ -517,7 +517,7 @@ fn thumb_color(disabled: bool, hovered: bool) -> Color {
fn checkbox(
asset_server: &AssetServer,
caption: &str,
on_change: Option<SystemId<In<bool>>>,
on_change: Callback<In<bool>>,
) -> impl Bundle {
(
Node {

View File

@ -1,7 +1,7 @@
//! This example shows off the various Bevy Feathers widgets.
use bevy::{
core_widgets::{CoreWidgetsPlugin, SliderStep},
core_widgets::{Callback, CoreWidgetsPlugin, SliderStep},
feathers::{
controls::{button, slider, ButtonProps, ButtonVariant, SliderProps},
dark_theme::create_dark_theme,
@ -80,7 +80,7 @@ fn demo_root(commands: &mut Commands) -> impl Bundle {
children![
button(
ButtonProps {
on_click: Some(commands.register_system(|| {
on_click: Callback::System(commands.register_system(|| {
info!("Normal button clicked!");
})),
..default()
@ -90,7 +90,7 @@ fn demo_root(commands: &mut Commands) -> impl Bundle {
),
button(
ButtonProps {
on_click: Some(commands.register_system(|| {
on_click: Callback::System(commands.register_system(|| {
info!("Disabled button clicked!");
})),
..default()
@ -100,7 +100,7 @@ fn demo_root(commands: &mut Commands) -> impl Bundle {
),
button(
ButtonProps {
on_click: Some(commands.register_system(|| {
on_click: Callback::System(commands.register_system(|| {
info!("Primary button clicked!");
})),
variant: ButtonVariant::Primary,
@ -123,7 +123,7 @@ fn demo_root(commands: &mut Commands) -> impl Bundle {
children![
button(
ButtonProps {
on_click: Some(commands.register_system(|| {
on_click: Callback::System(commands.register_system(|| {
info!("Left button clicked!");
})),
corners: RoundedCorners::Left,
@ -134,7 +134,7 @@ fn demo_root(commands: &mut Commands) -> impl Bundle {
),
button(
ButtonProps {
on_click: Some(commands.register_system(|| {
on_click: Callback::System(commands.register_system(|| {
info!("Center button clicked!");
})),
corners: RoundedCorners::None,
@ -145,7 +145,7 @@ fn demo_root(commands: &mut Commands) -> impl Bundle {
),
button(
ButtonProps {
on_click: Some(commands.register_system(|| {
on_click: Callback::System(commands.register_system(|| {
info!("Right button clicked!");
})),
variant: ButtonVariant::Primary,
@ -158,7 +158,7 @@ fn demo_root(commands: &mut Commands) -> impl Bundle {
),
button(
ButtonProps {
on_click: Some(commands.register_system(|| {
on_click: Callback::System(commands.register_system(|| {
info!("Wide button clicked!");
})),
..default()

View File

@ -1,6 +1,6 @@
---
title: Headless Widgets
authors: ["@viridia"]
authors: ["@viridia", "@ickshonpe", "@alice-i-cecile"]
pull_requests: [19366, 19584, 19665, 19778, 19803]
---
@ -38,7 +38,7 @@ sliders, checkboxes and radio buttons.
- `CoreCheckbox` can be used for checkboxes and toggle switches.
- `CoreRadio` and `CoreRadioGroup` can be used for radio buttons.
## Widget Interaction States
## Widget Interaction Marker Components
Many of the core widgets will define supplementary ECS components that are used to store the widget's
state, similar to how the old `Interaction` component worked, but in a way that is more flexible.
@ -65,13 +65,18 @@ Applications need a way to be notified when the user interacts with a widget. On
is using Bevy observers. This approach is useful in cases where you want the widget notifications
to bubble up the hierarchy.
However, in UI work it's often desirable to connect widget interactions in ways that cut across the
hierarchy. For these kinds of situations, the core widgets offer a different approach: one-shot
systems. You can register a function as a one-shot system and get the resulting `SystemId`. This can
then be passed as a parameter to the widget when it is constructed, so when the button subsequently
However, in UI work it's often desirable to send notifications "point-to-point" in ways that cut
across the hierarchy. For these kinds of situations, the core widgets offer a different
approach: callbacks. The `Callback` enum allows different options for triggering a notification
when a widget's state is updated. For example, you can pass in the `SystemId` of a registered
one-shot system as a widget parameter when it is constructed. When the button subsequently
gets clicked or the slider is dragged, the system gets run. Because it's an ECS system, it can
inject any additional parameters it needs to update the Bevy world in response to the interaction.
## State Management
See the [Wikipedia Article on State Management](https://en.wikipedia.org/wiki/State_management).
Most of the core widgets support "external state management" - something that is referred to in the
React.js world as "controlled" widgets. This means that for widgets that edit a parameter value
(such as checkboxes and sliders), the widget doesn't automatically update its own internal value,
@ -86,9 +91,10 @@ interacting with that widget. Externalizing the state avoids the need for two-wa
instead allows simpler one-way data binding that aligns well with the traditional "Model / View /
Controller" (MVC) design pattern.
That being said, the choice of internal or external state management is up to you: if the widget
has an `on_change` callback that is not `None`, then the callback is used. If the callback
is `None`, however, the widget will update its own state. (This is similar to how React.js does it.)
That being said, the choice of internal or external state management is up to you: if the widget has
an `on_change` callback that is not `Callback::Ignore`, then the callback is used. If the callback
is `Callback::Ignore`, however, the widget will update its own state automatically. (This is similar
to how React.js does it.)
There are two exceptions to this rule about external state management. First, widgets which don't
edit a value, but which merely trigger an event (such as buttons), don't fall under this rule.