From 7c69d2aaf600e3b41423544231b00216ee5669cc Mon Sep 17 00:00:00 2001 From: Talin Date: Fri, 19 Sep 2025 13:11:33 -0700 Subject: [PATCH 01/14] Add `Event` option to `Callback`. --- crates/bevy_ui_widgets/src/callback.rs | 42 ++++++-------------------- crates/bevy_ui_widgets/src/lib.rs | 7 +++-- 2 files changed, 14 insertions(+), 35 deletions(-) diff --git a/crates/bevy_ui_widgets/src/callback.rs b/crates/bevy_ui_widgets/src/callback.rs index 6dba1f9056b9e..737b13e60db94 100644 --- a/crates/bevy_ui_widgets/src/callback.rs +++ b/crates/bevy_ui_widgets/src/callback.rs @@ -1,3 +1,4 @@ +use bevy_ecs::event::Event; use bevy_ecs::system::{Commands, SystemId, SystemInput}; use bevy_ecs::world::{DeferredWorld, World}; use bevy_reflect::{prelude::ReflectDefault, Reflect}; @@ -33,6 +34,8 @@ use bevy_reflect::{prelude::ReflectDefault, Reflect}; pub enum Callback { /// Invoke a one-shot system System(SystemId), + /// Emit an event + Event, /// Ignore this notification #[default] Ignore, @@ -40,75 +43,50 @@ pub enum Callback { /// 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; + I: SystemInput: Send + Event: Default>> + '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, + I: SystemInput: Send + Event: Default>> + 'static, { match callback { Callback::System(system_id) => self.run_system_with(*system_id, input), + Callback::Event => self.trigger(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, + I: SystemInput: Send + Event: Default>> + 'static, { match callback { Callback::System(system_id) => { let _ = self.run_system_with(*system_id, input); } + Callback::Event => self.trigger(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, + I: SystemInput: Send + Event: Default>> + 'static, { match callback { Callback::System(system_id) => { self.commands().run_system_with(*system_id, input); } + Callback::Event => self.trigger(input), Callback::Ignore => (), } } diff --git a/crates/bevy_ui_widgets/src/lib.rs b/crates/bevy_ui_widgets/src/lib.rs index d1bf311dc1ed1..05698abffcea2 100644 --- a/crates/bevy_ui_widgets/src/lib.rs +++ b/crates/bevy_ui_widgets/src/lib.rs @@ -33,7 +33,7 @@ pub use scrollbar::*; pub use slider::*; use bevy_app::{PluginGroup, PluginGroupBuilder}; -use bevy_ecs::entity::Entity; +use bevy_ecs::{entity::Entity, event::EntityEvent}; /// A plugin group that registers the observers for all of the widgets in this crate. If you don't want to /// use all of the widgets, you can import the individual widget plugins instead. @@ -51,13 +51,14 @@ impl PluginGroup for UiWidgetsPlugins { } /// Notification sent by a button or menu item. -#[derive(Copy, Clone, Debug, PartialEq)] +#[derive(Copy, Clone, Debug, PartialEq, EntityEvent)] pub struct Activate(pub Entity); /// Notification sent by a widget that edits a scalar value. -#[derive(Copy, Clone, Debug, PartialEq)] +#[derive(Copy, Clone, Debug, PartialEq, EntityEvent)] pub struct ValueChange { /// The id of the widget that produced this value. + #[event_target] pub source: Entity, /// The new value. pub value: T, From 802911812f316a89eed7ab8f6eed4fb7a4855e2f Mon Sep 17 00:00:00 2001 From: Talin Date: Sat, 27 Sep 2025 17:20:03 -0700 Subject: [PATCH 02/14] Delete Callback. --- crates/bevy_ui_widgets/src/observe.rs | 74 +++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 crates/bevy_ui_widgets/src/observe.rs diff --git a/crates/bevy_ui_widgets/src/observe.rs b/crates/bevy_ui_widgets/src/observe.rs new file mode 100644 index 0000000000000..a7b81fa5b7488 --- /dev/null +++ b/crates/bevy_ui_widgets/src/observe.rs @@ -0,0 +1,74 @@ +#![expect(unsafe_code, reason = "Unsafe code is used to improve performance.")] + +use core::marker::PhantomData; + +use bevy_ecs::{ + bundle::{Bundle, DynamicBundle}, + event::EntityEvent, + system::IntoObserverSystem, +}; + +/// Helper struct that adds an observer when inserted as a [`Bundle`]. +pub struct AddObserver> { + observer: I, + marker: PhantomData<(E, B, M)>, +} + +// SAFETY: Empty method bodies. +unsafe impl< + E: EntityEvent, + B: Bundle, + M: Send + Sync + 'static, + I: IntoObserverSystem + Send + Sync, + > Bundle for AddObserver +{ + #[inline] + fn component_ids( + _components: &mut bevy_ecs::component::ComponentsRegistrator, + _ids: &mut impl FnMut(bevy_ecs::component::ComponentId), + ) { + // SAFETY: Empty function body + } + + #[inline] + fn get_component_ids( + _components: &bevy_ecs::component::Components, + _ids: &mut impl FnMut(Option), + ) { + // SAFETY: Empty function body + } +} + +impl> DynamicBundle + for AddObserver +{ + type Effect = Self; + + #[inline] + unsafe fn get_components( + _ptr: bevy_ecs::ptr::MovingPtr<'_, Self>, + _func: &mut impl FnMut(bevy_ecs::component::StorageType, bevy_ecs::ptr::OwningPtr<'_>), + ) { + // SAFETY: Empty function body + } + + #[inline] + unsafe fn apply_effect( + ptr: bevy_ecs::ptr::MovingPtr<'_, core::mem::MaybeUninit>, + entity: &mut bevy_ecs::world::EntityWorldMut, + ) { + let add_observer = unsafe { ptr.assume_init() }; + let add_observer = add_observer.read(); + entity.observe(add_observer.observer); + } +} + +/// Adds an observer as a bundle effect. +pub fn observe>( + observer: I, +) -> AddObserver { + AddObserver { + observer, + marker: PhantomData, + } +} From eb5e105dee32074a49c622c95407e3d9ad523f0e Mon Sep 17 00:00:00 2001 From: Talin Date: Sat, 27 Sep 2025 17:21:37 -0700 Subject: [PATCH 03/14] Missing files. --- crates/bevy_feathers/src/controls/button.rs | 10 +- crates/bevy_feathers/src/controls/checkbox.rs | 16 +- .../src/controls/color_slider.rs | 10 +- crates/bevy_feathers/src/controls/mod.rs | 4 +- crates/bevy_feathers/src/controls/slider.rs | 8 +- .../src/controls/toggle_switch.rs | 17 +- .../src/controls/virtual_keyboard.rs | 18 +- crates/bevy_ui_widgets/src/button.rs | 23 +- crates/bevy_ui_widgets/src/callback.rs | 93 ---- crates/bevy_ui_widgets/src/checkbox.rs | 87 ++-- crates/bevy_ui_widgets/src/lib.rs | 4 +- crates/bevy_ui_widgets/src/observe.rs | 3 + crates/bevy_ui_widgets/src/radio.rs | 35 +- crates/bevy_ui_widgets/src/slider.rs | 128 ++--- examples/ui/feathers.rs | 491 +++++++++--------- examples/ui/standard_widgets.rs | 105 ++-- examples/ui/standard_widgets_observers.rs | 74 ++- 17 files changed, 464 insertions(+), 662 deletions(-) delete mode 100644 crates/bevy_ui_widgets/src/callback.rs diff --git a/crates/bevy_feathers/src/controls/button.rs b/crates/bevy_feathers/src/controls/button.rs index 1067792097d5c..38695f679f373 100644 --- a/crates/bevy_feathers/src/controls/button.rs +++ b/crates/bevy_feathers/src/controls/button.rs @@ -9,13 +9,13 @@ use bevy_ecs::{ reflect::ReflectComponent, schedule::IntoScheduleConfigs, spawn::{SpawnRelated, SpawnableList}, - system::{Commands, In, Query}, + system::{Commands, Query}, }; use bevy_input_focus::tab_navigation::TabIndex; use bevy_picking::{hover::Hovered, PickingSystems}; use bevy_reflect::{prelude::ReflectDefault, Reflect}; use bevy_ui::{AlignItems, InteractionDisabled, JustifyContent, Node, Pressed, UiRect, Val}; -use bevy_ui_widgets::{Activate, Button, Callback}; +use bevy_ui_widgets::Button; use crate::{ constants::{fonts, size}, @@ -47,8 +47,6 @@ pub struct ButtonProps { pub variant: ButtonVariant, /// Rounded corners options pub corners: RoundedCorners, - /// Click handler - pub on_click: Callback>, } /// Template function to spawn a button. @@ -71,9 +69,7 @@ pub fn button + Send + Sync + 'static, B: Bundle>( flex_grow: 1.0, ..Default::default() }, - Button { - on_activate: props.on_click, - }, + Button, props.variant, Hovered::default(), EntityCursor::System(bevy_window::SystemCursorIcon::Pointer), diff --git a/crates/bevy_feathers/src/controls/checkbox.rs b/crates/bevy_feathers/src/controls/checkbox.rs index ebeff7fe3b171..01dec1edd645e 100644 --- a/crates/bevy_feathers/src/controls/checkbox.rs +++ b/crates/bevy_feathers/src/controls/checkbox.rs @@ -11,7 +11,7 @@ use bevy_ecs::{ reflect::ReflectComponent, schedule::IntoScheduleConfigs, spawn::{Spawn, SpawnRelated, SpawnableList}, - system::{Commands, In, Query}, + system::{Commands, Query}, }; use bevy_input_focus::tab_navigation::TabIndex; use bevy_math::Rot2; @@ -21,7 +21,7 @@ use bevy_ui::{ AlignItems, BorderRadius, Checked, Display, FlexDirection, InteractionDisabled, JustifyContent, Node, PositionType, UiRect, UiTransform, Val, }; -use bevy_ui_widgets::{Callback, Checkbox, ValueChange}; +use bevy_ui_widgets::Checkbox; use crate::{ constants::{fonts, size}, @@ -32,13 +32,6 @@ use crate::{ tokens, }; -/// Parameters for the checkbox template, passed to [`checkbox`] function. -#[derive(Default)] -pub struct CheckboxProps { - /// Change handler - pub on_change: Callback>>, -} - /// Marker for the checkbox frame (contains both checkbox and label) #[derive(Component, Default, Clone, Reflect)] #[reflect(Component, Clone, Default)] @@ -61,7 +54,6 @@ struct CheckboxMark; /// * `overrides` - a bundle of components that are merged in with the normal checkbox components. /// * `label` - the label of the checkbox. pub fn checkbox + Send + Sync + 'static, B: Bundle>( - props: CheckboxProps, overrides: B, label: C, ) -> impl Bundle { @@ -74,9 +66,7 @@ pub fn checkbox + Send + Sync + 'static, B: Bundle>( column_gap: Val::Px(4.0), ..Default::default() }, - Checkbox { - on_change: props.on_change, - }, + Checkbox, CheckboxFrame, Hovered::default(), EntityCursor::System(bevy_window::SystemCursorIcon::Pointer), diff --git a/crates/bevy_feathers/src/controls/color_slider.rs b/crates/bevy_feathers/src/controls/color_slider.rs index 0ef776e3b7808..b4dcdcbc7e02a 100644 --- a/crates/bevy_feathers/src/controls/color_slider.rs +++ b/crates/bevy_feathers/src/controls/color_slider.rs @@ -12,7 +12,7 @@ use bevy_ecs::{ query::{Changed, Or, With}, schedule::IntoScheduleConfigs, spawn::SpawnRelated, - system::{In, Query}, + system::Query, }; use bevy_input_focus::tab_navigation::TabIndex; use bevy_log::warn_once; @@ -23,9 +23,7 @@ use bevy_ui::{ UiRect, UiTransform, Val, Val2, ZIndex, }; use bevy_ui_render::ui_material::MaterialNode; -use bevy_ui_widgets::{ - Callback, Slider, SliderRange, SliderThumb, SliderValue, TrackClick, ValueChange, -}; +use bevy_ui_widgets::{Slider, SliderRange, SliderThumb, SliderValue, TrackClick}; use crate::{ alpha_pattern::{AlphaPattern, AlphaPatternMaterial}, @@ -146,8 +144,6 @@ pub struct SliderBaseColor(pub Color); pub struct ColorSliderProps { /// Slider current value pub value: f32, - /// On-change handler - pub on_change: Callback>>, /// Which color component we're editing pub channel: ColorChannel, } @@ -156,7 +152,6 @@ impl Default for ColorSliderProps { fn default() -> Self { Self { value: 0.0, - on_change: Callback::Ignore, channel: ColorChannel::Alpha, } } @@ -195,7 +190,6 @@ pub fn color_slider(props: ColorSliderProps, overrides: B) -> impl Bu ..Default::default() }, Slider { - on_change: props.on_change, track_click: TrackClick::Snap, }, ColorSlider { diff --git a/crates/bevy_feathers/src/controls/mod.rs b/crates/bevy_feathers/src/controls/mod.rs index f5b9ef4c43dd5..c777c58648810 100644 --- a/crates/bevy_feathers/src/controls/mod.rs +++ b/crates/bevy_feathers/src/controls/mod.rs @@ -11,14 +11,14 @@ mod toggle_switch; mod virtual_keyboard; pub use button::{button, ButtonPlugin, ButtonProps, ButtonVariant}; -pub use checkbox::{checkbox, CheckboxPlugin, CheckboxProps}; +pub use checkbox::{checkbox, CheckboxPlugin}; pub use color_slider::{ color_slider, ColorChannel, ColorSlider, ColorSliderPlugin, ColorSliderProps, SliderBaseColor, }; pub use color_swatch::{color_swatch, ColorSwatch, ColorSwatchFg}; pub use radio::{radio, RadioPlugin}; pub use slider::{slider, SliderPlugin, SliderProps}; -pub use toggle_switch::{toggle_switch, ToggleSwitchPlugin, ToggleSwitchProps}; +pub use toggle_switch::{toggle_switch, ToggleSwitchPlugin}; pub use virtual_keyboard::virtual_keyboard; use crate::alpha_pattern::AlphaPatternPlugin; diff --git a/crates/bevy_feathers/src/controls/slider.rs b/crates/bevy_feathers/src/controls/slider.rs index fbad8c7a362b5..1bcb2449f9370 100644 --- a/crates/bevy_feathers/src/controls/slider.rs +++ b/crates/bevy_feathers/src/controls/slider.rs @@ -13,7 +13,7 @@ use bevy_ecs::{ reflect::ReflectComponent, schedule::IntoScheduleConfigs, spawn::SpawnRelated, - system::{Commands, In, Query, Res}, + system::{Commands, Query, Res}, }; use bevy_input_focus::tab_navigation::TabIndex; use bevy_picking::PickingSystems; @@ -23,7 +23,7 @@ use bevy_ui::{ InteractionDisabled, InterpolationColorSpace, JustifyContent, LinearGradient, Node, PositionType, UiRect, Val, }; -use bevy_ui_widgets::{Callback, Slider, SliderRange, SliderValue, TrackClick, ValueChange}; +use bevy_ui_widgets::{Slider, SliderRange, SliderValue, TrackClick}; use crate::{ constants::{fonts, size}, @@ -43,8 +43,6 @@ pub struct SliderProps { pub min: f32, /// Slider maximum value pub max: f32, - /// On-change handler - pub on_change: Callback>>, } impl Default for SliderProps { @@ -53,7 +51,6 @@ impl Default for SliderProps { value: 0.0, min: 0.0, max: 1.0, - on_change: Callback::Ignore, } } } @@ -86,7 +83,6 @@ pub fn slider(props: SliderProps, overrides: B) -> impl Bundle { ..Default::default() }, Slider { - on_change: props.on_change, track_click: TrackClick::Drag, }, SliderStyle, diff --git a/crates/bevy_feathers/src/controls/toggle_switch.rs b/crates/bevy_feathers/src/controls/toggle_switch.rs index 94a49ca1972b5..dea53f169f0f8 100644 --- a/crates/bevy_feathers/src/controls/toggle_switch.rs +++ b/crates/bevy_feathers/src/controls/toggle_switch.rs @@ -12,14 +12,14 @@ use bevy_ecs::{ reflect::ReflectComponent, schedule::IntoScheduleConfigs, spawn::SpawnRelated, - system::{Commands, In, Query}, + system::{Commands, Query}, world::Mut, }; use bevy_input_focus::tab_navigation::TabIndex; use bevy_picking::{hover::Hovered, PickingSystems}; use bevy_reflect::{prelude::ReflectDefault, Reflect}; use bevy_ui::{BorderRadius, Checked, InteractionDisabled, Node, PositionType, UiRect, Val}; -use bevy_ui_widgets::{Callback, Checkbox, ValueChange}; +use bevy_ui_widgets::Checkbox; use crate::{ constants::size, @@ -28,13 +28,6 @@ use crate::{ tokens, }; -/// Parameters for the toggle switch template, passed to [`toggle_switch`] function. -#[derive(Default)] -pub struct ToggleSwitchProps { - /// Change handler - pub on_change: Callback>>, -} - /// Marker for the toggle switch outline #[derive(Component, Default, Clone, Reflect)] #[reflect(Component, Clone, Default)] @@ -50,7 +43,7 @@ struct ToggleSwitchSlide; /// # Arguments /// * `props` - construction properties for the toggle switch. /// * `overrides` - a bundle of components that are merged in with the normal toggle switch components. -pub fn toggle_switch(props: ToggleSwitchProps, overrides: B) -> impl Bundle { +pub fn toggle_switch(overrides: B) -> impl Bundle { ( Node { width: size::TOGGLE_WIDTH, @@ -58,9 +51,7 @@ pub fn toggle_switch(props: ToggleSwitchProps, overrides: B) -> impl border: UiRect::all(Val::Px(2.0)), ..Default::default() }, - Checkbox { - on_change: props.on_change, - }, + Checkbox, ToggleSwitchOutline, BorderRadius::all(Val::Px(5.0)), ThemeBackgroundColor(tokens::SWITCH_BG), diff --git a/crates/bevy_feathers/src/controls/virtual_keyboard.rs b/crates/bevy_feathers/src/controls/virtual_keyboard.rs index acc62afc82478..0670e73dca696 100644 --- a/crates/bevy_feathers/src/controls/virtual_keyboard.rs +++ b/crates/bevy_feathers/src/controls/virtual_keyboard.rs @@ -2,15 +2,16 @@ use bevy_ecs::{ bundle::Bundle, component::Component, hierarchy::{ChildOf, Children}, + observer::On, relationship::RelatedSpawner, spawn::{Spawn, SpawnRelated, SpawnWith}, - system::{In, SystemId}, + system::{Commands, In, SystemId}, }; use bevy_input_focus::tab_navigation::TabGroup; use bevy_ui::Node; use bevy_ui::Val; use bevy_ui::{widget::Text, FlexDirection}; -use bevy_ui_widgets::{Activate, Callback}; +use bevy_ui_widgets::{observe, Activate}; use crate::controls::{button, ButtonProps}; @@ -39,13 +40,12 @@ where }, Children::spawn(SpawnWith(move |parent: &mut RelatedSpawner| { for (label, key_id) in row.into_iter() { - parent.spawn(button( - ButtonProps { - on_click: Callback::System(on_key_press), - ..Default::default() - }, - (key_id,), - Spawn(Text::new(label)), + parent.spawn(( + button(ButtonProps::default(), (key_id,), Spawn(Text::new(label))), + observe(move |activate: On, mut commands: Commands| { + // TODO: Turn this into an event as well, or use event forwarding. + commands.run_system_with(on_key_press, *activate); + }), )); } })), diff --git a/crates/bevy_ui_widgets/src/button.rs b/crates/bevy_ui_widgets/src/button.rs index 46fea109ac62d..db9b74f3b2188 100644 --- a/crates/bevy_ui_widgets/src/button.rs +++ b/crates/bevy_ui_widgets/src/button.rs @@ -2,7 +2,6 @@ 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, @@ -16,25 +15,21 @@ use bevy_input_focus::FocusedInput; use bevy_picking::events::{Cancel, Click, DragEnd, Pointer, Press, Release}; use bevy_ui::{InteractionDisabled, Pressed}; -use crate::{Activate, Callback, Notify}; +use crate::Activate; /// 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 an [`Activate`] /// event when the button is un-pressed. #[derive(Component, Default, Debug)] #[require(AccessibilityNode(accesskit::Node::new(Role::Button)))] -pub struct Button { - /// 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 struct Button; fn button_on_key_event( mut event: On>, - q_state: Query<(&Button, Has)>, + q_state: Query, With