Add custom cursors (#14284)

# Objective

- Add custom images as cursors
- Fixes #9557 

## Solution

- Change cursor type to accommodate both native and image cursors
- I don't really like this solution because I couldn't use
`Handle<Image>` directly. I would need to import `bevy_assets` and that
causes a circular dependency. Alternatively we could use winit's
`CustomCursor` smart pointers, but that seems hard because the event
loop is needed to create those and is not easily accessable for users.
So now I need to copy around rgba buffers which is sad.
- I use a cache because especially on the web creating cursor images is
really slow
- Sorry to #14196 for yoinking, I just wanted to make a quick solution
for myself and thought that I should probably share it too.

Update:
- Now uses `Handle<Image>`, reads rgba data in `bevy_render` and uses
resources to send the data to `bevy_winit`, where the final cursors are
created.

## Testing

- Added example which works fine at least on Linux Wayland (winit side
has been tested with all platforms).
- I haven't tested if the url cursor works.

## Migration Guide

- `CursorIcon` is no longer a field in `Window`, but a separate
component can be inserted to a window entity. It has been changed to an
enum that can hold custom images in addition to system icons.
- `Cursor` is renamed to `CursorOptions` and `cursor` field of `Window`
is renamed to `cursor_options`
- `CursorIcon` is renamed to `SystemCursorIcon`

---------

Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com>
Co-authored-by: Jan Hohenheim <jan@hohenheim.ch>
This commit is contained in:
Eero Lehtinen 2024-08-12 18:49:03 +03:00 committed by GitHub
parent d4ec80d5d2
commit 47c4e3084a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 375 additions and 111 deletions

View file

@ -58,6 +58,7 @@ bevy_render_macros = { path = "macros", version = "0.15.0-dev" }
bevy_time = { path = "../bevy_time", version = "0.15.0-dev" }
bevy_transform = { path = "../bevy_transform", version = "0.15.0-dev" }
bevy_window = { path = "../bevy_window", version = "0.15.0-dev" }
bevy_winit = { path = "../bevy_winit", version = "0.15.0-dev" }
bevy_utils = { path = "../bevy_utils", version = "0.15.0-dev" }
bevy_tasks = { path = "../bevy_tasks", version = "0.15.0-dev" }

View file

@ -0,0 +1,175 @@
use bevy_asset::{AssetId, Assets, Handle};
use bevy_ecs::{
change_detection::DetectChanges,
component::Component,
entity::Entity,
query::With,
reflect::ReflectComponent,
system::{Commands, Local, Query, Res},
world::Ref,
};
use bevy_reflect::{std_traits::ReflectDefault, Reflect};
use bevy_utils::{tracing::warn, HashSet};
use bevy_window::{SystemCursorIcon, Window};
use bevy_winit::{
convert_system_cursor_icon, CursorSource, CustomCursorCache, CustomCursorCacheKey,
PendingCursor,
};
use wgpu::TextureFormat;
use crate::prelude::Image;
/// Insert into a window entity to set the cursor for that window.
#[derive(Component, Debug, Clone, Reflect, PartialEq, Eq)]
#[reflect(Component, Debug, Default)]
pub enum CursorIcon {
/// Custom cursor image.
Custom(CustomCursor),
/// System provided cursor icon.
System(SystemCursorIcon),
}
impl Default for CursorIcon {
fn default() -> Self {
CursorIcon::System(Default::default())
}
}
impl From<SystemCursorIcon> for CursorIcon {
fn from(icon: SystemCursorIcon) -> Self {
CursorIcon::System(icon)
}
}
impl From<CustomCursor> for CursorIcon {
fn from(cursor: CustomCursor) -> Self {
CursorIcon::Custom(cursor)
}
}
/// Custom cursor image data.
#[derive(Debug, Clone, Reflect, PartialEq, Eq, Hash)]
pub enum CustomCursor {
/// Image to use as a cursor.
Image {
/// The image must be in 8 bit int or 32 bit float rgba. PNG images
/// work well for this.
handle: Handle<Image>,
/// X and Y coordinates of the hotspot in pixels. The hotspot must be
/// within the image bounds.
hotspot: (u16, u16),
},
#[cfg(all(target_family = "wasm", target_os = "unknown"))]
/// A URL to an image to use as the cursor.
Url {
/// Web URL to an image to use as the cursor. PNGs preferred. Cursor
/// creation can fail if the image is invalid or not reachable.
url: String,
/// X and Y coordinates of the hotspot in pixels. The hotspot must be
/// within the image bounds.
hotspot: (u16, u16),
},
}
pub fn update_cursors(
mut commands: Commands,
mut windows: Query<(Entity, Ref<CursorIcon>), With<Window>>,
cursor_cache: Res<CustomCursorCache>,
images: Res<Assets<Image>>,
mut queue: Local<HashSet<Entity>>,
) {
for (entity, cursor) in windows.iter_mut() {
if !(queue.remove(&entity) || cursor.is_changed()) {
continue;
}
let cursor_source = match cursor.as_ref() {
CursorIcon::Custom(CustomCursor::Image { handle, hotspot }) => {
let cache_key = match handle.id() {
AssetId::Index { index, .. } => {
CustomCursorCacheKey::AssetIndex(index.to_bits())
}
AssetId::Uuid { uuid } => CustomCursorCacheKey::AssetUuid(uuid.as_u128()),
};
if cursor_cache.0.contains_key(&cache_key) {
CursorSource::CustomCached(cache_key)
} else {
let Some(image) = images.get(handle) else {
warn!(
"Cursor image {handle:?} is not loaded yet and couldn't be used. Trying again next frame."
);
queue.insert(entity);
continue;
};
let Some(rgba) = image_to_rgba_pixels(image) else {
warn!("Cursor image {handle:?} not accepted because it's not rgba8 or rgba32float format");
continue;
};
let width = image.texture_descriptor.size.width;
let height = image.texture_descriptor.size.height;
let source = match bevy_winit::WinitCustomCursor::from_rgba(
rgba,
width as u16,
height as u16,
hotspot.0,
hotspot.1,
) {
Ok(source) => source,
Err(err) => {
warn!("Cursor image {handle:?} is invalid: {err}");
continue;
}
};
CursorSource::Custom((cache_key, source))
}
}
#[cfg(all(target_family = "wasm", target_os = "unknown"))]
CursorIcon::Custom(CustomCursor::Url { url, hotspot }) => {
let cache_key = CustomCursorCacheKey::Url(url.clone());
if cursor_cache.0.contains_key(&cache_key) {
CursorSource::CustomCached(cache_key)
} else {
use bevy_winit::CustomCursorExtWebSys;
let source =
bevy_winit::WinitCustomCursor::from_url(url.clone(), hotspot.0, hotspot.1);
CursorSource::Custom((cache_key, source))
}
}
CursorIcon::System(system_cursor_icon) => {
CursorSource::System(convert_system_cursor_icon(*system_cursor_icon))
}
};
commands
.entity(entity)
.insert(PendingCursor(Some(cursor_source)));
}
}
/// Returns the image data as a `Vec<u8>`.
/// Only supports rgba8 and rgba32float formats.
fn image_to_rgba_pixels(image: &Image) -> Option<Vec<u8>> {
match image.texture_descriptor.format {
TextureFormat::Rgba8Unorm
| TextureFormat::Rgba8UnormSrgb
| TextureFormat::Rgba8Snorm
| TextureFormat::Rgba8Uint
| TextureFormat::Rgba8Sint => Some(image.data.clone()),
TextureFormat::Rgba32Float => Some(
image
.data
.chunks(4)
.map(|chunk| {
let chunk = chunk.try_into().unwrap();
let num = bytemuck::cast_ref::<[u8; 4], f32>(chunk);
(num * 255.0) as u8
})
.collect(),
),
_ => None,
}
}

View file

@ -6,7 +6,7 @@ use crate::{
texture::TextureFormatPixelInfo,
Extract, ExtractSchedule, Render, RenderApp, RenderSet, WgpuWrapper,
};
use bevy_app::{App, Plugin};
use bevy_app::{App, Last, Plugin};
use bevy_ecs::{entity::EntityHashMap, prelude::*};
#[cfg(target_os = "linux")]
use bevy_utils::warn_once;
@ -14,6 +14,7 @@ use bevy_utils::{default, tracing::debug, HashSet};
use bevy_window::{
CompositeAlphaMode, PresentMode, PrimaryWindow, RawHandleWrapper, Window, WindowClosing,
};
use bevy_winit::CustomCursorCache;
use std::{
num::NonZeroU32,
ops::{Deref, DerefMut},
@ -24,17 +25,22 @@ use wgpu::{
TextureViewDescriptor,
};
pub mod cursor;
pub mod screenshot;
use screenshot::{
ScreenshotManager, ScreenshotPlugin, ScreenshotPreparedState, ScreenshotToScreenPipeline,
};
use self::cursor::update_cursors;
pub struct WindowRenderPlugin;
impl Plugin for WindowRenderPlugin {
fn build(&self, app: &mut App) {
app.add_plugins(ScreenshotPlugin);
app.add_plugins(ScreenshotPlugin)
.init_resource::<CustomCursorCache>()
.add_systems(Last, update_cursors);
if let Some(render_app) = app.get_sub_app_mut(RenderApp) {
render_app

View file

@ -15,19 +15,19 @@ use std::sync::{Arc, Mutex};
use bevy_a11y::Focus;
mod cursor;
mod event;
mod monitor;
mod raw_handle;
mod system;
mod system_cursor;
mod window;
pub use crate::raw_handle::*;
pub use cursor::*;
pub use event::*;
pub use monitor::*;
pub use system::*;
pub use system_cursor::*;
pub use window::*;
#[allow(missing_docs)]
@ -35,7 +35,7 @@ pub mod prelude {
#[allow(deprecated)]
#[doc(hidden)]
pub use crate::{
CursorEntered, CursorIcon, CursorLeft, CursorMoved, FileDragAndDrop, Ime, MonitorSelection,
CursorEntered, CursorLeft, CursorMoved, FileDragAndDrop, Ime, MonitorSelection,
ReceivedCharacter, Window, WindowMoved, WindowPlugin, WindowPosition,
WindowResizeConstraints,
};

View file

@ -73,7 +73,7 @@ use bevy_reflect::{prelude::ReflectDefault, Reflect};
#[cfg(feature = "serialize")]
use bevy_reflect::{ReflectDeserialize, ReflectSerialize};
/// The icon to display for a [`Window`](crate::window::Window)'s [`Cursor`](crate::window::Cursor).
/// The icon to display for a window.
///
/// Examples of all of these cursors can be found [here](https://www.w3schools.com/cssref/playit.php?filename=playcss_cursor&preval=crosshair).
/// This `enum` is simply a copy of a similar `enum` found in [`winit`](https://docs.rs/winit/latest/winit/window/enum.CursorIcon.html).
@ -89,7 +89,7 @@ use bevy_reflect::{ReflectDeserialize, ReflectSerialize};
reflect(Serialize, Deserialize)
)]
#[reflect(Debug, PartialEq, Default)]
pub enum CursorIcon {
pub enum SystemCursorIcon {
/// The platform-dependent default cursor. Often rendered as arrow.
#[default]
Default,
@ -107,7 +107,7 @@ pub enum CursorIcon {
Pointer,
/// A progress indicator. The program is performing some processing, but is
/// different from [`CursorIcon::Wait`] in that the user may still interact
/// different from [`SystemCursorIcon::Wait`] in that the user may still interact
/// with the program.
Progress,

View file

@ -12,8 +12,6 @@ use bevy_reflect::{ReflectDeserialize, ReflectSerialize};
use bevy_utils::tracing::warn;
use crate::CursorIcon;
/// Marker [`Component`] for the window considered the primary window.
///
/// Currently this is assumed to only exist on 1 entity at a time.
@ -107,16 +105,16 @@ impl NormalizedWindowRef {
///
/// Because this component is synchronized with `winit`, it can be used to perform
/// OS-integrated windowing operations. For example, here's a simple system
/// to change the cursor type:
/// to change the window mode:
///
/// ```
/// # use bevy_ecs::query::With;
/// # use bevy_ecs::system::Query;
/// # use bevy_window::{CursorIcon, PrimaryWindow, Window};
/// fn change_cursor(mut windows: Query<&mut Window, With<PrimaryWindow>>) {
/// # use bevy_window::{WindowMode, PrimaryWindow, Window, MonitorSelection};
/// fn change_window_mode(mut windows: Query<&mut Window, With<PrimaryWindow>>) {
/// // Query returns one window typically.
/// for mut window in windows.iter_mut() {
/// window.cursor.icon = CursorIcon::Wait;
/// window.mode = WindowMode::Fullscreen(MonitorSelection::Current);
/// }
/// }
/// ```
@ -128,8 +126,9 @@ impl NormalizedWindowRef {
)]
#[reflect(Component, Default)]
pub struct Window {
/// The cursor of this window.
pub cursor: Cursor,
/// The cursor options of this window. Cursor icons are set with the `Cursor` component on the
/// window entity.
pub cursor_options: CursorOptions,
/// What presentation mode to give the window.
pub present_mode: PresentMode,
/// Which fullscreen or windowing mode should be used.
@ -316,7 +315,7 @@ impl Default for Window {
Self {
title: "App".to_owned(),
name: None,
cursor: Default::default(),
cursor_options: Default::default(),
present_mode: Default::default(),
mode: Default::default(),
position: Default::default(),
@ -543,23 +542,20 @@ impl WindowResizeConstraints {
}
/// Cursor data for a [`Window`].
#[derive(Debug, Copy, Clone, Reflect)]
#[derive(Debug, Clone, Reflect)]
#[cfg_attr(
feature = "serialize",
derive(serde::Serialize, serde::Deserialize),
reflect(Serialize, Deserialize)
)]
#[reflect(Debug, Default)]
pub struct Cursor {
/// What the cursor should look like while inside the window.
pub icon: CursorIcon,
pub struct CursorOptions {
/// Whether the cursor is visible or not.
///
/// ## Platform-specific
///
/// - **`Windows`**, **`X11`**, and **`Wayland`**: The cursor is hidden only when inside the window.
/// To stop the cursor from leaving the window, change [`Cursor::grab_mode`] to [`CursorGrabMode::Locked`] or [`CursorGrabMode::Confined`]
/// To stop the cursor from leaving the window, change [`CursorOptions::grab_mode`] to [`CursorGrabMode::Locked`] or [`CursorGrabMode::Confined`]
/// - **`macOS`**: The cursor is hidden only when the window is focused.
/// - **`iOS`** and **`Android`** do not have cursors
pub visible: bool,
@ -583,10 +579,9 @@ pub struct Cursor {
pub hit_test: bool,
}
impl Default for Cursor {
impl Default for CursorOptions {
fn default() -> Self {
Cursor {
icon: CursorIcon::Default,
CursorOptions {
visible: true,
grab_mode: CursorGrabMode::None,
hit_test: true,
@ -870,7 +865,7 @@ impl From<DVec2> for WindowResolution {
}
}
/// Defines if and how the [`Cursor`] is grabbed by a [`Window`].
/// Defines if and how the cursor is grabbed by a [`Window`].
///
/// ## Platform-specific
///

View file

@ -6,7 +6,7 @@ use bevy_input::{
ButtonState,
};
use bevy_math::Vec2;
use bevy_window::{CursorIcon, EnabledButtons, WindowLevel, WindowTheme};
use bevy_window::{EnabledButtons, SystemCursorIcon, WindowLevel, WindowTheme};
use winit::keyboard::{Key, NamedKey, NativeKey};
pub fn convert_keyboard_input(
@ -628,41 +628,42 @@ pub fn convert_native_key(native_key: &NativeKey) -> bevy_input::keyboard::Nativ
}
}
pub fn convert_cursor_icon(cursor_icon: CursorIcon) -> winit::window::CursorIcon {
/// Converts a [`SystemCursorIcon`] to a [`winit::window::CursorIcon`].
pub fn convert_system_cursor_icon(cursor_icon: SystemCursorIcon) -> winit::window::CursorIcon {
match cursor_icon {
CursorIcon::Crosshair => winit::window::CursorIcon::Crosshair,
CursorIcon::Pointer => winit::window::CursorIcon::Pointer,
CursorIcon::Move => winit::window::CursorIcon::Move,
CursorIcon::Text => winit::window::CursorIcon::Text,
CursorIcon::Wait => winit::window::CursorIcon::Wait,
CursorIcon::Help => winit::window::CursorIcon::Help,
CursorIcon::Progress => winit::window::CursorIcon::Progress,
CursorIcon::NotAllowed => winit::window::CursorIcon::NotAllowed,
CursorIcon::ContextMenu => winit::window::CursorIcon::ContextMenu,
CursorIcon::Cell => winit::window::CursorIcon::Cell,
CursorIcon::VerticalText => winit::window::CursorIcon::VerticalText,
CursorIcon::Alias => winit::window::CursorIcon::Alias,
CursorIcon::Copy => winit::window::CursorIcon::Copy,
CursorIcon::NoDrop => winit::window::CursorIcon::NoDrop,
CursorIcon::Grab => winit::window::CursorIcon::Grab,
CursorIcon::Grabbing => winit::window::CursorIcon::Grabbing,
CursorIcon::AllScroll => winit::window::CursorIcon::AllScroll,
CursorIcon::ZoomIn => winit::window::CursorIcon::ZoomIn,
CursorIcon::ZoomOut => winit::window::CursorIcon::ZoomOut,
CursorIcon::EResize => winit::window::CursorIcon::EResize,
CursorIcon::NResize => winit::window::CursorIcon::NResize,
CursorIcon::NeResize => winit::window::CursorIcon::NeResize,
CursorIcon::NwResize => winit::window::CursorIcon::NwResize,
CursorIcon::SResize => winit::window::CursorIcon::SResize,
CursorIcon::SeResize => winit::window::CursorIcon::SeResize,
CursorIcon::SwResize => winit::window::CursorIcon::SwResize,
CursorIcon::WResize => winit::window::CursorIcon::WResize,
CursorIcon::EwResize => winit::window::CursorIcon::EwResize,
CursorIcon::NsResize => winit::window::CursorIcon::NsResize,
CursorIcon::NeswResize => winit::window::CursorIcon::NeswResize,
CursorIcon::NwseResize => winit::window::CursorIcon::NwseResize,
CursorIcon::ColResize => winit::window::CursorIcon::ColResize,
CursorIcon::RowResize => winit::window::CursorIcon::RowResize,
SystemCursorIcon::Crosshair => winit::window::CursorIcon::Crosshair,
SystemCursorIcon::Pointer => winit::window::CursorIcon::Pointer,
SystemCursorIcon::Move => winit::window::CursorIcon::Move,
SystemCursorIcon::Text => winit::window::CursorIcon::Text,
SystemCursorIcon::Wait => winit::window::CursorIcon::Wait,
SystemCursorIcon::Help => winit::window::CursorIcon::Help,
SystemCursorIcon::Progress => winit::window::CursorIcon::Progress,
SystemCursorIcon::NotAllowed => winit::window::CursorIcon::NotAllowed,
SystemCursorIcon::ContextMenu => winit::window::CursorIcon::ContextMenu,
SystemCursorIcon::Cell => winit::window::CursorIcon::Cell,
SystemCursorIcon::VerticalText => winit::window::CursorIcon::VerticalText,
SystemCursorIcon::Alias => winit::window::CursorIcon::Alias,
SystemCursorIcon::Copy => winit::window::CursorIcon::Copy,
SystemCursorIcon::NoDrop => winit::window::CursorIcon::NoDrop,
SystemCursorIcon::Grab => winit::window::CursorIcon::Grab,
SystemCursorIcon::Grabbing => winit::window::CursorIcon::Grabbing,
SystemCursorIcon::AllScroll => winit::window::CursorIcon::AllScroll,
SystemCursorIcon::ZoomIn => winit::window::CursorIcon::ZoomIn,
SystemCursorIcon::ZoomOut => winit::window::CursorIcon::ZoomOut,
SystemCursorIcon::EResize => winit::window::CursorIcon::EResize,
SystemCursorIcon::NResize => winit::window::CursorIcon::NResize,
SystemCursorIcon::NeResize => winit::window::CursorIcon::NeResize,
SystemCursorIcon::NwResize => winit::window::CursorIcon::NwResize,
SystemCursorIcon::SResize => winit::window::CursorIcon::SResize,
SystemCursorIcon::SeResize => winit::window::CursorIcon::SeResize,
SystemCursorIcon::SwResize => winit::window::CursorIcon::SwResize,
SystemCursorIcon::WResize => winit::window::CursorIcon::WResize,
SystemCursorIcon::EwResize => winit::window::CursorIcon::EwResize,
SystemCursorIcon::NsResize => winit::window::CursorIcon::NsResize,
SystemCursorIcon::NeswResize => winit::window::CursorIcon::NeswResize,
SystemCursorIcon::NwseResize => winit::window::CursorIcon::NwseResize,
SystemCursorIcon::ColResize => winit::window::CursorIcon::ColResize,
SystemCursorIcon::RowResize => winit::window::CursorIcon::RowResize,
_ => winit::window::CursorIcon::Default,
}
}

View file

@ -24,9 +24,14 @@ use bevy_app::{App, Last, Plugin};
use bevy_ecs::prelude::*;
#[allow(deprecated)]
use bevy_window::{exit_on_all_closed, Window, WindowCreated};
pub use converters::convert_system_cursor_icon;
pub use state::{CursorSource, CustomCursorCache, CustomCursorCacheKey, PendingCursor};
use system::{changed_windows, despawn_windows};
pub use system::{create_monitors, create_windows};
pub use winit::event_loop::EventLoopProxy;
#[cfg(all(target_family = "wasm", target_os = "unknown"))]
pub use winit::platform::web::CustomCursorExtWebSys;
pub use winit::window::{CustomCursor as WinitCustomCursor, CustomCursorSource};
pub use winit_config::*;
pub use winit_event::*;
pub use winit_windows::*;

View file

@ -15,7 +15,7 @@ use bevy_log::{error, trace, warn};
use bevy_math::{ivec2, DVec2, Vec2};
#[cfg(not(target_arch = "wasm32"))]
use bevy_tasks::tick_global_task_pools_on_main_thread;
use bevy_utils::Instant;
use bevy_utils::{HashMap, Instant};
use std::marker::PhantomData;
use winit::application::ApplicationHandler;
use winit::dpi::PhysicalSize;
@ -85,7 +85,7 @@ struct WinitAppRunnerState<T: Event> {
impl<T: Event> WinitAppRunnerState<T> {
fn new(mut app: App) -> Self {
app.add_event::<T>();
app.add_event::<T>().init_resource::<CustomCursorCache>();
let event_writer_system_state: SystemState<(
EventWriter<WindowResized>,
@ -131,6 +131,39 @@ impl<T: Event> WinitAppRunnerState<T> {
}
}
/// Identifiers for custom cursors used in caching.
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
pub enum CustomCursorCacheKey {
/// u64 is used instead of `AssetId`, because `bevy_asset` can't be imported here.
AssetIndex(u64),
/// u128 is used instead of `AssetId`, because `bevy_asset` can't be imported here.
AssetUuid(u128),
/// A URL to a cursor.
Url(String),
}
/// Caches custom cursors. On many platforms, creating custom cursors is expensive, especially on
/// the web.
#[derive(Debug, Clone, Default, Resource)]
pub struct CustomCursorCache(pub HashMap<CustomCursorCacheKey, winit::window::CustomCursor>);
/// A source for a cursor. Is created in `bevy_render` and consumed by the winit event loop.
#[derive(Debug)]
pub enum CursorSource {
/// A custom cursor was identified to be cached, no reason to recreate it.
CustomCached(CustomCursorCacheKey),
/// A custom cursor was not cached, so it needs to be created by the winit event loop.
Custom((CustomCursorCacheKey, winit::window::CustomCursorSource)),
/// A system cursor was requested.
System(winit::window::CursorIcon),
}
/// Component that indicates what cursor should be used for a window. Inserted
/// automatically after changing `CursorIcon` and consumed by the winit event
/// loop.
#[derive(Component, Debug)]
pub struct PendingCursor(pub Option<CursorSource>);
impl<T: Event> ApplicationHandler<T> for WinitAppRunnerState<T> {
fn new_events(&mut self, event_loop: &ActiveEventLoop, cause: StartCause) {
if event_loop.exiting() {
@ -520,6 +553,7 @@ impl<T: Event> ApplicationHandler<T> for WinitAppRunnerState<T> {
// This is a temporary solution, full solution is mentioned here: https://github.com/bevyengine/bevy/issues/1343#issuecomment-770091684
if !self.ran_update_since_last_redraw || all_invisible {
self.run_app_update();
self.update_cursors(event_loop);
self.ran_update_since_last_redraw = true;
} else {
self.redraw_requested = true;
@ -528,7 +562,6 @@ impl<T: Event> ApplicationHandler<T> for WinitAppRunnerState<T> {
// Running the app may have changed the WinitSettings resource, so we have to re-extract it.
let (config, windows) = focused_windows_state.get(self.world());
let focused = windows.iter().any(|(_, window)| window.focused);
update_mode = config.update_mode(focused);
}
@ -750,6 +783,42 @@ impl<T: Event> WinitAppRunnerState<T> {
.resource_mut::<Events<WinitEvent>>()
.send_batch(buffered_events);
}
fn update_cursors(&mut self, event_loop: &ActiveEventLoop) {
let mut windows_state: SystemState<(
NonSendMut<WinitWindows>,
ResMut<CustomCursorCache>,
Query<(Entity, &mut PendingCursor), Changed<PendingCursor>>,
)> = SystemState::new(self.world_mut());
let (winit_windows, mut cursor_cache, mut windows) =
windows_state.get_mut(self.world_mut());
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 {
CursorSource::CustomCached(cache_key) => {
let Some(cached_cursor) = cursor_cache.0.get(&cache_key) else {
error!("Cursor should have been cached, but was not found");
continue;
};
cached_cursor.clone().into()
}
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);
}
}
}
/// The default [`App::runner`] for the [`WinitPlugin`](crate::WinitPlugin) plugin.

View file

@ -29,8 +29,7 @@ use crate::state::react_to_resize;
use crate::winit_monitors::WinitMonitors;
use crate::{
converters::{
self, convert_enabled_buttons, convert_window_level, convert_window_theme,
convert_winit_theme,
convert_enabled_buttons, convert_window_level, convert_window_theme, convert_winit_theme,
},
get_best_videomode, get_fitting_videomode, select_monitor, CreateMonitorParams,
CreateWindowParams, WinitWindows,
@ -365,21 +364,17 @@ pub(crate) fn changed_windows(
}
}
if window.cursor.icon != cache.window.cursor.icon {
winit_window.set_cursor(converters::convert_cursor_icon(window.cursor.icon));
if window.cursor_options.grab_mode != cache.window.cursor_options.grab_mode {
crate::winit_windows::attempt_grab(winit_window, window.cursor_options.grab_mode);
}
if window.cursor.grab_mode != cache.window.cursor.grab_mode {
crate::winit_windows::attempt_grab(winit_window, window.cursor.grab_mode);
if window.cursor_options.visible != cache.window.cursor_options.visible {
winit_window.set_cursor_visible(window.cursor_options.visible);
}
if window.cursor.visible != cache.window.cursor.visible {
winit_window.set_cursor_visible(window.cursor.visible);
}
if window.cursor.hit_test != cache.window.cursor.hit_test {
if let Err(err) = winit_window.set_cursor_hittest(window.cursor.hit_test) {
window.cursor.hit_test = cache.window.cursor.hit_test;
if window.cursor_options.hit_test != cache.window.cursor_options.hit_test {
if let Err(err) = winit_window.set_cursor_hittest(window.cursor_options.hit_test) {
window.cursor_options.hit_test = cache.window.cursor_options.hit_test;
warn!(
"Could not set cursor hit test for window {:?}: {:?}",
window.title, err

View file

@ -247,16 +247,16 @@ impl WinitWindows {
);
// Do not set the grab mode on window creation if it's none. It can fail on mobile.
if window.cursor.grab_mode != CursorGrabMode::None {
attempt_grab(&winit_window, window.cursor.grab_mode);
if window.cursor_options.grab_mode != CursorGrabMode::None {
attempt_grab(&winit_window, window.cursor_options.grab_mode);
}
winit_window.set_cursor_visible(window.cursor.visible);
winit_window.set_cursor_visible(window.cursor_options.visible);
// Do not set the cursor hittest on window creation if it's false, as it will always fail on
// some platforms and log an unfixable warning.
if !window.cursor.hit_test {
if let Err(err) = winit_window.set_cursor_hittest(window.cursor.hit_test) {
if !window.cursor_options.hit_test {
if let Err(err) = winit_window.set_cursor_hittest(window.cursor_options.hit_test) {
warn!(
"Could not set cursor hit test for window {:?}: {:?}",
window.title, err

View file

@ -237,7 +237,7 @@ fn update_cursor_hit_test(
// If the window has decorations (e.g. a border) then it should be clickable
if primary_window.decorations {
primary_window.cursor.hit_test = true;
primary_window.cursor_options.hit_test = true;
return;
}
@ -248,7 +248,7 @@ fn update_cursor_hit_test(
// If the cursor is within the radius of the Bevy logo make the window clickable otherwise the window is not clickable
let bevy_logo_transform = q_bevy_logo.single();
primary_window.cursor.hit_test = bevy_logo_transform
primary_window.cursor_options.hit_test = bevy_logo_transform
.translation
.truncate()
.distance(cursor_world_pos)

View file

@ -198,13 +198,13 @@ fn run_camera_controller(
continue;
}
window.cursor.grab_mode = CursorGrabMode::Locked;
window.cursor.visible = false;
window.cursor_options.grab_mode = CursorGrabMode::Locked;
window.cursor_options.visible = false;
}
} else {
for mut window in &mut windows {
window.cursor.grab_mode = CursorGrabMode::None;
window.cursor.visible = true;
window.cursor_options.grab_mode = CursorGrabMode::None;
window.cursor_options.visible = true;
}
}
}

View file

@ -19,12 +19,12 @@ fn grab_mouse(
let mut window = windows.single_mut();
if mouse.just_pressed(MouseButton::Left) {
window.cursor.visible = false;
window.cursor.grab_mode = CursorGrabMode::Locked;
window.cursor_options.visible = false;
window.cursor_options.grab_mode = CursorGrabMode::Locked;
}
if key.just_pressed(KeyCode::Escape) {
window.cursor.visible = true;
window.cursor.grab_mode = CursorGrabMode::None;
window.cursor_options.visible = true;
window.cursor_options.grab_mode = CursorGrabMode::None;
}
}

View file

@ -53,6 +53,6 @@ fn toggle_mouse_passthrough(
) {
if keyboard_input.just_pressed(KeyCode::KeyP) {
let mut window = windows.single_mut();
window.cursor.hit_test = !window.cursor.hit_test;
window.cursor_options.hit_test = !window.cursor_options.hit_test;
}
}

View file

@ -5,7 +5,8 @@ use bevy::{
core::FrameCount,
diagnostic::{FrameTimeDiagnosticsPlugin, LogDiagnosticsPlugin},
prelude::*,
window::{CursorGrabMode, PresentMode, WindowLevel, WindowTheme},
render::view::cursor::{CursorIcon, CustomCursor},
window::{CursorGrabMode, PresentMode, SystemCursorIcon, WindowLevel, WindowTheme},
};
fn main() {
@ -37,6 +38,7 @@ fn main() {
LogDiagnosticsPlugin::default(),
FrameTimeDiagnosticsPlugin,
))
.add_systems(Startup, init_cursor_icons)
.add_systems(
Update,
(
@ -137,8 +139,8 @@ fn toggle_cursor(mut windows: Query<&mut Window>, input: Res<ButtonInput<KeyCode
if input.just_pressed(KeyCode::Space) {
let mut window = windows.single_mut();
window.cursor.visible = !window.cursor.visible;
window.cursor.grab_mode = match window.cursor.grab_mode {
window.cursor_options.visible = !window.cursor_options.visible;
window.cursor_options.grab_mode = match window.cursor_options.grab_mode {
CursorGrabMode::None => CursorGrabMode::Locked,
CursorGrabMode::Locked | CursorGrabMode::Confined => CursorGrabMode::None,
};
@ -159,31 +161,46 @@ fn toggle_theme(mut windows: Query<&mut Window>, input: Res<ButtonInput<KeyCode>
}
}
#[derive(Resource)]
struct CursorIcons(Vec<CursorIcon>);
fn init_cursor_icons(mut commands: Commands, asset_server: Res<AssetServer>) {
commands.insert_resource(CursorIcons(vec![
SystemCursorIcon::Default.into(),
SystemCursorIcon::Pointer.into(),
SystemCursorIcon::Wait.into(),
SystemCursorIcon::Text.into(),
CustomCursor::Image {
handle: asset_server.load("branding/icon.png"),
hotspot: (128, 128),
}
.into(),
]));
}
/// This system cycles the cursor's icon through a small set of icons when clicking
fn cycle_cursor_icon(
mut windows: Query<&mut Window>,
mut commands: Commands,
windows: Query<Entity, With<Window>>,
input: Res<ButtonInput<MouseButton>>,
mut index: Local<usize>,
cursor_icons: Res<CursorIcons>,
) {
let mut window = windows.single_mut();
const ICONS: &[CursorIcon] = &[
CursorIcon::Default,
CursorIcon::Pointer,
CursorIcon::Wait,
CursorIcon::Text,
CursorIcon::Copy,
];
let window_entity = windows.single();
if input.just_pressed(MouseButton::Left) {
*index = (*index + 1) % ICONS.len();
*index = (*index + 1) % cursor_icons.0.len();
commands
.entity(window_entity)
.insert(cursor_icons.0[*index].clone());
} else if input.just_pressed(MouseButton::Right) {
*index = if *index == 0 {
ICONS.len() - 1
cursor_icons.0.len() - 1
} else {
*index - 1
};
commands
.entity(window_entity)
.insert(cursor_icons.0[*index].clone());
}
window.cursor.icon = ICONS[*index];
}