1use crate::{
4 converters::convert_system_cursor_icon,
5 state::{CursorSource, PendingCursor},
6};
7#[cfg(feature = "custom_cursor")]
8use crate::{
9 custom_cursor::{
10 calculate_effective_rect, extract_and_transform_rgba_pixels, extract_rgba_pixels,
11 transform_hotspot, CustomCursorPlugin,
12 },
13 state::{CustomCursorCache, CustomCursorCacheKey},
14 WinitCustomCursor,
15};
16use bevy_app::{App, Last, Plugin};
17#[cfg(feature = "custom_cursor")]
18use bevy_asset::Assets;
19#[cfg(feature = "custom_cursor")]
20use bevy_ecs::system::Res;
21use bevy_ecs::{
22 change_detection::DetectChanges,
23 component::Component,
24 entity::Entity,
25 observer::Trigger,
26 query::With,
27 reflect::ReflectComponent,
28 system::{Commands, Local, Query},
29 world::{OnRemove, Ref},
30};
31#[cfg(feature = "custom_cursor")]
32use bevy_image::{Image, TextureAtlasLayout};
33use bevy_platform::collections::HashSet;
34use bevy_reflect::{std_traits::ReflectDefault, Reflect};
35use bevy_window::{SystemCursorIcon, Window};
36#[cfg(feature = "custom_cursor")]
37use tracing::warn;
38
39#[cfg(feature = "custom_cursor")]
40pub use crate::custom_cursor::{CustomCursor, CustomCursorImage};
41
42pub(crate) struct CursorPlugin;
43
44impl Plugin for CursorPlugin {
45 fn build(&self, app: &mut App) {
46 #[cfg(feature = "custom_cursor")]
47 app.add_plugins(CustomCursorPlugin);
48
49 app.register_type::<CursorIcon>()
50 .add_systems(Last, update_cursors);
51
52 app.add_observer(on_remove_cursor_icon);
53 }
54}
55
56#[derive(Component, Debug, Clone, Reflect, PartialEq, Eq)]
58#[reflect(Component, Debug, Default, PartialEq, Clone)]
59pub enum CursorIcon {
60 #[cfg(feature = "custom_cursor")]
61 Custom(CustomCursor),
63 System(SystemCursorIcon),
65}
66
67impl Default for CursorIcon {
68 fn default() -> Self {
69 CursorIcon::System(Default::default())
70 }
71}
72
73impl From<SystemCursorIcon> for CursorIcon {
74 fn from(icon: SystemCursorIcon) -> Self {
75 CursorIcon::System(icon)
76 }
77}
78
79fn update_cursors(
80 mut commands: Commands,
81 windows: Query<(Entity, Ref<CursorIcon>), With<Window>>,
82 #[cfg(feature = "custom_cursor")] cursor_cache: Res<CustomCursorCache>,
83 #[cfg(feature = "custom_cursor")] images: Res<Assets<Image>>,
84 #[cfg(feature = "custom_cursor")] texture_atlases: Res<Assets<TextureAtlasLayout>>,
85 mut queue: Local<HashSet<Entity>>,
86) {
87 for (entity, cursor) in windows.iter() {
88 if !(queue.remove(&entity) || cursor.is_changed()) {
89 continue;
90 }
91
92 let cursor_source = match cursor.as_ref() {
93 #[cfg(feature = "custom_cursor")]
94 CursorIcon::Custom(CustomCursor::Image(c)) => {
95 let CustomCursorImage {
96 handle,
97 texture_atlas,
98 flip_x,
99 flip_y,
100 rect,
101 hotspot,
102 } = c;
103
104 let cache_key = CustomCursorCacheKey::Image {
105 id: handle.id(),
106 texture_atlas_layout_id: texture_atlas.as_ref().map(|a| a.layout.id()),
107 texture_atlas_index: texture_atlas.as_ref().map(|a| a.index),
108 flip_x: *flip_x,
109 flip_y: *flip_y,
110 rect: *rect,
111 };
112
113 if cursor_cache.0.contains_key(&cache_key) {
114 CursorSource::CustomCached(cache_key)
115 } else {
116 let Some(image) = images.get(handle) else {
117 warn!(
118 "Cursor image {handle:?} is not loaded yet and couldn't be used. Trying again next frame."
119 );
120 queue.insert(entity);
121 continue;
122 };
123
124 let (rect, needs_sub_image) =
125 calculate_effective_rect(&texture_atlases, image, texture_atlas, rect);
126
127 let (maybe_rgba, hotspot) = if *flip_x || *flip_y || needs_sub_image {
128 (
129 extract_and_transform_rgba_pixels(image, *flip_x, *flip_y, rect),
130 transform_hotspot(*hotspot, *flip_x, *flip_y, rect),
131 )
132 } else {
133 (extract_rgba_pixels(image), *hotspot)
134 };
135
136 let Some(rgba) = maybe_rgba else {
137 warn!("Cursor image {handle:?} not accepted because it's not rgba8 or rgba32float format");
138 continue;
139 };
140
141 let source = match WinitCustomCursor::from_rgba(
142 rgba,
143 rect.width() as u16,
144 rect.height() as u16,
145 hotspot.0,
146 hotspot.1,
147 ) {
148 Ok(source) => source,
149 Err(err) => {
150 warn!("Cursor image {handle:?} is invalid: {err}");
151 continue;
152 }
153 };
154
155 CursorSource::Custom((cache_key, source))
156 }
157 }
158 #[cfg(all(
159 feature = "custom_cursor",
160 target_family = "wasm",
161 target_os = "unknown"
162 ))]
163 CursorIcon::Custom(CustomCursor::Url(c)) => {
164 let cache_key = CustomCursorCacheKey::Url(c.url.clone());
165
166 if cursor_cache.0.contains_key(&cache_key) {
167 CursorSource::CustomCached(cache_key)
168 } else {
169 use crate::CustomCursorExtWebSys;
170 let source =
171 WinitCustomCursor::from_url(c.url.clone(), c.hotspot.0, c.hotspot.1);
172 CursorSource::Custom((cache_key, source))
173 }
174 }
175 CursorIcon::System(system_cursor_icon) => {
176 CursorSource::System(convert_system_cursor_icon(*system_cursor_icon))
177 }
178 };
179
180 commands
181 .entity(entity)
182 .insert(PendingCursor(Some(cursor_source)));
183 }
184}
185
186fn on_remove_cursor_icon(trigger: Trigger<OnRemove, CursorIcon>, mut commands: Commands) {
188 commands
190 .entity(trigger.target())
191 .try_insert(PendingCursor(Some(CursorSource::System(
192 convert_system_cursor_icon(SystemCursorIcon::Default),
193 ))));
194}