diff --git a/crates/bevy_feathers/Cargo.toml b/crates/bevy_feathers/Cargo.toml index 07d883704ac73..91f28bee423a5 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 ad479f1ec5202..3566639993764 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 db37f82623c09..549a3737e5d5f 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 a08ffcfa8d136..aa5afa5efb426 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 632d0f2cb6eef..49a7a4c0cc633 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 e3437a829d6a5..58b6b474244db 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 a9811edfb294d..5fd7de19ba7a0 100644 --- a/crates/bevy_feathers/src/cursor.rs +++ b/crates/bevy_feathers/src/cursor.rs @@ -1,31 +1,82 @@ //! 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; +#[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 DefaultCursorIcon(pub CursorIcon); +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. +/// +/// 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 to_cursor_icon(&self) -> CursorIcon { + match self { + #[cfg(feature = "custom_cursor")] + EntityCursor::Custom(custom_cursor) => CursorIcon::Custom(custom_cursor.clone()), + 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), CursorIcon::System(cursor_icon)) => { + *system == *cursor_icon + } + _ => false, + } + } +} + +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 -/// the cursor in the [`DefaultCursorIcon`] resource. +/// the cursor in the [`DefaultCursor`] resource. 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 +92,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 +101,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.to_cursor_icon()); } else { - commands.entity(*entity).insert(r_default_cursor.0.clone()); + commands + .entity(*entity) + .insert(r_default_cursor.0.to_cursor_icon()); } }); } @@ -62,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 ab02304a85b30..11f2d0f488bf4 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, DefaultCursor, EntityCursor}, theme::{ThemedText, UiTheme}, }; @@ -61,7 +60,7 @@ impl Plugin for FeathersPlugin { HierarchyPropagatePlugin::>::default(), )); - app.insert_resource(DefaultCursorIcon(CursorIcon::System( + app.insert_resource(DefaultCursor(EntityCursor::System( bevy_window::SystemCursorIcon::Default, ))); diff --git a/release-content/release-notes/feathers.md b/release-content/release-notes/feathers.md index 16d0cd5b709e7..86d04f5997666 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,