srdusr
aboutsummaryrefslogtreecommitdiffstats
path: root/crates/wayland/src
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
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')
-rw-r--r--crates/wayland/src/cursor.rs171
-rw-r--r--crates/wayland/src/input.rs67
-rw-r--r--crates/wayland/src/lib.rs1
-rw-r--r--crates/wayland/src/protocols.rs7
-rw-r--r--crates/wayland/src/state.rs7
-rw-r--r--crates/wayland/src/udev.rs44
-rw-r--r--crates/wayland/src/winit.rs2
7 files changed, 295 insertions, 4 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");
+ }
+}
diff --git a/crates/wayland/src/input.rs b/crates/wayland/src/input.rs
index 76607ee..6959c8f 100644
--- a/crates/wayland/src/input.rs
+++ b/crates/wayland/src/input.rs
@@ -20,6 +20,11 @@ use smithay::wayland::shell::wlr_layer::{KeyboardInteractivity, Layer};
use srdwm_core::{Event as CoreEvent, Modifiers, TitlebarHit, WindowId};
+/// Modifier that turns a drag anywhere in a window into move/resize.
+/// Matches the `SUPER` the shipped and ported configs use for
+/// `bindm ... movewindow` / `resizewindow`.
+const DRAG_MODIFIER: Modifiers = Modifiers::SUPER;
+
use crate::state::CompState;
pub(crate) fn last_pointer_pos(state: &CompState) -> Point<f64, Logical> {
@@ -134,6 +139,7 @@ pub(crate) fn focus_window(state: &mut CompState, id: WindowId) {
pub(crate) fn handle_pointer_button(state: &mut CompState, pos: Point<f64, Logical>, button: u32, pressed: bool, time: u32) {
const BTN_LEFT: u32 = 0x110;
+ const BTN_RIGHT: u32 = 0x111;
let serial = SERIAL_COUNTER.next_serial();
// Locked: forward the click to the lock surface (it may have a button or
@@ -146,6 +152,33 @@ pub(crate) fn handle_pointer_button(state: &mut CompState, pos: Point<f64, Logic
return;
}
+ // Modifier+drag: with the modifier held, dragging *anywhere* in a window
+ // moves it (left button) or resizes it from the nearest corner (right
+ // button) - the `bindm SUPER, mouse:272/273` gesture. Without this a
+ // window can only be moved by its titlebar, which is useless for
+ // windows that have none (fullscreen, CSD apps, layer surfaces).
+ //
+ // Checked before the titlebar hit-test so the modifier wins over the
+ // decoration: holding the modifier and grabbing the titlebar should
+ // still move, not press a titlebar button.
+ if pressed && (button == BTN_LEFT || button == BTN_RIGHT) {
+ let mods = state.seat.get_keyboard().map(|k| core_modifiers_from_xkb(&k.modifier_state()));
+ if mods.is_some_and(|m| m.contains(DRAG_MODIFIER)) {
+ let target = state.wm.borrow().window_at(pos.x as i32, pos.y as i32);
+ if let Some(id) = target {
+ focus_window(state, id);
+ let mut wm = state.wm.borrow_mut();
+ if button == BTN_LEFT {
+ wm.start_drag(id, pos.x as i32, pos.y as i32);
+ } else {
+ let edge = wm.nearest_corner(id, pos.x as i32, pos.y as i32);
+ wm.start_resize(id, edge, pos.x as i32, pos.y as i32);
+ }
+ return;
+ }
+ }
+ }
+
if pressed && button == BTN_LEFT {
let layer_hit = layer_surface_under(state, pos);
if let Some((surface, _)) = &layer_hit {
@@ -275,3 +308,37 @@ pub(crate) fn core_modifiers_from_xkb(mods: &smithay::input::keyboard::Modifiers
}
m
}
+
+/// Modifier+scroll cycles workspaces, consuming the event.
+///
+/// Returns `true` if it handled the scroll, in which case the caller must
+/// *not* also forward it to the client. Generic over the input backend for
+/// the same reason the keyboard handler is: both backends deliver scroll
+/// through smithay's `PointerAxisEvent` trait.
+pub(crate) fn handle_workspace_scroll<B, E>(state: &mut CompState, event: &E) -> bool
+where
+ B: smithay::backend::input::InputBackend,
+ E: smithay::backend::input::PointerAxisEvent<B>,
+{
+ use smithay::backend::input::Axis;
+
+ if state.lock.locked {
+ return false;
+ }
+ let mods = state.seat.get_keyboard().map(|k| core_modifiers_from_xkb(&k.modifier_state()));
+ if !mods.is_some_and(|m| m.contains(DRAG_MODIFIER)) {
+ return false;
+ }
+ let Some(v) = event.amount(Axis::Vertical).filter(|v| *v != 0.0) else { return false };
+
+ let mut wm = state.wm.borrow_mut();
+ let ids: Vec<_> = wm.workspaces().iter().map(|w| w.id).collect();
+ if ids.is_empty() {
+ return false;
+ }
+ let current = ids.iter().position(|&id| id == wm.current_workspace()).unwrap_or(0);
+ // Scrolling down (positive) advances, matching `workspace, e+1`.
+ let next = if v > 0.0 { (current + 1) % ids.len() } else { (current + ids.len() - 1) % ids.len() };
+ wm.switch_workspace(ids[next]);
+ true
+}
diff --git a/crates/wayland/src/lib.rs b/crates/wayland/src/lib.rs
index 053e6ad..001c803 100644
--- a/crates/wayland/src/lib.rs
+++ b/crates/wayland/src/lib.rs
@@ -36,6 +36,7 @@
//! - xdg-decoration is forced to server-side mode (`Mode::ServerSide`) so
//! well-behaved clients don't also draw their own client-side titlebar.
+mod cursor;
mod decoration;
mod input;
mod lock;
diff --git a/crates/wayland/src/protocols.rs b/crates/wayland/src/protocols.rs
index 582a622..f51b35d 100644
--- a/crates/wayland/src/protocols.rs
+++ b/crates/wayland/src/protocols.rs
@@ -131,7 +131,12 @@ impl SeatHandler for CompState {
}
fn focus_changed(&mut self, _seat: &Seat<Self>, _focused: Option<&WlSurface>) {}
- fn cursor_image(&mut self, _seat: &Seat<Self>, _image: CursorImageStatus) {}
+ /// Clients set their own cursor (an I-beam over text, a hand over a
+ /// link). Recorded here and drawn by the render paths - on a bare TTY
+ /// nothing else would draw it. See `cursor.rs`.
+ fn cursor_image(&mut self, _seat: &Seat<Self>, image: CursorImageStatus) {
+ self.cursor_status = image;
+ }
}
impl WlrLayerShellHandler for CompState {
diff --git a/crates/wayland/src/state.rs b/crates/wayland/src/state.rs
index 6bc6f1a..a1704cb 100644
--- a/crates/wayland/src/state.rs
+++ b/crates/wayland/src/state.rs
@@ -120,6 +120,13 @@ pub(crate) struct CompState {
/// content is never rendered and input never reaches normal clients --
/// see `SessionLockHandler` below.
pub(crate) lock: SessionLock,
+ /// What the pointer should look like, as set by the focused client (or
+ /// the default when no client has said). See `cursor.rs` for why this
+ /// has to be drawn by us on the DRM backend.
+ pub(crate) cursor_status: smithay::input::pointer::CursorImageStatus,
+ /// Bitmap for the built-in arrow, built once at startup rather than
+ /// per frame.
+ pub(crate) cursor_buffer: MemoryRenderBuffer,
pub(crate) wm: Rc<RefCell<WindowManager>>,
pub(crate) surface_to_id: HashMap<WlSurface, WindowId>,
pub(crate) id_to_window: HashMap<WindowId, DWindow>,
diff --git a/crates/wayland/src/udev.rs b/crates/wayland/src/udev.rs
index aca4865..30517f8 100644
--- a/crates/wayland/src/udev.rs
+++ b/crates/wayland/src/udev.rs
@@ -159,6 +159,9 @@ impl CompState {
// Drained before the `&mut self.udev` borrow below, so screencopy can
// be serviced with the renderer that borrow owns.
let mut captures = std::mem::take(&mut self.screencopy_pending);
+ // Same reason: the cursor needs the renderer that borrow owns.
+ let cursor_status = self.cursor_status.clone();
+ let cursor_buffer = self.cursor_buffer.clone();
// Which heads are eligible, and what each needs, gathered before the
// mutable borrow of `self.udev`.
@@ -196,6 +199,19 @@ impl CompState {
let mut custom_elements: Vec<MemoryRenderBufferRenderElement<PixmanRenderer>> = Vec::new();
if !locked {
+ // Cursor first: `render_output` draws custom elements
+ // front-to-back, so the earliest element is topmost. On a
+ // bare TTY nothing else draws a pointer - see `cursor.rs`.
+ let pointer_pos = udev.pointer_pos;
+ let hsize = udev.heads[index].size;
+ custom_elements.extend(crate::cursor::render_elements(
+ &cursor_status,
+ &cursor_buffer,
+ &mut udev.renderer,
+ pointer_pos,
+ origin,
+ hsize,
+ ));
for (geom, deco) in &decorations {
let pos = ((geom.x - origin.x) as f64, (geom.y - origin.y) as f64);
match MemoryRenderBufferRenderElement::from_buffer(&mut udev.renderer, pos, deco, None, None, None, Kind::Unspecified) {
@@ -538,6 +554,8 @@ impl UdevPlatform {
_screencopy_state: crate::screencopy::ScreencopyState::new::<CompState>(&display_handle),
screencopy_pending: Vec::new(),
lock: Default::default(),
+ cursor_status: smithay::input::pointer::CursorImageStatus::default_named(),
+ cursor_buffer: crate::cursor::make_buffer(),
wm: wm.clone(),
surface_to_id: HashMap::new(),
id_to_window: HashMap::new(),
@@ -832,10 +850,30 @@ fn handle_libinput_event(state: &mut CompState, event: InputEvent<LibinputInputB
let pressed = event.state() == BackendButtonState::Pressed;
handle_pointer_button(state, pos, button, pressed, event.time_msec());
}
+ // Laptop lid. libinput reports this as a switch toggle; without
+ // handling it, closing the lid does nothing at all - no lock, no
+ // suspend - which is a genuine problem on a laptop rather than a
+ // missing nicety.
+ InputEvent::SwitchToggle { event } => {
+ // Fully qualified: libinput's own `Switch` is also in scope here.
+ use smithay::backend::input::{SwitchState, SwitchToggleEvent};
+ if matches!(event.switch(), Some(smithay::reexports::input::event::switch::Switch::Lid)) {
+ let closed = event.state() == SwitchState::On;
+ log::info!("lid {}", if closed { "closed" } else { "opened" });
+ state.pending.borrow_mut().push(CoreEvent::LidSwitch { closed });
+ }
+ }
InputEvent::PointerAxis { event } => {
- // Scroll: forwarded to the focused client via the pointer axis
- // frame, no WM-level handling (matches the winit backend, which
- // doesn't handle scroll either).
+ // Modifier+scroll switches workspace instead of reaching the
+ // client - the `bind = SUPER, mouse_down/up, workspace, e+1/e-1`
+ // gesture. Checked first so the client never sees these events;
+ // forwarding them too would scroll the window under the cursor
+ // as a side effect of changing workspace.
+ if crate::input::handle_workspace_scroll(state, &event) {
+ return;
+ }
+ // Otherwise: forwarded to the focused client via the pointer axis
+ // frame, no WM-level handling.
let Some(pointer) = state.seat.get_pointer() else { return };
let source = event.source();
let mut frame = AxisFrame::new(event.time_msec()).source(source);
diff --git a/crates/wayland/src/winit.rs b/crates/wayland/src/winit.rs
index 85be223..b21ca06 100644
--- a/crates/wayland/src/winit.rs
+++ b/crates/wayland/src/winit.rs
@@ -130,6 +130,8 @@ impl WaylandPlatform {
_screencopy_state: screencopy::ScreencopyState::new::<CompState>(&dh),
screencopy_pending: Vec::new(),
lock: SessionLock::default(),
+ cursor_status: smithay::input::pointer::CursorImageStatus::default_named(),
+ cursor_buffer: crate::cursor::make_buffer(),
wm: wm.clone(),
surface_to_id: HashMap::new(),
id_to_window: HashMap::new(),