Skip to content

Add custom cursors #14284

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 24 commits into from
Aug 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions crates/bevy_render/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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" }

Expand Down
175 changes: 175 additions & 0 deletions crates/bevy_render/src/view/window/cursor.rs
Original file line number Diff line number Diff line change
@@ -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,
}
}
10 changes: 8 additions & 2 deletions crates/bevy_render/src/view/window/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,15 @@ 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;
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},
Expand All @@ -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
Expand Down
6 changes: 3 additions & 3 deletions crates/bevy_window/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,27 +15,27 @@ 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)]
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,
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand All @@ -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,
Expand All @@ -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,

Expand Down
33 changes: 14 additions & 19 deletions crates/bevy_window/src/window.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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);
/// }
/// }
/// ```
Expand All @@ -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.
Expand Down Expand Up @@ -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(),
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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
///
Expand Down
Loading
Loading