From 00cf02158acd210096e82620e4c6887629956cf2 Mon Sep 17 00:00:00 2001 From: Ottatop Date: Thu, 20 Jun 2024 21:02:25 -0500 Subject: [PATCH 1/6] Impl proper xcursor support --- Cargo.toml | 3 + src/backend/udev.rs | 162 ++++++++---------------- src/backend/winit.rs | 35 +++--- src/cursor.rs | 262 +++++++++++++++++++++++++++++---------- src/handlers.rs | 33 +++-- src/handlers/xwayland.rs | 11 +- src/input.rs | 2 - src/main.rs | 1 + src/render.rs | 65 +--------- src/render/pointer.rs | 170 ++++++++++++++----------- src/state.rs | 15 ++- 11 files changed, 420 insertions(+), 339 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 181a357..85b8460 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -66,6 +66,9 @@ features = [ [workspace.lints.clippy] too_many_arguments = "allow" +new_without_default = "allow" +type_complexity = "allow" +let_and_return = "allow" ########################################################################yo😎########### diff --git a/src/backend/udev.rs b/src/backend/udev.rs index 7db805c..22d2020 100644 --- a/src/backend/udev.rs +++ b/src/backend/udev.rs @@ -32,11 +32,9 @@ use smithay::{ libinput::{LibinputInputBackend, LibinputSessionInterface}, renderer::{ self, damage, - element::{ - self, surface::render_elements_from_surface_tree, texture::TextureBuffer, Element, - }, + element::{self, surface::render_elements_from_surface_tree, Element}, gles::{GlesRenderbuffer, GlesRenderer}, - multigpu::{gbm::GbmGlesBackend, GpuManager, MultiRenderer, MultiTexture}, + multigpu::{gbm::GbmGlesBackend, GpuManager, MultiRenderer}, sync::SyncPoint, utils::{CommitCounter, DamageSet}, Bind, Blit, BufferType, ExportMem, ImportDma, ImportEgl, ImportMemWl, Offscreen, @@ -57,8 +55,8 @@ use smithay::{ reexports::{ ash::vk::ExtPhysicalDeviceDrmFn, calloop::{ - self, generic::Generic, Dispatcher, Idle, Interest, LoopHandle, PostAction, - RegistrationToken, + self, generic::Generic, timer::Timer, Dispatcher, Idle, Interest, LoopHandle, + PostAction, RegistrationToken, }, drm::control::{connector, crtc, ModeTypeFlags}, input::Libinput, @@ -86,8 +84,8 @@ use crate::{ config::ConnectorSavedState, output::{BlankingState, OutputMode, OutputName}, render::{ - pointer::PointerElement, pointer_render_elements, take_presentation_feedback, - OutputRenderElement, CLEAR_COLOR, CLEAR_COLOR_LOCKED, + pointer::pointer_render_elements, take_presentation_feedback, OutputRenderElement, + CLEAR_COLOR, CLEAR_COLOR_LOCKED, }, state::{Pinnacle, State, SurfaceDmabufFeedback, WithState}, }; @@ -134,9 +132,6 @@ pub struct Udev { allocator: Option>>, pub(super) gpu_manager: GpuManager>, backends: HashMap, - pointer_images: Vec<(xcursor::parser::Image, TextureBuffer)>, - pointer_element: PointerElement, - pointer_image: crate::cursor::Cursor, pub(super) upscale_filter: TextureFilter, pub(super) downscale_filter: TextureFilter, @@ -225,9 +220,6 @@ impl Udev { gpu_manager, allocator: None, backends: HashMap::new(), - pointer_image: crate::cursor::Cursor::load(), - pointer_images: Vec::new(), - pointer_element: PointerElement::default(), upscale_filter: TextureFilter::Linear, downscale_filter: TextureFilter::Linear, @@ -531,8 +523,10 @@ impl Udev { match &surface.render_state { RenderState::Idle => { + tracing::info!("inserting idle render"); let output = output.clone(); let token = loop_handle.insert_idle(move |state| { + tracing::info!("actually rendering"); state .backend .udev_mut() @@ -543,6 +537,7 @@ impl Udev { } RenderState::Scheduled(_) => (), RenderState::WaitingForVblank { dirty: _ } => { + tracing::info!("making dirty"); surface.render_state = RenderState::WaitingForVblank { dirty: true } } } @@ -933,7 +928,7 @@ impl Udev { state .backend .udev_mut() - .on_vblank(&state.pinnacle, node, crtc, metadata); + .on_vblank(&mut state.pinnacle, node, crtc, metadata); } DrmEvent::Error(error) => { error!("{:?}", error); @@ -1271,7 +1266,7 @@ impl Udev { /// Mark [`OutputPresentationFeedback`]s as presented and schedule a new render on idle. fn on_vblank( &mut self, - pinnacle: &Pinnacle, + pinnacle: &mut Pinnacle, dev_id: DrmNode, crtc: crtc::Handle, metadata: &mut Option, @@ -1369,6 +1364,20 @@ impl Udev { ); } } + + // Schedule a render when the next frame of an animated cursor should be drawn. + if let Some(until) = pinnacle + .cursor_state + .time_until_next_animated_cursor_frame() + { + let _ = pinnacle.loop_handle.insert_source( + Timer::from_duration(until), + move |_, _, state| { + state.schedule_render(&output); + calloop::timer::TimeoutAction::Drop + }, + ); + } } /// Render to the [`RenderSurface`] associated with the given `output`. @@ -1392,13 +1401,6 @@ impl Udev { assert!(matches!(surface.render_state, RenderState::Scheduled(_))); - // TODO get scale from the rendersurface when supporting HiDPI - let frame = self.pointer_image.get_image( - 1, - // output.current_scale().integer_scale() as u32, - pinnacle.clock.now().into(), - ); - let render_node = surface.render_node; let primary_gpu = self.primary_gpu; let mut renderer = if primary_gpu == render_node { @@ -1413,32 +1415,19 @@ impl Udev { let _ = renderer.upscale_filter(self.upscale_filter); let _ = renderer.downscale_filter(self.downscale_filter); - let pointer_images = &mut self.pointer_images; - let (pointer_image, hotspot) = pointer_images - .iter() - .find_map(|(image, texture)| { - if image == &frame { - Some((texture.clone(), (frame.xhot as i32, frame.yhot as i32))) - } else { - None - } - }) - .unwrap_or_else(|| { - let texture = TextureBuffer::from_memory( - &mut renderer, - &frame.pixels_rgba, - Fourcc::Abgr8888, - (frame.width as i32, frame.height as i32), - false, - 1, - Transform::Normal, - None, - ) - .expect("Failed to import cursor bitmap"); - let hotspot = (frame.xhot as i32, frame.yhot as i32); - pointer_images.push((frame, texture.clone())); - (texture, hotspot) - }); + /////////////////////////////////////////////////////////////////////////////////////////// + + // draw the cursor as relevant and + // reset the cursor if the surface is no longer alive + if let CursorImageStatus::Surface(surface) = &pinnacle.cursor_state.cursor_image() { + if !surface.alive() { + pinnacle + .cursor_state + .set_cursor_image(CursorImageStatus::default_named()); + } + } + + /////////////////////////////////////////////////////////////////////////////////////////// let pointer_location = pinnacle .seat @@ -1446,71 +1435,26 @@ impl Udev { .map(|ptr| ptr.current_location()) .unwrap_or((0.0, 0.0).into()); - // set cursor - self.pointer_element.set_texture(pointer_image.clone()); - - // draw the cursor as relevant and - // reset the cursor if the surface is no longer alive - if let CursorImageStatus::Surface(surface) = &pinnacle.cursor_status { - if !surface.alive() { - pinnacle.cursor_status = CursorImageStatus::default_named(); - } - } - - self.pointer_element - .set_status(pinnacle.cursor_status.clone()); - - let pending_screencopy_with_cursor = - output.with_state(|state| state.screencopy.as_ref().map(|sc| sc.overlay_cursor())); - let mut output_render_elements = Vec::new(); let should_blank = pinnacle.lock_state.is_locking() || (pinnacle.lock_state.is_locked() && output.with_state(|state| state.lock_surface.is_none())); - // If there isn't a pending screencopy that doesn't want to overlay the cursor, - // render it. - match pending_screencopy_with_cursor { - Some(include_cursor) if pinnacle.lock_state.is_unlocked() => { - if include_cursor { - // HACK: Doing `RenderFrameResult::blit_frame_result` with something on the - // | cursor plane causes the cursor to overwrite the pixels underneath it, - // | leading to a transparent hole under the cursor. - // | To circumvent that, we set the cursor to render on the primary plane instead. - // | Unfortunately that means I can't composite the cursor separately from - // | the screencopy, meaning if you have an active screencopy recording - // | without cursor overlay then the cursor will dim/flicker out/disappear. - self.pointer_element - .set_element_kind(element::Kind::Unspecified); - let pointer_render_elements = pointer_render_elements( - output, - &mut renderer, - &pinnacle.space, - pointer_location, - &mut pinnacle.cursor_status, - pinnacle.dnd_icon.as_ref(), - hotspot.into(), - &self.pointer_element, - ); - self.pointer_element.set_element_kind(element::Kind::Cursor); - output_render_elements.extend(pointer_render_elements); - } - } - _ => { - let pointer_render_elements = pointer_render_elements( - output, - &mut renderer, - &pinnacle.space, - pointer_location, - &mut pinnacle.cursor_status, - pinnacle.dnd_icon.as_ref(), - hotspot.into(), - &self.pointer_element, - ); - output_render_elements.extend(pointer_render_elements); - } - } + let pointer_render_elements = pointer_render_elements( + output, + &mut renderer, + &mut pinnacle.cursor_state, + &pinnacle.space, + pointer_location, + pinnacle.dnd_icon.as_ref(), + &pinnacle.clock, + ); + output_render_elements.extend( + pointer_render_elements + .into_iter() + .map(OutputRenderElement::from), + ); if should_blank { output.with_state_mut(|state| { @@ -1601,7 +1545,7 @@ impl Udev { scanout_feedback: &feedback.scanout_feedback, }), Duration::from(pinnacle.clock.now()), - &pinnacle.cursor_status, + pinnacle.cursor_state.cursor_image(), ); let rendered = !render_frame_result.is_empty; diff --git a/src/backend/winit.rs b/src/backend/winit.rs index 2cad2a3..8c81ef6 100644 --- a/src/backend/winit.rs +++ b/src/backend/winit.rs @@ -10,7 +10,7 @@ use smithay::{ self, buffer_type, damage::{self, OutputDamageTracker, RenderOutputResult}, element::{self, surface::render_elements_from_surface_tree}, - gles::{GlesRenderbuffer, GlesRenderer, GlesTexture}, + gles::{GlesRenderbuffer, GlesRenderer}, Bind, Blit, BufferType, ExportMem, ImportDma, ImportEgl, ImportMemWl, Offscreen, TextureFilter, }, @@ -38,8 +38,8 @@ use tracing::{debug, error, trace, warn}; use crate::{ output::{BlankingState, OutputMode}, render::{ - pointer::PointerElement, pointer_render_elements, take_presentation_feedback, CLEAR_COLOR, - CLEAR_COLOR_LOCKED, + pointer::pointer_render_elements, take_presentation_feedback, OutputRenderElement, + CLEAR_COLOR, CLEAR_COLOR_LOCKED, }, state::{Pinnacle, State, WithState}, }; @@ -263,17 +263,21 @@ impl Winit { let full_redraw = &mut self.full_redraw; *full_redraw = full_redraw.saturating_sub(1); - if let CursorImageStatus::Surface(surface) = &pinnacle.cursor_status { + if let CursorImageStatus::Surface(surface) = pinnacle.cursor_state.cursor_image() { if !surface.alive() { - pinnacle.cursor_status = CursorImageStatus::default_named(); + pinnacle + .cursor_state + .set_cursor_image(CursorImageStatus::default_named()); } } - let cursor_visible = !matches!(pinnacle.cursor_status, CursorImageStatus::Surface(_)); + let cursor_visible = !matches!( + pinnacle.cursor_state.cursor_image(), + CursorImageStatus::Surface(_) + ); - let mut pointer_element = PointerElement::::new(); - - pointer_element.set_status(pinnacle.cursor_status.clone()); + // TODO: make winit cursor disappear if not surface + // TODO: set winit cursor to named cursor when possible // The z-index of these is determined by `state.fixup_z_layering()`, which is called at the end // of every event loop cycle @@ -300,14 +304,17 @@ impl Winit { let pointer_render_elements = pointer_render_elements( &self.output, self.backend.renderer(), + &mut pinnacle.cursor_state, &pinnacle.space, pointer_location, - &mut pinnacle.cursor_status, pinnacle.dnd_icon.as_ref(), - (0, 0).into(), // Nonsurface cursors are hidden - &pointer_element, + &pinnacle.clock, + ); + output_render_elements.extend( + pointer_render_elements + .into_iter() + .map(OutputRenderElement::from), ); - output_render_elements.extend(pointer_render_elements); } let should_blank = pinnacle.lock_state.is_locking() @@ -421,7 +428,7 @@ impl Winit { &pinnacle.space, None, time.into(), - &pinnacle.cursor_status, + pinnacle.cursor_state.cursor_image(), ); if has_rendered { diff --git a/src/cursor.rs b/src/cursor.rs index 60dcae0..ce59e76 100644 --- a/src/cursor.rs +++ b/src/cursor.rs @@ -1,91 +1,227 @@ // SPDX-License-Identifier: GPL-3.0-or-later -use std::{io::Read, time::Duration}; +use std::time::{Duration, Instant}; +use std::{collections::HashMap, rc::Rc}; -use xcursor::{parser::Image, CursorTheme}; +use anyhow::Context; +use smithay::backend::allocator::Fourcc; +use smithay::{ + backend::renderer::element::memory::MemoryRenderBuffer, + input::pointer::{CursorIcon, CursorImageStatus}, + utils::Transform, +}; +use xcursor::{ + parser::{parse_xcursor, Image}, + CursorTheme, +}; + +use crate::render::pointer::PointerElement; static FALLBACK_CURSOR_DATA: &[u8] = include_bytes!("../resources/cursor.rgba"); -pub struct Cursor { - icons: Vec, +pub struct CursorState { + start_time: Instant, + current_cursor_image: CursorImageStatus, + theme: CursorTheme, size: u32, + // memory buffer cache + mem_buffer_cache: Vec<(Image, MemoryRenderBuffer)>, + // map of cursor icons to loaded images + loaded_images: HashMap>>, } -impl Cursor { - pub fn load() -> Self { - let name = std::env::var("XCURSOR_THEME") - .ok() - .unwrap_or_else(|| "default".into()); - let size = std::env::var("XCURSOR_SIZE") - .ok() - .and_then(|s| s.parse().ok()) - .unwrap_or(24); +impl CursorState { + pub fn new() -> Self { + let (theme, size) = load_xcursor_theme(); - let theme = CursorTheme::load(&name); - let icons = load_icon(&theme) - .map_err(|err| tracing::warn!("Unable to load xcursor: {}, using fallback cursor", err)) - .unwrap_or_else(|_| { - vec![Image { - size: 32, - width: 64, - height: 64, - xhot: 1, - yhot: 1, - delay: 1, - pixels_rgba: Vec::from(FALLBACK_CURSOR_DATA), - pixels_argb: vec![], //unused - }] - }); - - Cursor { icons, size } + Self { + start_time: Instant::now(), + current_cursor_image: CursorImageStatus::default_named(), + theme, + size, + mem_buffer_cache: Default::default(), + loaded_images: Default::default(), + } } - pub fn get_image(&self, scale: u32, time: Duration) -> Image { - let size = self.size * scale; - frame(time.as_millis() as u32, size, &self.icons) + pub fn set_theme_and_size(&mut self, theme: CursorTheme, size: u32) { + self.theme = theme; + self.size = size; + self.mem_buffer_cache.clear(); + self.loaded_images.clear(); + } + + pub fn cursor_size(&self) -> u32 { + self.size + } + + pub fn set_cursor_image(&mut self, image: CursorImageStatus) { + self.current_cursor_image = image; + } + + pub fn cursor_image(&self) -> &CursorImageStatus { + &self.current_cursor_image + } + + pub fn get_xcursor_images(&mut self, icon: CursorIcon) -> Option> { + self.loaded_images + .entry(icon) + .or_insert_with_key(|icon| { + let mut images = load_xcursor_images(&self.theme, *icon); + if *icon == CursorIcon::Default && images.is_err() { + images = Ok(fallback_cursor()); + } + images.ok().map(Rc::new) + }) + .clone() + } + + pub fn buffer_for_image(&mut self, image: Image) -> MemoryRenderBuffer { + self.mem_buffer_cache + .iter() + .find_map(|(img, buf)| (*img == image).then(|| buf.clone())) + .unwrap_or_else(|| { + // TODO: scale + let buffer = MemoryRenderBuffer::from_slice( + &image.pixels_rgba, + Fourcc::Abgr8888, + (image.width as i32, image.height as i32), + 1, + Transform::Normal, + None, + ); + + self.mem_buffer_cache.push((image, buffer.clone())); + + buffer + }) + } + + pub fn pointer_element(&mut self) -> PointerElement { + match &self.current_cursor_image { + CursorImageStatus::Hidden => PointerElement::Hidden, + CursorImageStatus::Named(icon) => { + let cursor = self + .get_xcursor_images(*icon) + .or_else(|| self.get_xcursor_images(CursorIcon::Default)) + .unwrap(); + PointerElement::Named { + cursor, + size: self.size, + } + } + CursorImageStatus::Surface(surface) => PointerElement::Surface { + surface: surface.clone(), + }, + } + } + + /// If the current cursor is named and animated, get the time to the next frame, in milliseconds. + pub fn time_until_next_animated_cursor_frame(&mut self) -> Option { + match &self.current_cursor_image { + CursorImageStatus::Hidden => None, + CursorImageStatus::Named(icon) => { + let cursor = self + .get_xcursor_images(*icon) + .or_else(|| self.get_xcursor_images(CursorIcon::Default)) + .unwrap(); + + if cursor.images.len() <= 1 { + return None; + } + + // FIXME: copied from below, unify + let mut millis = self.start_time.duration_since(Instant::now()).as_millis() as u32; + let animation_length_ms = nearest_size_images(self.size, &cursor.images) + .fold(0, |acc, image| acc + image.delay); + millis %= animation_length_ms; + + for img in nearest_size_images(self.size, &cursor.images) { + if millis < img.delay { + return Some(Duration::from_millis((img.delay - millis).into())); + } + millis -= img.delay; + } + + unreachable!() + } + CursorImageStatus::Surface(_) => None, + } } } -fn nearest_images(size: u32, images: &[Image]) -> impl Iterator { +pub struct XCursor { + images: Vec, +} + +impl XCursor { + pub fn image(&self, time: Duration, size: u32) -> Image { + let mut millis = time.as_millis() as u32; + let animation_length_ms = + nearest_size_images(size, &self.images).fold(0, |acc, image| acc + image.delay); + millis %= animation_length_ms; + + for img in nearest_size_images(size, &self.images) { + if millis < img.delay { + return img.clone(); + } + millis -= img.delay; + } + + unreachable!() + } +} + +fn nearest_size_images(size: u32, images: &[Image]) -> impl Iterator { // Follow the nominal size of the cursor to choose the nearest let nearest_image = images .iter() .min_by_key(|image| (size as i32 - image.size as i32).abs()) - .expect("no nearest image"); + .unwrap(); images.iter().filter(move |image| { image.width == nearest_image.width && image.height == nearest_image.height }) } -fn frame(mut millis: u32, size: u32, images: &[Image]) -> Image { - let total = nearest_images(size, images).fold(0, |acc, image| acc + image.delay); - millis %= total; +/// Load a theme and size from $XCURSOR_THEME and $XCURSOR_SIZE +fn load_xcursor_theme() -> (CursorTheme, u32) { + let theme = std::env::var("XCURSOR_THEME").unwrap_or_else(|_| "default".into()); + let size = std::env::var("XCURSOR_SIZE") + .ok() + .and_then(|size| size.parse::().ok()) + .unwrap_or(24); - for img in nearest_images(size, images) { - if millis < img.delay { - return img.clone(); - } - millis -= img.delay; + (CursorTheme::load(&theme), size) +} + +/// Load xcursor images for the given theme and icon. +/// +/// Looks through legacy names as fallback. +fn load_xcursor_images(theme: &CursorTheme, icon: CursorIcon) -> anyhow::Result { + let icon_path = std::iter::once(&icon.name()) + .chain(icon.alt_names()) + .find_map(|name| theme.load_icon(name)) + .context("no images for icon")?; + + let cursor_bytes = std::fs::read(icon_path).context("failed to read xcursor file")?; + + parse_xcursor(&cursor_bytes) + .map(|images| XCursor { images }) + .context("failed to parse xcursor bytes") +} + +fn fallback_cursor() -> XCursor { + XCursor { + images: vec![Image { + size: 32, + width: 64, + height: 64, + xhot: 1, + yhot: 1, + delay: 1, + pixels_rgba: Vec::from(FALLBACK_CURSOR_DATA), + pixels_argb: vec![], // unused + }], } - - unreachable!() -} - -#[derive(thiserror::Error, Debug)] -enum Error { - #[error("Theme has no default cursor")] - NoDefaultCursor, - #[error("Error opening xcursor file: {0}")] - File(#[from] std::io::Error), - #[error("Failed to parse XCursor file")] - Parse, -} - -fn load_icon(theme: &CursorTheme) -> Result, Error> { - let icon_path = theme.load_icon("default").ok_or(Error::NoDefaultCursor)?; - let mut cursor_file = std::fs::File::open(icon_path)?; - let mut cursor_data = Vec::new(); - cursor_file.read_to_end(&mut cursor_data)?; - xcursor::parser::parse_xcursor(&cursor_data).ok_or(Error::Parse) } diff --git a/src/handlers.rs b/src/handlers.rs index 3ff6acd..57f9671 100644 --- a/src/handlers.rs +++ b/src/handlers.rs @@ -10,11 +10,15 @@ mod xwayland; use std::{collections::HashMap, mem, os::fd::OwnedFd, sync::Arc}; use smithay::{ - backend::renderer::utils::{self, with_renderer_surface_state}, - delegate_compositor, delegate_data_control, delegate_data_device, delegate_fractional_scale, - delegate_layer_shell, delegate_output, delegate_pointer_constraints, delegate_presentation, - delegate_primary_selection, delegate_relative_pointer, delegate_seat, - delegate_security_context, delegate_shm, delegate_viewporter, delegate_xwayland_shell, + backend::{ + input::TabletToolDescriptor, + renderer::utils::{self, with_renderer_surface_state}, + }, + delegate_compositor, delegate_cursor_shape, delegate_data_control, delegate_data_device, + delegate_fractional_scale, delegate_layer_shell, delegate_output, delegate_pointer_constraints, + delegate_presentation, delegate_primary_selection, delegate_relative_pointer, delegate_seat, + delegate_security_context, delegate_shm, delegate_tablet_manager, delegate_viewporter, + delegate_xwayland_shell, desktop::{ self, find_popup_root_surface, get_popup_toplevel_coords, layer_map_for_output, PopupKind, PopupManager, WindowSurfaceType, @@ -62,13 +66,11 @@ use smithay::{ SelectionHandler, SelectionSource, SelectionTarget, }, shell::{ - wlr_layer::{ - self, Layer, LayerSurfaceCachedState, LayerSurfaceData, WlrLayerShellHandler, - WlrLayerShellState, - }, + wlr_layer::{self, Layer, LayerSurfaceData, WlrLayerShellHandler, WlrLayerShellState}, xdg::{PopupSurface, XdgPopupSurfaceData, XdgToplevelSurfaceData}, }, shm::{ShmHandler, ShmState}, + tablet_manager::TabletSeatHandler, xwayland_shell::{XWaylandShellHandler, XWaylandShellState}, }, xwayland::XWaylandClientData, @@ -541,7 +543,7 @@ impl SeatHandler for State { } fn cursor_image(&mut self, _seat: &Seat, image: CursorImageStatus) { - self.pinnacle.cursor_status = image; + self.pinnacle.cursor_state.set_cursor_image(image); } fn focus_changed(&mut self, seat: &Seat, focused: Option<&Self::KeyboardFocus>) { @@ -898,6 +900,17 @@ impl OutputPowerManagementHandler for State { } delegate_output_power_management!(State); +impl TabletSeatHandler for State { + fn tablet_tool_image(&mut self, tool: &TabletToolDescriptor, image: CursorImageStatus) { + // TODO: + let _ = tool; + let _ = image; + } +} +delegate_tablet_manager!(State); + +delegate_cursor_shape!(State); + impl Pinnacle { fn position_popup(&self, popup: &PopupSurface) { trace!("State::position_popup"); diff --git a/src/handlers/xwayland.rs b/src/handlers/xwayland.rs index 530864f..d2f0612 100644 --- a/src/handlers/xwayland.rs +++ b/src/handlers/xwayland.rs @@ -4,6 +4,7 @@ use std::{process::Stdio, time::Duration}; use smithay::{ desktop::Window, + input::pointer::CursorIcon, utils::{Logical, Point, Rectangle, Size, SERIAL_COUNTER}, wayland::selection::{ data_device::{ @@ -24,7 +25,6 @@ use smithay::{ use tracing::{debug, error, trace, warn}; use crate::{ - cursor::Cursor, focus::keyboard::KeyboardFocusTarget, state::{Pinnacle, State, WithState}, window::{window_state::FloatingOrTiled, WindowElement}, @@ -505,8 +505,13 @@ impl Pinnacle { ) .expect("Failed to attach x11wm"); - let cursor = Cursor::load(); - let image = cursor.get_image(1, Duration::ZERO); + let cursor = state + .pinnacle + .cursor_state + .get_xcursor_images(CursorIcon::Default) + .unwrap(); + let image = + cursor.image(Duration::ZERO, state.pinnacle.cursor_state.cursor_size()); wm.set_cursor( &image.pixels_rgba, Size::from((image.width as u16, image.height as u16)), diff --git a/src/input.rs b/src/input.rs index 497937a..ab7660d 100644 --- a/src/input.rs +++ b/src/input.rs @@ -438,7 +438,6 @@ impl State { if self.pinnacle.lock_state.is_unlocked() { // Focus the topmost exclusive layer, if any - let mut exclusive_layers_exist = false; for layer in self.pinnacle.layer_shell_state.layer_surfaces().rev() { let data = compositor::with_states(layer.wl_surface(), |states| { *states.cached_state.current::() @@ -456,7 +455,6 @@ impl State { }); if let Some(layer_surface) = layer_surface { - exclusive_layers_exist = true; keyboard.set_focus( self, Some(KeyboardFocusTarget::LayerSurface(layer_surface)), diff --git a/src/main.rs b/src/main.rs index 2a3e04f..87ea940 100644 --- a/src/main.rs +++ b/src/main.rs @@ -223,6 +223,7 @@ async fn main() -> anyhow::Result<()> { } event_loop.run(Duration::from_secs(1), &mut state, |state| { + tracing::info!("after idles"); state.on_event_loop_cycle_completion(); })?; diff --git a/src/render.rs b/src/render.rs index dc633fd..78748f9 100644 --- a/src/render.rs +++ b/src/render.rs @@ -5,7 +5,7 @@ pub mod render_elements; pub mod texture; pub mod util; -use std::{ops::Deref, sync::Mutex}; +use std::ops::Deref; use smithay::{ backend::renderer::{ @@ -22,11 +22,9 @@ use smithay::{ }, PopupManager, Space, WindowSurface, }, - input::pointer::{CursorImageAttributes, CursorImageStatus}, output::Output, - reexports::wayland_server::protocol::wl_surface::WlSurface, utils::{Logical, Point, Scale}, - wayland::{compositor, shell::wlr_layer}, + wayland::shell::wlr_layer, }; use crate::{ @@ -38,8 +36,7 @@ use crate::{ }; use self::{ - pointer::{PointerElement, PointerRenderElement}, - texture::CommonTextureRenderElement, + pointer::PointerRenderElement, texture::CommonTextureRenderElement, util::surface::texture_render_elements_from_surface_tree, }; @@ -269,62 +266,6 @@ fn window_render_elements( ) } -pub fn pointer_render_elements( - output: &Output, - renderer: &mut R, - space: &Space, - pointer_location: Point, - cursor_status: &mut CursorImageStatus, - dnd_icon: Option<&WlSurface>, - fallback_hotspot: Point, - pointer_element: &PointerElement<::TextureId>, -) -> Vec> { - let mut output_render_elements = Vec::new(); - - let Some(output_geometry) = space.output_geometry(output) else { - return output_render_elements; - }; - let scale = Scale::from(output.current_scale().fractional_scale()); - - if output_geometry.to_f64().contains(pointer_location) { - let cursor_hotspot = if let CursorImageStatus::Surface(ref surface) = cursor_status { - compositor::with_states(surface, |states| { - states - .data_map - .get::>() - .expect("surface data map had no CursorImageAttributes") - .lock() - .expect("failed to lock mutex") - .hotspot - }) - } else { - fallback_hotspot - }; - - let cursor_pos = pointer_location - output_geometry.loc.to_f64() - cursor_hotspot.to_f64(); - let cursor_pos_scaled = cursor_pos.to_physical_precise_round(scale); - - output_render_elements.extend(pointer_element.render_elements( - renderer, - cursor_pos_scaled, - scale, - 1.0, - )); - - if let Some(dnd_icon) = dnd_icon { - output_render_elements.extend(AsRenderElements::render_elements( - &smithay::desktop::space::SurfaceTree::from_surface(dnd_icon), - renderer, - cursor_pos_scaled, - scale, - 1.0, - )); - } - } - - output_render_elements -} - /// Render elements for any pending layout transaction. /// /// Returns fullscreen_and_up elements then under_fullscreen elements. diff --git a/src/render/pointer.rs b/src/render/pointer.rs index 523e367..d41d965 100644 --- a/src/render/pointer.rs +++ b/src/render/pointer.rs @@ -1,99 +1,123 @@ // SPDX-License-Identifier: GPL-3.0-or-later +use std::{rc::Rc, sync::Mutex}; + use smithay::{ backend::renderer::{ element::{ self, - surface::{self, WaylandSurfaceRenderElement}, - texture::{TextureBuffer, TextureRenderElement}, + memory::MemoryRenderBufferRenderElement, + surface::{render_elements_from_surface_tree, WaylandSurfaceRenderElement}, AsRenderElements, }, - ImportAll, Renderer, Texture, + ImportAll, ImportMem, }, - input::pointer::CursorImageStatus, + desktop::Space, + input::pointer::CursorImageAttributes, + output::Output, + reexports::wayland_server::protocol::wl_surface::WlSurface, render_elements, - utils::{Physical, Point, Scale}, + utils::{Clock, Logical, Monotonic, Point, Scale}, + wayland::compositor, +}; + +use crate::{ + cursor::{CursorState, XCursor}, + window::WindowElement, }; use super::PRenderer; -pub struct PointerElement { - texture: Option>, - status: CursorImageStatus, - kind: element::Kind, -} - -impl Default for PointerElement { - fn default() -> Self { - Self { - texture: Default::default(), - status: CursorImageStatus::default_named(), - kind: element::Kind::Cursor, - } - } -} - -impl PointerElement { - pub fn new() -> Self { - Default::default() - } - - pub fn set_status(&mut self, status: CursorImageStatus) { - self.status = status; - } - - pub fn set_texture(&mut self, texture: TextureBuffer) { - self.texture = Some(texture); - } - - pub fn set_element_kind(&mut self, kind: element::Kind) { - self.kind = kind; - } +pub enum PointerElement { + Hidden, + Named { cursor: Rc, size: u32 }, + Surface { surface: WlSurface }, } render_elements! { #[derive(Debug)] - pub PointerRenderElement where R: ImportAll; - Surface=WaylandSurfaceRenderElement, - Texture=TextureRenderElement<::TextureId>, + pub PointerRenderElement where R: ImportAll + ImportMem; + Surface = WaylandSurfaceRenderElement, + Memory = MemoryRenderBufferRenderElement, } -impl AsRenderElements for PointerElement { - type RenderElement = PointerRenderElement; +pub fn pointer_render_elements( + output: &Output, + renderer: &mut R, + cursor_state: &mut CursorState, + space: &Space, + pointer_location: Point, + dnd_icon: Option<&WlSurface>, + clock: &Clock, +) -> Vec> { + let mut pointer_render_elements = Vec::new(); - fn render_elements>( - &self, - renderer: &mut R, - location: Point, - scale: Scale, - alpha: f32, - ) -> Vec { - match &self.status { - CursorImageStatus::Hidden => vec![], - CursorImageStatus::Named(_) => { - if let Some(texture) = self.texture.as_ref() { - vec![PointerRenderElement::::from( - TextureRenderElement::from_texture_buffer( - location.to_f64(), - texture, - None, - None, - None, - self.kind, - ), - ) - .into()] - } else { - vec![] - } + let Some(output_geometry) = space.output_geometry(output) else { + return pointer_render_elements; + }; + + let scale = Scale::from(output.current_scale().fractional_scale()); + + let pointer_elem = cursor_state.pointer_element(); + + if output_geometry.to_f64().contains(pointer_location) { + let cursor_pos = pointer_location - output_geometry.loc.to_f64(); + + let mut elements = match &pointer_elem { + PointerElement::Hidden => vec![], + PointerElement::Named { cursor, size } => { + let image = cursor.image(clock.now().into(), *size); + let hotspot = (image.xhot as i32, image.yhot as i32); + let buffer = cursor_state.buffer_for_image(image); + let elem = MemoryRenderBufferRenderElement::from_buffer( + renderer, + (cursor_pos - Point::from(hotspot).to_f64()).to_physical_precise_round(scale), + &buffer, + None, + None, + None, + element::Kind::Cursor, + ); + + elem.map(|elem| vec![PointerRenderElement::Memory(elem)]) + .unwrap_or_default() } - CursorImageStatus::Surface(surface) => { - let elements: Vec> = - surface::render_elements_from_surface_tree( - renderer, surface, location, scale, alpha, self.kind, - ); - elements.into_iter().map(C::from).collect() + PointerElement::Surface { surface } => { + let hotspot = compositor::with_states(surface, |states| { + states + .data_map + .get::>() + .expect("surface data map had no CursorImageAttributes") + .lock() + .expect("failed to lock mutex") + .hotspot + }); + + let elems = render_elements_from_surface_tree( + renderer, + surface, + (cursor_pos - hotspot.to_f64()).to_physical_precise_round(scale), + scale, + 1.0, + element::Kind::Cursor, + ); + + elems } + }; + + if let Some(dnd_icon) = dnd_icon { + elements.extend(AsRenderElements::render_elements( + &smithay::desktop::space::SurfaceTree::from_surface(dnd_icon), + renderer, + cursor_pos.to_physical_precise_round(scale), + scale, + 1.0, + )); } + + pointer_render_elements = elements; } + + pointer_render_elements } diff --git a/src/state.rs b/src/state.rs index feb9f5e..c923594 100644 --- a/src/state.rs +++ b/src/state.rs @@ -5,6 +5,7 @@ use crate::{ backend::{self, udev::Udev, winit::Winit, Backend}, cli::{self, Cli}, config::Config, + cursor::CursorState, focus::OutputFocusStack, grab::resize_grab::ResizeSurfaceState, handlers::session_lock::LockState, @@ -23,7 +24,7 @@ use indexmap::IndexMap; use pinnacle_api_defs::pinnacle::v0alpha1::ShutdownWatchResponse; use smithay::{ desktop::{PopupManager, Space}, - input::{keyboard::XkbConfig, pointer::CursorImageStatus, Seat, SeatState}, + input::{keyboard::XkbConfig, Seat, SeatState}, output::Output, reexports::{ calloop::{generic::Generic, Interest, LoopHandle, LoopSignal, Mode, PostAction}, @@ -36,6 +37,7 @@ use smithay::{ utils::{Clock, Monotonic}, wayland::{ compositor::{self, CompositorClientState, CompositorState}, + cursor_shape::CursorShapeManagerState, dmabuf::DmabufFeedback, fractional_scale::FractionalScaleManagerState, idle_notify::IdleNotifierState, @@ -51,6 +53,7 @@ use smithay::{ shell::{wlr_layer::WlrLayerShellState, xdg::XdgShellState}, shm::ShmState, socket::ListeningSocketSource, + tablet_manager::TabletManagerState, viewporter::ViewporterState, xwayland_shell::XWaylandShellState, }, @@ -112,6 +115,7 @@ pub struct Pinnacle { pub idle_notifier_state: IdleNotifierState, pub output_management_manager_state: OutputManagementManagerState, pub output_power_management_state: OutputPowerManagementState, + pub tablet_manager_state: TabletManagerState, pub lock_state: LockState, @@ -123,7 +127,6 @@ pub struct Pinnacle { pub popup_manager: PopupManager, - pub cursor_status: CursorImageStatus, pub dnd_icon: Option, /// The main window vec @@ -160,6 +163,9 @@ pub struct Pinnacle { pub snowcap_shutdown_ping: Option, #[cfg(feature = "snowcap")] pub snowcap_join_handle: Option>, + + pub cursor_shape_manager_state: CursorShapeManagerState, + pub cursor_state: CursorState, } impl State { @@ -286,7 +292,6 @@ impl Pinnacle { seat_state, shm_state: ShmState::new::(&display_handle, vec![]), space: Space::::default(), - cursor_status: CursorImageStatus::default_named(), output_manager_state: OutputManagerState::new_with_xdg_output::(&display_handle), xdg_shell_state: XdgShellState::new::(&display_handle), viewporter_state: ViewporterState::new::(&display_handle), @@ -333,6 +338,7 @@ impl Pinnacle { &display_handle, filter_restricted_client, ), + tablet_manager_state: TabletManagerState::new::(&display_handle), lock_state: LockState::default(), @@ -378,6 +384,9 @@ impl Pinnacle { snowcap_shutdown_ping: None, #[cfg(feature = "snowcap")] snowcap_join_handle: None, + + cursor_shape_manager_state: CursorShapeManagerState::new::(&display_handle), + cursor_state: CursorState::new(), }; Ok(pinnacle) From 85fb1be6ea736f3d800d7eb8569d54be524e4480 Mon Sep 17 00:00:00 2001 From: Ottatop Date: Fri, 21 Jun 2024 13:11:55 -0500 Subject: [PATCH 2/6] Impl cursor scaling --- src/backend/udev.rs | 22 +++++++++++++++++++--- src/cursor.rs | 12 ++++++------ src/handlers/xwayland.rs | 2 +- src/main.rs | 1 - src/render/pointer.rs | 8 +++++--- 5 files changed, 31 insertions(+), 14 deletions(-) diff --git a/src/backend/udev.rs b/src/backend/udev.rs index 22d2020..3714604 100644 --- a/src/backend/udev.rs +++ b/src/backend/udev.rs @@ -523,10 +523,8 @@ impl Udev { match &surface.render_state { RenderState::Idle => { - tracing::info!("inserting idle render"); let output = output.clone(); let token = loop_handle.insert_idle(move |state| { - tracing::info!("actually rendering"); state .backend .udev_mut() @@ -537,7 +535,6 @@ impl Udev { } RenderState::Scheduled(_) => (), RenderState::WaitingForVblank { dirty: _ } => { - tracing::info!("making dirty"); surface.render_state = RenderState::WaitingForVblank { dirty: true } } } @@ -1366,6 +1363,22 @@ impl Udev { } // Schedule a render when the next frame of an animated cursor should be drawn. + // + // TODO: Remove this and improve the render pipeline. + // Because of how the event loop works and the current implementation of rendering, + // immediately queuing a render here has the possibility of not submitting a new frame to + // DRM, meaning no vblank. The event loop will wait as it has no events, so things like + // animated cursors may hitch and only update when, for example, the cursor is actively + // moving as this generates events. + // + // What we should do is what Niri does: if `render_surface` doesn't cause any damage, + // instead of setting the `RenderState` to Idle, set it to some "waiting for estimated + // vblank" state and have `render_surface` always schedule a timer to fire at the estimated + // vblank time that will attempt another render schedule. + // + // This has the advantage of scheduling a render in a source and not in an idle callback, + // meaning we are guarenteed to have a render happen immediately and we won't have to wait + // for another event or call `loop_signal.wakeup()`. if let Some(until) = pinnacle .cursor_state .time_until_next_animated_cursor_frame() @@ -1568,6 +1581,9 @@ impl Udev { match result { Ok(true) => surface.render_state = RenderState::WaitingForVblank { dirty: false }, + // TODO: Don't immediately set this to Idle; this allows hot loops of `render_surface`. + // Instead, pull a Niri and schedule a timer for the next estimated vblank to allow + // another scheduled render. Ok(false) | Err(_) => surface.render_state = RenderState::Idle, } diff --git a/src/cursor.rs b/src/cursor.rs index ce59e76..288c668 100644 --- a/src/cursor.rs +++ b/src/cursor.rs @@ -51,8 +51,8 @@ impl CursorState { self.loaded_images.clear(); } - pub fn cursor_size(&self) -> u32 { - self.size + pub fn cursor_size(&self, scale: i32) -> u32 { + self.size * scale as u32 } pub fn set_cursor_image(&mut self, image: CursorImageStatus) { @@ -76,7 +76,7 @@ impl CursorState { .clone() } - pub fn buffer_for_image(&mut self, image: Image) -> MemoryRenderBuffer { + pub fn buffer_for_image(&mut self, image: Image, scale: i32) -> MemoryRenderBuffer { self.mem_buffer_cache .iter() .find_map(|(img, buf)| (*img == image).then(|| buf.clone())) @@ -86,7 +86,7 @@ impl CursorState { &image.pixels_rgba, Fourcc::Abgr8888, (image.width as i32, image.height as i32), - 1, + scale, Transform::Normal, None, ); @@ -116,6 +116,7 @@ impl CursorState { } } + // TODO: update render to wait for est vblank, then you can remove this /// If the current cursor is named and animated, get the time to the next frame, in milliseconds. pub fn time_until_next_animated_cursor_frame(&mut self) -> Option { match &self.current_cursor_image { @@ -130,7 +131,6 @@ impl CursorState { return None; } - // FIXME: copied from below, unify let mut millis = self.start_time.duration_since(Instant::now()).as_millis() as u32; let animation_length_ms = nearest_size_images(self.size, &cursor.images) .fold(0, |acc, image| acc + image.delay); @@ -143,7 +143,7 @@ impl CursorState { millis -= img.delay; } - unreachable!() + None } CursorImageStatus::Surface(_) => None, } diff --git a/src/handlers/xwayland.rs b/src/handlers/xwayland.rs index d2f0612..fe90fa5 100644 --- a/src/handlers/xwayland.rs +++ b/src/handlers/xwayland.rs @@ -511,7 +511,7 @@ impl Pinnacle { .get_xcursor_images(CursorIcon::Default) .unwrap(); let image = - cursor.image(Duration::ZERO, state.pinnacle.cursor_state.cursor_size()); + cursor.image(Duration::ZERO, state.pinnacle.cursor_state.cursor_size(1)); // TODO: scale wm.set_cursor( &image.pixels_rgba, Size::from((image.width as u16, image.height as u16)), diff --git a/src/main.rs b/src/main.rs index 87ea940..2a3e04f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -223,7 +223,6 @@ async fn main() -> anyhow::Result<()> { } event_loop.run(Duration::from_secs(1), &mut state, |state| { - tracing::info!("after idles"); state.on_event_loop_cycle_completion(); })?; diff --git a/src/render/pointer.rs b/src/render/pointer.rs index d41d965..3c8dad3 100644 --- a/src/render/pointer.rs +++ b/src/render/pointer.rs @@ -57,6 +57,7 @@ pub fn pointer_render_elements( }; let scale = Scale::from(output.current_scale().fractional_scale()); + let integer_scale = output.current_scale().integer_scale(); let pointer_elem = cursor_state.pointer_element(); @@ -66,12 +67,13 @@ pub fn pointer_render_elements( let mut elements = match &pointer_elem { PointerElement::Hidden => vec![], PointerElement::Named { cursor, size } => { - let image = cursor.image(clock.now().into(), *size); + let image = cursor.image(clock.now().into(), *size * integer_scale as u32); let hotspot = (image.xhot as i32, image.yhot as i32); - let buffer = cursor_state.buffer_for_image(image); + let buffer = cursor_state.buffer_for_image(image, integer_scale); let elem = MemoryRenderBufferRenderElement::from_buffer( renderer, - (cursor_pos - Point::from(hotspot).to_f64()).to_physical_precise_round(scale), + (cursor_pos - Point::from(hotspot).downscale(integer_scale).to_f64()) + .to_physical_precise_round(scale), &buffer, None, None, From 58c33467dd97eea07ea65fbee1c438412cca6237 Mon Sep 17 00:00:00 2001 From: Ottatop Date: Fri, 21 Jun 2024 15:03:32 -0500 Subject: [PATCH 3/6] Change cursor on move and resize grab --- src/api/window.rs | 9 ++++++++- src/grab/move_grab.rs | 26 +++++++++++++++++++------- src/grab/resize_grab.rs | 39 ++++++++++++++++++++++++++++++++------- 3 files changed, 59 insertions(+), 15 deletions(-) diff --git a/src/api/window.rs b/src/api/window.rs index 352a4af..c0f1acd 100644 --- a/src/api/window.rs +++ b/src/api/window.rs @@ -489,7 +489,6 @@ impl window_service_server::WindowService for WindowService { return; }; let Some(window) = pointer_focus.window_for(state) else { - tracing::info!("Move grabs are currently not implemented for non-windows"); return; }; let Some(wl_surf) = window.wl_surface() else { @@ -498,6 +497,10 @@ impl window_service_server::WindowService for WindowService { let seat = state.pinnacle.seat.clone(); state.move_request_server(&wl_surf, &seat, SERIAL_COUNTER.next_serial(), button); + + if let Some(output) = state.pinnacle.focused_output().cloned() { + state.schedule_render(&output); + } }) .await } @@ -579,6 +582,10 @@ impl window_service_server::WindowService for WindowService { edges.into(), button, ); + + if let Some(output) = state.pinnacle.focused_output().cloned() { + state.schedule_render(&output); + } }) .await } diff --git a/src/grab/move_grab.rs b/src/grab/move_grab.rs index 95f3c1e..edb7bcc 100644 --- a/src/grab/move_grab.rs +++ b/src/grab/move_grab.rs @@ -6,10 +6,11 @@ use smithay::{ // | input::keyboard input::{ pointer::{ - AxisFrame, ButtonEvent, Focus, GestureHoldBeginEvent, GestureHoldEndEvent, - GesturePinchBeginEvent, GesturePinchEndEvent, GesturePinchUpdateEvent, - GestureSwipeBeginEvent, GestureSwipeEndEvent, GestureSwipeUpdateEvent, GrabStartData, - MotionEvent, PointerGrab, PointerInnerHandle, RelativeMotionEvent, + AxisFrame, ButtonEvent, CursorIcon, CursorImageStatus, Focus, GestureHoldBeginEvent, + GestureHoldEndEvent, GesturePinchBeginEvent, GesturePinchEndEvent, + GesturePinchUpdateEvent, GestureSwipeBeginEvent, GestureSwipeEndEvent, + GestureSwipeUpdateEvent, GrabStartData, MotionEvent, PointerGrab, PointerInnerHandle, + RelativeMotionEvent, }, Seat, SeatHandler, }, @@ -46,6 +47,10 @@ impl PointerGrab for MoveSurfaceGrab { handle.motion(state, None, event); if !self.window.alive() { + state + .pinnacle + .cursor_state + .set_cursor_image(CursorImageStatus::default_named()); handle.unset_grab(self, state, event.serial, event.time, true); return; } @@ -168,6 +173,9 @@ impl PointerGrab for MoveSurfaceGrab { handle.button(data, event); if !handle.current_pressed().contains(&self.start_data.button) { + data.pinnacle + .cursor_state + .set_cursor_image(CursorImageStatus::default_named()); handle.unset_grab(self, data, event.serial, event.time, true); } } @@ -311,9 +319,9 @@ impl State { .to_f64(); // TODO: add space f64 support or move away from space let start_data = smithay::input::pointer::GrabStartData { - focus: pointer - .current_focus() - .map(|focus| (focus, initial_window_loc)), + // If Some and same as the dragged window then the window is allowed to + // change the cursor, which we don't want, therefore this is None + focus: None, button: button_used, location: pointer.current_location(), }; @@ -325,5 +333,9 @@ impl State { }; pointer.set_grab(self, grab, serial, Focus::Clear); + + self.pinnacle + .cursor_state + .set_cursor_image(CursorImageStatus::Named(CursorIcon::Grabbing)); } } diff --git a/src/grab/resize_grab.rs b/src/grab/resize_grab.rs index 80adb82..086702f 100644 --- a/src/grab/resize_grab.rs +++ b/src/grab/resize_grab.rs @@ -4,10 +4,10 @@ use smithay::{ desktop::{space::SpaceElement, WindowSurface}, input::{ pointer::{ - AxisFrame, ButtonEvent, Focus, GestureHoldBeginEvent, GestureHoldEndEvent, - GesturePinchBeginEvent, GesturePinchEndEvent, GesturePinchUpdateEvent, - GestureSwipeBeginEvent, GestureSwipeEndEvent, GestureSwipeUpdateEvent, GrabStartData, - PointerGrab, PointerInnerHandle, + AxisFrame, ButtonEvent, CursorIcon, CursorImageStatus, Focus, GestureHoldBeginEvent, + GestureHoldEndEvent, GesturePinchBeginEvent, GesturePinchEndEvent, + GesturePinchUpdateEvent, GestureSwipeBeginEvent, GestureSwipeEndEvent, + GestureSwipeUpdateEvent, GrabStartData, PointerGrab, PointerInnerHandle, }, Seat, SeatHandler, }, @@ -49,6 +49,23 @@ impl From for ResizeEdge { } } +impl ResizeEdge { + fn cursor_icon(&self) -> CursorIcon { + match self.0 { + xdg_toplevel::ResizeEdge::None => CursorIcon::Default, // TODO: possibly different icon here? + xdg_toplevel::ResizeEdge::Top => CursorIcon::NResize, + xdg_toplevel::ResizeEdge::Bottom => CursorIcon::SResize, + xdg_toplevel::ResizeEdge::Left => CursorIcon::WResize, + xdg_toplevel::ResizeEdge::TopLeft => CursorIcon::NwResize, + xdg_toplevel::ResizeEdge::BottomLeft => CursorIcon::SwResize, + xdg_toplevel::ResizeEdge::Right => CursorIcon::EResize, + xdg_toplevel::ResizeEdge::TopRight => CursorIcon::NeResize, + xdg_toplevel::ResizeEdge::BottomRight => CursorIcon::SeResize, + _ => CursorIcon::Default, + } + } +} + pub struct ResizeSurfaceGrab { start_data: GrabStartData, window: WindowElement, @@ -146,6 +163,9 @@ impl PointerGrab for ResizeSurfaceGrab { handle.motion(data, None, event); if !self.window.alive() { + data.pinnacle + .cursor_state + .set_cursor_image(CursorImageStatus::default_named()); handle.unset_grab(self, data, event.serial, event.time, true); return; } @@ -242,6 +262,9 @@ impl PointerGrab for ResizeSurfaceGrab { handle.button(data, event); if !handle.current_pressed().contains(&self.button_used) { + data.pinnacle + .cursor_state + .set_cursor_image(CursorImageStatus::default_named()); handle.unset_grab(self, data, event.serial, event.time, true); } } @@ -558,9 +581,7 @@ impl State { } let start_data = smithay::input::pointer::GrabStartData { - focus: pointer - .current_focus() - .map(|focus| (focus, initial_window_loc)), + focus: None, button: button_used, location: pointer.current_location(), }; @@ -576,6 +597,10 @@ impl State { if let Some(grab) = grab { pointer.set_grab(self, grab, serial, Focus::Clear); + + self.pinnacle + .cursor_state + .set_cursor_image(CursorImageStatus::Named(edges.cursor_icon())); } } } From 98bb5268c8e18d9203f2d915e6d3324c94a530c0 Mon Sep 17 00:00:00 2001 From: Ottatop Date: Fri, 21 Jun 2024 15:26:57 -0500 Subject: [PATCH 4/6] Disable winit cursor --- src/backend/winit.rs | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/src/backend/winit.rs b/src/backend/winit.rs index 8c81ef6..6b1b876 100644 --- a/src/backend/winit.rs +++ b/src/backend/winit.rs @@ -172,6 +172,8 @@ impl Winit { tracing::info!("EGL hardware-acceleration enabled"); } + winit_backend.window().set_cursor_visible(false); + let mut winit = Winit { backend: winit_backend, damage_tracker: OutputDamageTracker::from_output(&output), @@ -271,14 +273,6 @@ impl Winit { } } - let cursor_visible = !matches!( - pinnacle.cursor_state.cursor_image(), - CursorImageStatus::Surface(_) - ); - - // TODO: make winit cursor disappear if not surface - // TODO: set winit cursor to named cursor when possible - // The z-index of these is determined by `state.fixup_z_layering()`, which is called at the end // of every event loop cycle let windows = pinnacle.space.elements().cloned().collect::>(); @@ -418,8 +412,6 @@ impl Winit { } } - self.backend.window().set_cursor_visible(cursor_visible); - let time = pinnacle.clock.now(); super::post_repaint( From aa1e79c715d4b161def0cb037b437d4f12efef55 Mon Sep 17 00:00:00 2001 From: Ottatop Date: Fri, 21 Jun 2024 17:43:05 -0500 Subject: [PATCH 5/6] (Re)workaround cursor overwriting transparency in screencopy --- Cargo.lock | 2 +- src/backend/udev.rs | 34 +++++++++++++++++++++++++++++----- src/backend/winit.rs | 3 ++- src/cursor.rs | 4 +++- src/render/pointer.rs | 18 +++++++++++++----- 5 files changed, 48 insertions(+), 13 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 354f3f3..9ad4e85 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2117,7 +2117,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c2a198fb6b0eada2a8df47933734e6d35d350665a33a3593d7164fa52c75c19" dependencies = [ "cfg-if", - "windows-targets 0.52.5", + "windows-targets 0.48.5", ] [[package]] diff --git a/src/backend/udev.rs b/src/backend/udev.rs index 3714604..4935a2f 100644 --- a/src/backend/udev.rs +++ b/src/backend/udev.rs @@ -32,7 +32,7 @@ use smithay::{ libinput::{LibinputInputBackend, LibinputSessionInterface}, renderer::{ self, damage, - element::{self, surface::render_elements_from_surface_tree, Element}, + element::{self, surface::render_elements_from_surface_tree, Element, Id}, gles::{GlesRenderbuffer, GlesRenderer}, multigpu::{gbm::GbmGlesBackend, GpuManager, MultiRenderer}, sync::SyncPoint, @@ -1454,7 +1454,20 @@ impl Udev { || (pinnacle.lock_state.is_locked() && output.with_state(|state| state.lock_surface.is_none())); - let pointer_render_elements = pointer_render_elements( + // HACK: Doing `blit_frame_result` with something on the cursor/overlay plane overwrites + // transparency. This workaround makes the cursor not be on the cursor plane for blitting. + let kind = if output.with_state(|state| { + state + .screencopy + .as_ref() + .is_some_and(|sc| sc.overlay_cursor()) + }) { + element::Kind::Unspecified + } else { + element::Kind::Cursor + }; + + let (pointer_render_elements, cursor_ids) = pointer_render_elements( output, &mut renderer, &mut pinnacle.cursor_state, @@ -1462,6 +1475,7 @@ impl Udev { pointer_location, pinnacle.dnd_icon.as_ref(), &pinnacle.clock, + kind, ); output_render_elements.extend( pointer_render_elements @@ -1543,6 +1557,7 @@ impl Udev { surface, &render_frame_result, &pinnacle.loop_handle, + cursor_ids, ); } @@ -1610,6 +1625,7 @@ fn handle_pending_screencopy<'a>( surface: &mut RenderSurface, render_frame_result: &UdevRenderFrameResult<'a>, loop_handle: &LoopHandle<'static, State>, + cursor_ids: Vec, ) { let Some(mut screencopy) = output.with_state_mut(|state| state.screencopy.take()) else { return; @@ -1733,7 +1749,7 @@ fn handle_pending_screencopy<'a>( output.current_scale().fractional_scale(), renderer, [screencopy.physical_region()], - [], + cursor_ids, )?)) } else { // `RenderFrameResult::blit_frame_result` doesn't expose a way to @@ -1760,7 +1776,11 @@ fn handle_pending_screencopy<'a>( Point::from((0, 0)), untransformed_output_size, )], - [], + if !screencopy.overlay_cursor() { + cursor_ids + } else { + Vec::new() + }, )?; // ayo are we supposed to wait this here (granted it doesn't do anything @@ -1840,7 +1860,11 @@ fn handle_pending_screencopy<'a>( Point::from((0, 0)), untransformed_output_size, )], - [], + if !screencopy.overlay_cursor() { + cursor_ids + } else { + Vec::new() + }, )?; // Can someone explain to me why it feels like some things are diff --git a/src/backend/winit.rs b/src/backend/winit.rs index 6b1b876..2d1ea09 100644 --- a/src/backend/winit.rs +++ b/src/backend/winit.rs @@ -295,7 +295,7 @@ impl Winit { .map(|ptr| ptr.current_location()) .unwrap_or((0.0, 0.0).into()); - let pointer_render_elements = pointer_render_elements( + let (pointer_render_elements, _cursor_ids) = pointer_render_elements( &self.output, self.backend.renderer(), &mut pinnacle.cursor_state, @@ -303,6 +303,7 @@ impl Winit { pointer_location, pinnacle.dnd_icon.as_ref(), &pinnacle.clock, + element::Kind::Cursor, ); output_render_elements.extend( pointer_render_elements diff --git a/src/cursor.rs b/src/cursor.rs index 288c668..5051d14 100644 --- a/src/cursor.rs +++ b/src/cursor.rs @@ -84,7 +84,9 @@ impl CursorState { // TODO: scale let buffer = MemoryRenderBuffer::from_slice( &image.pixels_rgba, - Fourcc::Abgr8888, + // Don't make Abgr, then the format doesn't match the + // cursor bo and this doesn't get put on the cursor plane + Fourcc::Argb8888, (image.width as i32, image.height as i32), scale, Transform::Normal, diff --git a/src/render/pointer.rs b/src/render/pointer.rs index 3c8dad3..e271301 100644 --- a/src/render/pointer.rs +++ b/src/render/pointer.rs @@ -8,7 +8,7 @@ use smithay::{ self, memory::MemoryRenderBufferRenderElement, surface::{render_elements_from_surface_tree, WaylandSurfaceRenderElement}, - AsRenderElements, + AsRenderElements, Element, Id, }, ImportAll, ImportMem, }, @@ -41,6 +41,9 @@ render_elements! { Memory = MemoryRenderBufferRenderElement, } +/// Render pointer elements. +/// +/// Additionally returns the ids of cursor elements for use in screencopy. pub fn pointer_render_elements( output: &Output, renderer: &mut R, @@ -49,11 +52,13 @@ pub fn pointer_render_elements( pointer_location: Point, dnd_icon: Option<&WlSurface>, clock: &Clock, -) -> Vec> { + kind: element::Kind, +) -> (Vec>, Vec) { let mut pointer_render_elements = Vec::new(); + let mut cursor_ids = Vec::new(); let Some(output_geometry) = space.output_geometry(output) else { - return pointer_render_elements; + return (pointer_render_elements, cursor_ids); }; let scale = Scale::from(output.current_scale().fractional_scale()); @@ -78,7 +83,7 @@ pub fn pointer_render_elements( None, None, None, - element::Kind::Cursor, + kind, ); elem.map(|elem| vec![PointerRenderElement::Memory(elem)]) @@ -108,6 +113,9 @@ pub fn pointer_render_elements( } }; + // rust analyzer is so broken wtf why is `elem` {unknown} + cursor_ids = elements.iter().map(|elem| elem.id()).cloned().collect(); + if let Some(dnd_icon) = dnd_icon { elements.extend(AsRenderElements::render_elements( &smithay::desktop::space::SurfaceTree::from_surface(dnd_icon), @@ -121,5 +129,5 @@ pub fn pointer_render_elements( pointer_render_elements = elements; } - pointer_render_elements + (pointer_render_elements, cursor_ids) } From b51d10649f2de5dd659b45edd0e6fb304babc8b4 Mon Sep 17 00:00:00 2001 From: Ottatop Date: Fri, 21 Jun 2024 18:34:54 -0500 Subject: [PATCH 6/6] Add API calls for xcursor settings --- api/lua/pinnacle/grpc/defs.lua | 11 +++++ api/lua/pinnacle/input.lua | 24 +++++++++++ .../pinnacle/input/v0alpha1/input.proto | 7 ++++ api/rust/src/input.rs | 42 ++++++++++++++++++- src/api.rs | 27 +++++++++++- src/cursor.rs | 28 +++++++++---- 6 files changed, 130 insertions(+), 9 deletions(-) diff --git a/api/lua/pinnacle/grpc/defs.lua b/api/lua/pinnacle/grpc/defs.lua index 7c00f27..5bb9309 100644 --- a/api/lua/pinnacle/grpc/defs.lua +++ b/api/lua/pinnacle/grpc/defs.lua @@ -350,6 +350,10 @@ local pinnacle_input_v0alpha1_SetLibinputSettingRequest_TapButtonMap = { ---@field tap_drag_lock boolean? ---@field tap boolean? +---@class SetXcursorRequest +---@field theme string? +---@field size integer? + -- Process ---@class pinnacle.process.v0alpha1.SpawnRequest @@ -770,6 +774,13 @@ defs.pinnacle = { request = "pinnacle.input.v0alpha1.SetLibinputSettingRequest", response = "google.protobuf.Empty", }, + ---@type GrpcRequestArgs + SetXcursor = { + service = "pinnacle.input.v0alpha1.InputService", + method = "SetXcursor", + request = "pinnacle.input.v0alpha1.SetXcursorRequest", + response = "google.protobuf.Empty", + }, }, }, }, diff --git a/api/lua/pinnacle/input.lua b/api/lua/pinnacle/input.lua index 1f178a5..86f983d 100644 --- a/api/lua/pinnacle/input.lua +++ b/api/lua/pinnacle/input.lua @@ -352,4 +352,28 @@ function input.set_libinput_settings(settings) end end +---Sets the current xcursor theme. +--- +---Pinnacle reads `$XCURSOR_THEME` on startup to set the theme. +---This allows you to set it at runtime. +--- +---@param theme string +function input.set_xcursor_theme(theme) + client.unary_request(input_service.SetXcursor, { + theme = theme, + }) +end + +---Sets the current xcursor size. +--- +---Pinnacle reads `$XCURSOR_SIZE` on startup to set the cursor size. +---This allows you to set it at runtime. +--- +---@param size integer +function input.set_xcursor_size(size) + client.unary_request(input_service.SetXcursor, { + size = size, + }) +end + return input diff --git a/api/protocol/pinnacle/input/v0alpha1/input.proto b/api/protocol/pinnacle/input/v0alpha1/input.proto index 0ad65d3..a2e6ae3 100644 --- a/api/protocol/pinnacle/input/v0alpha1/input.proto +++ b/api/protocol/pinnacle/input/v0alpha1/input.proto @@ -143,6 +143,11 @@ message SetLibinputSettingRequest { } } +message SetXcursorRequest { + optional string theme = 1; + optional uint32 size = 2; +} + service InputService { rpc SetKeybind(SetKeybindRequest) returns (stream SetKeybindResponse); rpc SetMousebind(SetMousebindRequest) returns (stream SetMousebindResponse); @@ -153,4 +158,6 @@ service InputService { rpc SetRepeatRate(SetRepeatRateRequest) returns (google.protobuf.Empty); rpc SetLibinputSetting(SetLibinputSettingRequest) returns (google.protobuf.Empty); + + rpc SetXcursor(SetXcursorRequest) returns (google.protobuf.Empty); } diff --git a/api/rust/src/input.rs b/api/rust/src/input.rs index 0804499..ae80ada 100644 --- a/api/rust/src/input.rs +++ b/api/rust/src/input.rs @@ -16,7 +16,7 @@ use pinnacle_api_defs::pinnacle::input::{ input_service_client::InputServiceClient, set_libinput_setting_request::{CalibrationMatrix, Setting}, KeybindDescriptionsRequest, SetKeybindRequest, SetLibinputSettingRequest, - SetMousebindRequest, SetRepeatRateRequest, SetXkbConfigRequest, + SetMousebindRequest, SetRepeatRateRequest, SetXcursorRequest, SetXkbConfigRequest, }, }; use tokio::sync::mpsc::UnboundedSender; @@ -402,6 +402,46 @@ impl Input { })) .unwrap(); } + + /// Set the xcursor theme. + /// + /// Pinnacle reads `$XCURSOR_THEME` on startup to determine the theme. + /// This allows you to set it at runtime. + /// + /// # Examples + /// + /// ``` + /// input.set_xcursor_theme("Adwaita"); + /// ``` + pub fn set_xcursor_theme(&self, theme: impl ToString) { + let mut client = self.create_input_client(); + + block_on_tokio(client.set_xcursor(SetXcursorRequest { + theme: Some(theme.to_string()), + size: None, + })) + .unwrap(); + } + + /// Set the xcursor size. + /// + /// Pinnacle reads `$XCURSOR_SIZE` on startup to determine the cursor size. + /// This allows you to set it at runtime. + /// + /// # Examples + /// + /// ``` + /// input.set_xcursor_size(64); + /// ``` + pub fn set_xcursor_size(&self, size: u32) { + let mut client = self.create_input_client(); + + block_on_tokio(client.set_xcursor(SetXcursorRequest { + theme: None, + size: Some(size), + })) + .unwrap(); + } } /// A trait that designates anything that can be converted into a [`Keysym`]. diff --git a/src/api.rs b/src/api.rs index 33934a3..671587a 100644 --- a/src/api.rs +++ b/src/api.rs @@ -11,7 +11,7 @@ use pinnacle_api_defs::pinnacle::{ set_mousebind_request::MouseEdge, KeybindDescription, KeybindDescriptionsRequest, KeybindDescriptionsResponse, Modifier, SetKeybindRequest, SetKeybindResponse, SetLibinputSettingRequest, SetMousebindRequest, - SetMousebindResponse, SetRepeatRateRequest, SetXkbConfigRequest, + SetMousebindResponse, SetRepeatRateRequest, SetXcursorRequest, SetXkbConfigRequest, }, output::{ self, @@ -586,6 +586,31 @@ impl input_service_server::InputService for InputService { }) .await } + + async fn set_xcursor( + &self, + request: Request, + ) -> Result, Status> { + let request = request.into_inner(); + + let theme = request.theme; + let size = request.size; + + run_unary_no_response(&self.sender, move |state| { + if let Some(theme) = theme { + state.pinnacle.cursor_state.set_theme(&theme); + } + + if let Some(size) = size { + state.pinnacle.cursor_state.set_size(size); + } + + if let Some(output) = state.pinnacle.focused_output().cloned() { + state.schedule_render(&output) + } + }) + .await + } } pub struct ProcessService { diff --git a/src/cursor.rs b/src/cursor.rs index 5051d14..a17c6ae 100644 --- a/src/cursor.rs +++ b/src/cursor.rs @@ -32,20 +32,32 @@ pub struct CursorState { impl CursorState { pub fn new() -> Self { - let (theme, size) = load_xcursor_theme(); + let (theme, size) = load_xcursor_theme_from_env(); + + std::env::set_var("XCURSOR_THEME", &theme); + std::env::set_var("XCURSOR_SIZE", size.to_string()); Self { start_time: Instant::now(), current_cursor_image: CursorImageStatus::default_named(), - theme, + theme: CursorTheme::load(&theme), size, mem_buffer_cache: Default::default(), loaded_images: Default::default(), } } - pub fn set_theme_and_size(&mut self, theme: CursorTheme, size: u32) { - self.theme = theme; + pub fn set_theme(&mut self, theme: &str) { + std::env::set_var("XCURSOR_THEME", theme); + + self.theme = CursorTheme::load(theme); + self.mem_buffer_cache.clear(); + self.loaded_images.clear(); + } + + pub fn set_size(&mut self, size: u32) { + std::env::set_var("XCURSOR_SIZE", size.to_string()); + self.size = size; self.mem_buffer_cache.clear(); self.loaded_images.clear(); @@ -186,15 +198,17 @@ fn nearest_size_images(size: u32, images: &[Image]) -> impl Iterator (CursorTheme, u32) { +/// Loads a theme and size from $XCURSOR_THEME and $XCURSOR_SIZE. +/// +/// Defaults to "default" and 24 respectively. +fn load_xcursor_theme_from_env() -> (String, u32) { let theme = std::env::var("XCURSOR_THEME").unwrap_or_else(|_| "default".into()); let size = std::env::var("XCURSOR_SIZE") .ok() .and_then(|size| size.parse::().ok()) .unwrap_or(24); - (CursorTheme::load(&theme), size) + (theme, size) } /// Load xcursor images for the given theme and icon.