Changing the notification protocol for core_widgets.

Notifications now include the source entity. This is useful for
callbacks that are responsible for more than one widget.
This commit is contained in:
Talin 2025-07-11 08:52:54 -07:00
parent 20dfae9a2d
commit 4885b85bec
12 changed files with 140 additions and 73 deletions

View File

@ -2,6 +2,7 @@ use accesskit::Role;
use bevy_a11y::AccessibilityNode;
use bevy_app::{App, Plugin};
use bevy_ecs::query::Has;
use bevy_ecs::system::In;
use bevy_ecs::{
component::Component,
entity::Entity,
@ -15,7 +16,7 @@ use bevy_input_focus::FocusedInput;
use bevy_picking::events::{Cancel, Click, DragEnd, Pointer, Press, Release};
use bevy_ui::{InteractionDisabled, Pressed};
use crate::{Callback, Notify};
use crate::{Activate, 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`
@ -25,7 +26,7 @@ use crate::{Callback, Notify};
pub struct CoreButton {
/// 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,
pub on_activate: Callback<In<Activate>>,
}
fn button_on_key_event(
@ -41,7 +42,7 @@ fn button_on_key_event(
&& (event.key_code == KeyCode::Enter || event.key_code == KeyCode::Space)
{
trigger.propagate(false);
commands.notify(&bstate.on_activate);
commands.notify_with(&bstate.on_activate, Activate(trigger.target()));
}
}
}
@ -55,7 +56,7 @@ fn button_on_pointer_click(
if let Ok((bstate, pressed, disabled)) = q_state.get_mut(trigger.target()) {
trigger.propagate(false);
if pressed && !disabled {
commands.notify(&bstate.on_activate);
commands.notify_with(&bstate.on_activate, Activate(trigger.target()));
}
}
}

View File

@ -15,7 +15,7 @@ use bevy_input_focus::{FocusedInput, InputFocus, InputFocusVisible};
use bevy_picking::events::{Click, Pointer};
use bevy_ui::{Checkable, Checked, InteractionDisabled};
use crate::{Callback, Notify as _};
use crate::{Callback, Notify as _, ValueChange};
/// 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
@ -34,7 +34,7 @@ pub struct CoreCheckbox {
/// 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>>,
pub on_change: Callback<In<ValueChange<bool>>>,
}
fn checkbox_on_key_input(
@ -162,7 +162,13 @@ fn set_checkbox_state(
new_state: bool,
) {
if !matches!(checkbox.on_change, Callback::Ignore) {
commands.notify_with(&checkbox.on_change, new_state);
commands.notify_with(
&checkbox.on_change,
ValueChange {
source: entity.into(),
value: new_state,
},
);
} else if new_state {
commands.entity(entity.into()).insert(Checked);
} else {

View File

@ -6,7 +6,6 @@ use bevy_ecs::query::Has;
use bevy_ecs::system::In;
use bevy_ecs::{
component::Component,
entity::Entity,
observer::On,
query::With,
system::{Commands, Query},
@ -17,7 +16,7 @@ use bevy_input_focus::FocusedInput;
use bevy_picking::events::{Click, Pointer};
use bevy_ui::{Checkable, Checked, InteractionDisabled};
use crate::{Callback, Notify};
use crate::{Activate, 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
@ -38,7 +37,7 @@ use crate::{Callback, Notify};
#[require(AccessibilityNode(accesskit::Node::new(Role::RadioGroup)))]
pub struct CoreRadioGroup {
/// Callback which is called when the selected radio button changes.
pub on_change: Callback<In<Entity>>,
pub on_change: Callback<In<Activate>>,
}
/// Headless widget implementation for radio buttons. These should be enclosed within a
@ -133,7 +132,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
commands.notify_with(on_change, next_id);
commands.notify_with(on_change, Activate(next_id));
}
}
}
@ -201,7 +200,7 @@ fn radio_group_on_button_click(
}
// Trigger the on_change event for the newly checked radio button
commands.notify_with(on_change, radio_id);
commands.notify_with(on_change, Activate(radio_id));
}
}

View File

@ -23,7 +23,7 @@ use bevy_math::ops;
use bevy_picking::events::{Drag, DragEnd, DragStart, Pointer, Press};
use bevy_ui::{ComputedNode, ComputedNodeTarget, InteractionDisabled, UiGlobalTransform, UiScale};
use crate::{Callback, Notify};
use crate::{Callback, Notify, ValueChange};
/// Defines how the slider should behave when you click on the track (not the thumb).
#[derive(Debug, Default, PartialEq, Clone, Copy)]
@ -78,7 +78,7 @@ 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 `Callback::Ignore`, then the slider will update it's own
/// internal [`SliderValue`] state without notification.
pub on_change: Callback<In<f32>>,
pub on_change: Callback<In<ValueChange<f32>>>,
/// Set the track-clicking behavior for this slider.
pub track_click: TrackClick,
// TODO: Think about whether we want a "vertical" option.
@ -298,7 +298,13 @@ pub(crate) fn slider_on_pointer_down(
.entity(trigger.target())
.insert(SliderValue(new_value));
} else {
commands.notify_with(&slider.on_change, new_value);
commands.notify_with(
&slider.on_change,
ValueChange {
source: trigger.target(),
value: new_value,
},
);
}
}
}
@ -370,7 +376,13 @@ pub(crate) fn slider_on_drag(
.entity(trigger.target())
.insert(SliderValue(rounded_value));
} else {
commands.notify_with(&slider.on_change, rounded_value);
commands.notify_with(
&slider.on_change,
ValueChange {
source: trigger.target(),
value: rounded_value,
},
);
}
}
}
@ -417,7 +429,13 @@ fn slider_on_key_input(
.entity(trigger.target())
.insert(SliderValue(new_value));
} else {
commands.notify_with(&slider.on_change, new_value);
commands.notify_with(
&slider.on_change,
ValueChange {
source: trigger.target(),
value: new_value,
},
);
}
}
}
@ -509,7 +527,13 @@ fn slider_on_set_value(
.entity(trigger.target())
.insert(SliderValue(new_value));
} else {
commands.notify_with(&slider.on_change, new_value);
commands.notify_with(
&slider.on_change,
ValueChange {
source: trigger.target(),
value: new_value,
},
);
}
}
}

View File

@ -23,6 +23,7 @@ mod core_slider;
use bevy_app::{PluginGroup, PluginGroupBuilder};
use bevy_ecs::entity::Entity;
pub use callback::{Callback, Notify};
pub use core_button::{CoreButton, CoreButtonPlugin};
pub use core_checkbox::{CoreCheckbox, CoreCheckboxPlugin, SetChecked, ToggleChecked};
@ -50,3 +51,16 @@ impl PluginGroup for CoreWidgetsPlugins {
.add(CoreSliderPlugin)
}
}
/// Notification sent by a button or menu item.
#[derive(Copy, Clone, Debug, PartialEq)]
pub struct Activate(pub Entity);
/// Notification sent by a widget that edits a scalar value.
#[derive(Copy, Clone, Debug, PartialEq)]
pub struct ValueChange<T> {
/// The id of the widget that produced this value.
pub source: Entity,
/// The new value.
pub value: T,
}

View File

@ -1,5 +1,5 @@
use bevy_app::{Plugin, PreUpdate};
use bevy_core_widgets::{Callback, CoreButton};
use bevy_core_widgets::{Activate, 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},
system::{Commands, In, Query},
};
use bevy_input_focus::tab_navigation::TabIndex;
use bevy_picking::{hover::Hovered, PickingSystems};
@ -45,7 +45,7 @@ pub struct ButtonProps {
/// Rounded corners options
pub corners: RoundedCorners,
/// Click handler
pub on_click: Callback,
pub on_click: Callback<In<Activate>>,
}
/// Template function to spawn a button.

View File

@ -1,5 +1,5 @@
use bevy_app::{Plugin, PreUpdate};
use bevy_core_widgets::{Callback, CoreCheckbox};
use bevy_core_widgets::{Callback, CoreCheckbox, ValueChange};
use bevy_ecs::{
bundle::Bundle,
children,
@ -34,7 +34,7 @@ use crate::{
#[derive(Default)]
pub struct CheckboxProps {
/// Change handler
pub on_change: Callback<In<bool>>,
pub on_change: Callback<In<ValueChange<bool>>>,
}
/// Marker for the checkbox frame (contains both checkbox and label)

View File

@ -2,7 +2,7 @@ use core::f32::consts::PI;
use bevy_app::{Plugin, PreUpdate};
use bevy_color::Color;
use bevy_core_widgets::{Callback, CoreSlider, SliderRange, SliderValue, TrackClick};
use bevy_core_widgets::{Callback, CoreSlider, SliderRange, SliderValue, TrackClick, ValueChange};
use bevy_ecs::{
bundle::Bundle,
children,
@ -42,7 +42,7 @@ pub struct SliderProps {
/// Slider maximum value
pub max: f32,
/// On-change handler
pub on_change: Callback<In<f32>>,
pub on_change: Callback<In<ValueChange<f32>>>,
}
impl Default for SliderProps {

View File

@ -1,7 +1,7 @@
use accesskit::Role;
use bevy_a11y::AccessibilityNode;
use bevy_app::{Plugin, PreUpdate};
use bevy_core_widgets::{Callback, CoreCheckbox};
use bevy_core_widgets::{Callback, CoreCheckbox, ValueChange};
use bevy_ecs::{
bundle::Bundle,
children,
@ -30,7 +30,7 @@ use crate::{
#[derive(Default)]
pub struct ToggleSwitchProps {
/// Change handler
pub on_change: Callback<In<bool>>,
pub on_change: Callback<In<ValueChange<bool>>>,
}
/// Marker for the toggle switch outline

View File

@ -3,9 +3,9 @@
use bevy::{
color::palettes::basic::*,
core_widgets::{
Callback, CoreButton, CoreCheckbox, CoreRadio, CoreRadioGroup, CoreSlider,
Activate, Callback, CoreButton, CoreCheckbox, CoreRadio, CoreRadioGroup, CoreSlider,
CoreSliderDragState, CoreSliderThumb, CoreWidgetsPlugins, SliderRange, SliderValue,
TrackClick,
TrackClick, ValueChange,
},
input_focus::{
tab_navigation::{TabGroup, TabIndex, TabNavigationPlugin},
@ -120,24 +120,24 @@ fn update_widget_values(
fn setup(mut commands: Commands, assets: Res<AssetServer>) {
// System to print a value when the button is clicked.
let on_click = commands.register_system(|| {
let on_click = commands.register_system(|_: In<Activate>| {
info!("Button clicked!");
});
// System to update a resource when the slider value changes. Note that we could have
// updated the slider value directly, but we want to demonstrate externalizing the state.
let on_change_value = commands.register_system(
|value: In<f32>, mut widget_states: ResMut<DemoWidgetStates>| {
widget_states.slider_value = *value;
|value: In<ValueChange<f32>>, mut widget_states: ResMut<DemoWidgetStates>| {
widget_states.slider_value = value.0.value;
},
);
// System to update a resource when the radio group changes.
let on_change_radio = commands.register_system(
|value: In<Entity>,
|value: In<Activate>,
mut widget_states: ResMut<DemoWidgetStates>,
q_radios: Query<&DemoRadio>| {
if let Ok(radio) = q_radios.get(*value) {
if let Ok(radio) = q_radios.get(value.0 .0) {
widget_states.slider_click = radio.0;
}
},
@ -155,9 +155,9 @@ fn setup(mut commands: Commands, assets: Res<AssetServer>) {
fn demo_root(
asset_server: &AssetServer,
on_click: Callback,
on_change_value: Callback<In<f32>>,
on_change_radio: Callback<In<Entity>>,
on_click: Callback<In<Activate>>,
on_change_value: Callback<In<ValueChange<f32>>>,
on_change_radio: Callback<In<Activate>>,
) -> impl Bundle {
(
Node {
@ -181,7 +181,7 @@ fn demo_root(
)
}
fn button(asset_server: &AssetServer, on_click: Callback) -> impl Bundle {
fn button(asset_server: &AssetServer, on_click: Callback<In<Activate>>) -> impl Bundle {
(
Node {
width: Val::Px(150.0),
@ -324,7 +324,12 @@ fn set_button_style(
}
/// Create a demo slider
fn slider(min: f32, max: f32, value: f32, on_change: Callback<In<f32>>) -> impl Bundle {
fn slider(
min: f32,
max: f32,
value: f32,
on_change: Callback<In<ValueChange<f32>>>,
) -> impl Bundle {
(
Node {
display: Display::Flex,
@ -469,7 +474,7 @@ fn thumb_color(disabled: bool, hovered: bool) -> Color {
fn checkbox(
asset_server: &AssetServer,
caption: &str,
on_change: Callback<In<bool>>,
on_change: Callback<In<ValueChange<bool>>>,
) -> impl Bundle {
(
Node {
@ -662,7 +667,7 @@ fn set_checkbox_or_radio_style(
}
/// Create a demo radio group
fn radio_group(asset_server: &AssetServer, on_change: Callback<In<Entity>>) -> impl Bundle {
fn radio_group(asset_server: &AssetServer, on_change: Callback<In<Activate>>) -> impl Bundle {
(
Node {
display: Display::Flex,

View File

@ -3,8 +3,8 @@
use bevy::{
color::palettes::basic::*,
core_widgets::{
Callback, CoreButton, CoreCheckbox, CoreSlider, CoreSliderThumb, CoreWidgetsPlugins,
SliderRange, SliderValue,
Activate, Callback, CoreButton, CoreCheckbox, CoreSlider, CoreSliderThumb,
CoreWidgetsPlugins, SliderRange, SliderValue, ValueChange,
},
ecs::system::SystemId,
input_focus::{
@ -85,15 +85,15 @@ struct DemoWidgetStates {
fn setup(mut commands: Commands, assets: Res<AssetServer>) {
// System to print a value when the button is clicked.
let on_click = commands.register_system(|| {
let on_click = commands.register_system(|_: In<Activate>| {
info!("Button clicked!");
});
// System to update a resource when the slider value changes. Note that we could have
// updated the slider value directly, but we want to demonstrate externalizing the state.
let on_change_value = commands.register_system(
|value: In<f32>, mut widget_states: ResMut<DemoWidgetStates>| {
widget_states.slider_value = *value;
|value: In<ValueChange<f32>>, mut widget_states: ResMut<DemoWidgetStates>| {
widget_states.slider_value = value.0.value;
},
);
@ -104,8 +104,8 @@ fn setup(mut commands: Commands, assets: Res<AssetServer>) {
fn demo_root(
asset_server: &AssetServer,
on_click: SystemId,
on_change_value: SystemId<In<f32>>,
on_click: SystemId<In<Activate>>,
on_change_value: SystemId<In<ValueChange<f32>>>,
) -> impl Bundle {
(
Node {
@ -128,7 +128,7 @@ fn demo_root(
)
}
fn button(asset_server: &AssetServer, on_click: Callback) -> impl Bundle {
fn button(asset_server: &AssetServer, on_click: Callback<In<Activate>>) -> impl Bundle {
(
Node {
width: Val::Px(150.0),
@ -351,7 +351,12 @@ fn set_button_style(
}
/// Create a demo slider
fn slider(min: f32, max: f32, value: f32, on_change: Callback<In<f32>>) -> impl Bundle {
fn slider(
min: f32,
max: f32,
value: f32,
on_change: Callback<In<ValueChange<f32>>>,
) -> impl Bundle {
(
Node {
display: Display::Flex,
@ -517,7 +522,7 @@ fn thumb_color(disabled: bool, hovered: bool) -> Color {
fn checkbox(
asset_server: &AssetServer,
caption: &str,
on_change: Callback<In<bool>>,
on_change: Callback<In<ValueChange<bool>>>,
) -> impl Bundle {
(
Node {

View File

@ -2,7 +2,8 @@
use bevy::{
core_widgets::{
Callback, CoreRadio, CoreRadioGroup, CoreWidgetsPlugins, SliderPrecision, SliderStep,
Activate, Callback, CoreRadio, CoreRadioGroup, CoreWidgetsPlugins, SliderPrecision,
SliderStep,
},
feathers::{
controls::{
@ -49,9 +50,9 @@ fn setup(mut commands: Commands) {
fn demo_root(commands: &mut Commands) -> impl Bundle {
// Update radio button states based on notification from radio group.
let radio_exclusion = commands.register_system(
|ent: In<Entity>, q_radio: Query<Entity, With<CoreRadio>>, mut commands: Commands| {
|ent: In<Activate>, q_radio: Query<Entity, With<CoreRadio>>, mut commands: Commands| {
for radio in q_radio.iter() {
if radio == *ent {
if radio == ent.0 .0 {
commands.entity(radio).insert(Checked);
} else {
commands.entity(radio).remove::<Checked>();
@ -98,9 +99,11 @@ fn demo_root(commands: &mut Commands) -> impl Bundle {
children![
button(
ButtonProps {
on_click: Callback::System(commands.register_system(|| {
info!("Normal button clicked!");
})),
on_click: Callback::System(commands.register_system(
|_: In<Activate>| {
info!("Normal button clicked!");
}
)),
..default()
},
(),
@ -108,9 +111,11 @@ fn demo_root(commands: &mut Commands) -> impl Bundle {
),
button(
ButtonProps {
on_click: Callback::System(commands.register_system(|| {
info!("Disabled button clicked!");
})),
on_click: Callback::System(commands.register_system(
|_: In<Activate>| {
info!("Disabled button clicked!");
}
)),
..default()
},
InteractionDisabled,
@ -118,9 +123,11 @@ fn demo_root(commands: &mut Commands) -> impl Bundle {
),
button(
ButtonProps {
on_click: Callback::System(commands.register_system(|| {
info!("Primary button clicked!");
})),
on_click: Callback::System(commands.register_system(
|_: In<Activate>| {
info!("Primary button clicked!");
}
)),
variant: ButtonVariant::Primary,
..default()
},
@ -141,9 +148,11 @@ fn demo_root(commands: &mut Commands) -> impl Bundle {
children![
button(
ButtonProps {
on_click: Callback::System(commands.register_system(|| {
info!("Left button clicked!");
})),
on_click: Callback::System(commands.register_system(
|_: In<Activate>| {
info!("Left button clicked!");
}
)),
corners: RoundedCorners::Left,
..default()
},
@ -152,9 +161,11 @@ fn demo_root(commands: &mut Commands) -> impl Bundle {
),
button(
ButtonProps {
on_click: Callback::System(commands.register_system(|| {
info!("Center button clicked!");
})),
on_click: Callback::System(commands.register_system(
|_: In<Activate>| {
info!("Center button clicked!");
}
)),
corners: RoundedCorners::None,
..default()
},
@ -163,9 +174,11 @@ fn demo_root(commands: &mut Commands) -> impl Bundle {
),
button(
ButtonProps {
on_click: Callback::System(commands.register_system(|| {
info!("Right button clicked!");
})),
on_click: Callback::System(commands.register_system(
|_: In<Activate>| {
info!("Right button clicked!");
}
)),
variant: ButtonVariant::Primary,
corners: RoundedCorners::Right,
},
@ -176,7 +189,7 @@ fn demo_root(commands: &mut Commands) -> impl Bundle {
),
button(
ButtonProps {
on_click: Callback::System(commands.register_system(|| {
on_click: Callback::System(commands.register_system(|_: In<Activate>| {
info!("Wide button clicked!");
})),
..default()