mirror of
https://github.com/bevyengine/bevy
synced 2024-11-21 20:23:28 +00:00
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:
parent
d4ec80d5d2
commit
47c4e3084a
16 changed files with 375 additions and 111 deletions
|
@ -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" }
|
||||
|
||||
|
|
175
crates/bevy_render/src/view/window/cursor.rs
Normal file
175
crates/bevy_render/src/view/window/cursor.rs
Normal 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,
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
|
|
@ -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
|
||||
///
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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::*;
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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];
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue