From 20e7baa2eed210d61e85cbcbecaac61c56630f7d Mon Sep 17 00:00:00 2001 From: Talin Date: Wed, 16 Jul 2025 17:29:37 -0700 Subject: [PATCH] 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, )));