diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 200af61..4da6b03 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -57,6 +57,24 @@ For testing, `foot` is installed as a small Wayland-native terminal for server-s sudo apt-get install -y foot ``` +Cursor assets are stored as standard Xcursor files under `assets/cursors/`, with +editable SVG/PNG/config sources under `assets/cursors/source/`. Install +`xcursorgen` from `x11-apps` and ImageMagick when editing cursor assets: + +```sh +sudo apt-get install -y x11-apps imagemagick +``` + +Regenerate the resize cursor assets from the workspace root with: + +```sh +magick assets/cursors/source/ns-resize.svg assets/cursors/source/ns-resize.png +magick assets/cursors/source/ew-resize.svg assets/cursors/source/ew-resize.png +magick assets/cursors/source/nwse-resize.svg assets/cursors/source/nwse-resize.png +magick assets/cursors/source/nesw-resize.svg assets/cursors/source/nesw-resize.png +(cd assets/cursors/source && xcursorgen ns-resize.cursor ../ns-resize && xcursorgen ew-resize.cursor ../ew-resize && xcursorgen nwse-resize.cursor ../nwse-resize && xcursorgen nesw-resize.cursor ../nesw-resize) +``` + ### Optional Test Apps Built-in test apps are gated behind the Cargo feature `test-apps` so normal compositor and shell builds do not require their dependencies. diff --git a/assets/cursors/ew-resize b/assets/cursors/ew-resize new file mode 100644 index 0000000000000000000000000000000000000000..fdf2ff5210ffec831576c6cff831ff821605872f GIT binary patch literal 2368 zcmaz@E-eyZ00Bk@Mg|53CWgQNB^VeOWEdD2RFL=}IgmIA^Dr{*~F)=Z* z|NsC0M-B-PA0&<}Mh+Wf2FNTBM%EAFgD^-QS)3F$NH53?7)I6$69ds8b;xqWut9o2 zdXX`*K4fta8>Ajt9v>T|7NiG+zkdA+qHtnld5}JQ=7QuvYG8()KYtz+r!X#7nt_4g zKS&)+5~Lqw4mJ#ugQ)}Q0qG?cgVe+Hg3Q5YK1?3uXJXAoRtM4tGXtCXAbVl*Aon2a zCBz1)hv@~GgUx)97)TDL4x|TUE{utLFVAYAT==c$bN>2A=Ak6Abt4E zMV14pMK%Y-MwSQhkugX;vOF|Oi@Sg-g zj*rB|#KefTf*kWfdXVj4U|{%D;Ob`v?!@>&024Qr)$b67I2!qUniG$c6F%Sls z334ll2Jx|Bm_BSYvU-rYAPf@&nTc*TNFIb?YCtqLb75j2d2AS$*)V;`cEH52(J-?? z?gWJ^h)>9Dm^m;zV0>&e$UG1RnGeFmnvKmYY+@j@K^SB<41??diGeW4jo9=ND+V$H pgkfgGXjoX$%6yPLAbUZW5<5WpiNWOBL5$fjHN=Mbs5;ys006ECH{$>R literal 0 HcmV?d00001 diff --git a/assets/cursors/ns-resize b/assets/cursors/ns-resize new file mode 100644 index 0000000000000000000000000000000000000000..a40fe71c91d6d127266c9ca4c4bc7f2a52820dcf GIT binary patch literal 2368 zcmaz@E-eyZ00Bk@Mg|53CWgQNB^VeOWEdD2RFL=}IgmIA^Dr3}b z{r~^}KZvHDsSuMuW`fLyVGti8LpcdD3uf-uuU}zIs`vxh?DOZ(|7T!e_z$9CmVo5R z^#MpP%p8!JAhTf@#D~d&)RAfjNDoXuC`@4HBGVvom>Q6JV(kE_h3P}4kuVG=A)}6hYvCvgoldxAh%+}*xZkAEXAP7Gx#}gVH&&*)TRp93&6IAoU=9q+pO9kX{f5dV<%7(mmD!ZKjaK>r>>B6> literal 0 HcmV?d00001 diff --git a/assets/cursors/nwse-resize b/assets/cursors/nwse-resize new file mode 100644 index 0000000000000000000000000000000000000000..c37a48ad10ef54b11785044486bf87f1235198f0 GIT binary patch literal 2368 zcmaz@E-eyZ00Bk@Mg|53CWgQNB^VeOWEdD2RFL=}IgmIA^Dr~rP@Sg-g zj?0NPUQA3(j1u!17#RMeW0>9KnhkOf%nWq1U%q?^mZHRLT;{J@wF+!Dh@iIlAhSV! z0ns4yL6{OdK>9%#WF{<3KxTvZAQ~hG!r1HpiGk!m;eZQ+DsUSj#U>;Rb!!XUFjm{{|$sl#Rm$ZQY>nGM3&^bsotGanWvAah|D tWHtyBYZkgXnAuP>aF$Q#`iS8pn+-A_A11~;bT#JIco$0syWEH{$>R literal 0 HcmV?d00001 diff --git a/assets/cursors/source/ew-resize.cursor b/assets/cursors/source/ew-resize.cursor new file mode 100644 index 0000000..fd29115 --- /dev/null +++ b/assets/cursors/source/ew-resize.cursor @@ -0,0 +1 @@ +24 12 12 ew-resize.png 0 diff --git a/assets/cursors/source/ew-resize.png b/assets/cursors/source/ew-resize.png new file mode 100644 index 0000000000000000000000000000000000000000..393bc016aef373e8678b7bc3412af6ab7d3e497f GIT binary patch literal 396 zcmeAS@N?(olHy`uVBq!ia0y~yV2}V|4mJh`h6m-gKNuJo6p}rHd>I(3)EF2VS{N99 zF)%PRykKA`HDF+PmB7GYHG_dcykO3*KpO@I2DT(`cNYdQ`02d6o`HcuwZt`|BqgyV z)hf9tHL)a>!N|bKOxM6%*T6W$(A3Jrz{+^6l#7a#l$cZ+lK2!_4{A4=cD9dy>*= + + + diff --git a/assets/cursors/source/nesw-resize.cursor b/assets/cursors/source/nesw-resize.cursor new file mode 100644 index 0000000..e712502 --- /dev/null +++ b/assets/cursors/source/nesw-resize.cursor @@ -0,0 +1 @@ +24 12 12 nesw-resize.png 0 diff --git a/assets/cursors/source/nesw-resize.png b/assets/cursors/source/nesw-resize.png new file mode 100644 index 0000000000000000000000000000000000000000..9b77cd4769ed9f3d84d511c63e0373c490de14b9 GIT binary patch literal 421 zcmeAS@N?(olHy`uVBq!ia0y~yV2}V|4mJh`h6m-gKNuJo6p}rHd>I(3)EF2VS{N99 zF)%PRykKA`HDF+PmB7GYHG_dcykO3*KpO@I2DT(`cNYdQ`02d6o`HcuwZt`|BqgyV z)hf9tHL)a>!N|bKOxM6%*T6W$(A3Jrz{3PGu3|@)&KXcY*o>8K6~#@YJ$=2FHa}ML^40P;&lJLQ2Bgqg_A+2 z_BQdzsZLsOZH<;q-t~?J6Kk5%16kKRklV9TQgj3R`7ljRh6{I9e*`LVKhk}9@M((v zSF;~e74oi|Fy8t+U3D7gk&UxwzBOh&yZ`@_*LxTaA3rPoW=iiNP!M{$`njxgN@xNA Dt5%S% literal 0 HcmV?d00001 diff --git a/assets/cursors/source/nesw-resize.svg b/assets/cursors/source/nesw-resize.svg new file mode 100644 index 0000000..9a956b7 --- /dev/null +++ b/assets/cursors/source/nesw-resize.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/cursors/source/ns-resize.cursor b/assets/cursors/source/ns-resize.cursor new file mode 100644 index 0000000..90dfd84 --- /dev/null +++ b/assets/cursors/source/ns-resize.cursor @@ -0,0 +1 @@ +24 12 12 ns-resize.png 0 diff --git a/assets/cursors/source/ns-resize.png b/assets/cursors/source/ns-resize.png new file mode 100644 index 0000000000000000000000000000000000000000..6e2c76977e9e93c035cc96816906d726e401fa4f GIT binary patch literal 420 zcmeAS@N?(olHy`uVBq!ia0y~yV2}V|4mJh`h6m-gKNuJo6p}rHd>I(3)EF2VS{N99 zF)%PRykKA`HDF+PmB7GYHG_dcykO3*KpO@I2DT(`cNYdQ`02d6o`HcuwZt`|BqgyV z)hf9tHL)a>!N|bKOxM6%*T6W$(A3Jrz{}(K|w~f6@Nk>wet+&*oc<*4Ble>dX~>f + + + diff --git a/assets/cursors/source/nwse-resize.cursor b/assets/cursors/source/nwse-resize.cursor new file mode 100644 index 0000000..5ffdb11 --- /dev/null +++ b/assets/cursors/source/nwse-resize.cursor @@ -0,0 +1 @@ +24 12 12 nwse-resize.png 0 diff --git a/assets/cursors/source/nwse-resize.png b/assets/cursors/source/nwse-resize.png new file mode 100644 index 0000000000000000000000000000000000000000..1f16903448388ee65c5822a2f7769a4151294653 GIT binary patch literal 429 zcmeAS@N?(olHy`uVBq!ia0y~yV2}V|4mJh`h6m-gKNuJo6p}rHd>I(3)EF2VS{N99 zF)%PRykKA`HDF+PmB7GYHG_dcykO3*KpO@I2DT(`cNYdQ`02d6o`HcuwZt`|BqgyV z)hf9tHL)a>!N|bKOxM6%*T6W$(A3Jrz{k~51x)V$!&dQo6j@jP^NB zz8tH%ckp~ruxFgIt%Ye-pbS%t@lnPVH~F8I-Y*U|_%h$(*9+DyKhBE_io7WW1*NB} KpUXO@geCw5ERUrC literal 0 HcmV?d00001 diff --git a/assets/cursors/source/nwse-resize.svg b/assets/cursors/source/nwse-resize.svg new file mode 100644 index 0000000..7315d0f --- /dev/null +++ b/assets/cursors/source/nwse-resize.svg @@ -0,0 +1,4 @@ + + + + diff --git a/crates/hearthspace-compositor/src/compositor/cursor.rs b/crates/hearthspace-compositor/src/compositor/cursor.rs index 917736d..a5a6aa9 100644 --- a/crates/hearthspace-compositor/src/compositor/cursor.rs +++ b/crates/hearthspace-compositor/src/compositor/cursor.rs @@ -30,6 +30,14 @@ pub(in crate::compositor) struct CursorImage { } const DEFAULT_CURSOR: &[u8] = include_bytes!("../../../../assets/cursors/default"); +#[cfg(feature = "udev")] +const NS_RESIZE_CURSOR: &[u8] = include_bytes!("../../../../assets/cursors/ns-resize"); +#[cfg(feature = "udev")] +const EW_RESIZE_CURSOR: &[u8] = include_bytes!("../../../../assets/cursors/ew-resize"); +#[cfg(feature = "udev")] +const NWSE_RESIZE_CURSOR: &[u8] = include_bytes!("../../../../assets/cursors/nwse-resize"); +#[cfg(feature = "udev")] +const NESW_RESIZE_CURSOR: &[u8] = include_bytes!("../../../../assets/cursors/nesw-resize"); const DESIRED_CURSOR_SIZE: u32 = 24; pub(super) fn standard_software_cursor() -> SoftwareCursor { @@ -52,13 +60,29 @@ pub(super) fn standard_software_cursor() -> SoftwareCursor { } } +#[cfg(feature = "udev")] +pub(in crate::compositor) fn cursor_image_for_icon(icon: CursorIcon) -> CursorImage { + match icon { + CursorIcon::Default => cursor_image_from_xcursor(DEFAULT_CURSOR), + CursorIcon::NsResize => cursor_image_from_xcursor(NS_RESIZE_CURSOR), + CursorIcon::EwResize => cursor_image_from_xcursor(EW_RESIZE_CURSOR), + CursorIcon::NwseResize => cursor_image_from_xcursor(NWSE_RESIZE_CURSOR), + CursorIcon::NeswResize => cursor_image_from_xcursor(NESW_RESIZE_CURSOR), + #[cfg(feature = "winit")] + _ => cursor_image_from_xcursor(DEFAULT_CURSOR), + } +} + pub(in crate::compositor) fn standard_cursor_image() -> CursorImage { - let images = - parse_xcursor(DEFAULT_CURSOR).expect("packaged default cursor must be valid Xcursor data"); + cursor_image_from_xcursor(DEFAULT_CURSOR) +} + +fn cursor_image_from_xcursor(contents: &[u8]) -> CursorImage { + let images = parse_xcursor(contents).expect("packaged cursor must be valid Xcursor data"); let image = images .iter() .min_by_key(|image| image.size.abs_diff(DESIRED_CURSOR_SIZE)) - .expect("packaged default cursor must contain at least one image"); + .expect("packaged cursor must contain at least one image"); let hotspot = ( i32::try_from(image.xhot).expect("cursor x hotspot must fit in i32"), diff --git a/crates/hearthspace-compositor/src/compositor/udev/device.rs b/crates/hearthspace-compositor/src/compositor/udev/device.rs index 9713395..dd9a215 100644 --- a/crates/hearthspace-compositor/src/compositor/udev/device.rs +++ b/crates/hearthspace-compositor/src/compositor/udev/device.rs @@ -26,7 +26,7 @@ use smithay::{ }; use tracing::{debug, error, info, warn}; -use super::super::cursor::{CursorImage, standard_cursor_image}; +use super::super::cursor::{CursorIcon, CursorImage, cursor_image_for_icon}; pub(super) struct UdevDevice { pub(super) path: PathBuf, @@ -63,6 +63,7 @@ pub(super) struct KmsOutputSurface { struct HardwareCursor { buffer: dumbbuffer::DumbBuffer, hotspot: (i32, i32), + icon: CursorIcon, } #[derive(Debug, Clone, PartialEq)] @@ -249,9 +250,16 @@ impl UdevDevice { } pub(super) fn update_hardware_cursor(&mut self, state: &super::super::App) { - let Some(cursor) = self.hardware_cursor.as_ref() else { + let Some(cursor) = self.hardware_cursor.as_mut() else { return; }; + if cursor.icon != state.cursor_icon { + update_hardware_cursor_image(&self.scanout_node, cursor, state.cursor_icon); + if let Some(crtc) = self.hardware_cursor_crtc { + set_legacy_cursor(&self.scanout_node, crtc, cursor); + } + self.hardware_cursor_position = None; + } if !self.hardware_cursor_initialized { clear_all_drm_cursor_planes(&self.scanout_node); @@ -444,7 +452,8 @@ fn clear_all_drm_cursor_planes(scanout_node: &ScanoutNode) { } fn create_hardware_cursor(scanout_node: &ScanoutNode) -> Option { - let image = standard_cursor_image(); + let icon = CursorIcon::Default; + let image = cursor_image_for_icon(icon); let width = cursor_capability(scanout_node, DriverCapability::CursorWidth) .unwrap_or(image.width) .max(image.width); @@ -485,7 +494,44 @@ fn create_hardware_cursor(scanout_node: &ScanoutNode) -> Option ?hotspot, "created DRM hardware cursor buffer" ); - Some(HardwareCursor { buffer, hotspot }) + Some(HardwareCursor { + buffer, + hotspot, + icon, + }) +} + +fn update_hardware_cursor_image( + scanout_node: &ScanoutNode, + cursor: &mut HardwareCursor, + icon: CursorIcon, +) { + let image = cursor_image_for_icon(icon); + let (width, height) = cursor.buffer.size(); + let pitch = match usize::try_from(cursor.buffer.pitch()) { + Ok(pitch) => pitch, + Err(error) => { + warn!(%error, "failed to read DRM hardware cursor pitch"); + return; + } + }; + let mut mapping = match scanout_node.drm_device.map_dumb_buffer(&mut cursor.buffer) { + Ok(mapping) => mapping, + Err(error) => { + warn!(%error, ?icon, "failed to map DRM hardware cursor buffer for icon update"); + return; + } + }; + mapping.fill(0); + copy_cursor_rgba_to_argb8888(&image, mapping.as_mut(), pitch, width, height); + drop(mapping); + + cursor.hotspot = ( + cmp::min(image.hotspot.0.max(0), width.saturating_sub(1) as i32), + cmp::min(image.hotspot.1.max(0), height.saturating_sub(1) as i32), + ); + cursor.icon = icon; + debug!(?icon, hotspot = ?cursor.hotspot, "updated DRM hardware cursor image"); } fn cursor_capability(scanout_node: &ScanoutNode, capability: DriverCapability) -> Option {