diff options
| author | srdusr <[email protected]> | 2024-05-14 00:42:00 +0200 |
|---|---|---|
| committer | srdusr <[email protected]> | 2024-05-14 00:42:00 +0200 |
| commit | dd31bf5ac2a692617d478478d36c733586b93cf8 (patch) | |
| tree | 2adb42ad8b24172cbbea3bf362a0d284acd54439 /crates/wayland/src/cursor.rs | |
| parent | a9dd8a6d4947cb537f7919c5a66ba4624af61800 (diff) | |
| download | srdwm-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.rs | 171 |
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"); + } +} |