diff --git a/crates/bevy_core_widgets/src/callback.rs b/crates/bevy_core_widgets/src/callback.rs new file mode 100644 index 0000000000..37905e221c --- /dev/null +++ b/crates/bevy_core_widgets/src/callback.rs @@ -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 { + /// Invoke a one-shot system + System(SystemId), + /// 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(&mut self, callback: &Callback, input: I::Inner<'static>) + where + I: SystemInput: 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(&mut self, callback: &Callback, input: I::Inner<'static>) + where + I: SystemInput: 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(&mut self, callback: &Callback, input: I::Inner<'static>) + where + I: SystemInput: 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(&mut self, callback: &Callback, input: I::Inner<'static>) + where + I: SystemInput: Send> + 'static, + { + match callback { + Callback::System(system_id) => { + self.commands().run_system_with(*system_id, input); + } + Callback::Ignore => (), + } + } +} diff --git a/crates/bevy_core_widgets/src/core_button.rs b/crates/bevy_core_widgets/src/core_button.rs index 0c0c638d20..8c4ec9b22e 100644 --- a/crates/bevy_core_widgets/src/core_button.rs +++ b/crates/bevy_core_widgets/src/core_button.rs @@ -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, + /// 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); - } + trigger.propagate(false); + 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); } } } diff --git a/crates/bevy_core_widgets/src/core_checkbox.rs b/crates/bevy_core_widgets/src/core_checkbox.rs index fc12811055..05edc53c44 100644 --- a/crates/bevy_core_widgets/src/core_checkbox.rs +++ b/crates/bevy_core_widgets/src/core_checkbox.rs @@ -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>>, + /// 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>, } 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 { diff --git a/crates/bevy_core_widgets/src/core_radio.rs b/crates/bevy_core_widgets/src/core_radio.rs index a0857575c4..6e6fd82d0c 100644 --- a/crates/bevy_core_widgets/src/core_radio.rs +++ b/crates/bevy_core_widgets/src/core_radio.rs @@ -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>>, + pub on_change: Callback>, } /// 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); } } diff --git a/crates/bevy_core_widgets/src/core_slider.rs b/crates/bevy_core_widgets/src/core_slider.rs index 2fab75738a..8a5e27f885 100644 --- a/crates/bevy_core_widgets/src/core_slider.rs +++ b/crates/bevy_core_widgets/src/core_slider.rs @@ -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>>, + /// interaction. If this value is `Callback::Ignore`, then the slider will update it's own + /// internal [`SliderValue`] state without notification. + pub on_change: Callback>, /// 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); } } } diff --git a/crates/bevy_core_widgets/src/lib.rs b/crates/bevy_core_widgets/src/lib.rs index a0ddfa5eb8..2a3fc1ac09 100644 --- a/crates/bevy_core_widgets/src/lib.rs +++ b/crates/bevy_core_widgets/src/lib.rs @@ -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}; diff --git a/crates/bevy_feathers/src/controls/button.rs b/crates/bevy_feathers/src/controls/button.rs index fd12a8ef1b..5b6ad7117b 100644 --- a/crates/bevy_feathers/src/controls/button.rs +++ b/crates/bevy_feathers/src/controls/button.rs @@ -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, + pub on_click: Callback, } /// Template function to spawn a button. @@ -69,7 +69,7 @@ pub fn button + Send + Sync + 'static, B: Bundle>( ..Default::default() }, CoreButton { - on_click: props.on_click, + on_activate: props.on_click, }, props.variant, Hovered::default(), diff --git a/crates/bevy_feathers/src/controls/slider.rs b/crates/bevy_feathers/src/controls/slider.rs index f4773fb343..fa1978e06c 100644 --- a/crates/bevy_feathers/src/controls/slider.rs +++ b/crates/bevy_feathers/src/controls/slider.rs @@ -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>>, + pub on_change: Callback>, } 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, } } } diff --git a/examples/ui/core_widgets.rs b/examples/ui/core_widgets.rs index 7bf1e99966..318b824d0e 100644 --- a/examples/ui/core_widgets.rs +++ b/examples/ui/core_widgets.rs @@ -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) { 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>, - on_change_radio: SystemId>, + on_click: Callback, + on_change_value: Callback>, + on_change_radio: Callback>, ) -> 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>>) -> impl Bundle { +fn slider(min: f32, max: f32, value: f32, on_change: Callback>) -> 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>>, + on_change: Callback>, ) -> 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>>) -> impl Bundle { +fn radio_group(asset_server: &AssetServer, on_change: Callback>) -> impl Bundle { ( Node { display: Display::Flex, diff --git a/examples/ui/core_widgets_observers.rs b/examples/ui/core_widgets_observers.rs index 4e24a646b2..1ab4cda3b0 100644 --- a/examples/ui/core_widgets_observers.rs +++ b/examples/ui/core_widgets_observers.rs @@ -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>>) -> impl Bundle { +fn slider(min: f32, max: f32, value: f32, on_change: Callback>) -> 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>>, + on_change: Callback>, ) -> impl Bundle { ( Node { diff --git a/examples/ui/feathers.rs b/examples/ui/feathers.rs index 54bae222ec..c5954f45c5 100644 --- a/examples/ui/feathers.rs +++ b/examples/ui/feathers.rs @@ -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() diff --git a/release-content/release-notes/headless-widgets.md b/release-content/release-notes/headless-widgets.md index bb0398b43c..5e2a91c556 100644 --- a/release-content/release-notes/headless-widgets.md +++ b/release-content/release-notes/headless-widgets.md @@ -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.