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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,6 @@
--- ---
title: Headless Widgets title: Headless Widgets
authors: ["@viridia"] authors: ["@viridia", "@ickshonpe", "@alice-i-cecile"]
pull_requests: [19366, 19584, 19665, 19778, 19803] 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. - `CoreCheckbox` can be used for checkboxes and toggle switches.
- `CoreRadio` and `CoreRadioGroup` can be used for radio buttons. - `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 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. 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 is using Bevy observers. This approach is useful in cases where you want the widget notifications
to bubble up the hierarchy. to bubble up the hierarchy.
However, in UI work it's often desirable to connect widget interactions in ways that cut across the However, in UI work it's often desirable to send notifications "point-to-point" in ways that cut
hierarchy. For these kinds of situations, the core widgets offer a different approach: one-shot across the hierarchy. For these kinds of situations, the core widgets offer a different
systems. You can register a function as a one-shot system and get the resulting `SystemId`. This can approach: callbacks. The `Callback` enum allows different options for triggering a notification
then be passed as a parameter to the widget when it is constructed, so when the button subsequently 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 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. 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 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 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, (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 / instead allows simpler one-way data binding that aligns well with the traditional "Model / View /
Controller" (MVC) design pattern. Controller" (MVC) design pattern.
That being said, the choice of internal or external state management is up to you: if the widget That being said, the choice of internal or external state management is up to you: if the widget has
has an `on_change` callback that is not `None`, then the callback is used. If the callback an `on_change` callback that is not `Callback::Ignore`, 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.) 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 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. edit a value, but which merely trigger an event (such as buttons), don't fall under this rule.