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.
This commit is contained in:
Talin 2025-07-16 17:29:37 -07:00
parent f964ee1e3a
commit 20e7baa2ee
8 changed files with 73 additions and 22 deletions

View File

@ -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

View File

@ -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<C: SpawnableList<ChildOf> + 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),

View File

@ -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<C: SpawnableList<ChildOf> + 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 {

View File

@ -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<C: SpawnableList<ChildOf> + 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 {

View File

@ -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<B: Bundle>(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

View File

@ -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<B: Bundle>(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![(

View File

@ -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<Res<HoverMap>>,
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<DefaultCursorIcon>,
r_default_cursor: Res<DefaultEntityCursor>,
) {
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<Entity> = 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::<DefaultCursorIcon>().is_none() {
app.init_resource::<DefaultCursorIcon>();
if app.world().get_resource::<DefaultEntityCursor>().is_none() {
app.init_resource::<DefaultEntityCursor>();
}
app.add_systems(PreUpdate, update_cursor.in_set(PickingSystems::Last));
}

View File

@ -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::<TextFont, With<ThemedText>>::default(),
));
app.insert_resource(DefaultCursorIcon(CursorIcon::System(
app.insert_resource(DefaultEntityCursor(EntityCursor::System(
bevy_window::SystemCursorIcon::Default,
)));