Skip to main content

wlr_capture/
overlay.rs

1//! Interactive frozen-screen overlay: a `wlr-layer-shell` surface per output showing
2//! that output's frozen capture, shared by several interactions ([`Mode`]):
3//! [`select_region`] (drag a rectangle), [`pick_point`] (the `wlr-peek` colour picker,
4//! with a loupe) and [`magnify`] (a full-screen zoom that follows the cursor).
5//! Coordinates are tracked in the global logical space so they span outputs. Rendered
6//! with egui on the shared [`render::Gpu`](crate::render::Gpu), like the chooser's
7//! overlay — one surface (and GL context) per output.
8
9use crate::capture::OutputCapture;
10use crate::render::Gpu;
11use crate::wl::Region;
12use anyhow::Result;
13use smithay_client_toolkit::{
14    compositor::{CompositorHandler, CompositorState},
15    delegate_compositor, delegate_keyboard, delegate_layer, delegate_output, delegate_pointer,
16    delegate_registry, delegate_seat,
17    output::{OutputHandler, OutputState},
18    registry::{ProvidesRegistryState, RegistryState},
19    registry_handlers,
20    seat::{
21        Capability, SeatHandler, SeatState,
22        keyboard::{KeyEvent, KeyboardHandler, Keysym, Modifiers, RawModifiers},
23        pointer::{PointerEvent, PointerEventKind, PointerHandler},
24    },
25    shell::{
26        WaylandSurface,
27        wlr_layer::{
28            Anchor, KeyboardInteractivity, Layer, LayerShell, LayerShellHandler, LayerSurface,
29            LayerSurfaceConfigure,
30        },
31    },
32};
33use wayland_client::{
34    Connection, QueueHandle,
35    globals::registry_queue_init,
36    protocol::{wl_keyboard, wl_output, wl_pointer, wl_seat, wl_surface},
37};
38
39const ACCENT: egui::Color32 = egui::Color32::from_rgb(0x4d, 0x9a, 0xff);
40
41/// What the frozen overlay collects from the user.
42#[derive(Clone, Copy, PartialEq, Eq)]
43pub enum Mode {
44    /// Drag a rectangle — the region selector ([`select_region`]).
45    Region,
46    /// Move and click to pick a single pixel — the colour picker ([`pick_point`]),
47    /// with a magnifying loupe and a live hex readout.
48    Point,
49    /// Magnify the frozen screen around the cursor ([`magnify`]); scroll to zoom,
50    /// `Esc` to quit. There is no selection to confirm.
51    Magnify,
52}
53
54/// The overlay's result, in global logical coordinates. Which variant comes back is
55/// determined by the [`Mode`] the overlay ran in.
56enum Outcome {
57    Region(Region),
58    Point { x: i32, y: i32 },
59}
60
61/// One output's overlay surface, its frozen backdrop, and its GL context.
62struct Surface {
63    layer: LayerSurface,
64    /// Output top-left in the global logical space (to map pointer ↔ selection).
65    logical_x: i32,
66    logical_y: i32,
67    egui_ctx: egui::Context,
68    gpu: Option<Gpu>,
69    tex: Option<egui::TextureHandle>,
70    /// Frozen pixels (RGBA). Uploaded to `tex` for the backdrop, and kept around so
71    /// the colour picker's loupe can sample exact pixel values.
72    rgba: Vec<u8>,
73    img_w: usize,
74    img_h: usize,
75    /// Configured logical size (points) and integer scale.
76    width: u32,
77    height: u32,
78    scale: u32,
79}
80
81impl Surface {
82    fn ensure_gpu(&mut self, conn: &Connection) {
83        if self.gpu.is_some() || self.width == 0 {
84            return;
85        }
86        let (pw, ph) = (
87            (self.width * self.scale) as i32,
88            (self.height * self.scale) as i32,
89        );
90        self.gpu = Some(Gpu::new(conn, self.layer.wl_surface(), pw, ph));
91    }
92
93    /// Render the frozen backdrop plus the mode-specific overlay (selection
94    /// rectangle, or crosshair + loupe), in global logical coordinates.
95    fn render(
96        &mut self,
97        conn: &Connection,
98        mode: Mode,
99        selection: Option<Region>,
100        pointer: Option<(f64, f64)>,
101        zoom: f32,
102    ) {
103        self.ensure_gpu(conn);
104        if self.width == 0 {
105            return;
106        }
107        if self.tex.is_none() {
108            let img =
109                egui::ColorImage::from_rgba_unmultiplied([self.img_w, self.img_h], &self.rgba);
110            self.tex = Some(self.egui_ctx.load_texture(
111                "frozen",
112                img,
113                egui::TextureOptions::LINEAR,
114            ));
115        }
116        let (pw, ph) = (self.width * self.scale, self.height * self.scale);
117        let raw_input = egui::RawInput {
118            screen_rect: Some(egui::Rect::from_min_size(
119                egui::Pos2::ZERO,
120                egui::vec2(self.width as f32, self.height as f32),
121            )),
122            ..Default::default()
123        };
124        let tex = self.tex.clone();
125        let (w, h) = (self.width as f32, self.height as f32);
126        let (lx, ly) = (self.logical_x, self.logical_y);
127        let (img_w, img_h) = (self.img_w, self.img_h);
128        let frozen: &[u8] = &self.rgba;
129        let Some(gpu) = self.gpu.as_mut() else {
130            return;
131        };
132        gpu.render(
133            &self.egui_ctx,
134            raw_input,
135            self.scale as f32,
136            (pw, ph),
137            [0.0, 0.0, 0.0, 1.0],
138            |ui, _importer| match mode {
139                Mode::Region => {
140                    draw_region_overlay(ui, tex.as_ref(), w, h, lx, ly, selection, pointer)
141                }
142                Mode::Point => draw_point_overlay(
143                    ui,
144                    tex.as_ref(),
145                    w,
146                    h,
147                    lx,
148                    ly,
149                    pointer,
150                    frozen,
151                    img_w,
152                    img_h,
153                ),
154                Mode::Magnify => {
155                    draw_magnify_overlay(ui, tex.as_ref(), w, h, lx, ly, pointer, zoom)
156                }
157            },
158        );
159        self.layer.commit();
160    }
161}
162
163/// Paint one surface: the frozen image, a dim veil, the bright selection and its
164/// outline + size label.
165#[allow(clippy::too_many_arguments)]
166fn draw_region_overlay(
167    ui: &mut egui::Ui,
168    tex: Option<&egui::TextureHandle>,
169    w: f32,
170    h: f32,
171    lx: i32,
172    ly: i32,
173    selection: Option<Region>,
174    pointer: Option<(f64, f64)>,
175) {
176    let full_uv = egui::Rect::from_min_max(egui::pos2(0.0, 0.0), egui::pos2(1.0, 1.0));
177    egui::CentralPanel::default()
178        .frame(egui::Frame::NONE)
179        .show_inside(ui, |ui| {
180            let p = ui.painter();
181            let screen = egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(w, h));
182            if let Some(t) = tex {
183                p.image(t.id(), screen, full_uv, egui::Color32::WHITE);
184            }
185
186            match selection {
187                None => {
188                    // Idle: a light veil makes it obvious the screen is frozen and
189                    // waiting for a selection.
190                    p.rect_filled(screen, 0.0, egui::Color32::from_black_alpha(48));
191                }
192                Some(sel) => {
193                    // Selection in this surface's local (point) coordinates.
194                    let local = egui::Rect::from_min_size(
195                        egui::pos2(sel.x as f32 - lx as f32, sel.y as f32 - ly as f32),
196                        egui::vec2(sel.w as f32, sel.h as f32),
197                    );
198                    // Dim everything, then restore the selected area at full brightness.
199                    p.rect_filled(screen, 0.0, egui::Color32::from_black_alpha(120));
200                    let vis = local.intersect(screen);
201                    if vis.width() > 0.5
202                        && vis.height() > 0.5
203                        && let Some(t) = tex
204                    {
205                        let uv = egui::Rect::from_min_max(
206                            egui::pos2(vis.min.x / w, vis.min.y / h),
207                            egui::pos2(vis.max.x / w, vis.max.y / h),
208                        );
209                        p.image(t.id(), vis, uv, egui::Color32::WHITE);
210                    }
211                    p.rect_stroke(
212                        local,
213                        0.0,
214                        egui::Stroke::new(2.0, ACCENT),
215                        egui::StrokeKind::Inside,
216                    );
217                    // Size label, once, on the surface holding the selection's top-left.
218                    if screen.contains(local.min) {
219                        p.text(
220                            local.min + egui::vec2(6.0, 6.0),
221                            egui::Align2::LEFT_TOP,
222                            format!("{} × {}", sel.w, sel.h),
223                            egui::FontId::monospace(13.0),
224                            egui::Color32::WHITE,
225                        );
226                    }
227                }
228            }
229
230            // Crosshair following the cursor on whichever surface holds it, plus an
231            // idle hint so the selection mode is obvious.
232            if let Some((gx, gy)) = pointer {
233                let (cx, cy) = (gx as f32 - lx as f32, gy as f32 - ly as f32);
234                if (0.0..=w).contains(&cx) && (0.0..=h).contains(&cy) {
235                    let col = egui::Color32::from_white_alpha(150);
236                    p.line_segment(
237                        [egui::pos2(0.0, cy), egui::pos2(w, cy)],
238                        egui::Stroke::new(1.0, col),
239                    );
240                    p.line_segment(
241                        [egui::pos2(cx, 0.0), egui::pos2(cx, h)],
242                        egui::Stroke::new(1.0, col),
243                    );
244                    if selection.is_none() {
245                        let galley = p.layout_no_wrap(
246                            crate::tr!("overlay-region-hint"),
247                            egui::FontId::proportional(14.0),
248                            egui::Color32::WHITE,
249                        );
250                        let at = egui::pos2(cx + 16.0, cy + 16.0);
251                        let bg = egui::Rect::from_min_size(
252                            at - egui::vec2(8.0, 5.0),
253                            galley.size() + egui::vec2(16.0, 10.0),
254                        );
255                        p.rect_filled(bg, 6.0, egui::Color32::from_black_alpha(200));
256                        p.galley(at, galley, egui::Color32::WHITE);
257                    }
258                }
259            }
260        });
261}
262
263/// Paint the colour-picker surface: the true-colour frozen image, a crosshair, and a
264/// magnifying loupe around the cursor showing individual pixels plus the hex value of
265/// the one under the centre. The loupe samples `frozen` (RGBA) directly so the zoom
266/// is pixel-exact regardless of the backdrop texture's filtering.
267#[allow(clippy::too_many_arguments)]
268fn draw_point_overlay(
269    ui: &mut egui::Ui,
270    tex: Option<&egui::TextureHandle>,
271    w: f32,
272    h: f32,
273    lx: i32,
274    ly: i32,
275    pointer: Option<(f64, f64)>,
276    frozen: &[u8],
277    img_w: usize,
278    img_h: usize,
279) {
280    let full_uv = egui::Rect::from_min_max(egui::pos2(0.0, 0.0), egui::pos2(1.0, 1.0));
281    egui::CentralPanel::default()
282        .frame(egui::Frame::NONE)
283        .show_inside(ui, |ui| {
284            let p = ui.painter();
285            let screen = egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(w, h));
286            if let Some(t) = tex {
287                p.image(t.id(), screen, full_uv, egui::Color32::WHITE);
288            }
289            let Some((gx, gy)) = pointer else { return };
290            let (cx, cy) = (gx as f32 - lx as f32, gy as f32 - ly as f32);
291            if !((0.0..=w).contains(&cx) && (0.0..=h).contains(&cy)) {
292                return;
293            }
294
295            // Map the cursor (local logical points) to a frozen image pixel, then a
296            // sampler over the surrounding pixels.
297            let px = ((cx / w) * img_w as f32).floor() as isize;
298            let py = ((cy / h) * img_h as f32).floor() as isize;
299            let sample = |ix: isize, iy: isize| -> Option<egui::Color32> {
300                if ix < 0 || iy < 0 || ix >= img_w as isize || iy >= img_h as isize {
301                    return None;
302                }
303                let idx = ((iy as usize) * img_w + ix as usize) * 4;
304                frozen
305                    .get(idx..idx + 3)
306                    .map(|c| egui::Color32::from_rgb(c[0], c[1], c[2]))
307            };
308
309            // Crosshair across the whole surface.
310            let col = egui::Color32::from_white_alpha(120);
311            p.line_segment(
312                [egui::pos2(0.0, cy), egui::pos2(w, cy)],
313                egui::Stroke::new(1.0, col),
314            );
315            p.line_segment(
316                [egui::pos2(cx, 0.0), egui::pos2(cx, h)],
317                egui::Stroke::new(1.0, col),
318            );
319
320            // Loupe: a (2R+1)² grid of source pixels magnified to Z×Z squares,
321            // offset from the cursor and flipped to stay on-screen.
322            const R: isize = 6;
323            const Z: f32 = 11.0;
324            let loupe = (2 * R + 1) as f32 * Z;
325            let mut origin = egui::pos2(cx + 20.0, cy + 20.0);
326            if origin.x + loupe > w {
327                origin.x = cx - 20.0 - loupe;
328            }
329            if origin.y + loupe + 26.0 > h {
330                origin.y = cy - 20.0 - loupe - 26.0;
331            }
332            origin.x = origin.x.clamp(2.0, (w - loupe).max(2.0));
333            origin.y = origin.y.clamp(2.0, (h - loupe - 26.0).max(2.0));
334
335            let frame = egui::Rect::from_min_size(origin, egui::vec2(loupe, loupe));
336            p.rect_filled(frame.expand(3.0), 4.0, egui::Color32::from_black_alpha(220));
337            for dy in -R..=R {
338                for dx in -R..=R {
339                    let c = sample(px + dx, py + dy).unwrap_or(egui::Color32::from_gray(20));
340                    let cell = egui::Rect::from_min_size(
341                        origin + egui::vec2((dx + R) as f32 * Z, (dy + R) as f32 * Z),
342                        egui::vec2(Z, Z),
343                    );
344                    p.rect_filled(cell, 0.0, c);
345                }
346            }
347            // Outline the centre cell (the pixel that will be picked).
348            let centre = egui::Rect::from_min_size(
349                origin + egui::vec2(R as f32 * Z, R as f32 * Z),
350                egui::vec2(Z, Z),
351            );
352            p.rect_stroke(
353                centre,
354                0.0,
355                egui::Stroke::new(1.5, ACCENT),
356                egui::StrokeKind::Outside,
357            );
358
359            // Hex readout + colour swatch under the loupe.
360            if let Some(c) = sample(px, py) {
361                let hex = format!("#{:02X}{:02X}{:02X}", c.r(), c.g(), c.b());
362                let at = egui::pos2(origin.x, origin.y + loupe + 4.0);
363                let galley =
364                    p.layout_no_wrap(hex, egui::FontId::monospace(15.0), egui::Color32::WHITE);
365                let bg = egui::Rect::from_min_size(
366                    at - egui::vec2(4.0, 2.0),
367                    galley.size() + egui::vec2(30.0, 6.0),
368                );
369                p.rect_filled(bg, 4.0, egui::Color32::from_black_alpha(220));
370                let sw = egui::Rect::from_min_size(
371                    egui::pos2(bg.max.x - 20.0, bg.min.y + 3.0),
372                    egui::vec2(14.0, bg.height() - 6.0),
373                );
374                p.rect_filled(sw, 2.0, c);
375                p.galley(at, galley, egui::Color32::WHITE);
376
377                // Hint below the readout.
378                let hint = p.layout_no_wrap(
379                    crate::tr!("overlay-pick-hint"),
380                    egui::FontId::proportional(12.0),
381                    egui::Color32::from_white_alpha(200),
382                );
383                let hat = egui::pos2(bg.min.x + 2.0, bg.max.y + 4.0);
384                let hbg = egui::Rect::from_min_size(
385                    hat - egui::vec2(4.0, 2.0),
386                    hint.size() + egui::vec2(8.0, 4.0),
387                );
388                p.rect_filled(hbg, 4.0, egui::Color32::from_black_alpha(200));
389                p.galley(hat, hint, egui::Color32::from_white_alpha(200));
390            }
391        });
392}
393
394/// Paint the magnifier surface: the frozen image zoomed `zoom`× around the cursor (the
395/// point under the cursor stays put), with a crosshair and a zoom + quit-hint readout.
396/// A surface not holding the cursor shows its frozen image at 1×.
397#[allow(clippy::too_many_arguments)]
398fn draw_magnify_overlay(
399    ui: &mut egui::Ui,
400    tex: Option<&egui::TextureHandle>,
401    w: f32,
402    h: f32,
403    lx: i32,
404    ly: i32,
405    pointer: Option<(f64, f64)>,
406    zoom: f32,
407) {
408    let full_uv = egui::Rect::from_min_max(egui::pos2(0.0, 0.0), egui::pos2(1.0, 1.0));
409    egui::CentralPanel::default()
410        .frame(egui::Frame::NONE)
411        .show_inside(ui, |ui| {
412            let p = ui.painter();
413            let screen = egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(w, h));
414            let Some(t) = tex else { return };
415
416            // Cursor in this surface's local coordinates, if it is here at all.
417            let here = pointer
418                .map(|(gx, gy)| (gx as f32 - lx as f32, gy as f32 - ly as f32))
419                .filter(|(cx, cy)| (0.0..=w).contains(cx) && (0.0..=h).contains(cy));
420            let Some((cx, cy)) = here else {
421                // Cursor elsewhere: show this output's frozen image at 1×.
422                p.image(t.id(), screen, full_uv, egui::Color32::WHITE);
423                return;
424            };
425
426            // Map the screen to a uv window so the point under the cursor stays put
427            // and everything around it is magnified `zoom`×.
428            let (cu, cv) = (cx / w, cy / h);
429            let uv = egui::Rect::from_min_max(
430                egui::pos2(cu - cx / (zoom * w), cv - cy / (zoom * h)),
431                egui::pos2(cu + (w - cx) / (zoom * w), cv + (h - cy) / (zoom * h)),
432            );
433            p.image(t.id(), screen, uv, egui::Color32::WHITE);
434
435            // Crosshair at the focus point.
436            let col = egui::Color32::from_white_alpha(120);
437            p.line_segment(
438                [egui::pos2(0.0, cy), egui::pos2(w, cy)],
439                egui::Stroke::new(1.0, col),
440            );
441            p.line_segment(
442                [egui::pos2(cx, 0.0), egui::pos2(cx, h)],
443                egui::Stroke::new(1.0, col),
444            );
445
446            // Zoom readout + quit hint, top-left.
447            let label = format!("{zoom:.1}×  ·  {}", crate::tr!("overlay-magnify-hint"));
448            let galley = p.layout_no_wrap(
449                label,
450                egui::FontId::proportional(13.0),
451                egui::Color32::WHITE,
452            );
453            let at = egui::pos2(14.0, 14.0);
454            let bg = egui::Rect::from_min_size(
455                at - egui::vec2(6.0, 4.0),
456                galley.size() + egui::vec2(12.0, 8.0),
457            );
458            p.rect_filled(bg, 6.0, egui::Color32::from_black_alpha(200));
459            p.galley(at, galley, egui::Color32::WHITE);
460        });
461}
462
463struct State {
464    registry_state: RegistryState,
465    seat_state: SeatState,
466    output_state: OutputState,
467    keyboard: Option<wl_keyboard::WlKeyboard>,
468    pointer: Option<wl_pointer::WlPointer>,
469    surfaces: Vec<Surface>,
470    /// Which interaction the overlay is running.
471    mode: Mode,
472
473    /// Live pointer position (global logical), for the crosshair + hint.
474    pointer_pos: Option<(f64, f64)>,
475    /// Drag anchor and current point, in global logical coordinates (region mode).
476    start: Option<(f64, f64)>,
477    cur: Option<(f64, f64)>,
478    /// Magnification factor (magnify mode); changed with the scroll wheel.
479    zoom: f32,
480    /// The overlay must be redrawn on all surfaces.
481    dirty: bool,
482    /// Resolved result (on confirm) and whether the loop should exit.
483    result: Option<Outcome>,
484    done: bool,
485}
486
487impl State {
488    /// Current selection rectangle (normalized) from the drag, if any.
489    fn selection(&self) -> Option<Region> {
490        let (sx, sy) = self.start?;
491        let (cx, cy) = self.cur?;
492        let (x0, y0) = (sx.min(cx), sy.min(cy));
493        let (x1, y1) = (sx.max(cx), sy.max(cy));
494        let r = Region {
495            x: x0.floor() as i32,
496            y: y0.floor() as i32,
497            w: (x1 - x0).round() as u32,
498            h: (y1 - y0).round() as u32,
499        };
500        (!r.is_empty()).then_some(r)
501    }
502
503    /// Map a pointer event's surface-local position to global logical coordinates.
504    fn to_global(&self, surface: &wl_surface::WlSurface, pos: (f64, f64)) -> Option<(f64, f64)> {
505        self.surfaces
506            .iter()
507            .find(|s| s.layer.wl_surface() == surface)
508            .map(|s| (s.logical_x as f64 + pos.0, s.logical_y as f64 + pos.1))
509    }
510
511    fn redraw_all(&mut self, conn: &Connection) {
512        let sel = self.selection();
513        let ptr = self.pointer_pos;
514        let mode = self.mode;
515        for s in &mut self.surfaces {
516            s.render(conn, mode, sel, ptr, self.zoom);
517        }
518    }
519}
520
521/// Drag a rectangle on a frozen overlay spanning every captured output; returns the
522/// chosen region (global logical coordinates) or `None` if cancelled (`Esc`).
523pub fn select_region(captures: &[OutputCapture]) -> Result<Option<Region>> {
524    let conn = Connection::connect_to_env()?;
525    select_region_on(&conn, captures)
526}
527
528/// [`select_region`] on a caller-provided connection. Use this to chain a second
529/// GPU/EGL overlay (e.g. a live mirror) in the *same* process: EGL caches its display
530/// by the `wl_display` pointer, so a second connection there can alias a freed one and
531/// fail (`eglCreateWindowSurface: BadAlloc`). Sharing one connection (one `EGLDisplay`)
532/// avoids it — the same way the per-output surfaces already share one here.
533pub fn select_region_on(conn: &Connection, captures: &[OutputCapture]) -> Result<Option<Region>> {
534    Ok(run(conn, captures, Mode::Region)?.map(|o| match o {
535        Outcome::Region(r) => r,
536        Outcome::Point { .. } => unreachable!("region mode yields a region"),
537    }))
538}
539
540/// Pick a single pixel on a frozen overlay (with a magnifying loupe); returns its
541/// position in global logical coordinates, or `None` if cancelled (`Esc`).
542pub fn pick_point(captures: &[OutputCapture]) -> Result<Option<(i32, i32)>> {
543    let conn = Connection::connect_to_env()?;
544    pick_point_on(&conn, captures)
545}
546
547/// [`pick_point`] on a caller-provided connection. Establish this connection *before*
548/// capturing: capture may open and drop a transient EGL connection (the GPU readback),
549/// and EGL caches its display by the `wl_display` pointer, so an overlay connection
550/// opened afterwards can alias the freed one and fail (`eglCreateWindowSurface:
551/// BadAlloc`). Creating the overlay connection first keeps its `EGLDisplay` valid.
552pub fn pick_point_on(conn: &Connection, captures: &[OutputCapture]) -> Result<Option<(i32, i32)>> {
553    Ok(run(conn, captures, Mode::Point)?.map(|o| match o {
554        Outcome::Point { x, y } => (x, y),
555        Outcome::Region(_) => unreachable!("point mode yields a point"),
556    }))
557}
558
559/// Magnify the frozen `captures` around the cursor: a full-screen zoom that pans as
560/// the pointer moves, scroll to change the zoom, `Esc` to quit. Returns when the user
561/// quits. Not live (the screen is frozen on entry) — a fullscreen live magnifier
562/// would capture its own output.
563pub fn magnify(captures: &[OutputCapture]) -> Result<()> {
564    let conn = Connection::connect_to_env()?;
565    magnify_on(&conn, captures)
566}
567
568/// [`magnify`] on a caller-provided connection. As with [`pick_point_on`], open this
569/// connection before capturing so the overlay's `EGLDisplay` can't alias a freed one.
570pub fn magnify_on(conn: &Connection, captures: &[OutputCapture]) -> Result<()> {
571    run(conn, captures, Mode::Magnify)?;
572    Ok(())
573}
574
575/// Run the frozen overlay over `captures` in the given [`Mode`] on `conn`; returns the
576/// user's choice or `None` if cancelled.
577fn run(conn: &Connection, captures: &[OutputCapture], mode: Mode) -> Result<Option<Outcome>> {
578    let (globals, mut queue) = registry_queue_init(conn)?;
579    let qh = queue.handle();
580
581    let compositor =
582        CompositorState::bind(&globals, &qh).map_err(|e| anyhow::anyhow!("wl_compositor: {e}"))?;
583    let layer_shell =
584        LayerShell::bind(&globals, &qh).map_err(|e| anyhow::anyhow!("layer-shell missing: {e}"))?;
585
586    let mut state = State {
587        registry_state: RegistryState::new(&globals),
588        seat_state: SeatState::new(&globals, &qh),
589        output_state: OutputState::new(&globals, &qh),
590        keyboard: None,
591        pointer: None,
592        surfaces: Vec::new(),
593        mode,
594        pointer_pos: None,
595        start: None,
596        cur: None,
597        zoom: 3.0,
598        dirty: false,
599        result: None,
600        done: false,
601    };
602
603    // Let outputs (and their logical geometry) come in, then build one overlay per
604    // output that we have a frozen capture for.
605    queue.roundtrip(&mut state)?;
606
607    let outputs: Vec<_> = state.output_state.outputs().collect();
608    for wl_out in outputs {
609        let Some(info) = state.output_state.info(&wl_out) else {
610            continue;
611        };
612        let Some(name) = info.name.clone() else {
613            continue;
614        };
615        let Some(cap) = captures.iter().find(|c| c.output.name == name) else {
616            continue;
617        };
618        let (lx, ly) = info
619            .logical_position
620            .unwrap_or((cap.output.logical_x, cap.output.logical_y));
621
622        let surface = compositor.create_surface(&qh);
623        let layer = layer_shell.create_layer_surface(
624            &qh,
625            surface,
626            Layer::Overlay,
627            Some("wlr-overlay"),
628            Some(&wl_out),
629        );
630        layer.set_anchor(Anchor::TOP | Anchor::BOTTOM | Anchor::LEFT | Anchor::RIGHT);
631        layer.set_keyboard_interactivity(KeyboardInteractivity::Exclusive);
632        layer.set_exclusive_zone(-1);
633        layer.commit();
634
635        let egui_ctx = egui::Context::default();
636        state.surfaces.push(Surface {
637            layer,
638            logical_x: lx,
639            logical_y: ly,
640            egui_ctx,
641            gpu: None,
642            tex: None,
643            rgba: cap.image.rgba.clone(),
644            img_w: cap.image.width as usize,
645            img_h: cap.image.height as usize,
646            width: 0,
647            height: 0,
648            scale: 1,
649        });
650    }
651
652    if state.surfaces.is_empty() {
653        anyhow::bail!("no output to select");
654    }
655
656    while !state.done {
657        queue.blocking_dispatch(&mut state)?;
658        if state.dirty {
659            state.dirty = false;
660            state.redraw_all(conn);
661        }
662    }
663
664    Ok(state.result)
665}
666
667impl CompositorHandler for State {
668    fn scale_factor_changed(
669        &mut self,
670        conn: &Connection,
671        _: &QueueHandle<Self>,
672        surface: &wl_surface::WlSurface,
673        new_factor: i32,
674    ) {
675        if let Some(s) = self
676            .surfaces
677            .iter_mut()
678            .find(|s| s.layer.wl_surface() == surface)
679        {
680            s.scale = new_factor.max(1) as u32;
681            s.layer.wl_surface().set_buffer_scale(new_factor.max(1));
682            if let (Some(gpu), true) = (s.gpu.as_ref(), s.width > 0) {
683                gpu.resize((s.width * s.scale) as i32, (s.height * s.scale) as i32);
684            }
685        }
686        let sel = self.selection();
687        let ptr = self.pointer_pos;
688        let mode = self.mode;
689        if let Some(s) = self
690            .surfaces
691            .iter_mut()
692            .find(|s| s.layer.wl_surface() == surface)
693        {
694            s.render(conn, mode, sel, ptr, self.zoom);
695        }
696    }
697
698    fn transform_changed(
699        &mut self,
700        _: &Connection,
701        _: &QueueHandle<Self>,
702        _: &wl_surface::WlSurface,
703        _: wl_output::Transform,
704    ) {
705    }
706
707    fn frame(
708        &mut self,
709        conn: &Connection,
710        _: &QueueHandle<Self>,
711        surface: &wl_surface::WlSurface,
712        _: u32,
713    ) {
714        let sel = self.selection();
715        let ptr = self.pointer_pos;
716        let mode = self.mode;
717        if let Some(s) = self
718            .surfaces
719            .iter_mut()
720            .find(|s| s.layer.wl_surface() == surface)
721        {
722            s.render(conn, mode, sel, ptr, self.zoom);
723        }
724    }
725
726    fn surface_enter(
727        &mut self,
728        _: &Connection,
729        _: &QueueHandle<Self>,
730        _: &wl_surface::WlSurface,
731        _: &wl_output::WlOutput,
732    ) {
733    }
734    fn surface_leave(
735        &mut self,
736        _: &Connection,
737        _: &QueueHandle<Self>,
738        _: &wl_surface::WlSurface,
739        _: &wl_output::WlOutput,
740    ) {
741    }
742}
743
744impl LayerShellHandler for State {
745    fn closed(&mut self, _: &Connection, _: &QueueHandle<Self>, _: &LayerSurface) {
746        self.done = true; // a surface went away: bail out (cancel)
747    }
748
749    fn configure(
750        &mut self,
751        conn: &Connection,
752        _: &QueueHandle<Self>,
753        layer: &LayerSurface,
754        configure: LayerSurfaceConfigure,
755        _: u32,
756    ) {
757        let (w, h) = configure.new_size;
758        let sel = self.selection();
759        let ptr = self.pointer_pos;
760        let mode = self.mode;
761        if let Some(s) = self.surfaces.iter_mut().find(|s| &s.layer == layer) {
762            if w > 0 && h > 0 {
763                s.width = w;
764                s.height = h;
765            }
766            if s.width == 0 {
767                return;
768            }
769            if let Some(gpu) = s.gpu.as_ref() {
770                gpu.resize((s.width * s.scale) as i32, (s.height * s.scale) as i32);
771            }
772            s.render(conn, mode, sel, ptr, self.zoom);
773        }
774    }
775}
776
777impl SeatHandler for State {
778    fn seat_state(&mut self) -> &mut SeatState {
779        &mut self.seat_state
780    }
781    fn new_seat(&mut self, _: &Connection, _: &QueueHandle<Self>, _: wl_seat::WlSeat) {}
782    fn new_capability(
783        &mut self,
784        _: &Connection,
785        qh: &QueueHandle<Self>,
786        seat: wl_seat::WlSeat,
787        cap: Capability,
788    ) {
789        if cap == Capability::Keyboard && self.keyboard.is_none() {
790            self.keyboard = self.seat_state.get_keyboard(qh, &seat, None).ok();
791        }
792        if cap == Capability::Pointer && self.pointer.is_none() {
793            self.pointer = self.seat_state.get_pointer(qh, &seat).ok();
794        }
795    }
796    fn remove_capability(
797        &mut self,
798        _: &Connection,
799        _: &QueueHandle<Self>,
800        _: wl_seat::WlSeat,
801        _: Capability,
802    ) {
803    }
804    fn remove_seat(&mut self, _: &Connection, _: &QueueHandle<Self>, _: wl_seat::WlSeat) {}
805}
806
807impl KeyboardHandler for State {
808    fn enter(
809        &mut self,
810        _: &Connection,
811        _: &QueueHandle<Self>,
812        _: &wl_keyboard::WlKeyboard,
813        _: &wl_surface::WlSurface,
814        _: u32,
815        _: &[u32],
816        _: &[Keysym],
817    ) {
818    }
819    fn leave(
820        &mut self,
821        _: &Connection,
822        _: &QueueHandle<Self>,
823        _: &wl_keyboard::WlKeyboard,
824        _: &wl_surface::WlSurface,
825        _: u32,
826    ) {
827    }
828    fn press_key(
829        &mut self,
830        _: &Connection,
831        _: &QueueHandle<Self>,
832        _: &wl_keyboard::WlKeyboard,
833        _: u32,
834        event: KeyEvent,
835    ) {
836        match event.keysym {
837            Keysym::Escape => {
838                self.result = None;
839                self.done = true;
840            }
841            Keysym::Return | Keysym::KP_Enter => match self.mode {
842                Mode::Region => {
843                    if let Some(r) = self.selection() {
844                        self.result = Some(Outcome::Region(r));
845                        self.done = true;
846                    }
847                }
848                Mode::Point => {
849                    if let Some((gx, gy)) = self.pointer_pos {
850                        self.result = Some(Outcome::Point {
851                            x: gx.round() as i32,
852                            y: gy.round() as i32,
853                        });
854                        self.done = true;
855                    }
856                }
857                // Nothing to confirm; quit on Esc only.
858                Mode::Magnify => {}
859            },
860            _ => {}
861        }
862    }
863    fn release_key(
864        &mut self,
865        _: &Connection,
866        _: &QueueHandle<Self>,
867        _: &wl_keyboard::WlKeyboard,
868        _: u32,
869        _: KeyEvent,
870    ) {
871    }
872    fn repeat_key(
873        &mut self,
874        _: &Connection,
875        _: &QueueHandle<Self>,
876        _: &wl_keyboard::WlKeyboard,
877        _: u32,
878        _: KeyEvent,
879    ) {
880    }
881    fn update_modifiers(
882        &mut self,
883        _: &Connection,
884        _: &QueueHandle<Self>,
885        _: &wl_keyboard::WlKeyboard,
886        _: u32,
887        _: Modifiers,
888        _: RawModifiers,
889        _: u32,
890    ) {
891    }
892}
893
894impl PointerHandler for State {
895    fn pointer_frame(
896        &mut self,
897        _: &Connection,
898        _: &QueueHandle<Self>,
899        _: &wl_pointer::WlPointer,
900        events: &[PointerEvent],
901    ) {
902        let mode = self.mode;
903        for e in events {
904            match e.kind {
905                PointerEventKind::Enter { .. } => {
906                    self.pointer_pos = self.to_global(&e.surface, e.position);
907                    self.dirty = true;
908                }
909                PointerEventKind::Leave { .. } => {
910                    self.pointer_pos = None;
911                    self.dirty = true;
912                }
913                PointerEventKind::Press { button: 0x110, .. } => {
914                    let g = self.to_global(&e.surface, e.position);
915                    match mode {
916                        Mode::Region => {
917                            if let Some(g) = g {
918                                self.start = Some(g);
919                                self.cur = Some(g);
920                                self.pointer_pos = Some(g);
921                                self.dirty = true;
922                            }
923                        }
924                        // Point mode: a click picks the pixel under the cursor.
925                        Mode::Point => {
926                            if let Some((gx, gy)) = g {
927                                self.result = Some(Outcome::Point {
928                                    x: gx.round() as i32,
929                                    y: gy.round() as i32,
930                                });
931                                self.done = true;
932                            }
933                        }
934                        Mode::Magnify => {}
935                    }
936                }
937                // Magnify mode: scroll wheel changes the zoom (scroll up = zoom in).
938                PointerEventKind::Axis { vertical, .. } if mode == Mode::Magnify => {
939                    let notches = if vertical.discrete != 0 {
940                        vertical.discrete as f32
941                    } else {
942                        (vertical.absolute / 15.0) as f32
943                    };
944                    if notches != 0.0 {
945                        // Wayland axis is positive downward; scrolling up zooms in.
946                        self.zoom = (self.zoom * 1.15_f32.powf(-notches)).clamp(1.5, 40.0);
947                        self.dirty = true;
948                    }
949                }
950                PointerEventKind::Motion { .. } => {
951                    // Track the cursor for the crosshair/loupe, and extend a drag.
952                    if let Some(g) = self.to_global(&e.surface, e.position) {
953                        self.pointer_pos = Some(g);
954                        if mode == Mode::Region && self.start.is_some() {
955                            self.cur = Some(g);
956                        }
957                        self.dirty = true;
958                    }
959                }
960                PointerEventKind::Release { button: 0x110, .. } if mode == Mode::Region => {
961                    if let Some(g) = self.to_global(&e.surface, e.position) {
962                        self.cur = Some(g);
963                    }
964                    if let Some(r) = self.selection() {
965                        self.result = Some(Outcome::Region(r));
966                        self.done = true;
967                    } else {
968                        // Empty (a click without a drag): reset, keep waiting.
969                        self.start = None;
970                        self.cur = None;
971                        self.dirty = true;
972                    }
973                }
974                _ => {}
975            }
976        }
977    }
978}
979
980impl OutputHandler for State {
981    fn output_state(&mut self) -> &mut OutputState {
982        &mut self.output_state
983    }
984    fn new_output(&mut self, _: &Connection, _: &QueueHandle<Self>, _: wl_output::WlOutput) {}
985    fn update_output(&mut self, _: &Connection, _: &QueueHandle<Self>, _: wl_output::WlOutput) {}
986    fn output_destroyed(&mut self, _: &Connection, _: &QueueHandle<Self>, _: wl_output::WlOutput) {}
987}
988
989impl ProvidesRegistryState for State {
990    fn registry(&mut self) -> &mut RegistryState {
991        &mut self.registry_state
992    }
993    registry_handlers![OutputState, SeatState];
994}
995
996delegate_compositor!(State);
997delegate_output!(State);
998delegate_seat!(State);
999delegate_keyboard!(State);
1000delegate_pointer!(State);
1001delegate_layer!(State);
1002delegate_registry!(State);