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:
parent
c6ba3d31cf
commit
7b6c5f4431
113
crates/bevy_core_widgets/src/callback.rs
Normal file
113
crates/bevy_core_widgets/src/callback.rs
Normal 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 => (),
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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};
|
||||
|
@ -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(),
|
||||
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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 {
|
||||
|
@ -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()
|
||||
|
@ -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.
|
||||
|
Loading…
Reference in New Issue
Block a user