This commit is contained in:
Talin 2025-07-18 02:02:56 +00:00 committed by GitHub
commit b6d4da6203
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 79 additions and 25 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,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<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<DefaultCursor>,
) {
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<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 +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::<DefaultCursorIcon>().is_none() {
app.init_resource::<DefaultCursorIcon>();
if app.world().get_resource::<DefaultCursor>().is_none() {
app.init_resource::<DefaultCursor>();
}
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, DefaultCursor, 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(DefaultCursor(EntityCursor::System(
bevy_window::SystemCursorIcon::Default,
)));

View File

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