bevy_winit/
cursor.rs

1//! Components to customize winit cursor
2
3use 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/// Insert into a window entity to set the cursor for that window.
57#[derive(Component, Debug, Clone, Reflect, PartialEq, Eq)]
58#[reflect(Component, Debug, Default, PartialEq, Clone)]
59pub enum CursorIcon {
60    #[cfg(feature = "custom_cursor")]
61    /// Custom cursor image.
62    Custom(CustomCursor),
63    /// System provided cursor icon.
64    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
186/// Resets the cursor to the default icon when `CursorIcon` is removed.
187fn on_remove_cursor_icon(trigger: Trigger<OnRemove, CursorIcon>, mut commands: Commands) {
188    // Use `try_insert` to avoid panic if the window is being destroyed.
189    commands
190        .entity(trigger.target())
191        .try_insert(PendingCursor(Some(CursorSource::System(
192            convert_system_cursor_icon(SystemCursorIcon::Default),
193        ))));
194}