From 20e7baa2eed210d61e85cbcbecaac61c56630f7d Mon Sep 17 00:00:00 2001 From: Talin Date: Wed, 16 Jul 2025 17:29:37 -0700 Subject: [PATCH 1/7] Changed the way cursors are defined in bevy_feathers. Originally, we re-used the `CursorIcon` component which was intended to be placed on the window, and expanded its use so that it could be placed on any hoverable entity. In this PR, we restore `CursorIcon` to its original usage, and instead introduce a new type `EntityCursor` intended to be placed on hoverable entities. Part of the motivation for this is that it will make it easier to support BSN templates without having to add new trait impls in bevy_winit. --- crates/bevy_feathers/Cargo.toml | 2 + crates/bevy_feathers/src/controls/button.rs | 4 +- crates/bevy_feathers/src/controls/checkbox.rs | 4 +- crates/bevy_feathers/src/controls/radio.rs | 4 +- crates/bevy_feathers/src/controls/slider.rs | 4 +- .../src/controls/toggle_switch.rs | 4 +- crates/bevy_feathers/src/cursor.rs | 68 ++++++++++++++++--- crates/bevy_feathers/src/lib.rs | 5 +- 8 files changed, 73 insertions(+), 22 deletions(-) diff --git a/crates/bevy_feathers/Cargo.toml b/crates/bevy_feathers/Cargo.toml index 07d883704a..91f28bee42 100644 --- a/crates/bevy_feathers/Cargo.toml +++ b/crates/bevy_feathers/Cargo.toml @@ -22,6 +22,7 @@ bevy_math = { path = "../bevy_math", version = "0.17.0-dev" } bevy_picking = { path = "../bevy_picking", version = "0.17.0-dev" } bevy_platform = { path = "../bevy_platform", version = "0.17.0-dev" } bevy_render = { path = "../bevy_render", version = "0.17.0-dev" } +bevy_reflect = { path = "../bevy_reflect", version = "0.17.0-dev" } bevy_text = { path = "../bevy_text", version = "0.17.0-dev" } bevy_ui = { path = "../bevy_ui", version = "0.17.0-dev", features = [ "bevy_ui_picking_backend", @@ -34,6 +35,7 @@ accesskit = "0.19" [features] default = [] +custom_cursor = ["bevy_winit/custom_cursor"] [lints] workspace = true diff --git a/crates/bevy_feathers/src/controls/button.rs b/crates/bevy_feathers/src/controls/button.rs index ad479f1ec5..3566639993 100644 --- a/crates/bevy_feathers/src/controls/button.rs +++ b/crates/bevy_feathers/src/controls/button.rs @@ -14,10 +14,10 @@ use bevy_ecs::{ use bevy_input_focus::tab_navigation::TabIndex; use bevy_picking::{hover::Hovered, PickingSystems}; use bevy_ui::{AlignItems, InteractionDisabled, JustifyContent, Node, Pressed, UiRect, Val}; -use bevy_winit::cursor::CursorIcon; use crate::{ constants::{fonts, size}, + cursor::EntityCursor, font_styles::InheritableFont, handle_or_path::HandleOrPath, rounded_corners::RoundedCorners, @@ -73,7 +73,7 @@ pub fn button + Send + Sync + 'static, B: Bundle>( }, props.variant, Hovered::default(), - CursorIcon::System(bevy_window::SystemCursorIcon::Pointer), + EntityCursor::System(bevy_window::SystemCursorIcon::Pointer), TabIndex(0), props.corners.to_border_radius(4.0), ThemeBackgroundColor(tokens::BUTTON_BG), diff --git a/crates/bevy_feathers/src/controls/checkbox.rs b/crates/bevy_feathers/src/controls/checkbox.rs index db37f82623..549a3737e5 100644 --- a/crates/bevy_feathers/src/controls/checkbox.rs +++ b/crates/bevy_feathers/src/controls/checkbox.rs @@ -20,10 +20,10 @@ use bevy_ui::{ AlignItems, BorderRadius, Checked, Display, FlexDirection, InteractionDisabled, JustifyContent, Node, PositionType, UiRect, UiTransform, Val, }; -use bevy_winit::cursor::CursorIcon; use crate::{ constants::{fonts, size}, + cursor::EntityCursor, font_styles::InheritableFont, handle_or_path::HandleOrPath, theme::{ThemeBackgroundColor, ThemeBorderColor, ThemeFontColor}, @@ -74,7 +74,7 @@ pub fn checkbox + Send + Sync + 'static, B: Bundle>( }, CheckboxFrame, Hovered::default(), - CursorIcon::System(bevy_window::SystemCursorIcon::Pointer), + EntityCursor::System(bevy_window::SystemCursorIcon::Pointer), TabIndex(0), ThemeFontColor(tokens::CHECKBOX_TEXT), InheritableFont { diff --git a/crates/bevy_feathers/src/controls/radio.rs b/crates/bevy_feathers/src/controls/radio.rs index a08ffcfa8d..aa5afa5efb 100644 --- a/crates/bevy_feathers/src/controls/radio.rs +++ b/crates/bevy_feathers/src/controls/radio.rs @@ -19,10 +19,10 @@ use bevy_ui::{ AlignItems, BorderRadius, Checked, Display, FlexDirection, InteractionDisabled, JustifyContent, Node, UiRect, Val, }; -use bevy_winit::cursor::CursorIcon; use crate::{ constants::{fonts, size}, + cursor::EntityCursor, font_styles::InheritableFont, handle_or_path::HandleOrPath, theme::{ThemeBackgroundColor, ThemeBorderColor, ThemeFontColor}, @@ -58,7 +58,7 @@ pub fn radio + Send + Sync + 'static, B: Bundle>( }, CoreRadio, Hovered::default(), - CursorIcon::System(bevy_window::SystemCursorIcon::Pointer), + EntityCursor::System(bevy_window::SystemCursorIcon::Pointer), TabIndex(0), ThemeFontColor(tokens::RADIO_TEXT), InheritableFont { diff --git a/crates/bevy_feathers/src/controls/slider.rs b/crates/bevy_feathers/src/controls/slider.rs index 632d0f2cb6..49a7a4c0cc 100644 --- a/crates/bevy_feathers/src/controls/slider.rs +++ b/crates/bevy_feathers/src/controls/slider.rs @@ -22,10 +22,10 @@ use bevy_ui::{ InteractionDisabled, InterpolationColorSpace, JustifyContent, LinearGradient, Node, UiRect, Val, }; -use bevy_winit::cursor::CursorIcon; use crate::{ constants::{fonts, size}, + cursor::EntityCursor, font_styles::InheritableFont, handle_or_path::HandleOrPath, rounded_corners::RoundedCorners, @@ -87,7 +87,7 @@ pub fn slider(props: SliderProps, overrides: B) -> impl Bundle { SliderStyle, SliderValue(props.value), SliderRange::new(props.min, props.max), - CursorIcon::System(bevy_window::SystemCursorIcon::EwResize), + EntityCursor::System(bevy_window::SystemCursorIcon::EwResize), TabIndex(0), RoundedCorners::All.to_border_radius(6.0), // Use a gradient to draw the moving bar diff --git a/crates/bevy_feathers/src/controls/toggle_switch.rs b/crates/bevy_feathers/src/controls/toggle_switch.rs index e3437a829d..58b6b47424 100644 --- a/crates/bevy_feathers/src/controls/toggle_switch.rs +++ b/crates/bevy_feathers/src/controls/toggle_switch.rs @@ -18,10 +18,10 @@ use bevy_ecs::{ use bevy_input_focus::tab_navigation::TabIndex; use bevy_picking::{hover::Hovered, PickingSystems}; use bevy_ui::{BorderRadius, Checked, InteractionDisabled, Node, PositionType, UiRect, Val}; -use bevy_winit::cursor::CursorIcon; use crate::{ constants::size, + cursor::EntityCursor, theme::{ThemeBackgroundColor, ThemeBorderColor}, tokens, }; @@ -63,7 +63,7 @@ pub fn toggle_switch(props: ToggleSwitchProps, overrides: B) -> impl ThemeBorderColor(tokens::SWITCH_BORDER), AccessibilityNode(accesskit::Node::new(Role::Switch)), Hovered::default(), - CursorIcon::System(bevy_window::SystemCursorIcon::Pointer), + EntityCursor::System(bevy_window::SystemCursorIcon::Pointer), TabIndex(0), overrides, children![( diff --git a/crates/bevy_feathers/src/cursor.rs b/crates/bevy_feathers/src/cursor.rs index a9811edfb2..6d0bfc13c9 100644 --- a/crates/bevy_feathers/src/cursor.rs +++ b/crates/bevy_feathers/src/cursor.rs @@ -1,20 +1,68 @@ //! Provides a way to automatically set the mouse cursor based on hovered entity. use bevy_app::{App, Plugin, PreUpdate}; use bevy_ecs::{ + component::Component, entity::Entity, hierarchy::ChildOf, + reflect::ReflectComponent, resource::Resource, schedule::IntoScheduleConfigs, system::{Commands, Query, Res}, }; use bevy_picking::{hover::HoverMap, pointer::PointerId, PickingSystems}; -use bevy_window::Window; +use bevy_reflect::{std_traits::ReflectDefault, Reflect}; +use bevy_window::{SystemCursorIcon, Window}; use bevy_winit::cursor::CursorIcon; /// A component that specifies the cursor icon to be used when the mouse is not hovering over /// any other entity. This is used to set the default cursor icon for the window. #[derive(Resource, Debug, Clone, Default)] -pub struct DefaultCursorIcon(pub CursorIcon); +pub struct DefaultEntityCursor(pub EntityCursor); + +/// A component that specifies the cursor shape to be used when the pointer hovers over an entity. +/// This is copied to the windows's [`CursorIcon`] component. +/// +/// This is effectively the same type as [`CustomCursor`] but with different methods, and used +/// in different places. +#[derive(Component, Debug, Clone, Reflect, PartialEq, Eq)] +#[reflect(Component, Debug, Default, PartialEq, Clone)] +pub enum EntityCursor { + #[cfg(feature = "custom_cursor")] + /// Custom cursor image. + Custom(CustomCursor), + /// System provided cursor icon. + System(SystemCursorIcon), +} + +impl EntityCursor { + /// Convert the [`EntityCursor`] to a [`CursorIcon`] so that it can be inserted into a + /// window. + pub fn as_cursor_icon(&self) -> CursorIcon { + match self { + #[cfg(feature = "custom_cursor")] + EntityCursor::Custom(custom_cursor) => CursorIcon::Custom(custom_cursor), + EntityCursor::System(icon) => CursorIcon::from(*icon), + } + } + + /// Compare the [`EntityCursor`] to a [`CursorIcon`] so that we can see whether or not + /// the window cursor needs to be changed. + pub fn eq_cursor_icon(&self, cursor_icon: &CursorIcon) -> bool { + match (self, cursor_icon) { + #[cfg(feature = "custom_cursor")] + (EntityCursor::Custom(custom), CursorIcon::Custom(other)) => custom == other, + (EntityCursor::System(system), cursor_icon) => { + CursorIcon::from(*system) == *cursor_icon + } + } + } +} + +impl Default for EntityCursor { + fn default() -> Self { + EntityCursor::System(Default::default()) + } +} /// System which updates the window cursor icon whenever the mouse hovers over an entity with /// a [`CursorIcon`] component. If no entity is hovered, the cursor icon is set to @@ -23,9 +71,9 @@ pub(crate) fn update_cursor( mut commands: Commands, hover_map: Option>, parent_query: Query<&ChildOf>, - cursor_query: Query<&CursorIcon>, + cursor_query: Query<&EntityCursor>, mut q_windows: Query<(Entity, &mut Window, Option<&CursorIcon>)>, - r_default_cursor: Res, + r_default_cursor: Res, ) { let cursor = hover_map.and_then(|hover_map| match hover_map.get(&PointerId::Mouse) { Some(hover_set) => hover_set.keys().find_map(|entity| { @@ -41,7 +89,7 @@ pub(crate) fn update_cursor( let mut windows_to_change: Vec = Vec::new(); for (entity, _window, prev_cursor) in q_windows.iter_mut() { match (cursor, prev_cursor) { - (Some(cursor), Some(prev_cursor)) if cursor == prev_cursor => continue, + (Some(cursor), Some(prev_cursor)) if cursor.eq_cursor_icon(prev_cursor) => continue, (None, None) => continue, _ => { windows_to_change.push(entity); @@ -50,9 +98,11 @@ pub(crate) fn update_cursor( } windows_to_change.iter().for_each(|entity| { if let Some(cursor) = cursor { - commands.entity(*entity).insert(cursor.clone()); + commands.entity(*entity).insert(cursor.as_cursor_icon()); } else { - commands.entity(*entity).insert(r_default_cursor.0.clone()); + commands + .entity(*entity) + .insert(r_default_cursor.0.as_cursor_icon()); } }); } @@ -62,8 +112,8 @@ pub struct CursorIconPlugin; impl Plugin for CursorIconPlugin { fn build(&self, app: &mut App) { - if app.world().get_resource::().is_none() { - app.init_resource::(); + if app.world().get_resource::().is_none() { + app.init_resource::(); } app.add_systems(PreUpdate, update_cursor.in_set(PickingSystems::Last)); } diff --git a/crates/bevy_feathers/src/lib.rs b/crates/bevy_feathers/src/lib.rs index ab02304a85..95594182cd 100644 --- a/crates/bevy_feathers/src/lib.rs +++ b/crates/bevy_feathers/src/lib.rs @@ -22,11 +22,10 @@ use bevy_app::{HierarchyPropagatePlugin, Plugin, PostUpdate}; use bevy_asset::embedded_asset; use bevy_ecs::query::With; use bevy_text::{TextColor, TextFont}; -use bevy_winit::cursor::CursorIcon; use crate::{ controls::ControlsPlugin, - cursor::{CursorIconPlugin, DefaultCursorIcon}, + cursor::{CursorIconPlugin, DefaultEntityCursor, EntityCursor}, theme::{ThemedText, UiTheme}, }; @@ -61,7 +60,7 @@ impl Plugin for FeathersPlugin { HierarchyPropagatePlugin::>::default(), )); - app.insert_resource(DefaultCursorIcon(CursorIcon::System( + app.insert_resource(DefaultEntityCursor(EntityCursor::System( bevy_window::SystemCursorIcon::Default, ))); From 08b19fce59e6fd2a689b906b798803e5f28b43eb Mon Sep 17 00:00:00 2001 From: Talin Date: Wed, 16 Jul 2025 18:27:27 -0700 Subject: [PATCH 2/7] Build fix. --- crates/bevy_feathers/src/cursor.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/bevy_feathers/src/cursor.rs b/crates/bevy_feathers/src/cursor.rs index 6d0bfc13c9..f1ac37a083 100644 --- a/crates/bevy_feathers/src/cursor.rs +++ b/crates/bevy_feathers/src/cursor.rs @@ -13,6 +13,8 @@ use bevy_picking::{hover::HoverMap, pointer::PointerId, PickingSystems}; use bevy_reflect::{std_traits::ReflectDefault, Reflect}; use bevy_window::{SystemCursorIcon, Window}; use bevy_winit::cursor::CursorIcon; +#[cfg(feature = "custom_cursor")] +use bevy_winit::cursor::CustomCursor; /// A component that specifies the cursor icon to be used when the mouse is not hovering over /// any other entity. This is used to set the default cursor icon for the window. From 1c326a3db9eb4642937469650d2caa4e28c2b82f Mon Sep 17 00:00:00 2001 From: Talin Date: Wed, 16 Jul 2025 18:53:34 -0700 Subject: [PATCH 3/7] More CI. --- crates/bevy_feathers/src/cursor.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/bevy_feathers/src/cursor.rs b/crates/bevy_feathers/src/cursor.rs index f1ac37a083..58f9bb3e96 100644 --- a/crates/bevy_feathers/src/cursor.rs +++ b/crates/bevy_feathers/src/cursor.rs @@ -39,10 +39,10 @@ pub enum EntityCursor { impl EntityCursor { /// Convert the [`EntityCursor`] to a [`CursorIcon`] so that it can be inserted into a /// window. - pub fn as_cursor_icon(&self) -> CursorIcon { + pub fn to_cursor_icon(&self) -> CursorIcon { match self { #[cfg(feature = "custom_cursor")] - EntityCursor::Custom(custom_cursor) => CursorIcon::Custom(custom_cursor), + EntityCursor::Custom(custom_cursor) => CursorIcon::Custom(custom_cursor.clone()), EntityCursor::System(icon) => CursorIcon::from(*icon), } } @@ -100,11 +100,11 @@ pub(crate) fn update_cursor( } windows_to_change.iter().for_each(|entity| { if let Some(cursor) = cursor { - commands.entity(*entity).insert(cursor.as_cursor_icon()); + commands.entity(*entity).insert(cursor.to_cursor_icon()); } else { commands .entity(*entity) - .insert(r_default_cursor.0.as_cursor_icon()); + .insert(r_default_cursor.0.to_cursor_icon()); } }); } From 2294d5b4f17aab9fac1797636dd8c027d4148e7d Mon Sep 17 00:00:00 2001 From: Talin Date: Wed, 16 Jul 2025 19:16:07 -0700 Subject: [PATCH 4/7] More build fixes, sorry. --- crates/bevy_feathers/src/cursor.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/crates/bevy_feathers/src/cursor.rs b/crates/bevy_feathers/src/cursor.rs index 58f9bb3e96..61e1ad6bf3 100644 --- a/crates/bevy_feathers/src/cursor.rs +++ b/crates/bevy_feathers/src/cursor.rs @@ -53,9 +53,10 @@ impl EntityCursor { match (self, cursor_icon) { #[cfg(feature = "custom_cursor")] (EntityCursor::Custom(custom), CursorIcon::Custom(other)) => custom == other, - (EntityCursor::System(system), cursor_icon) => { - CursorIcon::from(*system) == *cursor_icon + (EntityCursor::System(system), CursorIcon::System(cursor_icon)) => { + *system == *cursor_icon } + _ => false, } } } @@ -68,7 +69,7 @@ impl Default for EntityCursor { /// System which updates the window cursor icon whenever the mouse hovers over an entity with /// a [`CursorIcon`] component. If no entity is hovered, the cursor icon is set to -/// the cursor in the [`DefaultCursorIcon`] resource. +/// the cursor in the [`DefaultEntityCursor`] resource. pub(crate) fn update_cursor( mut commands: Commands, hover_map: Option>, From e1e5e6bbc4313ad5074779a105314d2b998e4aa6 Mon Sep 17 00:00:00 2001 From: Talin Date: Thu, 17 Jul 2025 08:46:00 -0700 Subject: [PATCH 5/7] Fix doc comment. --- crates/bevy_feathers/src/cursor.rs | 2 +- release-content/release-notes/feathers.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/bevy_feathers/src/cursor.rs b/crates/bevy_feathers/src/cursor.rs index 61e1ad6bf3..8c154521f8 100644 --- a/crates/bevy_feathers/src/cursor.rs +++ b/crates/bevy_feathers/src/cursor.rs @@ -16,7 +16,7 @@ use bevy_winit::cursor::CursorIcon; #[cfg(feature = "custom_cursor")] use bevy_winit::cursor::CustomCursor; -/// A component that specifies the cursor icon to be used when the mouse is not hovering over +/// A resource that specifies the cursor icon to be used when the mouse is not hovering over /// any other entity. This is used to set the default cursor icon for the window. #[derive(Resource, Debug, Clone, Default)] pub struct DefaultEntityCursor(pub EntityCursor); diff --git a/release-content/release-notes/feathers.md b/release-content/release-notes/feathers.md index 16d0cd5b70..86d04f5997 100644 --- a/release-content/release-notes/feathers.md +++ b/release-content/release-notes/feathers.md @@ -1,7 +1,7 @@ --- title: Bevy Feathers authors: ["@viridia", "@Atlas16A"] -pull_requests: [19730, 19900, 19928] +pull_requests: [19730, 19900, 19928, 20169] --- To make it easier for Bevy engine developers and third-party tool creators to make comfortable, visually cohesive tooling, From 9aa3e5ea508e814893999a6b07715bb17b9285e2 Mon Sep 17 00:00:00 2001 From: Talin Date: Thu, 17 Jul 2025 13:08:14 -0700 Subject: [PATCH 6/7] Apply naming suggestion. --- crates/bevy_feathers/src/cursor.rs | 8 ++++---- crates/bevy_feathers/src/lib.rs | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/crates/bevy_feathers/src/cursor.rs b/crates/bevy_feathers/src/cursor.rs index 8c154521f8..2b542af74d 100644 --- a/crates/bevy_feathers/src/cursor.rs +++ b/crates/bevy_feathers/src/cursor.rs @@ -19,7 +19,7 @@ use bevy_winit::cursor::CustomCursor; /// A resource that specifies the cursor icon to be used when the mouse is not hovering over /// any other entity. This is used to set the default cursor icon for the window. #[derive(Resource, Debug, Clone, Default)] -pub struct DefaultEntityCursor(pub EntityCursor); +pub struct DefaultCursor(pub EntityCursor); /// A component that specifies the cursor shape to be used when the pointer hovers over an entity. /// This is copied to the windows's [`CursorIcon`] component. @@ -76,7 +76,7 @@ pub(crate) fn update_cursor( parent_query: Query<&ChildOf>, cursor_query: Query<&EntityCursor>, mut q_windows: Query<(Entity, &mut Window, Option<&CursorIcon>)>, - r_default_cursor: Res, + r_default_cursor: Res, ) { let cursor = hover_map.and_then(|hover_map| match hover_map.get(&PointerId::Mouse) { Some(hover_set) => hover_set.keys().find_map(|entity| { @@ -115,8 +115,8 @@ pub struct CursorIconPlugin; impl Plugin for CursorIconPlugin { fn build(&self, app: &mut App) { - if app.world().get_resource::().is_none() { - app.init_resource::(); + if app.world().get_resource::().is_none() { + app.init_resource::(); } app.add_systems(PreUpdate, update_cursor.in_set(PickingSystems::Last)); } diff --git a/crates/bevy_feathers/src/lib.rs b/crates/bevy_feathers/src/lib.rs index 95594182cd..11f2d0f488 100644 --- a/crates/bevy_feathers/src/lib.rs +++ b/crates/bevy_feathers/src/lib.rs @@ -25,7 +25,7 @@ use bevy_text::{TextColor, TextFont}; use crate::{ controls::ControlsPlugin, - cursor::{CursorIconPlugin, DefaultEntityCursor, EntityCursor}, + cursor::{CursorIconPlugin, DefaultCursor, EntityCursor}, theme::{ThemedText, UiTheme}, }; @@ -60,7 +60,7 @@ impl Plugin for FeathersPlugin { HierarchyPropagatePlugin::>::default(), )); - app.insert_resource(DefaultEntityCursor(EntityCursor::System( + app.insert_resource(DefaultCursor(EntityCursor::System( bevy_window::SystemCursorIcon::Default, ))); From bb9dd8e0f25202cea71db808f622be0a77b1ed32 Mon Sep 17 00:00:00 2001 From: Talin Date: Thu, 17 Jul 2025 19:02:49 -0700 Subject: [PATCH 7/7] Once again, renaming doesn't fix docs. --- crates/bevy_feathers/src/cursor.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bevy_feathers/src/cursor.rs b/crates/bevy_feathers/src/cursor.rs index 2b542af74d..5fd7de19ba 100644 --- a/crates/bevy_feathers/src/cursor.rs +++ b/crates/bevy_feathers/src/cursor.rs @@ -69,7 +69,7 @@ impl Default for EntityCursor { /// System which updates the window cursor icon whenever the mouse hovers over an entity with /// a [`CursorIcon`] component. If no entity is hovered, the cursor icon is set to -/// the cursor in the [`DefaultEntityCursor`] resource. +/// the cursor in the [`DefaultCursor`] resource. pub(crate) fn update_cursor( mut commands: Commands, hover_map: Option>,