srdusr
aboutsummaryrefslogtreecommitdiffstats
path: root/crates/wayland/src/cursor.rs
diff options
context:
space:
mode:
authorsrdusr <[email protected]>2024-05-14 00:42:00 +0200
committersrdusr <[email protected]>2024-05-14 00:42:00 +0200
commitdd31bf5ac2a692617d478478d36c733586b93cf8 (patch)
tree2adb42ad8b24172cbbea3bf362a0d284acd54439 /crates/wayland/src/cursor.rs
parenta9dd8a6d4947cb537f7919c5a66ba4624af61800 (diff)
downloadsrdwm-dd31bf5ac2a692617d478478d36c733586b93cf8.tar.gz
srdwm-dd31bf5ac2a692617d478478d36c733586b93cf8.zip
Draw a cursor, handle the lid, and everything a real config needs
Groundwork for actually daily-driving this: porting the user's Hyprland config exposed what srdwm couldn't yet express, and testing on a bare TTY exposed something worse. A visible mouse cursor. Nothing drew a pointer at all - on a bare TTY the mouse was simply invisible. It hid because the nested backend runs inside another compositor, which draws a cursor over srdwm's window; only the DRM backend, i.e. the actual session path, was affected. A built-in arrow is now composited above everything on the output the pointer is on. It's a reviewable ASCII bitmap rather than an XCursor theme: a cursor that is always present beats a prettier one that sometimes isn't, the same reasoning as decoration.rs's font fallback. Client-set cursor surfaces and named shapes are still not rendered, so an app asking for an I-beam gets the arrow. Lid switch. libinput switch events are handled and surfaced to config as srd.on("lid_closed"/"lid_open", fn), so closing the lid can lock and suspend instead of doing nothing. Config-driven additions, each needed by a binding in the ported config and none of which existed: fullscreen (Window.fullscreen was a dead field -- declared, never read or written), directional window move that swaps with the neighbour and reorders the stack so tiling follows, focus cycling, modifier+drag to move/resize anywhere in a window rather than only by the titlebar, modifier+scroll to change workspace, and 8 XF86 media/power keysyms taken from the system's own XF86keysym.h. The keysym tables are hand-maintained in both directions and a key missing from either fails silently, so a round-trip test now covers every one the configs bind. Verified in the QEMU VM: a bare-TTY screendump shows a recognisable arrow at the pointer position (113 white fill + 58 black outline pixels at screen centre, where the pointer starts).
Diffstat (limited to 'crates/wayland/src/cursor.rs')
-rw-r--r--crates/wayland/src/cursor.rs171
1 files changed, 171 insertions, 0 deletions
diff --git a/crates/wayland/src/cursor.rs b/crates/wayland/src/cursor.rs
new file mode 100644
index 0000000..121e8e1
--- /dev/null
+++ b/crates/wayland/src/cursor.rs
@@ -0,0 +1,171 @@
+//! Mouse cursor rendering.
+//!
+//! The nested (winit) backend gets a cursor for free - the *host*
+//! compositor draws one over srdwm's window - which is exactly why this was
+//! missing for so long without being noticed. On a bare TTY nothing else is
+//! drawing anything, so without this the pointer is simply invisible: you
+//! can move it, click with it, and drag windows with it, but you cannot see
+//! where it is. That makes the udev backend unusable as a real session.
+//!
+//! **Scope, stated plainly:** this draws one built-in arrow, always. It
+//! honours `CursorImageStatus::Hidden` (so a client that hides the pointer
+//! still gets its way), but it does *not* yet render a client's own cursor
+//! surface or a named shape - an app asking for an I-beam or a resize arrow
+//! still sees this arrow. That is a real limitation, and a visible one over
+//! text fields; it is also strictly better than the previous behaviour of
+//! drawing nothing at all.
+//!
+//! The built-in arrow is deliberate rather than loading an XCursor theme:
+//! theme loading pulls in a dependency, needs a theme to actually be
+//! installed, and has a search-path fallback story of its own. A cursor that
+//! is always present beats a prettier one that sometimes isn't there - the
+//! same reasoning as `decoration.rs`'s font fallback.
+
+use smithay::utils::{Logical, Point};
+
+/// Side length of the built-in cursor bitmap, in pixels.
+pub(crate) const CURSOR_SIZE: i32 = 24;
+
+/// A classic left-pointing arrow: white fill, black outline, with the
+/// hotspot at (0, 0) - the tip.
+///
+/// Encoded as a small bitmap rather than drawn with geometry so the shape is
+/// obvious and reviewable: `.` transparent, `#` black outline, `*` white
+/// fill. 24 rows of 24 columns.
+const ARROW: [&str; CURSOR_SIZE as usize] = [
+ "#.......................",
+ "##......................",
+ "#*#.....................",
+ "#**#....................",
+ "#***#...................",
+ "#****#..................",
+ "#*****#.................",
+ "#******#................",
+ "#*******#...............",
+ "#********#..............",
+ "#*********#.............",
+ "#**********#............",
+ "#***********#...........",
+ "#************#..........",
+ "#*************#.........",
+ "#******####### .........",
+ "#***#**#................",
+ "#**#.#**#...............",
+ "#*#..#**#...............",
+ "##....#**#..............",
+ "#.....#**#..............",
+ ".......###..............",
+ "........................",
+ "........................",
+];
+
+/// Rasterises the built-in arrow as premultiplied ARGB8888, the format
+/// `MemoryRenderBuffer` expects.
+pub(crate) fn arrow_bitmap() -> Vec<u8> {
+ let mut buf = vec![0u8; (CURSOR_SIZE * CURSOR_SIZE * 4) as usize];
+ for (y, row) in ARROW.iter().enumerate() {
+ for (x, ch) in row.chars().enumerate() {
+ if x >= CURSOR_SIZE as usize || y >= CURSOR_SIZE as usize {
+ break;
+ }
+ // Premultiplied: opaque pixels only, so colour == colour * 1.
+ let (b, g, r, a) = match ch {
+ '#' => (0x00, 0x00, 0x00, 0xff),
+ '*' => (0xff, 0xff, 0xff, 0xff),
+ _ => continue,
+ };
+ let i = (y * CURSOR_SIZE as usize + x) * 4;
+ buf[i] = b;
+ buf[i + 1] = g;
+ buf[i + 2] = r;
+ buf[i + 3] = a;
+ }
+ }
+ buf
+}
+
+
+/// Render elements for the pointer, to be drawn above everything else.
+///
+/// `pos` is in the global space and `origin` is the output's origin, since
+/// each head renders in its own coordinate space.
+///
+/// Returns nothing when the cursor is hidden, or when the pointer is not
+/// over this output - otherwise every monitor would draw its own copy.
+pub(crate) fn render_elements<R>(
+ status: &smithay::input::pointer::CursorImageStatus,
+ buffer: &smithay::backend::renderer::element::memory::MemoryRenderBuffer,
+ renderer: &mut R,
+ pos: Point<f64, Logical>,
+ origin: Point<i32, Logical>,
+ size: (i32, i32),
+) -> Vec<smithay::backend::renderer::element::memory::MemoryRenderBufferRenderElement<R>>
+where
+ R: smithay::backend::renderer::Renderer + smithay::backend::renderer::ImportMem,
+ R::TextureId: Clone + Send + 'static,
+{
+ use smithay::backend::renderer::element::memory::MemoryRenderBufferRenderElement;
+ use smithay::backend::renderer::element::Kind;
+ use smithay::input::pointer::CursorImageStatus;
+
+ if matches!(status, CursorImageStatus::Hidden) {
+ return Vec::new();
+ }
+ // Only the output the pointer is actually on draws it.
+ let local = (pos.x as i32 - origin.x, pos.y as i32 - origin.y);
+ if local.0 < 0 || local.1 < 0 || local.0 >= size.0 || local.1 >= size.1 {
+ return Vec::new();
+ }
+
+ // Built-in arrow: hotspot is the tip, so no offset.
+ let at = (local.0 as f64, local.1 as f64);
+ match MemoryRenderBufferRenderElement::from_buffer(renderer, at, buffer, None, None, None, Kind::Cursor) {
+ Ok(e) => vec![e],
+ Err(e) => {
+ log::warn!("cursor: failed to import bitmap: {e}");
+ Vec::new()
+ }
+ }
+}
+
+/// The built-in arrow as an uploadable buffer. Built once at startup rather
+/// than per frame - the bitmap never changes.
+pub(crate) fn make_buffer() -> smithay::backend::renderer::element::memory::MemoryRenderBuffer {
+ use smithay::backend::allocator::Fourcc;
+ use smithay::backend::renderer::element::memory::MemoryRenderBuffer;
+ use smithay::utils::Transform;
+ MemoryRenderBuffer::from_slice(&arrow_bitmap(), Fourcc::Argb8888, (CURSOR_SIZE, CURSOR_SIZE), 1, Transform::Normal, None)
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn arrow_is_the_expected_size_and_has_an_opaque_tip() {
+ let buf = arrow_bitmap();
+ assert_eq!(buf.len(), (CURSOR_SIZE * CURSOR_SIZE * 4) as usize);
+ // The hotspot pixel (0,0) is the arrow's tip and must be visible,
+ // otherwise the cursor appears offset from where clicks land.
+ assert_eq!(buf[3], 0xff, "tip pixel must be opaque");
+ }
+
+ #[test]
+ fn arrow_has_both_outline_and_fill() {
+ let buf = arrow_bitmap();
+ let mut black = 0;
+ let mut white = 0;
+ for px in buf.chunks_exact(4) {
+ if px[3] == 0 {
+ continue;
+ }
+ if px[0] == 0 && px[1] == 0 && px[2] == 0 {
+ black += 1;
+ } else {
+ white += 1;
+ }
+ }
+ assert!(black > 20, "expected a black outline, got {black} px");
+ assert!(white > 40, "expected a white fill, got {white} px");
+ }
+}