1use crate::{
4 converters::convert_system_cursor_icon,
5 state::{CursorSource, PendingCursor},
6};
7#[cfg(feature = "custom_cursor")]
8use crate::{
9 state::{CustomCursorCache, CustomCursorCacheKey},
10 WinitCustomCursor,
11};
12use bevy_app::{App, Last, Plugin};
13#[cfg(feature = "custom_cursor")]
14use bevy_asset::{Assets, Handle};
15#[cfg(feature = "custom_cursor")]
16use bevy_ecs::system::Res;
17use bevy_ecs::{
18 change_detection::DetectChanges,
19 component::Component,
20 entity::Entity,
21 observer::Trigger,
22 query::With,
23 reflect::ReflectComponent,
24 system::{Commands, Local, Query},
25 world::{OnRemove, Ref},
26};
27#[cfg(feature = "custom_cursor")]
28use bevy_image::Image;
29use bevy_reflect::{std_traits::ReflectDefault, Reflect};
30#[cfg(feature = "custom_cursor")]
31use bevy_utils::tracing::warn;
32use bevy_utils::HashSet;
33use bevy_window::{SystemCursorIcon, Window};
34#[cfg(feature = "custom_cursor")]
35use wgpu_types::TextureFormat;
36
37pub(crate) struct CursorPlugin;
38
39impl Plugin for CursorPlugin {
40 fn build(&self, app: &mut App) {
41 #[cfg(feature = "custom_cursor")]
42 app.init_resource::<CustomCursorCache>();
43
44 app.register_type::<CursorIcon>()
45 .add_systems(Last, update_cursors);
46
47 app.add_observer(on_remove_cursor_icon);
48 }
49}
50
51#[derive(Component, Debug, Clone, Reflect, PartialEq, Eq)]
53#[reflect(Component, Debug, Default, PartialEq)]
54pub enum CursorIcon {
55 #[cfg(feature = "custom_cursor")]
56 Custom(CustomCursor),
58 System(SystemCursorIcon),
60}
61
62impl Default for CursorIcon {
63 fn default() -> Self {
64 CursorIcon::System(Default::default())
65 }
66}
67
68impl From<SystemCursorIcon> for CursorIcon {
69 fn from(icon: SystemCursorIcon) -> Self {
70 CursorIcon::System(icon)
71 }
72}
73
74#[cfg(feature = "custom_cursor")]
75impl From<CustomCursor> for CursorIcon {
76 fn from(cursor: CustomCursor) -> Self {
77 CursorIcon::Custom(cursor)
78 }
79}
80
81#[cfg(feature = "custom_cursor")]
82#[derive(Debug, Clone, Reflect, PartialEq, Eq, Hash)]
84pub enum CustomCursor {
85 Image {
87 handle: Handle<Image>,
90 hotspot: (u16, u16),
93 },
94 #[cfg(all(target_family = "wasm", target_os = "unknown"))]
95 Url {
97 url: String,
100 hotspot: (u16, u16),
103 },
104}
105
106fn update_cursors(
107 mut commands: Commands,
108 windows: Query<(Entity, Ref<CursorIcon>), With<Window>>,
109 #[cfg(feature = "custom_cursor")] cursor_cache: Res<CustomCursorCache>,
110 #[cfg(feature = "custom_cursor")] images: Res<Assets<Image>>,
111 mut queue: Local<HashSet<Entity>>,
112) {
113 for (entity, cursor) in windows.iter() {
114 if !(queue.remove(&entity) || cursor.is_changed()) {
115 continue;
116 }
117
118 let cursor_source = match cursor.as_ref() {
119 #[cfg(feature = "custom_cursor")]
120 CursorIcon::Custom(CustomCursor::Image { handle, hotspot }) => {
121 let cache_key = CustomCursorCacheKey::Asset(handle.id());
122
123 if cursor_cache.0.contains_key(&cache_key) {
124 CursorSource::CustomCached(cache_key)
125 } else {
126 let Some(image) = images.get(handle) else {
127 warn!(
128 "Cursor image {handle:?} is not loaded yet and couldn't be used. Trying again next frame."
129 );
130 queue.insert(entity);
131 continue;
132 };
133 let Some(rgba) = image_to_rgba_pixels(image) else {
134 warn!("Cursor image {handle:?} not accepted because it's not rgba8 or rgba32float format");
135 continue;
136 };
137
138 let width = image.texture_descriptor.size.width;
139 let height = image.texture_descriptor.size.height;
140 let source = match WinitCustomCursor::from_rgba(
141 rgba,
142 width as u16,
143 height as u16,
144 hotspot.0,
145 hotspot.1,
146 ) {
147 Ok(source) => source,
148 Err(err) => {
149 warn!("Cursor image {handle:?} is invalid: {err}");
150 continue;
151 }
152 };
153
154 CursorSource::Custom((cache_key, source))
155 }
156 }
157 #[cfg(all(
158 feature = "custom_cursor",
159 target_family = "wasm",
160 target_os = "unknown"
161 ))]
162 CursorIcon::Custom(CustomCursor::Url { url, hotspot }) => {
163 let cache_key = CustomCursorCacheKey::Url(url.clone());
164
165 if cursor_cache.0.contains_key(&cache_key) {
166 CursorSource::CustomCached(cache_key)
167 } else {
168 use crate::CustomCursorExtWebSys;
169 let source = WinitCustomCursor::from_url(url.clone(), hotspot.0, hotspot.1);
170 CursorSource::Custom((cache_key, source))
171 }
172 }
173 CursorIcon::System(system_cursor_icon) => {
174 CursorSource::System(convert_system_cursor_icon(*system_cursor_icon))
175 }
176 };
177
178 commands
179 .entity(entity)
180 .insert(PendingCursor(Some(cursor_source)));
181 }
182}
183
184fn on_remove_cursor_icon(trigger: Trigger<OnRemove, CursorIcon>, mut commands: Commands) {
186 commands
188 .entity(trigger.entity())
189 .try_insert(PendingCursor(Some(CursorSource::System(
190 convert_system_cursor_icon(SystemCursorIcon::Default),
191 ))));
192}
193
194#[cfg(feature = "custom_cursor")]
195fn image_to_rgba_pixels(image: &Image) -> Option<Vec<u8>> {
198 match image.texture_descriptor.format {
199 TextureFormat::Rgba8Unorm
200 | TextureFormat::Rgba8UnormSrgb
201 | TextureFormat::Rgba8Snorm
202 | TextureFormat::Rgba8Uint
203 | TextureFormat::Rgba8Sint => Some(image.data.clone()),
204 TextureFormat::Rgba32Float => Some(
205 image
206 .data
207 .chunks(4)
208 .map(|chunk| {
209 let chunk = chunk.try_into().unwrap();
210 let num = bytemuck::cast_ref::<[u8; 4], f32>(chunk);
211 (num * 255.0) as u8
212 })
213 .collect(),
214 ),
215 _ => None,
216 }
217}