Add wlgrid: a window-thumbnail grid overlay for wlroots

A switcher to replace a wlthumbs + rofi pipeline, with the same look
(gruvbox, ceil(sqrt(n)) columns capped at 4, 16:9 tiles, a yellow
selection filling the element padding) but no thumbnails anywhere.

Each window is captured straight into a wl_shm buffer that is handed to
its own wl_subsurface, with wp_viewporter giving the compositor the
rectangle to scale it into. So there is no PNG encode, no scaler, no
full-resolution bitmap in this process, and the capture buffers are never
even mapped here — the compositor writes those pages and samples them
again for display. Opens in ~65ms for 8 windows (55ms of which is the
compositor reading pixels back out of the GPU) and holds ~9MB of RSS.

All capture sessions are opened before a single roundtrip and every frame
goes in flight together, the same batching wlthumbs uses, because the
readback is bandwidth-bound rather than latency-bound.

sway remains the source of truth: the window list, the con_ids and the
focusing all come from its IPC socket, joined to the Wayland side by
foreign_toplevel_identifier. Navigation reads raw evdev keycodes so it is
layout-independent, which does mean virtual-keyboard clients that invent
their own keymap can't drive it; that resolves when filtering brings xkb.

Labels, type-to-filter and live previews are next.
This commit is contained in:
Milad Alizadeh
2026-08-23 10:07:09 +01:00
commit dbfc06519a
8 changed files with 1662 additions and 0 deletions
+811
View File
@@ -0,0 +1,811 @@
//! wlgrid shows a thumbnail grid of every open window as a layer-shell overlay
//! and focuses the one you pick. It replaces a wlthumbs + rofi pipeline, so it
//! keeps that pipeline's contract: sway owns the window list and the focusing,
//! and the look comes straight from the rofi theme (see theme.rs).
//!
//! The pixels never pass through this process. Each window is captured into an
//! shm buffer handed straight to a subsurface, with wp_viewporter telling the
//! compositor which rectangle to scale it into — so there is no thumbnail
//! encoding, no scaler, and no full-resolution image in our address space.
mod shm;
mod sway;
mod theme;
use std::error::Error;
use std::os::fd::AsFd;
use std::process::ExitCode;
use std::time::{Duration, Instant};
use wayland_client::globals::{GlobalList, GlobalListContents, registry_queue_init};
use wayland_client::protocol::{
wl_buffer::WlBuffer,
wl_compositor::WlCompositor,
wl_keyboard::{self, WlKeyboard},
wl_output,
wl_registry::WlRegistry,
wl_seat::{self, WlSeat},
wl_shm::{self, WlShm},
wl_shm_pool::WlShmPool,
wl_subcompositor::WlSubcompositor,
wl_subsurface::WlSubsurface,
wl_surface::WlSurface,
};
use wayland_client::{
Connection, Dispatch, EventQueue, Proxy, QueueHandle, WEnum, delegate_noop, event_created_child,
};
use wayland_protocols::ext::foreign_toplevel_list::v1::client::{
ext_foreign_toplevel_handle_v1::{self, ExtForeignToplevelHandleV1},
ext_foreign_toplevel_list_v1::{self, ExtForeignToplevelListV1},
};
use wayland_protocols::ext::image_capture_source::v1::client::{
ext_foreign_toplevel_image_capture_source_manager_v1::ExtForeignToplevelImageCaptureSourceManagerV1,
ext_image_capture_source_v1::ExtImageCaptureSourceV1,
};
use wayland_protocols::ext::image_copy_capture::v1::client::{
ext_image_copy_capture_frame_v1::{self, ExtImageCopyCaptureFrameV1},
ext_image_copy_capture_manager_v1::{self, ExtImageCopyCaptureManagerV1},
ext_image_copy_capture_session_v1::{self, ExtImageCopyCaptureSessionV1},
};
use wayland_protocols::wp::viewporter::client::{
wp_viewport::WpViewport, wp_viewporter::WpViewporter,
};
use wayland_protocols_wlr::layer_shell::v1::client::{
zwlr_layer_shell_v1::{Layer, ZwlrLayerShellV1},
zwlr_layer_surface_v1::{self, KeyboardInteractivity, ZwlrLayerSurfaceV1},
};
use theme::{Layout, Rect, Theme, fit_centred};
// evdev keycodes: physical positions, so navigation works on any keyboard layout
// without an xkb keymap. Typing (and therefore xkb) arrives with filtering.
const KEY_ESC: u32 = 1;
const KEY_TAB: u32 = 15;
const KEY_Q: u32 = 16;
const KEY_ENTER: u32 = 28;
const KEY_LEFTSHIFT: u32 = 42;
const KEY_RIGHTSHIFT: u32 = 54;
const KEY_KPENTER: u32 = 96;
const KEY_HOME: u32 = 102;
const KEY_UP: u32 = 103;
const KEY_LEFT: u32 = 105;
const KEY_RIGHT: u32 = 106;
const KEY_END: u32 = 107;
const KEY_DOWN: u32 = 108;
/// One window: its sway identity, its capture plumbing, and its subsurface.
#[allow(dead_code)] // `handle` is held to keep the toplevel alive
struct Tile {
win: sway::Win,
handle: Option<ExtForeignToplevelHandleV1>,
session: Option<ExtImageCopyCaptureSessionV1>,
frame: Option<ExtImageCopyCaptureFrameV1>,
buffer: Option<WlBuffer>,
formats: Vec<wl_shm::Format>,
format: Option<wl_shm::Format>,
/// Buffer size the session requires: the window's full resolution.
size: (u32, u32),
transform: wl_output::Transform,
offset: usize,
session_done: bool,
ready: bool,
failed: bool,
settled: bool,
surface: Option<WlSurface>,
subsurface: Option<WlSubsurface>,
viewport: Option<WpViewport>,
}
impl Tile {
fn new(win: sway::Win) -> Self {
Self {
win,
handle: None,
session: None,
frame: None,
buffer: None,
formats: Vec::new(),
format: None,
size: (0, 0),
transform: wl_output::Transform::Normal,
offset: 0,
session_done: false,
ready: false,
failed: false,
settled: false,
surface: None,
subsurface: None,
viewport: None,
}
}
fn bytes(&self) -> usize {
self.size.0 as usize * 4 * self.size.1 as usize
}
/// Whether the buffer's contents are turned on their side relative to the
/// window, which flips the aspect ratio we have to fit.
fn rotated(&self) -> bool {
use wl_output::Transform;
matches!(
self.transform,
Transform::_90 | Transform::_270 | Transform::Flipped90 | Transform::Flipped270
)
}
}
struct App {
compositor: WlCompositor,
subcompositor: WlSubcompositor,
shm: WlShm,
viewporter: WpViewporter,
layer_shell: ZwlrLayerShellV1,
copy_mgr: ExtImageCopyCaptureManagerV1,
src_mgr: ExtForeignToplevelImageCaptureSourceManagerV1,
/// Toplevel handles as the compositor announces them, paired with the
/// identifier that joins them to sway's tree.
toplevels: Vec<(ExtForeignToplevelHandleV1, String)>,
tiles: Vec<Tile>,
theme: Theme,
layout: Layout,
scale: i32,
sel: usize,
shift: bool,
surface: Option<WlSurface>,
chrome: Option<shm::Chrome>,
chrome_buffers: Vec<WlBuffer>,
configured: bool,
quit: bool,
activate: Option<i64>,
}
impl App {
fn new(
globals: &GlobalList,
qh: &QueueHandle<Self>,
wins: Vec<sway::Win>,
theme: Theme,
scale: i32,
) -> Result<Self, Box<dyn Error>> {
let layout = Layout::new(&theme, wins.len() as i32);
// Bind everything up front so a compositor missing a protocol fails
// here, with a name, rather than halfway through a capture.
let app = Self {
compositor: globals.bind(qh, 1..=6, ())?,
subcompositor: globals.bind(qh, 1..=1, ())?,
shm: globals.bind(qh, 1..=1, ())?,
viewporter: globals.bind(qh, 1..=1, ())?,
layer_shell: globals.bind(qh, 1..=5, ())?,
copy_mgr: globals.bind(qh, 1..=1, ())?,
src_mgr: globals.bind(qh, 1..=1, ())?,
toplevels: Vec::new(),
tiles: wins.into_iter().map(Tile::new).collect(),
theme,
layout,
scale,
sel: 0,
shift: false,
surface: None,
chrome: None,
chrome_buffers: Vec::new(),
configured: false,
quit: false,
activate: None,
};
let _: ExtForeignToplevelListV1 = globals.bind(qh, 1..=1, ())?;
let _: WlSeat = globals.bind(qh, 1..=7, ())?;
Ok(app)
}
/// Open one capture session per window whose toplevel we recognise. They are
/// all opened before a single roundtrip, so every session's buffer
/// constraints arrive together instead of costing a round trip each.
fn open_sessions(&mut self, qh: &QueueHandle<Self>) {
for (i, tile) in self.tiles.iter_mut().enumerate() {
let Some(handle) = self
.toplevels
.iter()
.find(|(_, id)| !id.is_empty() && *id == tile.win.ft_id)
.map(|(h, _)| h.clone())
else {
// No identifier match: the tile stays label-only, and must not
// be waited on.
tile.settled = true;
continue;
};
let source: ExtImageCaptureSourceV1 = self.src_mgr.create_source(&handle, qh, ());
tile.handle = Some(handle);
tile.session = Some(self.copy_mgr.create_session(
&source,
ext_image_copy_capture_manager_v1::Options::empty(),
qh,
i,
));
source.destroy();
}
}
/// Allocate one pool for every capture buffer and put all the frames in
/// flight at once: the compositor is bandwidth-bound reading pixels back, so
/// serialising the captures only adds latency.
fn start_captures(&mut self, qh: &QueueHandle<Self>) -> Result<(), Box<dyn Error>> {
const PAGE: usize = 4096;
let mut total = 0usize;
for tile in &mut self.tiles {
if tile.session.is_none() {
continue;
}
if !tile.session_done || tile.size.0 == 0 || tile.size.1 == 0 {
tile.settled = true;
continue;
}
// Any 32-bit format will do: we never read these pixels, we hand the
// buffer straight back for display, so byte order stays the
// compositor's business on both ends.
tile.format = tile
.formats
.iter()
.copied()
.find(|f| matches!(f, wl_shm::Format::Xrgb8888 | wl_shm::Format::Argb8888))
.or_else(|| tile.formats.first().copied());
if tile.format.is_none() {
tile.settled = true;
continue;
}
tile.offset = total;
total += tile.bytes().div_ceil(PAGE) * PAGE;
}
if total == 0 {
return Ok(());
}
// Note: no mmap. The compositor writes these pages and samples them
// again for display; mapping them here would only cost us the faults.
let file = shm::memfd("wlgrid-capture", total)?;
let pool = self.shm.create_pool(file.as_fd(), total as i32, qh, ());
for i in 0..self.tiles.len() {
let (w, h, format, offset) = {
let t = &self.tiles[i];
if t.settled || t.session.is_none() || t.format.is_none() {
continue;
}
(
t.size.0 as i32,
t.size.1 as i32,
t.format.unwrap(),
t.offset as i32,
)
};
let buffer = pool.create_buffer(offset, w, h, w * 4, format, qh, ());
let session = self.tiles[i].session.clone().unwrap();
let frame = session.create_frame(qh, i);
frame.attach_buffer(&buffer);
frame.damage_buffer(0, 0, w, h);
frame.capture();
let t = &mut self.tiles[i];
t.buffer = Some(buffer);
t.frame = Some(frame);
}
pool.destroy(); // the buffers keep the mapping alive
Ok(())
}
fn captures_settled(&self) -> bool {
self.tiles.iter().all(|t| t.settled)
}
/// Map the overlay: a layer surface sized to hug the grid, plus the shm the
/// chrome is painted into.
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, ());
let layer = self.layer_shell.get_layer_surface(
&surface,
None, // let the compositor place it on the active output
Layer::Overlay,
"wlgrid".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("wlgrid-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)?);
self.surface = Some(surface);
Ok(())
}
/// Attach each captured buffer to its own subsurface and let the compositor
/// scale it into the tile rectangle.
fn place_tiles(&mut self, qh: &QueueHandle<Self>) {
let parent = self.surface.clone().expect("show() runs first");
for i in 0..self.tiles.len() {
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, self.layout.tile(i as i32));
let surface = self.compositor.create_surface(qh, ());
let subsurface = self.subcompositor.get_subsurface(&surface, &parent, qh, ());
let viewport = self.viewporter.get_viewport(&surface, qh, ());
subsurface.set_position(dst.x, dst.y);
// Tiles change independently of the chrome (selection moves now,
// live frames later), 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 and the compositor un-rotates it.
surface.set_buffer_transform(self.tiles[i].transform);
viewport.set_destination(dst.w, dst.h);
surface.attach(self.tiles[i].buffer.as_ref(), 0, 0);
surface.damage_buffer(0, 0, bw as i32, bh as i32);
surface.commit();
let t = &mut self.tiles[i];
t.surface = Some(surface);
t.subsurface = Some(subsurface);
t.viewport = Some(viewport);
}
// Subsurface placement is *parent* state: it only takes effect when the
// parent commits, desynced children included.
parent.commit();
}
/// Repaint background, selection highlight and border.
fn paint(&mut self) {
let (theme, scale, sel) = (&self.theme, self.scale, self.sel);
let elem = self.layout.elem(sel as i32);
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(theme.bg);
// The selection fills the whole element box, padding included — the same
// thing rofi's element background does.
p.rect(
Rect {
x: elem.x * scale,
y: elem.y * scale,
w: elem.w * scale,
h: elem.h * scale,
},
theme.sel_bg,
);
p.frame(theme.border_px * scale, theme.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) {
let n = self.tiles.len() as i32;
if n == 0 {
return;
}
self.sel = (self.sel as i32 + delta).rem_euclid(n) as usize;
self.paint();
}
fn move_row(&mut self, rows: i32) {
let n = self.tiles.len() as i32;
let target = self.sel as i32 + rows * self.layout.cols;
if target >= 0 && target < n {
self.sel = target as usize;
self.paint();
}
}
fn key(&mut self, code: u32) {
match code {
KEY_LEFTSHIFT | KEY_RIGHTSHIFT => self.shift = true,
KEY_ESC | KEY_Q => self.quit = true,
KEY_ENTER | KEY_KPENTER => {
self.activate = self.tiles.get(self.sel).map(|t| t.win.con_id);
self.quit = true;
}
KEY_TAB if self.shift => self.move_sel(-1),
KEY_TAB | KEY_RIGHT => self.move_sel(1),
KEY_LEFT => self.move_sel(-1),
KEY_DOWN => self.move_row(1),
KEY_UP => self.move_row(-1),
KEY_HOME => {
self.sel = 0;
self.paint();
}
KEY_END => {
self.sel = self.tiles.len().saturating_sub(1);
self.paint();
}
_ => {}
}
}
}
/// Phase timings, printed with --verbose. Opening latency is the whole point of
/// this tool, so it stays measurable.
struct Phases {
on: bool,
last: Instant,
}
impl Phases {
fn new(on: bool) -> Self {
Self {
on,
last: Instant::now(),
}
}
fn mark(&mut self, label: &str) {
if self.on {
let now = Instant::now();
eprintln!(
"{label:<12} {:6.1}ms",
(now - self.last).as_secs_f64() * 1000.0
);
self.last = now;
}
}
}
fn pump(
queue: &mut EventQueue<App>,
app: &mut App,
done: impl Fn(&App) -> bool,
) -> Result<(), Box<dyn Error>> {
while !done(app) {
queue.blocking_dispatch(app)?;
}
Ok(())
}
struct Args {
print: bool,
verbose: bool,
timeout: Option<Duration>,
}
fn parse_args() -> Result<Args, String> {
let mut args = Args {
print: false,
verbose: false,
timeout: None,
};
let mut it = std::env::args().skip(1);
while let Some(arg) = it.next() {
match arg.as_str() {
"--print" => args.print = true,
"-v" | "--verbose" => args.verbose = true,
"--timeout" => {
let v = it.next().ok_or("--timeout needs seconds")?;
let secs: f64 = v.parse().map_err(|_| format!("bad --timeout: {v}"))?;
args.timeout = Some(Duration::from_secs_f64(secs));
}
"-h" | "--help" => {
println!("usage: wlgrid [--print] [--verbose] [--timeout SECS]");
std::process::exit(0);
}
other => return Err(format!("unknown argument: {other}")),
}
}
Ok(args)
}
fn main() -> ExitCode {
match run() {
Ok(code) => code,
Err(e) => {
eprintln!("wlgrid: {e}");
ExitCode::FAILURE
}
}
}
fn run() -> Result<ExitCode, Box<dyn Error>> {
let args = parse_args().map_err(|e| -> Box<dyn Error> { e.into() })?;
// An exclusive keyboard grab makes a hung overlay unusable, so keep an
// escape hatch that cannot itself deadlock.
if let Some(d) = args.timeout {
std::thread::spawn(move || {
std::thread::sleep(d);
eprintln!("wlgrid: timeout");
std::process::exit(2);
});
}
let mut phases = Phases::new(args.verbose);
let mut sway_conn = swayipc::Connection::new()?;
let wins = sway::windows(&mut sway_conn)?;
if wins.is_empty() {
return Ok(ExitCode::SUCCESS);
}
let scale = sway_conn
.get_outputs()?
.iter()
.filter(|o| o.active)
.map(|o| o.scale.unwrap_or(1.0).ceil() as i32)
.max()
.unwrap_or(1)
.max(1);
phases.mark("sway-tree");
let conn = Connection::connect_to_env()?;
let (globals, mut queue) = registry_queue_init::<App>(&conn)?;
let qh = queue.handle();
let mut app = App::new(&globals, &qh, wins, Theme::default(), scale)?;
// Two roundtrips: one for the toplevel list, one for each handle's state.
queue.roundtrip(&mut app)?;
queue.roundtrip(&mut app)?;
phases.mark("toplevels");
app.open_sessions(&qh);
queue.roundtrip(&mut app)?; // every session's constraints at once
phases.mark("constraints");
app.start_captures(&qh)?;
pump(&mut queue, &mut app, |a| a.captures_settled())?;
phases.mark("capture");
if args.verbose {
let ready = app.tiles.iter().filter(|t| t.ready).count();
let matched = app.tiles.iter().filter(|t| t.handle.is_some()).count();
eprintln!(
"wlgrid: {} window(s), {matched} matched, {ready} captured; \
grid {}x{}, surface {}x{} logical at scale {}",
app.tiles.len(),
app.layout.cols,
app.layout.rows,
app.layout.width,
app.layout.height,
app.scale,
);
}
app.show(&qh)?;
pump(&mut queue, &mut app, |a| a.configured)?;
app.paint();
app.place_tiles(&qh);
conn.flush()?;
phases.mark("mapped");
pump(&mut queue, &mut app, |a| a.quit)?;
if let Some(con_id) = app.activate {
if args.print {
println!("{con_id}");
} else {
sway::focus(&mut sway_conn, con_id)?;
}
}
Ok(ExitCode::SUCCESS)
}
// --- event plumbing -------------------------------------------------------
impl Dispatch<WlRegistry, GlobalListContents> for App {
fn event(
_: &mut Self,
_: &WlRegistry,
_: <WlRegistry as Proxy>::Event,
_: &GlobalListContents,
_: &Connection,
_: &QueueHandle<Self>,
) {
}
}
impl Dispatch<ExtForeignToplevelListV1, ()> for App {
fn event(
app: &mut Self,
_: &ExtForeignToplevelListV1,
event: ext_foreign_toplevel_list_v1::Event,
_: &(),
_: &Connection,
_: &QueueHandle<Self>,
) {
if let ext_foreign_toplevel_list_v1::Event::Toplevel { toplevel } = event {
app.toplevels.push((toplevel, String::new()));
}
}
event_created_child!(App, ExtForeignToplevelListV1, [
ext_foreign_toplevel_list_v1::EVT_TOPLEVEL_OPCODE => (ExtForeignToplevelHandleV1, ()),
]);
}
impl Dispatch<ExtForeignToplevelHandleV1, ()> for App {
fn event(
app: &mut Self,
handle: &ExtForeignToplevelHandleV1,
event: ext_foreign_toplevel_handle_v1::Event,
_: &(),
_: &Connection,
_: &QueueHandle<Self>,
) {
if let ext_foreign_toplevel_handle_v1::Event::Identifier { identifier } = event
&& let Some(entry) = app.toplevels.iter_mut().find(|(h, _)| h == handle)
{
entry.1 = identifier;
}
}
}
impl Dispatch<ExtImageCopyCaptureSessionV1, usize> for App {
fn event(
app: &mut Self,
_: &ExtImageCopyCaptureSessionV1,
event: ext_image_copy_capture_session_v1::Event,
&i: &usize,
_: &Connection,
_: &QueueHandle<Self>,
) {
let Some(tile) = app.tiles.get_mut(i) else {
return;
};
match event {
ext_image_copy_capture_session_v1::Event::BufferSize { width, height } => {
tile.size = (width, height)
}
ext_image_copy_capture_session_v1::Event::ShmFormat {
format: WEnum::Value(f),
} => tile.formats.push(f),
ext_image_copy_capture_session_v1::Event::Done => tile.session_done = true,
ext_image_copy_capture_session_v1::Event::Stopped => {
tile.failed = true;
tile.settled = true;
}
_ => {}
}
}
}
impl Dispatch<ExtImageCopyCaptureFrameV1, usize> for App {
fn event(
app: &mut Self,
_: &ExtImageCopyCaptureFrameV1,
event: ext_image_copy_capture_frame_v1::Event,
&i: &usize,
_: &Connection,
_: &QueueHandle<Self>,
) {
let Some(tile) = app.tiles.get_mut(i) else {
return;
};
match event {
ext_image_copy_capture_frame_v1::Event::Transform {
transform: WEnum::Value(t),
} => tile.transform = t,
ext_image_copy_capture_frame_v1::Event::Ready => {
tile.ready = true;
tile.settled = true;
// The protocol wants the frame destroyed once ready; the buffer
// stays ours to display.
if let Some(frame) = tile.frame.take() {
frame.destroy();
}
}
ext_image_copy_capture_frame_v1::Event::Failed { reason } => {
eprintln!(
"wlgrid: capture failed for {:?} ({reason:?})",
tile.win.title
);
tile.failed = true;
tile.settled = true;
if let Some(frame) = tile.frame.take() {
frame.destroy();
}
}
_ => {}
}
}
}
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.quit = true,
_ => {}
}
}
}
impl Dispatch<WlSeat, ()> for App {
fn event(
_: &mut Self,
seat: &WlSeat,
event: wl_seat::Event,
_: &(),
_: &Connection,
qh: &QueueHandle<Self>,
) {
if let wl_seat::Event::Capabilities {
capabilities: WEnum::Value(caps),
} = event
&& caps.contains(wl_seat::Capability::Keyboard)
{
seat.get_keyboard(qh, ());
}
}
}
impl Dispatch<WlKeyboard, ()> for App {
fn event(
app: &mut Self,
_: &WlKeyboard,
event: wl_keyboard::Event,
_: &(),
_: &Connection,
_: &QueueHandle<Self>,
) {
if let wl_keyboard::Event::Key { key, state, .. } = event {
match state {
WEnum::Value(wl_keyboard::KeyState::Pressed) => app.key(key),
WEnum::Value(wl_keyboard::KeyState::Released)
if key == KEY_LEFTSHIFT || key == KEY_RIGHTSHIFT =>
{
app.shift = false
}
_ => {}
}
}
}
}
// Interfaces we drive but never listen to.
delegate_noop!(App: WlCompositor);
delegate_noop!(App: WlSubcompositor);
delegate_noop!(App: WlSubsurface);
delegate_noop!(App: ignore WlShm);
delegate_noop!(App: WlShmPool);
delegate_noop!(App: WpViewporter);
delegate_noop!(App: WpViewport);
delegate_noop!(App: ZwlrLayerShellV1);
delegate_noop!(App: ExtImageCopyCaptureManagerV1);
delegate_noop!(App: ExtForeignToplevelImageCaptureSourceManagerV1);
delegate_noop!(App: ExtImageCaptureSourceV1);
delegate_noop!(App: ignore WlSurface);
delegate_noop!(App: ignore WlBuffer);