Files
wl-tab/src/overlay.rs
T
2026-09-10 07:33:57 +02:00

590 lines
21 KiB
Rust

//! 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<Self>) -> Result<(), Box<dyn Error>> {
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<Self>) {
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<Self>) {
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<Self>) {
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>) {
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<usize> {
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<Self>) {
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<Self>) {
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<u8>, _qh: &QueueHandle<Self>) {
self.focused = true;
let held_keys: Vec<u32> = 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<u32> = 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<ZwlrLayerSurfaceV1, ()> for App {
fn event(
app: &mut Self,
layer: &ZwlrLayerSurfaceV1,
event: zwlr_layer_surface_v1::Event,
_: &(),
_: &Connection,
_: &QueueHandle<Self>,
) {
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<WlSeat, ()> for App {
fn event(
app: &mut Self,
seat: &WlSeat,
event: wl_seat::Event,
_: &(),
_: &Connection,
qh: &QueueHandle<Self>,
) {
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<WlKeyboard, ()> for App {
fn event(
app: &mut Self,
_: &WlKeyboard,
event: wl_keyboard::Event,
_: &(),
_: &Connection,
qh: &QueueHandle<Self>,
) {
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<WlPointer, ()> for App {
fn event(
app: &mut Self,
_: &WlPointer,
event: wl_pointer::Event,
_: &(),
_: &Connection,
qh: &QueueHandle<Self>,
) {
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)
}
_ => {}
}
}
}