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,
|
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 {
|
||||||
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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};
|
||||||
|
@ -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(),
|
||||||
|
@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
|
@ -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 {
|
||||||
|
@ -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()
|
||||||
|
@ -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.
|
||||||
|
Loading…
Reference in New Issue
Block a user