From db00be831293a1a136f29b4e0f7fa5fdd8abaa9a Mon Sep 17 00:00:00 2001 From: Kirill Chibisov Date: Sat, 28 Oct 2023 17:40:38 +0400 Subject: [PATCH] Add cursor-shape protocol --- src/cursor.rs | 295 ++++++++++++++++++++++++++----------- src/handlers/compositor.rs | 3 +- src/handlers/mod.rs | 11 +- src/niri.rs | 257 ++++++++++++++++++++------------ 4 files changed, 382 insertions(+), 184 deletions(-) diff --git a/src/cursor.rs b/src/cursor.rs index 1ed3600f5..ad3596c7e 100644 --- a/src/cursor.rs +++ b/src/cursor.rs @@ -2,113 +2,242 @@ use std::collections::HashMap; use std::env; use std::fs::File; use std::io::Read; +use std::sync::Mutex; use anyhow::{anyhow, Context}; -use smithay::backend::allocator::Fourcc; use smithay::backend::renderer::element::texture::TextureBuffer; -use smithay::backend::renderer::gles::{GlesRenderer, GlesTexture}; -use smithay::utils::{Physical, Point, Transform}; +use smithay::backend::renderer::gles::GlesTexture; +use smithay::input::pointer::{CursorIcon, CursorImageAttributes, CursorImageStatus}; +use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface; +use smithay::reexports::wayland_server::Resource; +use smithay::utils::{Logical, Point}; +use smithay::wayland::compositor::with_states; use xcursor::parser::{parse_xcursor, Image}; use xcursor::CursorTheme; +/// Some default looking `left_ptr` icon. static FALLBACK_CURSOR_DATA: &[u8] = include_bytes!("../resources/cursor.rgba"); -pub struct Cursor { - images: Vec, - size: i32, - cache: HashMap, Point)>, +pub struct CursorManager { + theme: CursorTheme, + size: u8, + current_cursor: CursorImageStatus, + named_cursor_cache: HashMap<(CursorIcon, i32), Option>, } -impl Cursor { - /// Load the said theme as well as set the `XCURSOR_THEME` and `XCURSOR_SIZE` - /// env variables. - pub fn load(theme: &str, size: u8) -> Self { - env::set_var("XCURSOR_THEME", theme); - env::set_var("XCURSOR_SIZE", size.to_string()); +impl CursorManager { + pub fn new(theme: &str, size: u8) -> Self { + Self::ensure_env(theme, size); - let images = match load_xcursor(theme) { - Ok(images) => images, - Err(err) => { - warn!("error loading xcursor default cursor: {err:?}"); - - vec![Image { - size: 32, - width: 64, - height: 64, - xhot: 1, - yhot: 1, - delay: 1, - pixels_rgba: Vec::from(FALLBACK_CURSOR_DATA), - pixels_argb: vec![], - }] - } - }; + let theme = CursorTheme::load(theme); Self { - images, - size: size as i32, - cache: HashMap::new(), + theme, + size, + current_cursor: CursorImageStatus::default_named(), + named_cursor_cache: Default::default(), } } - pub fn get( - &mut self, - renderer: &mut GlesRenderer, - scale: i32, - ) -> (TextureBuffer, Point) { - self.cache - .entry(scale) - .or_insert_with_key(|scale| { - let _span = tracy_client::span!("create cursor texture"); - - let size = self.size * scale; - - let nearest_image = self - .images - .iter() - .min_by_key(|image| (size - image.size as i32).abs()) - .unwrap(); - let frame = self - .images - .iter() - .find(move |image| { - image.width == nearest_image.width && image.height == nearest_image.height + /// Reload the cursor theme. + pub fn reload(&mut self, theme: &str, size: u8) { + Self::ensure_env(theme, size); + self.theme = CursorTheme::load(theme); + self.size = size; + self.named_cursor_cache.shrink_to(0); + } + + /// Get the current rendering cursor. + pub fn get_render_cursor(&mut self, scale: i32) -> Option> { + match self.current_cursor.clone() { + CursorImageStatus::Hidden => Some(RenderCursor::Hidden), + CursorImageStatus::Surface(surface) if surface.is_alive() => { + let hotspot = with_states(&surface, |states| { + states + .data_map + .get::>() + .unwrap() + .lock() + .unwrap() + .hotspot + }); + + Some(RenderCursor::Surface { hotspot, surface }) + } + CursorImageStatus::Surface(_) => { + self.current_cursor = CursorImageStatus::default_named(); + None + } + CursorImageStatus::Named(icon) => { + self.get_cursor_with_name(icon, scale) + .map(|cursor_buffer| RenderCursor::Named { + icon, + scale, + cursor: cursor_buffer, }) - .unwrap(); - - let texture = TextureBuffer::from_memory( - renderer, - &frame.pixels_rgba, - Fourcc::Abgr8888, - (frame.width as i32, frame.height as i32), - false, - *scale, - Transform::Normal, - None, + } + } + } + + /// Get named cursor for the given `icon` and `scale`. + pub fn get_cursor_with_name(&mut self, icon: CursorIcon, scale: i32) -> Option<&XCursor> { + self.named_cursor_cache + .entry((icon, scale)) + .or_insert_with_key(|(icon, scale)| { + Self::load_xcursor(&self.theme, icon.name(), self.size as i32 * scale).ok() + }) + .as_ref() + } + + /// Get default cursor. + /// + /// This function will automatically use fallback when theme misses one. + pub fn get_default_cursor(&mut self, scale: i32) -> &XCursor { + let icon = CursorIcon::Default; + self.named_cursor_cache + .entry((icon, scale)) + .or_insert_with_key(|(icon, scale)| { + Some( + Self::load_xcursor(&self.theme, icon.name(), self.size as i32 * scale) + .ok() + .unwrap_or_else(Self::fallback_cursor), ) - .unwrap(); - (texture, (frame.xhot as i32, frame.yhot as i32).into()) }) - .clone() + .as_ref() + .unwrap() + } + + /// Currenly used cursor_image as a cursor provider. + pub fn cursor_image(&self) -> &CursorImageStatus { + &self.current_cursor + } + + /// Set new cursor image provider. + pub fn set_cursor_image(&mut self, cursor: CursorImageStatus) { + self.current_cursor = cursor; + } + + /// Load the cursor with the given `name` from the file system picking the closest + /// one to the given `size`. + fn load_xcursor(theme: &CursorTheme, name: &str, size: i32) -> anyhow::Result { + let path = theme + .load_icon(name) + .ok_or_else(|| anyhow!("no default icon"))?; + + let mut file = File::open(path).context("error opening cursor icon file")?; + let mut buf = vec![]; + file.read_to_end(&mut buf) + .context("error reading cursor icon file")?; + + let mut images = parse_xcursor(&buf).context("error parsing cursor icon file")?; + + let (width, height) = images + .iter() + .min_by_key(|image| (size - image.size as i32).abs()) + .map(|image| (image.width, image.height)) + .unwrap(); + + images.retain(move |image| image.width == width && image.height == height); + + let animation_duration = images.iter().fold(0, |acc, image| acc + image.delay); + + Ok(XCursor { + images, + animation_duration, + }) + } + + /// Set the common XCURSOR env variables. + fn ensure_env(theme: &str, size: u8) { + env::set_var("XCURSOR_THEME", theme); + env::set_var("XCURSOR_SIZE", size.to_string()); } - pub fn get_cached_hotspot(&self, scale: i32) -> Option> { - self.cache.get(&scale).map(|(_, hotspot)| *hotspot) + fn fallback_cursor() -> XCursor { + let images = vec![Image { + size: 32, + width: 64, + height: 64, + xhot: 1, + yhot: 1, + delay: 0, + pixels_rgba: Vec::from(FALLBACK_CURSOR_DATA), + pixels_argb: vec![], + }]; + + XCursor { + images, + animation_duration: 0, + } } } -fn load_xcursor(theme: &str) -> anyhow::Result> { - let _span = tracy_client::span!(); +/// The cursor prepared for renderer. +pub enum RenderCursor<'a> { + Hidden, + Surface { + hotspot: Point, + surface: WlSurface, + }, + Named { + icon: CursorIcon, + scale: i32, + cursor: &'a XCursor, + }, +} + +/// Cached cursor buffer. +pub struct CachedCursorBuffer { + /// The icon used for cache. + pub icon: CursorIcon, + /// Scale of the given icon. + pub scale: i32, + /// Textures. + pub textures: Vec>, +} + +// The XCursorBuffer implementation is inspired by `wayland-rs`, thus provided under MIT license. + +/// The state of the `NamedCursor`. +pub struct XCursor { + /// The image for the underlying named cursor. + images: Vec, + /// The total duration of the animation. + animation_duration: u32, +} + +impl XCursor { + /// Given a time, calculate which frame to show, and how much time remains until the next frame. + /// + /// Time will wrap, so if for instance the cursor has an animation lasting 100ms, + /// then calling this function with 5ms and 105ms as input gives the same output. + pub fn frame(&self, mut millis: u32) -> (usize, &Image) { + millis %= self.animation_duration; + + let mut res = 0; + for (i, img) in self.images.iter().enumerate() { + if millis < img.delay { + res = i; + break; + } + millis -= img.delay; + } + + (res, &self.images[res]) + } - let theme = CursorTheme::load(theme); - let path = theme - .load_icon("default") - .ok_or_else(|| anyhow!("no default icon"))?; - let mut file = File::open(path).context("error opening cursor icon file")?; - let mut buf = vec![]; - file.read_to_end(&mut buf) - .context("error reading cursor icon file")?; - let images = parse_xcursor(&buf).context("error parsing cursor icon file")?; + /// Get the frames for the given `XCursor`. + pub fn frames(&self) -> &[Image] { + &self.images + } - Ok(images) + /// Check whether the cursor is animated. + pub fn is_animated_cursor(&self) -> bool { + self.images.len() > 1 + } + + /// Get hotspot for the given `image`. + pub fn hotspot(image: &Image) -> Point { + (image.xhot as i32, image.yhot as i32).into() + } } diff --git a/src/handlers/compositor.rs b/src/handlers/compositor.rs index 034f1ca5d..e43d075a4 100644 --- a/src/handlers/compositor.rs +++ b/src/handlers/compositor.rs @@ -150,7 +150,8 @@ impl CompositorHandler for State { self.layer_shell_handle_commit(surface); // This might be a cursor surface. - if matches!(&self.niri.cursor_image, CursorImageStatus::Surface(s) if s == surface) { + if matches!(&self.niri.cursor_manager.cursor_image(), CursorImageStatus::Surface(s) if s == surface) + { // FIXME: granular redraws for cursors. self.niri.queue_redraw_all(); } diff --git a/src/handlers/mod.rs b/src/handlers/mod.rs index 19742fcc0..517d952d6 100644 --- a/src/handlers/mod.rs +++ b/src/handlers/mod.rs @@ -34,10 +34,10 @@ use smithay::wayland::session_lock::{ LockSurface, SessionLockHandler, SessionLockManagerState, SessionLocker, }; use smithay::{ - delegate_data_control, delegate_data_device, delegate_dmabuf, delegate_input_method_manager, - delegate_output, delegate_pointer_gestures, delegate_presentation, delegate_primary_selection, - delegate_seat, delegate_session_lock, delegate_tablet_manager, delegate_text_input_manager, - delegate_virtual_keyboard_manager, + delegate_cursor_shape, delegate_data_control, delegate_data_device, delegate_dmabuf, + delegate_input_method_manager, delegate_output, delegate_pointer_gestures, + delegate_presentation, delegate_primary_selection, delegate_seat, delegate_session_lock, + delegate_tablet_manager, delegate_text_input_manager, delegate_virtual_keyboard_manager, }; use crate::layout::output_size; @@ -52,7 +52,7 @@ impl SeatHandler for State { } fn cursor_image(&mut self, _seat: &Seat, image: CursorImageStatus) { - self.niri.cursor_image = image; + self.niri.cursor_manager.set_cursor_image(image); // FIXME: more granular self.niri.queue_redraw_all(); } @@ -65,6 +65,7 @@ impl SeatHandler for State { } } delegate_seat!(State); +delegate_cursor_shape!(State); delegate_tablet_manager!(State); delegate_pointer_gestures!(State); delegate_text_input_manager!(State); diff --git a/src/niri.rs b/src/niri.rs index 9265153b3..47a6c5633 100644 --- a/src/niri.rs +++ b/src/niri.rs @@ -4,7 +4,7 @@ use std::ffi::OsString; use std::path::PathBuf; use std::rc::Rc; use std::sync::{Arc, Mutex}; -use std::time::Duration; +use std::time::{Duration, Instant}; use std::{env, mem, thread}; use _server_decoration::server::org_kde_kwin_server_decoration_manager::Mode as KdeDecorationsMode; @@ -14,7 +14,7 @@ use smithay::backend::renderer::element::solid::{SolidColorBuffer, SolidColorRen use smithay::backend::renderer::element::surface::{ render_elements_from_surface_tree, WaylandSurfaceRenderElement, }; -use smithay::backend::renderer::element::texture::TextureRenderElement; +use smithay::backend::renderer::element::texture::{TextureBuffer, TextureRenderElement}; use smithay::backend::renderer::element::{ default_primary_scanout_output_compare, render_elements, AsRenderElements, Kind, RenderElement, RenderElementStates, @@ -45,13 +45,14 @@ use smithay::reexports::wayland_server::backend::{ use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface; use smithay::reexports::wayland_server::{Display, DisplayHandle}; use smithay::utils::{ - ClockSource, IsAlive, Logical, Monotonic, Physical, Point, Rectangle, Scale, Size, Transform, + ClockSource, Logical, Monotonic, Physical, Point, Rectangle, Scale, Size, Transform, SERIAL_COUNTER, }; use smithay::wayland::compositor::{ with_states, with_surface_tree_downward, CompositorClientState, CompositorState, SurfaceData, TraversalAction, }; +use smithay::wayland::cursor_shape::CursorShapeManagerState; use smithay::wayland::dmabuf::DmabufFeedback; use smithay::wayland::input_method::InputMethodManagerState; use smithay::wayland::output::OutputManagerState; @@ -75,7 +76,7 @@ use smithay::wayland::virtual_keyboard::VirtualKeyboardManagerState; use crate::backend::{Backend, RenderResult, Tty, Winit}; use crate::config::Config; -use crate::cursor::Cursor; +use crate::cursor::{CachedCursorBuffer, CursorManager, RenderCursor, XCursor}; #[cfg(feature = "dbus")] use crate::dbus::gnome_shell_screenshot::{NiriToScreenshot, ScreenshotToNiri}; #[cfg(feature = "xdp-gnome-screencast")] @@ -97,6 +98,8 @@ pub struct Niri { pub display_handle: DisplayHandle, pub socket_name: OsString, + pub start_time: Instant, + // Each workspace corresponds to a Space. Each workspace generally has one Output mapped to it, // however it may have none (when there are no outputs connected) or mutiple (when mirroring). pub layout: Layout, @@ -137,8 +140,9 @@ pub struct Niri { pub seat: Seat, - pub default_cursor: Cursor, - pub cursor_image: CursorImageStatus, + pub cursor_manager: CursorManager, + pub cursor_shape_manager_state: CursorShapeManagerState, + pub cached_cursor_buffer: Option, pub dnd_icon: Option, pub pointer_focus: Option, @@ -387,8 +391,9 @@ impl State { let mut old_config = self.niri.config.borrow_mut(); if config.cursor != old_config.cursor { - self.niri.default_cursor = - Cursor::load(&config.cursor.xcursor_theme, config.cursor.xcursor_size); + self.niri + .cursor_manager + .reload(&config.cursor.xcursor_theme, config.cursor.xcursor_size); } *old_config = config; @@ -562,8 +567,9 @@ impl Niri { .unwrap(); seat.add_pointer(); - let default_cursor = - Cursor::load(&config_.cursor.xcursor_theme, config_.cursor.xcursor_size); + let cursor_shape_manager_state = CursorShapeManagerState::new::(&display_handle); + let cursor_manager = + CursorManager::new(&config_.cursor.xcursor_theme, config_.cursor.xcursor_size); let socket_source = ListeningSocketSource::new_auto().unwrap(); let socket_name = socket_source.socket_name().to_os_string(); @@ -601,8 +607,9 @@ impl Niri { event_loop, stop_signal, - display_handle, socket_name, + display_handle, + start_time: Instant::now(), layout, global_space: Space::default(), @@ -632,8 +639,9 @@ impl Niri { presentation_state, seat, - default_cursor, - cursor_image: CursorImageStatus::default_named(), + cursor_manager, + cursor_shape_manager_state, + cached_cursor_buffer: None, dnd_icon: None, pointer_focus: None, @@ -1073,56 +1081,106 @@ impl Niri { output: &Output, ) -> Vec> { let _span = tracy_client::span!("Niri::pointer_element"); - - let output_scale = Scale::from(output.current_scale().fractional_scale()); + let output_scale = output.current_scale(); let output_pos = self.global_space.output_geometry(output).unwrap().loc; let pointer_pos = self.seat.get_pointer().unwrap().current_location() - output_pos.to_f64(); - let output_scale_int = output.current_scale().integer_scale(); - let (default_buffer, default_hotspot) = self.default_cursor.get(renderer, output_scale_int); - let default_hotspot = default_hotspot.to_logical(output_scale_int); - - let hotspot = if let CursorImageStatus::Surface(surface) = &mut self.cursor_image { - if surface.alive() { - with_states(surface, |states| { - states - .data_map - .get::>() - .unwrap() - .lock() - .unwrap() - .hotspot - }) - } else { - self.cursor_image = CursorImageStatus::default_named(); - default_hotspot + // Get the render cursor to draw. + let cursor_scale = output_scale.integer_scale(); + let render_cursor = match self.cursor_manager.get_render_cursor(cursor_scale) { + Some(render_cursor) => render_cursor, + None => { + let cursor = self.cursor_manager.get_default_cursor(cursor_scale); + RenderCursor::Named { + icon: Default::default(), + scale: cursor_scale, + cursor, + } } - } else { - default_hotspot }; - let pointer_pos = (pointer_pos - hotspot.to_f64()).to_physical_precise_round(output_scale); - let mut pointer_elements = match &self.cursor_image { - CursorImageStatus::Hidden => vec![], - CursorImageStatus::Surface(surface) => render_elements_from_surface_tree( - renderer, - surface, - pointer_pos, - output_scale, - 1., - Kind::Cursor, - ), - // Default shape catch-all - _ => vec![OutputRenderElements::DefaultPointer( - TextureRenderElement::from_texture_buffer( - pointer_pos.to_f64(), - &default_buffer, - None, - None, - None, + let output_scale = Scale::from(output.current_scale().fractional_scale()); + + let (mut pointer_elements, pointer_pos) = match render_cursor { + RenderCursor::Hidden => (vec![], pointer_pos.to_physical_precise_round(output_scale)), + RenderCursor::Surface { surface, hotspot } => { + let pointer_pos = + (pointer_pos - hotspot.to_f64()).to_physical_precise_round(output_scale); + + let pointer_elements = render_elements_from_surface_tree( + renderer, + &surface, + pointer_pos, + output_scale, + 1., Kind::Cursor, - ), - )], + ); + + (pointer_elements, pointer_pos) + } + RenderCursor::Named { + icon, + scale, + cursor, + } => { + let (idx, frame) = cursor.frame(self.start_time.elapsed().as_millis() as u32); + let hotspot = XCursor::hotspot(frame); + let pointer_pos = + (pointer_pos - hotspot.to_f64()).to_physical_precise_round(output_scale); + + // NOTE: Mark that we're animating cursor on the given output. + let state = self.output_state.get_mut(output).unwrap(); + state.unfinished_animations_remain |= cursor.is_animated_cursor(); + + // Check if we need to invalidate cache. + if let Some(cursor_cache) = self.cached_cursor_buffer.as_ref() { + if cursor_cache.icon != icon || cursor_cache.scale != scale { + self.cached_cursor_buffer = None; + } + } + + if self.cached_cursor_buffer.is_none() { + let textures = cursor + .frames() + .iter() + .map(|frame| { + TextureBuffer::from_memory( + renderer, + &frame.pixels_rgba, + Fourcc::Abgr8888, + (frame.width as i32, frame.height as i32), + false, + scale, + Transform::Normal, + None, + ) + .unwrap() + }) + .collect(); + + self.cached_cursor_buffer = Some(CachedCursorBuffer { + icon, + scale, + textures, + }); + } + + // We just inserted texture if it was missing. + let texture = &self.cached_cursor_buffer.as_ref().unwrap().textures[idx]; + + let pointer_elements = vec![OutputRenderElements::NamedPointer( + TextureRenderElement::from_texture_buffer( + pointer_pos.to_f64(), + texture, + None, + None, + None, + Kind::Cursor, + ), + )]; + + (pointer_elements, pointer_pos) + } }; if let Some(dnd_icon) = &self.dnd_icon { @@ -1139,42 +1197,11 @@ impl Niri { pointer_elements } - pub fn refresh_pointer_outputs(&self) { + pub fn refresh_pointer_outputs(&mut self) { let _span = tracy_client::span!("Niri::refresh_pointer_outputs"); - match &self.cursor_image { - CursorImageStatus::Hidden | CursorImageStatus::Named(_) => { - // There's no cursor surface, but there might be a DnD icon. - let Some(surface) = &self.dnd_icon else { - return; - }; - - let pointer_pos = self.seat.get_pointer().unwrap().current_location(); - - for output in self.global_space.outputs() { - let geo = self.global_space.output_geometry(output).unwrap(); - - // The default cursor is rendered at the right scale for each output, which - // means that it may have a different hotspot for each output. - let output_scale = output.current_scale().integer_scale(); - let Some(hotspot) = self.default_cursor.get_cached_hotspot(output_scale) else { - // Oh well; it'll get cached next time we render. - continue; - }; - let hotspot = hotspot.to_logical(output_scale); - - let surface_pos = pointer_pos.to_i32_round() - hotspot; - let bbox = bbox_from_surface_tree(surface, surface_pos); - - if let Some(mut overlap) = geo.intersection(bbox) { - overlap.loc -= surface_pos; - output_update(output, Some(overlap), surface); - } else { - output_update(output, None, surface); - } - } - } - CursorImageStatus::Surface(surface) => { + match self.cursor_manager.cursor_image().clone() { + CursorImageStatus::Surface(ref surface) => { let hotspot = with_states(surface, |states| { states .data_map @@ -1216,6 +1243,46 @@ impl Niri { } } } + cursor_image => { + // There's no cursor surface, but there might be a DnD icon. + let Some(surface) = &self.dnd_icon else { + return; + }; + + let icon = if let CursorImageStatus::Named(icon) = cursor_image { + icon + } else { + Default::default() + }; + + let pointer_pos = self.seat.get_pointer().unwrap().current_location(); + + for output in self.global_space.outputs() { + let geo = self.global_space.output_geometry(output).unwrap(); + + // The default cursor is rendered at the right scale for each output, which + // means that it may have a different hotspot for each output. + let output_scale = output.current_scale().integer_scale(); + let cursor = if let Some(cursor) = + self.cursor_manager.get_cursor_with_name(icon, output_scale) + { + cursor + } else { + self.cursor_manager.get_default_cursor(output_scale) + }; + let hotspot = XCursor::hotspot(&cursor.frames()[0]); + + let surface_pos = pointer_pos.to_i32_round() - hotspot; + let bbox = bbox_from_surface_tree(surface, surface_pos); + + if let Some(mut overlap) = geo.intersection(bbox) { + overlap.loc -= surface_pos; + output_update(output, Some(overlap), surface); + } else { + output_update(output, None, surface); + } + } + } } } @@ -1446,7 +1513,7 @@ impl Niri { // // While we only have cursors and DnD icons crossing output boundaries though, it doesn't // matter all that much. - if let CursorImageStatus::Surface(surface) = &self.cursor_image { + if let CursorImageStatus::Surface(surface) = &self.cursor_manager.cursor_image() { with_surface_tree_downward( surface, (), @@ -1565,7 +1632,7 @@ impl Niri { ); } - if let CursorImageStatus::Surface(surface) = &self.cursor_image { + if let CursorImageStatus::Surface(surface) = &self.cursor_manager.cursor_image() { send_dmabuf_feedback_surface_tree( surface, output, @@ -1641,7 +1708,7 @@ impl Niri { send_frames_surface_tree(surface, output, frame_callback_time, None, should_send); } - if let CursorImageStatus::Surface(surface) = &self.cursor_image { + if let CursorImageStatus::Surface(surface) = self.cursor_manager.cursor_image() { send_frames_surface_tree(surface, output, frame_callback_time, None, should_send); } } @@ -1653,7 +1720,7 @@ impl Niri { ) -> OutputPresentationFeedback { let mut feedback = OutputPresentationFeedback::new(output); - if let CursorImageStatus::Surface(surface) = &self.cursor_image { + if let CursorImageStatus::Surface(surface) = &self.cursor_manager.cursor_image() { take_presentation_feedback_surface_tree( surface, &mut feedback, @@ -1996,7 +2063,7 @@ render_elements! { pub OutputRenderElements where R: ImportAll; Monitor = MonitorRenderElement, Wayland = WaylandSurfaceRenderElement, - DefaultPointer = TextureRenderElement<::TextureId>, + NamedPointer = TextureRenderElement<::TextureId>, SolidColor = SolidColorRenderElement, }