From 69dda5624b51e9fef204b4a459c6a2b360469b20 Mon Sep 17 00:00:00 2001 From: Timon Schelling Date: Sat, 6 Dec 2025 13:11:42 +0000 Subject: [PATCH] custom cursors with caching --- desktop/src/app.rs | 4 +-- desktop/src/cef.rs | 19 ++++++----- desktop/src/cef/internal/display_handler.rs | 19 +++++++++-- desktop/src/event.rs | 2 +- desktop/src/window.rs | 38 ++++++++++++++++++++- frontend/package-installer.js | 2 +- 6 files changed, 68 insertions(+), 16 deletions(-) diff --git a/desktop/src/app.rs b/desktop/src/app.rs index 96c877f82f..0e46aa6998 100644 --- a/desktop/src/app.rs +++ b/desktop/src/app.rs @@ -361,8 +361,8 @@ impl App { } } AppEvent::CursorChange(cursor) => { - if let Some(window) = &self.window { - window.set_cursor(cursor); + if let Some(window) = &mut self.window { + window.set_cursor(event_loop, cursor); } } AppEvent::CloseWindow => { diff --git a/desktop/src/cef.rs b/desktop/src/cef.rs index c28dad60e7..b9649e492d 100644 --- a/desktop/src/cef.rs +++ b/desktop/src/cef.rs @@ -12,16 +12,19 @@ //! //! The system gracefully falls back to CPU textures when hardware acceleration is unavailable. -use crate::event::{AppEvent, AppEventScheduler}; -use crate::render::FrameBufferRef; -use crate::wrapper::{WgpuContext, deserialize_editor_message}; use std::fs::File; -use std::io::{Cursor, Read}; +use std::io; +use std::io::Read; use std::path::PathBuf; use std::sync::mpsc::Receiver; use std::sync::{Arc, Mutex}; use std::time::Instant; +use crate::event::{AppEvent, AppEventScheduler}; +use crate::render::FrameBufferRef; +use crate::window::Cursor; +use crate::wrapper::{WgpuContext, deserialize_editor_message}; + mod consts; mod context; mod dirs; @@ -42,7 +45,7 @@ pub(crate) trait CefEventHandler: Send + Sync + 'static { #[cfg(feature = "accelerated_paint")] fn draw_gpu(&self, shared_texture: SharedTextureHandle); fn load_resource(&self, path: PathBuf) -> Option; - fn cursor_change(&self, cursor: winit::cursor::Cursor); + fn cursor_change(&self, cursor: Cursor); /// Schedule the main event loop to run the CEF event loop after the timeout. /// See [`_cef_browser_process_handler_t::on_schedule_message_pump_work`] for more documentation. fn schedule_cef_message_loop_work(&self, scheduled_time: Instant); @@ -105,7 +108,7 @@ pub(crate) struct Resource { #[expect(dead_code)] #[derive(Clone)] pub(crate) enum ResourceReader { - Embedded(Cursor<&'static [u8]>), + Embedded(io::Cursor<&'static [u8]>), File(Arc), } impl Read for ResourceReader { @@ -227,7 +230,7 @@ impl CefEventHandler for CefHandler { && let Some(file) = resources.get_file(&path) { return Some(Resource { - reader: ResourceReader::Embedded(Cursor::new(file.contents())), + reader: ResourceReader::Embedded(io::Cursor::new(file.contents())), mimetype, }); } @@ -252,7 +255,7 @@ impl CefEventHandler for CefHandler { None } - fn cursor_change(&self, cursor: winit::cursor::Cursor) { + fn cursor_change(&self, cursor: Cursor) { self.app_event_scheduler.schedule(AppEvent::CursorChange(cursor)); } diff --git a/desktop/src/cef/internal/display_handler.rs b/desktop/src/cef/internal/display_handler.rs index 2bc3b35b24..d1263e854c 100644 --- a/desktop/src/cef/internal/display_handler.rs +++ b/desktop/src/cef/internal/display_handler.rs @@ -1,6 +1,6 @@ use cef::rc::{Rc, RcImpl}; use cef::sys::{_cef_display_handler_t, cef_base_ref_counted_t, cef_cursor_type_t::*, cef_log_severity_t::*}; -use cef::{CefString, ImplDisplayHandler, WrapDisplayHandler}; +use cef::{CefString, ImplDisplayHandler, Point, Size, WrapDisplayHandler}; use winit::cursor::CursorIcon; use crate::cef::CefEventHandler; @@ -25,7 +25,21 @@ type CefCursorHandle = cef::CursorHandle; type CefCursorHandle = *mut u8; impl ImplDisplayHandler for DisplayHandlerImpl { - fn on_cursor_change(&self, _browser: Option<&mut cef::Browser>, _cursor: CefCursorHandle, cursor_type: cef::CursorType, _custom_cursor_info: Option<&cef::CursorInfo>) -> std::ffi::c_int { + fn on_cursor_change(&self, _browser: Option<&mut cef::Browser>, _cursor: CefCursorHandle, cursor_type: cef::CursorType, custom_cursor_info: Option<&cef::CursorInfo>) -> std::ffi::c_int { + if let Some(custom_cursor_info) = custom_cursor_info { + let Size { width, height } = custom_cursor_info.size; + let Point { x: hotspot_x, y: hotspot_y } = custom_cursor_info.hotspot; + let buffer_size = (width * height * 4) as usize; + let buffer_ptr = custom_cursor_info.buffer as *const u8; + + if !buffer_ptr.is_null() && buffer_ptr.align_offset(std::mem::align_of::()) == 0 { + let buffer = unsafe { std::slice::from_raw_parts(buffer_ptr, buffer_size) }.to_vec(); + let cursor = winit::cursor::CustomCursorSource::from_rgba(buffer, width as u16, height as u16, hotspot_x as u16, hotspot_y as u16).unwrap(); + self.event_handler.cursor_change(cursor.into()); + return 1; // We handled the cursor change. + } + } + let cursor = match cursor_type.into() { CT_POINTER => CursorIcon::Default, CT_CROSS => CursorIcon::Crosshair, @@ -72,7 +86,6 @@ impl ImplDisplayHandler for DisplayHandlerImpl { CT_GRABBING => CursorIcon::Grabbing, CT_MIDDLE_PANNING_VERTICAL => CursorIcon::AllScroll, CT_MIDDLE_PANNING_HORIZONTAL => CursorIcon::AllScroll, - CT_CUSTOM => CursorIcon::Default, CT_DND_NONE => CursorIcon::Default, CT_DND_MOVE => CursorIcon::Move, CT_DND_COPY => CursorIcon::Copy, diff --git a/desktop/src/event.rs b/desktop/src/event.rs index 6d8dbbe101..f334b0879c 100644 --- a/desktop/src/event.rs +++ b/desktop/src/event.rs @@ -3,7 +3,7 @@ use crate::wrapper::messages::DesktopWrapperMessage; pub(crate) enum AppEvent { UiUpdate(wgpu::Texture), - CursorChange(winit::cursor::Cursor), + CursorChange(crate::window::Cursor), ScheduleBrowserWork(std::time::Instant), WebCommunicationInitialized, DesktopWrapperMessage(DesktopWrapperMessage), diff --git a/desktop/src/window.rs b/desktop/src/window.rs index c98a90a2cc..549d454d0c 100644 --- a/desktop/src/window.rs +++ b/desktop/src/window.rs @@ -1,4 +1,6 @@ +use std::collections::HashMap; use std::sync::Arc; +use winit::cursor::{CursorIcon, CustomCursor, CustomCursorSource}; use winit::event_loop::ActiveEventLoop; use winit::window::{Window as WinitWindow, WindowAttributes}; @@ -35,6 +37,7 @@ pub(crate) struct Window { winit_window: Arc, #[allow(dead_code)] native_handle: native::NativeWindowImpl, + custom_cursors: HashMap, } impl Window { @@ -57,6 +60,7 @@ impl Window { Self { winit_window: winit_window.into(), native_handle, + custom_cursors: HashMap::new(), } } @@ -108,7 +112,24 @@ impl Window { self.native_handle.show_all(); } - pub(crate) fn set_cursor(&self, cursor: winit::cursor::Cursor) { + pub(crate) fn set_cursor(&mut self, event_loop: &dyn ActiveEventLoop, cursor: Cursor) { + let cursor = match cursor { + Cursor::Icon(cursor_icon) => cursor_icon.into(), + Cursor::Custom(custom_cursor_source) => { + let custom_cursor = match self.custom_cursors.get(&custom_cursor_source).cloned() { + Some(cursor) => cursor, + None => { + let Ok(custom_cursor) = event_loop.create_custom_cursor(custom_cursor_source.clone()) else { + tracing::error!("Failed to create custom cursor"); + return; + }; + self.custom_cursors.insert(custom_cursor_source, custom_cursor.clone()); + custom_cursor + } + }; + custom_cursor.into() + } + }; self.winit_window.set_cursor(cursor); } @@ -116,3 +137,18 @@ impl Window { self.native_handle.update_menu(entries); } } + +pub(crate) enum Cursor { + Icon(CursorIcon), + Custom(CustomCursorSource), +} +impl From for Cursor { + fn from(icon: CursorIcon) -> Self { + Cursor::Icon(icon) + } +} +impl From for Cursor { + fn from(custom: CustomCursorSource) -> Self { + Cursor::Custom(custom) + } +} diff --git a/frontend/package-installer.js b/frontend/package-installer.js index 64970e38a6..ae09741b06 100644 --- a/frontend/package-installer.js +++ b/frontend/package-installer.js @@ -32,7 +32,7 @@ if (isInstallNeeded()) { console.log("Finished installing npm packages."); } catch (_) { // eslint-disable-next-line no-console - console.error("Failed to install npm packages. Please run `npm install` from the `/frontend` directory."); + console.error("Failed to install npm packages. Please delete the `node_modules` folder and run `npm install` from the `/frontend` directory."); process.exit(1); } } else {