//! The overlay itself: a layer surface for the chrome, one subsurface per tile, //! and the keyboard. //! //! Scaling is the compositor's job. A tile attaches its capture buffer directly //! and wp_viewporter names the rectangle to fit it into, so nothing here touches //! a pixel of window content - only the background, selection and labels. use std::error::Error; use std::os::fd::AsFd; use wayland_client::protocol::{ wl_keyboard::{self, WlKeyboard}, wl_pointer::{self, WlPointer}, wl_seat::{self, WlSeat}, wl_shm, wl_surface::WlSurface, }; use wayland_client::{Connection, Dispatch, QueueHandle, WEnum}; use wayland_protocols_wlr::layer_shell::v1::client::{ zwlr_layer_shell_v1::Layer, zwlr_layer_surface_v1::{self, KeyboardInteractivity, ZwlrLayerSurfaceV1}, }; use wayland_protocols::wp::cursor_shape::v1::client::wp_cursor_shape_device_v1::Shape; use crate::app::{App, Ending}; use crate::shm; use crate::theme::{Rect, fit_centred}; // evdev keycodes: physical positions, so navigation works on any keyboard // layout without an xkb keymap. Reading typed characters would need one. const KEY_ESC: u32 = 1; const KEY_TAB: u32 = 15; const KEY_Q: u32 = 16; const KEY_ENTER: u32 = 28; const KEY_LEFTCTRL: u32 = 29; const KEY_LEFTSHIFT: u32 = 42; const KEY_RIGHTSHIFT: u32 = 54; const KEY_LEFTALT: u32 = 56; const KEY_KPENTER: u32 = 96; const KEY_RIGHTCTRL: u32 = 97; const KEY_RIGHTALT: u32 = 100; const KEY_HOME: u32 = 102; const KEY_UP: u32 = 103; const KEY_PGUP: u32 = 104; const KEY_LEFT: u32 = 105; const KEY_RIGHT: u32 = 106; const KEY_END: u32 = 107; const KEY_DOWN: u32 = 108; const KEY_PGDN: u32 = 109; const KEY_LEFTMETA: u32 = 125; const KEY_RIGHTMETA: u32 = 126; fn is_trigger_modifier(code: u32) -> bool { matches!( code, KEY_LEFTALT | KEY_RIGHTALT | KEY_LEFTMETA | KEY_RIGHTMETA | KEY_LEFTCTRL | KEY_RIGHTCTRL ) } /// Keys that should fire repeatedly while held. fn is_repeatable_key(code: u32) -> bool { matches!( code, KEY_TAB | KEY_RIGHT | KEY_LEFT | KEY_DOWN | KEY_UP | KEY_HOME | KEY_END | KEY_PGUP | KEY_PGDN ) } /// evdev button code, as wl_pointer reports it. const BTN_LEFT: u32 = 0x110; /// Where the pointer is: the surface it entered, and the position within it. /// Tiles are subsurfaces, so the surface alone usually names a tile; the /// position is only needed over the chrome around them. pub struct Hover { pub surface: WlSurface, pub x: f64, pub y: f64, } impl App { /// Map the overlay: a layer surface sized to hug the grid, plus the shm the /// chrome is painted into. pub fn show(&mut self, qh: &QueueHandle) -> Result<(), Box> { let (lw, lh) = (self.layout.width, self.layout.height); let surface = self.compositor.create_surface(qh, ()); // Map on the display the layout was sized against, not wherever the // compositor would otherwise put it. let output = self .outputs .iter() .find(|(_, name)| *name == self.output) .map(|(output, _)| output); let layer = self.layer_shell.get_layer_surface( &surface, output, Layer::Overlay, "wl-pick".to_string(), qh, (), ); layer.set_size(lw as u32, lh as u32); layer.set_keyboard_interactivity(KeyboardInteractivity::Exclusive); surface.set_buffer_scale(self.scale); surface.commit(); let (pw, ph) = (lw * self.scale, lh * self.scale); let len = shm::Chrome::slot_len(pw, ph) * shm::Chrome::SLOTS; let file = shm::memfd("wl-pick-chrome", len)?; let pool = self.shm.create_pool(file.as_fd(), len as i32, qh, ()); for slot in 0..shm::Chrome::SLOTS { self.chrome_buffers.push(pool.create_buffer( (slot * shm::Chrome::slot_len(pw, ph)) as i32, pw, ph, shm::Chrome::stride(pw), wl_shm::Format::Argb8888, qh, (), )); } pool.destroy(); self.chrome = Some(shm::Chrome::new(&file, pw, ph)?); if let (Some(mgr), Some(seat)) = (&self.inhibit_mgr, &self.seat) { self.inhibitor = Some(mgr.inhibit_shortcuts(&surface, seat, qh, ())); } self.surface = Some(surface); Ok(()) } /// Put every visible tile where the viewport says, and unmap the rest. /// /// Runs again after each scroll, so a tile scrolled off screen gets a null /// buffer - the way to hide a subsurface - rather than being left behind. /// Scaling stays the compositor's job: the capture buffer is attached as it /// is, and wp_viewporter names the rectangle to fit it into. pub fn sync_tiles(&mut self, qh: &QueueHandle) { let parent = self.surface.clone().expect("show() runs first"); let scroll = self.scroll; for i in 0..self.tiles.len() { let Some(box_) = self.layout.tile(i as i32, scroll) else { self.hide_tile(i); continue; }; if !self.tiles[i].ready { continue; } let (bw, bh) = self.tiles[i].size; let (fit_w, fit_h) = if self.tiles[i].rotated() { (bh as i32, bw as i32) } else { (bw as i32, bh as i32) }; let dst = fit_centred(fit_w, fit_h, box_); if self.tiles[i].surface.is_none() { let surface = self.compositor.create_surface(qh, ()); let subsurface = self.subcompositor.get_subsurface(&surface, &parent, qh, ()); let viewport = self.viewporter.get_viewport(&surface, qh, ()); // Tiles change independently of the chrome - a live frame // arrives whenever its window does - so they must not wait on a // parent commit. subsurface.set_desync(); // The capture protocol reports the transform the compositor // already applied to the buffer, which is exactly what this // request means, so it passes straight through. surface.set_buffer_transform(self.tiles[i].transform); let t = &mut self.tiles[i]; t.surface = Some(surface); t.subsurface = Some(subsurface); t.viewport = Some(viewport); } let t = &self.tiles[i]; let (surface, subsurface, viewport) = ( t.surface.clone().expect("just created"), t.subsurface.clone().expect("just created"), t.viewport.clone().expect("just created"), ); let slot = t.showing.expect("a ready tile has a slot"); subsurface.set_position(dst.x, dst.y); viewport.set_destination(dst.w, dst.h); surface.attach(Some(&t.slots[slot].buffer), 0, 0); surface.damage_buffer(0, 0, bw as i32, bh as i32); surface.commit(); } // Subsurface placement is *parent* state: it only takes effect when the // parent commits, desynced children included. parent.commit(); } /// Unmap a tile's subsurface by attaching nothing to it. fn hide_tile(&mut self, i: usize) { if let Some(surface) = self.tiles[i].surface.clone() { surface.attach(None, 0, 0); surface.commit(); } } /// Repaint background, selection highlight, labels and border. pub fn paint(&mut self) { let (scale, sel, scroll) = (self.scale, self.sel, self.scroll); // Gather geometry before borrowing the chrome and the labels together. let elem = self .layout .elem(sel as i32, scroll) .map(|r| r.scaled(scale)); let label_boxes: Vec<(usize, Rect)> = (0..self.tiles.len()) .filter_map(|i| { self.layout .label(i as i32, scroll) .map(|r| (i, r.scaled(scale))) }) .collect(); let bar = self .layout .scrollbar(scroll, (self.theme.margin / 3).max(2)) .map(|(track, thumb)| (track.scaled(scale), thumb.scaled(scale))); let t = &self.theme; let (bg, sel_bg, fg, sel_fg, border, border_px) = ( t.bg, t.sel_bg, t.fg, t.sel_fg, t.border, t.border_px * scale, ); let labels = self.labels.as_mut(); let Some(chrome) = self.chrome.as_mut() else { return; }; let slot = chrome.next_slot(); let (cw, ch) = (chrome.w, chrome.h); let mut p = chrome.painter(); p.fill(bg); // The selection fills the whole element box, padding included - the same // thing rofi's element background does. It can be scrolled out of sight. if let Some(elem) = elem { p.rect(elem, sel_bg); } if let Some(labels) = labels { for (i, at) in label_boxes { labels.draw(&mut p, i, at, if i == sel { sel_fg } else { fg }); } } // A scrollbar only appears when there is something to scroll, which // makes it a hint rather than furniture. if let Some((track, thumb)) = bar { p.rect(track, bg); p.rect(thumb, border); } p.frame(border_px, border); let surface = self.surface.clone().expect("show() runs first"); surface.attach(self.chrome_buffers.get(slot), 0, 0); surface.damage_buffer(0, 0, cw, ch); surface.commit(); } fn move_sel(&mut self, delta: i32, qh: &QueueHandle) { let n = self.tiles.len() as i32; if n == 0 { return; } self.select((self.sel as i32 + delta).rem_euclid(n) as usize, qh); } fn move_row(&mut self, rows: i32, qh: &QueueHandle) { let n = self.tiles.len() as i32; let target = self.sel as i32 + rows * self.layout.cols; if target >= 0 && target < n { self.select(target as usize, qh); } } /// Move the selection, scrolling the least that keeps it on screen. Every /// move goes through here, so the selection is never off-view and the /// subsurfaces always match the viewport. fn select(&mut self, i: usize, qh: &QueueHandle) { self.sel = i; let scroll = self.layout.reveal(i, self.scroll); if scroll != self.scroll { self.scroll = scroll; self.sync_tiles(qh); } self.paint(); } /// The tile under the pointer, if it is over one. A tile's own subsurface /// answers directly; over the parent surface - padding, labels, gaps - the /// layout is asked instead. fn tile_at_pointer(&self) -> Option { let hover = self.hover.as_ref()?; let on_tile = self .tiles .iter() .position(|t| t.surface.as_ref() == Some(&hover.surface)); on_tile.or_else(|| { (Some(&hover.surface) == self.surface.as_ref()) .then(|| self.layout.hit(hover.x as i32, hover.y as i32, self.scroll)) .flatten() }) } /// Press and release on the same tile picks it. Anywhere else - the margin, /// a gap, an empty cell of the last row - does nothing at all. fn click(&mut self, pressed: bool) { if pressed { self.pressed = self.tile_at_pointer(); return; } let released = self.tile_at_pointer(); if let Some(i) = self.pressed.take().filter(|i| Some(*i) == released) { self.picked = self.tiles.get(i).map(|t| t.target.clone()); self.ending = Ending::Picked; } } fn key(&mut self, code: u32, qh: &QueueHandle) { if is_trigger_modifier(code) { self.latched_modifiers.insert(code); } // Arm client-side repeat for navigation keys. if is_repeatable_key(code) { let delay = std::time::Duration::from_millis(self.repeat_delay_ms as u64); self.repeat_key = Some(code); self.repeat_next = Some(std::time::Instant::now() + delay); } else { // Non-repeating key clears any held repeat. self.repeat_key = None; self.repeat_next = None; } match code { KEY_LEFTSHIFT | KEY_RIGHTSHIFT => self.shift = true, KEY_ESC | KEY_Q => self.ending = Ending::Cancelled, KEY_ENTER | KEY_KPENTER => { self.picked = self.tiles.get(self.sel).map(|t| t.target.clone()); self.ending = Ending::Picked; } KEY_TAB if self.shift => self.move_sel(-1, qh), KEY_TAB | KEY_RIGHT => self.move_sel(1, qh), KEY_LEFT => self.move_sel(-1, qh), KEY_DOWN => self.move_sel(1, qh), KEY_UP => self.move_sel(-1, qh), KEY_HOME => self.select(0, qh), KEY_END => self.select(self.tiles.len().saturating_sub(1), qh), KEY_PGUP => self.move_row(-self.layout.visible_rows, qh), KEY_PGDN => self.move_row(self.layout.visible_rows, qh), _ => {} } } fn key_up(&mut self, code: u32) { if code == KEY_LEFTSHIFT || code == KEY_RIGHTSHIFT { self.shift = false; } // Clear repeat if this is the key that was held. if self.repeat_key == Some(code) { self.repeat_key = None; self.repeat_next = None; } if self.latched_modifiers.remove(&code) && self.latched_modifiers.is_empty() { if self.ending == Ending::Running { self.picked = self.tiles.get(self.sel).map(|t| t.target.clone()); self.ending = Ending::Picked; } } } /// Called by the main loop when the key-repeat timer fires. Fires the /// currently held navigation action, then arms the next repeat tick. pub fn fire_repeat(&mut self, qh: &QueueHandle) { let Some(code) = self.repeat_key else { return }; let rate = std::time::Duration::from_millis(self.repeat_rate_ms as u64); self.repeat_next = Some(std::time::Instant::now() + rate); // Re-run the navigation action without re-arming the delay. match code { KEY_TAB if self.shift => self.move_sel(-1, qh), KEY_TAB | KEY_RIGHT => self.move_sel(1, qh), KEY_LEFT => self.move_sel(-1, qh), KEY_DOWN => self.move_sel(1, qh), KEY_UP => self.move_sel(-1, qh), KEY_HOME => self.select(0, qh), KEY_END => self.select(self.tiles.len().saturating_sub(1), qh), KEY_PGUP => self.move_row(-self.layout.visible_rows, qh), KEY_PGDN => self.move_row(self.layout.visible_rows, qh), _ => {} } } fn keyboard_enter(&mut self, keys: Vec, _qh: &QueueHandle) { self.focused = true; let held_keys: Vec = keys .chunks_exact(4) .map(|chunk| u32::from_ne_bytes(chunk.try_into().unwrap())) .collect(); if held_keys .iter() .any(|&k| k == KEY_LEFTSHIFT || k == KEY_RIGHTSHIFT) { self.shift = true; } let held_modifiers: Vec = held_keys .iter() .copied() .filter(|&k| is_trigger_modifier(k)) .collect(); for &m in &held_modifiers { self.latched_modifiers.insert(m); } // If no modifier is held on enter, the modifier (and/or Tab) was // released before focus was acquired: commit selection immediately! if self.latched_modifiers.is_empty() { if self.ending == Ending::Running { self.picked = self.tiles.get(self.sel).map(|t| t.target.clone()); self.ending = Ending::Picked; } return; } // A modifier is held. If Tab is also held upon enter, arm key-repeat // immediately so holding Tab cycles through windows. if held_keys.iter().any(|&k| k == KEY_TAB) { let delay = std::time::Duration::from_millis(self.repeat_delay_ms as u64); self.repeat_key = Some(KEY_TAB); self.repeat_next = Some(std::time::Instant::now() + delay); } } } // --- event plumbing ------------------------------------------------------- impl Dispatch for App { fn event( app: &mut Self, layer: &ZwlrLayerSurfaceV1, event: zwlr_layer_surface_v1::Event, _: &(), _: &Connection, _: &QueueHandle, ) { match event { zwlr_layer_surface_v1::Event::Configure { serial, .. } => { layer.ack_configure(serial); app.configured = true; } zwlr_layer_surface_v1::Event::Closed => app.ending = Ending::Closed, _ => {} } } } impl Dispatch for App { fn event( app: &mut Self, seat: &WlSeat, event: wl_seat::Event, _: &(), _: &Connection, qh: &QueueHandle, ) { let wl_seat::Event::Capabilities { capabilities: WEnum::Value(caps), } = event else { return; }; if caps.contains(wl_seat::Capability::Keyboard) { seat.get_keyboard(qh, ()); } if caps.contains(wl_seat::Capability::Pointer) { let pointer = seat.get_pointer(qh, ()); app.cursor_device = app .cursor_shape .as_ref() .map(|mgr| mgr.get_pointer(&pointer, qh, ())); } } } impl Dispatch for App { fn event( app: &mut Self, _: &WlKeyboard, event: wl_keyboard::Event, _: &(), _: &Connection, qh: &QueueHandle, ) { match event { wl_keyboard::Event::Key { key, state, .. } => match state { WEnum::Value(wl_keyboard::KeyState::Pressed) => app.key(key, qh), WEnum::Value(wl_keyboard::KeyState::Released) => app.key_up(key), _ => {} }, // Store compositor key-repeat settings for our client-side timer. wl_keyboard::Event::RepeatInfo { rate, delay } => { // rate == 0 means repeat is disabled. if rate > 0 { app.repeat_rate_ms = (1000 / rate as u32).max(1); app.repeat_delay_ms = delay as u32; } else { app.repeat_key = None; app.repeat_next = None; app.repeat_delay_ms = 0; app.repeat_rate_ms = 0; } } // Focus is only tracked here. sway sends leave immediately // followed by enter on the same surface when the pointer crosses // it, so whether the grab is really gone is decided by the main // loop, once the event batch has been dispatched. wl_keyboard::Event::Enter { keys, .. } => app.keyboard_enter(keys, qh), wl_keyboard::Event::Leave { .. } => { app.focused = false; // Clear any held repeat - we no longer have the keyboard. app.repeat_key = None; app.repeat_next = None; } _ => {} } } } /// Hovering does not move the selection - that belongs to the keyboard - so the /// pointer only tracks where it is and what it clicked. Scrolling is a /// deliberate gesture, so that does move the selection. impl Dispatch for App { fn event( app: &mut Self, _: &WlPointer, event: wl_pointer::Event, _: &(), _: &Connection, qh: &QueueHandle, ) { match event { wl_pointer::Event::Enter { serial, surface, surface_x, surface_y, } => { // A client owns the cursor over its own surfaces; without this // the pointer keeps whatever shape the window below gave it. if let Some(device) = &app.cursor_device { device.set_shape(serial, Shape::Default); } app.hover = Some(Hover { surface, x: surface_x, y: surface_y, }); } wl_pointer::Event::Motion { surface_x, surface_y, .. } => { if let Some(hover) = app.hover.as_mut() { (hover.x, hover.y) = (surface_x, surface_y); } } wl_pointer::Event::Leave { .. } => { app.hover = None; app.pressed = None; } wl_pointer::Event::Button { button: BTN_LEFT, state: WEnum::Value(state), .. } => app.click(state == wl_pointer::ButtonState::Pressed), wl_pointer::Event::Axis { value, .. } => { app.move_sel(if value > 0.0 { 1 } else { -1 }, qh) } _ => {} } } }