#[cfg(feature = "custom_cursor")]
mod custom_cursor;
#[cfg(feature = "custom_cursor")]
pub use custom_cursor::*;
use crate::{converters::convert_system_cursor_icon, state::WinitAppRunnerState, WINIT_WINDOWS};
use bevy_app::{App, Last, Plugin};
#[cfg(feature = "custom_cursor")]
use bevy_asset::Assets;
use bevy_ecs::{prelude::*, system::SystemState};
#[cfg(feature = "custom_cursor")]
use bevy_image::{Image, TextureAtlasLayout};
use bevy_platform::collections::HashSet;
#[cfg(feature = "custom_cursor")]
use bevy_window::CustomCursor;
use bevy_window::{CursorIcon, SystemCursorIcon, Window};
#[cfg(feature = "custom_cursor")]
use winit::event_loop::ActiveEventLoop;
pub(crate) struct WinitCursorPlugin;
impl Plugin for WinitCursorPlugin {
fn build(&self, app: &mut App) {
#[cfg(feature = "custom_cursor")]
{
if !app.is_plugin_added::<bevy_image::TextureAtlasPlugin>() {
app.add_plugins(bevy_image::TextureAtlasPlugin);
}
app.init_resource::<WinitCustomCursorCache>();
}
app.add_systems(Last, update_cursors)
.add_observer(on_remove_cursor_icon);
}
}
#[derive(Debug)]
pub enum CursorSource {
#[cfg(feature = "custom_cursor")]
CustomCached(CustomCursorCacheKey),
#[cfg(feature = "custom_cursor")]
Custom((CustomCursorCacheKey, winit::window::CustomCursorSource)),
System(winit::window::CursorIcon),
}
#[derive(Component, Debug)]
pub struct PendingCursor(pub Option<CursorSource>);
impl WinitAppRunnerState {
pub(crate) fn update_cursors(
&mut self,
#[cfg(feature = "custom_cursor")] event_loop: &ActiveEventLoop,
) {
#[cfg(feature = "custom_cursor")]
let mut windows_state: SystemState<(
ResMut<WinitCustomCursorCache>,
Query<(Entity, &mut PendingCursor), Changed<PendingCursor>>,
)> = SystemState::new(self.world_mut());
#[cfg(feature = "custom_cursor")]
let (mut cursor_cache, mut windows) = windows_state.get_mut(self.world_mut());
#[cfg(not(feature = "custom_cursor"))]
let mut windows_state: SystemState<(
Query<(Entity, &mut PendingCursor), Changed<PendingCursor>>,
)> = SystemState::new(self.world_mut());
#[cfg(not(feature = "custom_cursor"))]
let (mut windows,) = windows_state.get_mut(self.world_mut());
WINIT_WINDOWS.with_borrow(|winit_windows| {
for (entity, mut pending_cursor) in windows.iter_mut() {
let Some(winit_window) = winit_windows.get_window(entity) else {
continue;
};
let Some(pending_cursor) = pending_cursor.0.take() else {
continue;
};
let final_cursor: winit::window::Cursor = match pending_cursor {
#[cfg(feature = "custom_cursor")]
CursorSource::CustomCached(cache_key) => {
let Some(cached_cursor) = cursor_cache.0.get(&cache_key) else {
tracing::error!("Cursor should have been cached, but was not found");
continue;
};
cached_cursor.clone().into()
}
#[cfg(feature = "custom_cursor")]
CursorSource::Custom((cache_key, cursor)) => {
let custom_cursor = event_loop.create_custom_cursor(cursor);
cursor_cache.0.insert(cache_key, custom_cursor.clone());
custom_cursor.into()
}
CursorSource::System(system_cursor) => system_cursor.into(),
};
winit_window.set_cursor(final_cursor);
}
});
}
}
fn update_cursors(
mut commands: Commands,
windows: Query<(Entity, Ref<CursorIcon>), With<Window>>,
#[cfg(feature = "custom_cursor")] cursor_cache: Res<WinitCustomCursorCache>,
#[cfg(feature = "custom_cursor")] images: Res<Assets<Image>>,
#[cfg(feature = "custom_cursor")] texture_atlases: Res<Assets<TextureAtlasLayout>>,
mut queue: Local<HashSet<Entity>>,
) {
for (entity, cursor) in windows.iter() {
if !(queue.remove(&entity) || cursor.is_changed()) {
continue;
}
let cursor_source = match cursor.as_ref() {
#[cfg(feature = "custom_cursor")]
CursorIcon::Custom(CustomCursor::Image(c)) => {
let bevy_window::CustomCursorImage {
handle,
texture_atlas,
flip_x,
flip_y,
rect,
hotspot,
} = c;
let cache_key = CustomCursorCacheKey::Image {
id: handle.id(),
texture_atlas_layout_id: texture_atlas.as_ref().map(|a| a.layout.id()),
texture_atlas_index: texture_atlas.as_ref().map(|a| a.index),
flip_x: *flip_x,
flip_y: *flip_y,
rect: *rect,
};
if cursor_cache.0.contains_key(&cache_key) {
CursorSource::CustomCached(cache_key)
} else {
let Some(image) = images.get(handle) else {
tracing::warn!(
"Cursor image {handle:?} is not loaded yet and couldn't be used. Trying again next frame."
);
queue.insert(entity);
continue;
};
let (rect, needs_sub_image) =
calculate_effective_rect(&texture_atlases, image, texture_atlas, rect);
let (maybe_rgba, hotspot) = if *flip_x || *flip_y || needs_sub_image {
(
extract_and_transform_rgba_pixels(image, *flip_x, *flip_y, rect),
transform_hotspot(*hotspot, *flip_x, *flip_y, rect),
)
} else {
(extract_rgba_pixels(image), *hotspot)
};
let Some(rgba) = maybe_rgba else {
tracing::warn!("Cursor image {handle:?} not accepted because it's not rgba8 or rgba32float format");
continue;
};
let source = match winit::window::CustomCursor::from_rgba(
rgba,
rect.width() as u16,
rect.height() as u16,
hotspot.0,
hotspot.1,
) {
Ok(source) => source,
Err(err) => {
tracing::warn!("Cursor image {handle:?} is invalid: {err}");
continue;
}
};
CursorSource::Custom((cache_key, source))
}
}
#[cfg(feature = "custom_cursor")]
CursorIcon::Custom(CustomCursor::Url(_c)) => {
#[cfg(all(target_family = "wasm", target_os = "unknown"))]
{
let cache_key = CustomCursorCacheKey::Url(_c.url.clone());
if cursor_cache.0.contains_key(&cache_key) {
CursorSource::CustomCached(cache_key)
} else {
use crate::CustomCursorExtWebSys;
let source = winit::window::CustomCursor::from_url(
_c.url.clone(),
_c.hotspot.0,
_c.hotspot.1,
);
CursorSource::Custom((cache_key, source))
}
}
#[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
{
bevy_log::error_once!("CustomCursor::Url is not supported on this platform. Falling back to CursorIcon::System(SystemCursorIcon::Default)");
CursorSource::System(winit::window::CursorIcon::Default)
}
}
CursorIcon::System(system_cursor_icon) => {
CursorSource::System(convert_system_cursor_icon(*system_cursor_icon))
}
};
commands
.entity(entity)
.insert(PendingCursor(Some(cursor_source)));
}
}
fn on_remove_cursor_icon(remove: On<Remove, CursorIcon>, mut commands: Commands) {
commands
.entity(remove.entity)
.try_insert(PendingCursor(Some(CursorSource::System(
convert_system_cursor_icon(SystemCursorIcon::Default),
))));
}