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
@ -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_time = { path = "../bevy_time", version = "0.15.0-dev" }
|
||||||
bevy_transform = { path = "../bevy_transform", 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_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_utils = { path = "../bevy_utils", version = "0.15.0-dev" }
|
||||||
bevy_tasks = { path = "../bevy_tasks", 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,
|
texture::TextureFormatPixelInfo,
|
||||||
Extract, ExtractSchedule, Render, RenderApp, RenderSet, WgpuWrapper,
|
Extract, ExtractSchedule, Render, RenderApp, RenderSet, WgpuWrapper,
|
||||||
};
|
};
|
||||||
use bevy_app::{App, Plugin};
|
use bevy_app::{App, Last, Plugin};
|
||||||
use bevy_ecs::{entity::EntityHashMap, prelude::*};
|
use bevy_ecs::{entity::EntityHashMap, prelude::*};
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
use bevy_utils::warn_once;
|
use bevy_utils::warn_once;
|
||||||
@ -14,6 +14,7 @@ use bevy_utils::{default, tracing::debug, HashSet};
|
|||||||
use bevy_window::{
|
use bevy_window::{
|
||||||
CompositeAlphaMode, PresentMode, PrimaryWindow, RawHandleWrapper, Window, WindowClosing,
|
CompositeAlphaMode, PresentMode, PrimaryWindow, RawHandleWrapper, Window, WindowClosing,
|
||||||
};
|
};
|
||||||
|
use bevy_winit::CustomCursorCache;
|
||||||
use std::{
|
use std::{
|
||||||
num::NonZeroU32,
|
num::NonZeroU32,
|
||||||
ops::{Deref, DerefMut},
|
ops::{Deref, DerefMut},
|
||||||
@ -24,17 +25,22 @@ use wgpu::{
|
|||||||
TextureViewDescriptor,
|
TextureViewDescriptor,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
pub mod cursor;
|
||||||
pub mod screenshot;
|
pub mod screenshot;
|
||||||
|
|
||||||
use screenshot::{
|
use screenshot::{
|
||||||
ScreenshotManager, ScreenshotPlugin, ScreenshotPreparedState, ScreenshotToScreenPipeline,
|
ScreenshotManager, ScreenshotPlugin, ScreenshotPreparedState, ScreenshotToScreenPipeline,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use self::cursor::update_cursors;
|
||||||
|
|
||||||
pub struct WindowRenderPlugin;
|
pub struct WindowRenderPlugin;
|
||||||
|
|
||||||
impl Plugin for WindowRenderPlugin {
|
impl Plugin for WindowRenderPlugin {
|
||||||
fn build(&self, app: &mut App) {
|
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) {
|
if let Some(render_app) = app.get_sub_app_mut(RenderApp) {
|
||||||
render_app
|
render_app
|
||||||
|
|||||||
@ -15,19 +15,19 @@ use std::sync::{Arc, Mutex};
|
|||||||
|
|
||||||
use bevy_a11y::Focus;
|
use bevy_a11y::Focus;
|
||||||
|
|
||||||
mod cursor;
|
|
||||||
mod event;
|
mod event;
|
||||||
mod monitor;
|
mod monitor;
|
||||||
mod raw_handle;
|
mod raw_handle;
|
||||||
mod system;
|
mod system;
|
||||||
|
mod system_cursor;
|
||||||
mod window;
|
mod window;
|
||||||
|
|
||||||
pub use crate::raw_handle::*;
|
pub use crate::raw_handle::*;
|
||||||
|
|
||||||
pub use cursor::*;
|
|
||||||
pub use event::*;
|
pub use event::*;
|
||||||
pub use monitor::*;
|
pub use monitor::*;
|
||||||
pub use system::*;
|
pub use system::*;
|
||||||
|
pub use system_cursor::*;
|
||||||
pub use window::*;
|
pub use window::*;
|
||||||
|
|
||||||
#[allow(missing_docs)]
|
#[allow(missing_docs)]
|
||||||
@ -35,7 +35,7 @@ pub mod prelude {
|
|||||||
#[allow(deprecated)]
|
#[allow(deprecated)]
|
||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
pub use crate::{
|
pub use crate::{
|
||||||
CursorEntered, CursorIcon, CursorLeft, CursorMoved, FileDragAndDrop, Ime, MonitorSelection,
|
CursorEntered, CursorLeft, CursorMoved, FileDragAndDrop, Ime, MonitorSelection,
|
||||||
ReceivedCharacter, Window, WindowMoved, WindowPlugin, WindowPosition,
|
ReceivedCharacter, Window, WindowMoved, WindowPlugin, WindowPosition,
|
||||||
WindowResizeConstraints,
|
WindowResizeConstraints,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -73,7 +73,7 @@ use bevy_reflect::{prelude::ReflectDefault, Reflect};
|
|||||||
#[cfg(feature = "serialize")]
|
#[cfg(feature = "serialize")]
|
||||||
use bevy_reflect::{ReflectDeserialize, ReflectSerialize};
|
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).
|
/// 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).
|
/// 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(Serialize, Deserialize)
|
||||||
)]
|
)]
|
||||||
#[reflect(Debug, PartialEq, Default)]
|
#[reflect(Debug, PartialEq, Default)]
|
||||||
pub enum CursorIcon {
|
pub enum SystemCursorIcon {
|
||||||
/// The platform-dependent default cursor. Often rendered as arrow.
|
/// The platform-dependent default cursor. Often rendered as arrow.
|
||||||
#[default]
|
#[default]
|
||||||
Default,
|
Default,
|
||||||
@ -107,7 +107,7 @@ pub enum CursorIcon {
|
|||||||
Pointer,
|
Pointer,
|
||||||
|
|
||||||
/// A progress indicator. The program is performing some processing, but is
|
/// 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.
|
/// with the program.
|
||||||
Progress,
|
Progress,
|
||||||
|
|
||||||
@ -12,8 +12,6 @@ use bevy_reflect::{ReflectDeserialize, ReflectSerialize};
|
|||||||
|
|
||||||
use bevy_utils::tracing::warn;
|
use bevy_utils::tracing::warn;
|
||||||
|
|
||||||
use crate::CursorIcon;
|
|
||||||
|
|
||||||
/// Marker [`Component`] for the window considered the primary window.
|
/// Marker [`Component`] for the window considered the primary window.
|
||||||
///
|
///
|
||||||
/// Currently this is assumed to only exist on 1 entity at a time.
|
/// 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
|
/// Because this component is synchronized with `winit`, it can be used to perform
|
||||||
/// OS-integrated windowing operations. For example, here's a simple system
|
/// 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::query::With;
|
||||||
/// # use bevy_ecs::system::Query;
|
/// # use bevy_ecs::system::Query;
|
||||||
/// # use bevy_window::{CursorIcon, PrimaryWindow, Window};
|
/// # use bevy_window::{WindowMode, PrimaryWindow, Window, MonitorSelection};
|
||||||
/// fn change_cursor(mut windows: Query<&mut Window, With<PrimaryWindow>>) {
|
/// fn change_window_mode(mut windows: Query<&mut Window, With<PrimaryWindow>>) {
|
||||||
/// // Query returns one window typically.
|
/// // Query returns one window typically.
|
||||||
/// for mut window in windows.iter_mut() {
|
/// 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)]
|
#[reflect(Component, Default)]
|
||||||
pub struct Window {
|
pub struct Window {
|
||||||
/// The cursor of this window.
|
/// The cursor options of this window. Cursor icons are set with the `Cursor` component on the
|
||||||
pub cursor: Cursor,
|
/// window entity.
|
||||||
|
pub cursor_options: CursorOptions,
|
||||||
/// What presentation mode to give the window.
|
/// What presentation mode to give the window.
|
||||||
pub present_mode: PresentMode,
|
pub present_mode: PresentMode,
|
||||||
/// Which fullscreen or windowing mode should be used.
|
/// Which fullscreen or windowing mode should be used.
|
||||||
@ -316,7 +315,7 @@ impl Default for Window {
|
|||||||
Self {
|
Self {
|
||||||
title: "App".to_owned(),
|
title: "App".to_owned(),
|
||||||
name: None,
|
name: None,
|
||||||
cursor: Default::default(),
|
cursor_options: Default::default(),
|
||||||
present_mode: Default::default(),
|
present_mode: Default::default(),
|
||||||
mode: Default::default(),
|
mode: Default::default(),
|
||||||
position: Default::default(),
|
position: Default::default(),
|
||||||
@ -543,23 +542,20 @@ impl WindowResizeConstraints {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Cursor data for a [`Window`].
|
/// Cursor data for a [`Window`].
|
||||||
#[derive(Debug, Copy, Clone, Reflect)]
|
#[derive(Debug, Clone, Reflect)]
|
||||||
#[cfg_attr(
|
#[cfg_attr(
|
||||||
feature = "serialize",
|
feature = "serialize",
|
||||||
derive(serde::Serialize, serde::Deserialize),
|
derive(serde::Serialize, serde::Deserialize),
|
||||||
reflect(Serialize, Deserialize)
|
reflect(Serialize, Deserialize)
|
||||||
)]
|
)]
|
||||||
#[reflect(Debug, Default)]
|
#[reflect(Debug, Default)]
|
||||||
pub struct Cursor {
|
pub struct CursorOptions {
|
||||||
/// What the cursor should look like while inside the window.
|
|
||||||
pub icon: CursorIcon,
|
|
||||||
|
|
||||||
/// Whether the cursor is visible or not.
|
/// Whether the cursor is visible or not.
|
||||||
///
|
///
|
||||||
/// ## Platform-specific
|
/// ## Platform-specific
|
||||||
///
|
///
|
||||||
/// - **`Windows`**, **`X11`**, and **`Wayland`**: The cursor is hidden only when inside the window.
|
/// - **`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.
|
/// - **`macOS`**: The cursor is hidden only when the window is focused.
|
||||||
/// - **`iOS`** and **`Android`** do not have cursors
|
/// - **`iOS`** and **`Android`** do not have cursors
|
||||||
pub visible: bool,
|
pub visible: bool,
|
||||||
@ -583,10 +579,9 @@ pub struct Cursor {
|
|||||||
pub hit_test: bool,
|
pub hit_test: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Cursor {
|
impl Default for CursorOptions {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Cursor {
|
CursorOptions {
|
||||||
icon: CursorIcon::Default,
|
|
||||||
visible: true,
|
visible: true,
|
||||||
grab_mode: CursorGrabMode::None,
|
grab_mode: CursorGrabMode::None,
|
||||||
hit_test: true,
|
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
|
/// ## Platform-specific
|
||||||
///
|
///
|
||||||
|
|||||||
@ -6,7 +6,7 @@ use bevy_input::{
|
|||||||
ButtonState,
|
ButtonState,
|
||||||
};
|
};
|
||||||
use bevy_math::Vec2;
|
use bevy_math::Vec2;
|
||||||
use bevy_window::{CursorIcon, EnabledButtons, WindowLevel, WindowTheme};
|
use bevy_window::{EnabledButtons, SystemCursorIcon, WindowLevel, WindowTheme};
|
||||||
use winit::keyboard::{Key, NamedKey, NativeKey};
|
use winit::keyboard::{Key, NamedKey, NativeKey};
|
||||||
|
|
||||||
pub fn convert_keyboard_input(
|
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 {
|
match cursor_icon {
|
||||||
CursorIcon::Crosshair => winit::window::CursorIcon::Crosshair,
|
SystemCursorIcon::Crosshair => winit::window::CursorIcon::Crosshair,
|
||||||
CursorIcon::Pointer => winit::window::CursorIcon::Pointer,
|
SystemCursorIcon::Pointer => winit::window::CursorIcon::Pointer,
|
||||||
CursorIcon::Move => winit::window::CursorIcon::Move,
|
SystemCursorIcon::Move => winit::window::CursorIcon::Move,
|
||||||
CursorIcon::Text => winit::window::CursorIcon::Text,
|
SystemCursorIcon::Text => winit::window::CursorIcon::Text,
|
||||||
CursorIcon::Wait => winit::window::CursorIcon::Wait,
|
SystemCursorIcon::Wait => winit::window::CursorIcon::Wait,
|
||||||
CursorIcon::Help => winit::window::CursorIcon::Help,
|
SystemCursorIcon::Help => winit::window::CursorIcon::Help,
|
||||||
CursorIcon::Progress => winit::window::CursorIcon::Progress,
|
SystemCursorIcon::Progress => winit::window::CursorIcon::Progress,
|
||||||
CursorIcon::NotAllowed => winit::window::CursorIcon::NotAllowed,
|
SystemCursorIcon::NotAllowed => winit::window::CursorIcon::NotAllowed,
|
||||||
CursorIcon::ContextMenu => winit::window::CursorIcon::ContextMenu,
|
SystemCursorIcon::ContextMenu => winit::window::CursorIcon::ContextMenu,
|
||||||
CursorIcon::Cell => winit::window::CursorIcon::Cell,
|
SystemCursorIcon::Cell => winit::window::CursorIcon::Cell,
|
||||||
CursorIcon::VerticalText => winit::window::CursorIcon::VerticalText,
|
SystemCursorIcon::VerticalText => winit::window::CursorIcon::VerticalText,
|
||||||
CursorIcon::Alias => winit::window::CursorIcon::Alias,
|
SystemCursorIcon::Alias => winit::window::CursorIcon::Alias,
|
||||||
CursorIcon::Copy => winit::window::CursorIcon::Copy,
|
SystemCursorIcon::Copy => winit::window::CursorIcon::Copy,
|
||||||
CursorIcon::NoDrop => winit::window::CursorIcon::NoDrop,
|
SystemCursorIcon::NoDrop => winit::window::CursorIcon::NoDrop,
|
||||||
CursorIcon::Grab => winit::window::CursorIcon::Grab,
|
SystemCursorIcon::Grab => winit::window::CursorIcon::Grab,
|
||||||
CursorIcon::Grabbing => winit::window::CursorIcon::Grabbing,
|
SystemCursorIcon::Grabbing => winit::window::CursorIcon::Grabbing,
|
||||||
CursorIcon::AllScroll => winit::window::CursorIcon::AllScroll,
|
SystemCursorIcon::AllScroll => winit::window::CursorIcon::AllScroll,
|
||||||
CursorIcon::ZoomIn => winit::window::CursorIcon::ZoomIn,
|
SystemCursorIcon::ZoomIn => winit::window::CursorIcon::ZoomIn,
|
||||||
CursorIcon::ZoomOut => winit::window::CursorIcon::ZoomOut,
|
SystemCursorIcon::ZoomOut => winit::window::CursorIcon::ZoomOut,
|
||||||
CursorIcon::EResize => winit::window::CursorIcon::EResize,
|
SystemCursorIcon::EResize => winit::window::CursorIcon::EResize,
|
||||||
CursorIcon::NResize => winit::window::CursorIcon::NResize,
|
SystemCursorIcon::NResize => winit::window::CursorIcon::NResize,
|
||||||
CursorIcon::NeResize => winit::window::CursorIcon::NeResize,
|
SystemCursorIcon::NeResize => winit::window::CursorIcon::NeResize,
|
||||||
CursorIcon::NwResize => winit::window::CursorIcon::NwResize,
|
SystemCursorIcon::NwResize => winit::window::CursorIcon::NwResize,
|
||||||
CursorIcon::SResize => winit::window::CursorIcon::SResize,
|
SystemCursorIcon::SResize => winit::window::CursorIcon::SResize,
|
||||||
CursorIcon::SeResize => winit::window::CursorIcon::SeResize,
|
SystemCursorIcon::SeResize => winit::window::CursorIcon::SeResize,
|
||||||
CursorIcon::SwResize => winit::window::CursorIcon::SwResize,
|
SystemCursorIcon::SwResize => winit::window::CursorIcon::SwResize,
|
||||||
CursorIcon::WResize => winit::window::CursorIcon::WResize,
|
SystemCursorIcon::WResize => winit::window::CursorIcon::WResize,
|
||||||
CursorIcon::EwResize => winit::window::CursorIcon::EwResize,
|
SystemCursorIcon::EwResize => winit::window::CursorIcon::EwResize,
|
||||||
CursorIcon::NsResize => winit::window::CursorIcon::NsResize,
|
SystemCursorIcon::NsResize => winit::window::CursorIcon::NsResize,
|
||||||
CursorIcon::NeswResize => winit::window::CursorIcon::NeswResize,
|
SystemCursorIcon::NeswResize => winit::window::CursorIcon::NeswResize,
|
||||||
CursorIcon::NwseResize => winit::window::CursorIcon::NwseResize,
|
SystemCursorIcon::NwseResize => winit::window::CursorIcon::NwseResize,
|
||||||
CursorIcon::ColResize => winit::window::CursorIcon::ColResize,
|
SystemCursorIcon::ColResize => winit::window::CursorIcon::ColResize,
|
||||||
CursorIcon::RowResize => winit::window::CursorIcon::RowResize,
|
SystemCursorIcon::RowResize => winit::window::CursorIcon::RowResize,
|
||||||
_ => winit::window::CursorIcon::Default,
|
_ => winit::window::CursorIcon::Default,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -24,9 +24,14 @@ use bevy_app::{App, Last, Plugin};
|
|||||||
use bevy_ecs::prelude::*;
|
use bevy_ecs::prelude::*;
|
||||||
#[allow(deprecated)]
|
#[allow(deprecated)]
|
||||||
use bevy_window::{exit_on_all_closed, Window, WindowCreated};
|
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};
|
use system::{changed_windows, despawn_windows};
|
||||||
pub use system::{create_monitors, create_windows};
|
pub use system::{create_monitors, create_windows};
|
||||||
pub use winit::event_loop::EventLoopProxy;
|
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_config::*;
|
||||||
pub use winit_event::*;
|
pub use winit_event::*;
|
||||||
pub use winit_windows::*;
|
pub use winit_windows::*;
|
||||||
|
|||||||
@ -15,7 +15,7 @@ use bevy_log::{error, trace, warn};
|
|||||||
use bevy_math::{ivec2, DVec2, Vec2};
|
use bevy_math::{ivec2, DVec2, Vec2};
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
use bevy_tasks::tick_global_task_pools_on_main_thread;
|
use bevy_tasks::tick_global_task_pools_on_main_thread;
|
||||||
use bevy_utils::Instant;
|
use bevy_utils::{HashMap, Instant};
|
||||||
use std::marker::PhantomData;
|
use std::marker::PhantomData;
|
||||||
use winit::application::ApplicationHandler;
|
use winit::application::ApplicationHandler;
|
||||||
use winit::dpi::PhysicalSize;
|
use winit::dpi::PhysicalSize;
|
||||||
@ -85,7 +85,7 @@ struct WinitAppRunnerState<T: Event> {
|
|||||||
|
|
||||||
impl<T: Event> WinitAppRunnerState<T> {
|
impl<T: Event> WinitAppRunnerState<T> {
|
||||||
fn new(mut app: App) -> Self {
|
fn new(mut app: App) -> Self {
|
||||||
app.add_event::<T>();
|
app.add_event::<T>().init_resource::<CustomCursorCache>();
|
||||||
|
|
||||||
let event_writer_system_state: SystemState<(
|
let event_writer_system_state: SystemState<(
|
||||||
EventWriter<WindowResized>,
|
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> {
|
impl<T: Event> ApplicationHandler<T> for WinitAppRunnerState<T> {
|
||||||
fn new_events(&mut self, event_loop: &ActiveEventLoop, cause: StartCause) {
|
fn new_events(&mut self, event_loop: &ActiveEventLoop, cause: StartCause) {
|
||||||
if event_loop.exiting() {
|
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
|
// 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 {
|
if !self.ran_update_since_last_redraw || all_invisible {
|
||||||
self.run_app_update();
|
self.run_app_update();
|
||||||
|
self.update_cursors(event_loop);
|
||||||
self.ran_update_since_last_redraw = true;
|
self.ran_update_since_last_redraw = true;
|
||||||
} else {
|
} else {
|
||||||
self.redraw_requested = true;
|
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.
|
// 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 (config, windows) = focused_windows_state.get(self.world());
|
||||||
let focused = windows.iter().any(|(_, window)| window.focused);
|
let focused = windows.iter().any(|(_, window)| window.focused);
|
||||||
|
|
||||||
update_mode = config.update_mode(focused);
|
update_mode = config.update_mode(focused);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -750,6 +783,42 @@ impl<T: Event> WinitAppRunnerState<T> {
|
|||||||
.resource_mut::<Events<WinitEvent>>()
|
.resource_mut::<Events<WinitEvent>>()
|
||||||
.send_batch(buffered_events);
|
.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.
|
/// 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::winit_monitors::WinitMonitors;
|
||||||
use crate::{
|
use crate::{
|
||||||
converters::{
|
converters::{
|
||||||
self, convert_enabled_buttons, convert_window_level, convert_window_theme,
|
convert_enabled_buttons, convert_window_level, convert_window_theme, convert_winit_theme,
|
||||||
convert_winit_theme,
|
|
||||||
},
|
},
|
||||||
get_best_videomode, get_fitting_videomode, select_monitor, CreateMonitorParams,
|
get_best_videomode, get_fitting_videomode, select_monitor, CreateMonitorParams,
|
||||||
CreateWindowParams, WinitWindows,
|
CreateWindowParams, WinitWindows,
|
||||||
@ -365,21 +364,17 @@ pub(crate) fn changed_windows(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if window.cursor.icon != cache.window.cursor.icon {
|
if window.cursor_options.grab_mode != cache.window.cursor_options.grab_mode {
|
||||||
winit_window.set_cursor(converters::convert_cursor_icon(window.cursor.icon));
|
crate::winit_windows::attempt_grab(winit_window, window.cursor_options.grab_mode);
|
||||||
}
|
}
|
||||||
|
|
||||||
if window.cursor.grab_mode != cache.window.cursor.grab_mode {
|
if window.cursor_options.visible != cache.window.cursor_options.visible {
|
||||||
crate::winit_windows::attempt_grab(winit_window, window.cursor.grab_mode);
|
winit_window.set_cursor_visible(window.cursor_options.visible);
|
||||||
}
|
}
|
||||||
|
|
||||||
if window.cursor.visible != cache.window.cursor.visible {
|
if window.cursor_options.hit_test != cache.window.cursor_options.hit_test {
|
||||||
winit_window.set_cursor_visible(window.cursor.visible);
|
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;
|
||||||
|
|
||||||
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;
|
|
||||||
warn!(
|
warn!(
|
||||||
"Could not set cursor hit test for window {:?}: {:?}",
|
"Could not set cursor hit test for window {:?}: {:?}",
|
||||||
window.title, err
|
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.
|
// 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 {
|
if window.cursor_options.grab_mode != CursorGrabMode::None {
|
||||||
attempt_grab(&winit_window, window.cursor.grab_mode);
|
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
|
// 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.
|
// some platforms and log an unfixable warning.
|
||||||
if !window.cursor.hit_test {
|
if !window.cursor_options.hit_test {
|
||||||
if let Err(err) = winit_window.set_cursor_hittest(window.cursor.hit_test) {
|
if let Err(err) = winit_window.set_cursor_hittest(window.cursor_options.hit_test) {
|
||||||
warn!(
|
warn!(
|
||||||
"Could not set cursor hit test for window {:?}: {:?}",
|
"Could not set cursor hit test for window {:?}: {:?}",
|
||||||
window.title, err
|
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 the window has decorations (e.g. a border) then it should be clickable
|
||||||
if primary_window.decorations {
|
if primary_window.decorations {
|
||||||
primary_window.cursor.hit_test = true;
|
primary_window.cursor_options.hit_test = true;
|
||||||
return;
|
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
|
// 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();
|
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
|
.translation
|
||||||
.truncate()
|
.truncate()
|
||||||
.distance(cursor_world_pos)
|
.distance(cursor_world_pos)
|
||||||
|
|||||||
@ -198,13 +198,13 @@ fn run_camera_controller(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
window.cursor.grab_mode = CursorGrabMode::Locked;
|
window.cursor_options.grab_mode = CursorGrabMode::Locked;
|
||||||
window.cursor.visible = false;
|
window.cursor_options.visible = false;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
for mut window in &mut windows {
|
for mut window in &mut windows {
|
||||||
window.cursor.grab_mode = CursorGrabMode::None;
|
window.cursor_options.grab_mode = CursorGrabMode::None;
|
||||||
window.cursor.visible = true;
|
window.cursor_options.visible = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -19,12 +19,12 @@ fn grab_mouse(
|
|||||||
let mut window = windows.single_mut();
|
let mut window = windows.single_mut();
|
||||||
|
|
||||||
if mouse.just_pressed(MouseButton::Left) {
|
if mouse.just_pressed(MouseButton::Left) {
|
||||||
window.cursor.visible = false;
|
window.cursor_options.visible = false;
|
||||||
window.cursor.grab_mode = CursorGrabMode::Locked;
|
window.cursor_options.grab_mode = CursorGrabMode::Locked;
|
||||||
}
|
}
|
||||||
|
|
||||||
if key.just_pressed(KeyCode::Escape) {
|
if key.just_pressed(KeyCode::Escape) {
|
||||||
window.cursor.visible = true;
|
window.cursor_options.visible = true;
|
||||||
window.cursor.grab_mode = CursorGrabMode::None;
|
window.cursor_options.grab_mode = CursorGrabMode::None;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -53,6 +53,6 @@ fn toggle_mouse_passthrough(
|
|||||||
) {
|
) {
|
||||||
if keyboard_input.just_pressed(KeyCode::KeyP) {
|
if keyboard_input.just_pressed(KeyCode::KeyP) {
|
||||||
let mut window = windows.single_mut();
|
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,
|
core::FrameCount,
|
||||||
diagnostic::{FrameTimeDiagnosticsPlugin, LogDiagnosticsPlugin},
|
diagnostic::{FrameTimeDiagnosticsPlugin, LogDiagnosticsPlugin},
|
||||||
prelude::*,
|
prelude::*,
|
||||||
window::{CursorGrabMode, PresentMode, WindowLevel, WindowTheme},
|
render::view::cursor::{CursorIcon, CustomCursor},
|
||||||
|
window::{CursorGrabMode, PresentMode, SystemCursorIcon, WindowLevel, WindowTheme},
|
||||||
};
|
};
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
@ -37,6 +38,7 @@ fn main() {
|
|||||||
LogDiagnosticsPlugin::default(),
|
LogDiagnosticsPlugin::default(),
|
||||||
FrameTimeDiagnosticsPlugin,
|
FrameTimeDiagnosticsPlugin,
|
||||||
))
|
))
|
||||||
|
.add_systems(Startup, init_cursor_icons)
|
||||||
.add_systems(
|
.add_systems(
|
||||||
Update,
|
Update,
|
||||||
(
|
(
|
||||||
@ -137,8 +139,8 @@ fn toggle_cursor(mut windows: Query<&mut Window>, input: Res<ButtonInput<KeyCode
|
|||||||
if input.just_pressed(KeyCode::Space) {
|
if input.just_pressed(KeyCode::Space) {
|
||||||
let mut window = windows.single_mut();
|
let mut window = windows.single_mut();
|
||||||
|
|
||||||
window.cursor.visible = !window.cursor.visible;
|
window.cursor_options.visible = !window.cursor_options.visible;
|
||||||
window.cursor.grab_mode = match window.cursor.grab_mode {
|
window.cursor_options.grab_mode = match window.cursor_options.grab_mode {
|
||||||
CursorGrabMode::None => CursorGrabMode::Locked,
|
CursorGrabMode::None => CursorGrabMode::Locked,
|
||||||
CursorGrabMode::Locked | CursorGrabMode::Confined => CursorGrabMode::None,
|
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
|
/// This system cycles the cursor's icon through a small set of icons when clicking
|
||||||
fn cycle_cursor_icon(
|
fn cycle_cursor_icon(
|
||||||
mut windows: Query<&mut Window>,
|
mut commands: Commands,
|
||||||
|
windows: Query<Entity, With<Window>>,
|
||||||
input: Res<ButtonInput<MouseButton>>,
|
input: Res<ButtonInput<MouseButton>>,
|
||||||
mut index: Local<usize>,
|
mut index: Local<usize>,
|
||||||
|
cursor_icons: Res<CursorIcons>,
|
||||||
) {
|
) {
|
||||||
let mut window = windows.single_mut();
|
let window_entity = windows.single();
|
||||||
|
|
||||||
const ICONS: &[CursorIcon] = &[
|
|
||||||
CursorIcon::Default,
|
|
||||||
CursorIcon::Pointer,
|
|
||||||
CursorIcon::Wait,
|
|
||||||
CursorIcon::Text,
|
|
||||||
CursorIcon::Copy,
|
|
||||||
];
|
|
||||||
|
|
||||||
if input.just_pressed(MouseButton::Left) {
|
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) {
|
} else if input.just_pressed(MouseButton::Right) {
|
||||||
*index = if *index == 0 {
|
*index = if *index == 0 {
|
||||||
ICONS.len() - 1
|
cursor_icons.0.len() - 1
|
||||||
} else {
|
} else {
|
||||||
*index - 1
|
*index - 1
|
||||||
};
|
};
|
||||||
|
commands
|
||||||
|
.entity(window_entity)
|
||||||
|
.insert(cursor_icons.0[*index].clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
window.cursor.icon = ICONS[*index];
|
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user