Skip to main content

halley_capit/
screencopy.rs

1use std::fs::File;
2use std::os::fd::AsFd;
3use std::path::{Path, PathBuf};
4
5use image::codecs::png::{CompressionType, FilterType, PngEncoder};
6use image::{ExtendedColorType, ImageEncoder, Rgba, RgbaImage, imageops};
7use memmap2::MmapMut;
8use smithay_client_toolkit::{
9    delegate_output, delegate_registry,
10    output::{OutputHandler, OutputState},
11    registry::{ProvidesRegistryState, RegistryState},
12    registry_handlers,
13};
14use tempfile::tempfile;
15use wayland_client::{
16    Connection, Dispatch, Proxy, QueueHandle, WEnum,
17    globals::registry_queue_init,
18    protocol::{wl_buffer, wl_output, wl_shm, wl_shm_pool},
19};
20use wayland_protocols_wlr::screencopy::v1::client::{
21    zwlr_screencopy_frame_v1, zwlr_screencopy_manager_v1,
22};
23
24use crate::capture::{CaptureCrop, ensure_parent_dir, temp_output_path};
25
26#[derive(Clone, Debug)]
27pub struct CaptureOutputInfo {
28    pub name: Option<String>,
29    pub x: i32,
30    pub y: i32,
31    pub width: i32,
32    pub height: i32,
33    pub scale: i32,
34}
35
36pub fn capture_desktop_to_temp_file(final_out_path: &Path) -> Result<PathBuf, String> {
37    let (conn, mut queue, qh, mut app) = connect_capture_app()?;
38    let outputs = app.capture_outputs();
39    if outputs.is_empty() {
40        return Err("no outputs available for capture".into());
41    }
42    let mut captures = Vec::new();
43    for (wl_output, info) in outputs {
44        captures.push(capture_single_output(
45            &conn, &mut queue, &qh, &mut app, &wl_output, info,
46        )?);
47    }
48    let desktop = stitch_logical_desktop(&captures)?;
49    let tmp_out = temp_output_path(final_out_path);
50    save_rgba_png(&desktop, &tmp_out)?;
51    Ok(tmp_out)
52}
53
54pub fn capture_crop_to_png(final_out_path: &Path, crop: CaptureCrop) -> Result<(), String> {
55    let (conn, mut queue, qh, mut app) = connect_capture_app()?;
56    let outputs = app.capture_outputs();
57    if outputs.is_empty() {
58        return Err("no outputs available for capture".into());
59    }
60
61    let output_infos = outputs
62        .iter()
63        .map(|(_, info)| info.clone())
64        .collect::<Vec<_>>();
65    let crop = clamp_crop_to_output_bounds(crop, &output_infos)?;
66
67    let mut captures = Vec::new();
68    for (wl_output, info) in outputs {
69        if !output_intersects_crop(&info, crop) {
70            continue;
71        }
72        captures.push(capture_single_output(
73            &conn, &mut queue, &qh, &mut app, &wl_output, info,
74        )?);
75    }
76    if captures.is_empty() {
77        return Err("no outputs intersect the requested capture crop".into());
78    }
79
80    let image = render_logical_crop(&captures, crop)?;
81    save_rgba_png(&image, final_out_path)
82}
83
84struct CaptureApp {
85    registry_state: RegistryState,
86    output_state: OutputState,
87    shm: Option<wl_shm::WlShm>,
88    screencopy: Option<zwlr_screencopy_manager_v1::ZwlrScreencopyManagerV1>,
89    active: Option<ActiveCapture>,
90}
91
92struct ActiveCapture {
93    frame: zwlr_screencopy_frame_v1::ZwlrScreencopyFrameV1,
94    output: CaptureOutputInfo,
95    buffer_spec: Option<BufferSpec>,
96    buffer_done: bool,
97    copied: bool,
98    failed: bool,
99    ready: bool,
100    y_invert: bool,
101    shm_buffer: Option<CaptureShmBuffer>,
102}
103
104#[derive(Clone, Copy)]
105struct BufferSpec {
106    format: wl_shm::Format,
107    width: i32,
108    height: i32,
109    stride: i32,
110}
111
112struct CaptureShmBuffer {
113    _file: File,
114    mmap: MmapMut,
115    _pool: wl_shm_pool::WlShmPool,
116    buffer: wl_buffer::WlBuffer,
117}
118
119struct CapturedOutput {
120    info: CaptureOutputInfo,
121    width: i32,
122    height: i32,
123    stride: i32,
124    format: wl_shm::Format,
125    y_invert: bool,
126    bytes: Vec<u8>,
127}
128
129fn connect_capture_app() -> Result<
130    (
131        Connection,
132        wayland_client::EventQueue<CaptureApp>,
133        QueueHandle<CaptureApp>,
134        CaptureApp,
135    ),
136    String,
137> {
138    let conn = Connection::connect_to_env().map_err(|e| format!("wayland connect: {e}"))?;
139    let (globals, mut queue) =
140        registry_queue_init(&conn).map_err(|e| format!("registry init: {e}"))?;
141    let qh = queue.handle();
142    let registry_state = RegistryState::new(&globals);
143    let output_state = OutputState::new(&globals, &qh);
144    let mut app = CaptureApp {
145        registry_state,
146        output_state,
147        shm: globals.bind::<wl_shm::WlShm, _, _>(&qh, 1..=1, ()).ok(),
148        screencopy: globals
149            .bind::<zwlr_screencopy_manager_v1::ZwlrScreencopyManagerV1, _, _>(&qh, 1..=3, ())
150            .ok(),
151        active: None,
152    };
153    queue
154        .roundtrip(&mut app)
155        .map_err(|e| format!("roundtrip 1: {e}"))?;
156    queue
157        .roundtrip(&mut app)
158        .map_err(|e| format!("roundtrip 2: {e}"))?;
159    if app.shm.is_none() {
160        return Err("wl_shm not available".into());
161    }
162    if app.screencopy.is_none() {
163        return Err("zwlr_screencopy_manager_v1 not available".into());
164    }
165    Ok((conn, queue, qh, app))
166}
167
168impl CaptureApp {
169    fn capture_outputs(&self) -> Vec<(wl_output::WlOutput, CaptureOutputInfo)> {
170        let mut out = self
171            .output_state
172            .outputs()
173            .into_iter()
174            .filter_map(|output| {
175                let info = self.output_state.info(&output)?;
176                Some((
177                    output,
178                    CaptureOutputInfo {
179                        name: info.name.clone(),
180                        x: info.logical_position.map(|(x, _)| x).unwrap_or(0),
181                        y: info.logical_position.map(|(_, y)| y).unwrap_or(0),
182                        width: info.logical_size.map(|(w, _)| w as i32).unwrap_or(0),
183                        height: info.logical_size.map(|(_, h)| h as i32).unwrap_or(0),
184                        scale: info.scale_factor.max(1),
185                    },
186                ))
187            })
188            .collect::<Vec<_>>();
189        out.sort_by_key(|(_, info)| (info.y, info.x));
190        out
191    }
192
193    fn maybe_issue_copy(&mut self, qh: &QueueHandle<Self>) -> Result<(), String> {
194        let Some(active) = self.active.as_mut() else {
195            return Ok(());
196        };
197        if active.copied || active.failed || active.ready {
198            return Ok(());
199        }
200        let Some(spec) = active.buffer_spec else {
201            return Ok(());
202        };
203        if active.frame.version() >= 3 && !active.buffer_done {
204            return Ok(());
205        }
206        let shm = self
207            .shm
208            .as_ref()
209            .ok_or("wl_shm unavailable during capture")?;
210        let buffer = CaptureShmBuffer::new(shm, qh, spec)?;
211        active.frame.copy(&buffer.buffer);
212        active.shm_buffer = Some(buffer);
213        active.copied = true;
214        Ok(())
215    }
216}
217
218impl ActiveCapture {
219    fn into_captured(self) -> Result<CapturedOutput, String> {
220        let spec = self.buffer_spec.ok_or_else(|| {
221            "screencopy frame never reported wl_shm buffer parameters".to_string()
222        })?;
223        let buffer = self
224            .shm_buffer
225            .ok_or_else(|| "screencopy frame never copied into a wl_shm buffer".to_string())?;
226        Ok(CapturedOutput {
227            info: self.output,
228            width: spec.width,
229            height: spec.height,
230            stride: spec.stride,
231            format: spec.format,
232            y_invert: self.y_invert,
233            bytes: buffer.mmap[..].to_vec(),
234        })
235    }
236}
237
238impl CaptureShmBuffer {
239    fn new(
240        shm: &wl_shm::WlShm,
241        qh: &QueueHandle<CaptureApp>,
242        spec: BufferSpec,
243    ) -> Result<Self, String> {
244        let width = spec.width.max(1);
245        let height = spec.height.max(1);
246        let stride = spec.stride.max(width.saturating_mul(4));
247        let size = stride.saturating_mul(height) as u64;
248        let file = tempfile().map_err(|e| format!("tempfile: {e}"))?;
249        file.set_len(size).map_err(|e| format!("set_len: {e}"))?;
250        let mmap = unsafe { MmapMut::map_mut(&file).map_err(|e| format!("mmap: {e}"))? };
251        let pool = shm.create_pool(file.as_fd(), size as i32, qh, ());
252        let buffer = pool.create_buffer(0, width, height, stride, spec.format, qh, ());
253        Ok(Self {
254            _file: file,
255            mmap,
256            _pool: pool,
257            buffer,
258        })
259    }
260}
261
262fn capture_single_output(
263    conn: &Connection,
264    queue: &mut wayland_client::EventQueue<CaptureApp>,
265    qh: &QueueHandle<CaptureApp>,
266    app: &mut CaptureApp,
267    wl_output: &wl_output::WlOutput,
268    info: CaptureOutputInfo,
269) -> Result<CapturedOutput, String> {
270    let manager = app
271        .screencopy
272        .as_ref()
273        .ok_or_else(|| "zwlr_screencopy_manager_v1 unavailable".to_string())?
274        .clone();
275    let frame = manager.capture_output(0, wl_output, qh, ());
276    app.active = Some(ActiveCapture {
277        frame,
278        output: info,
279        buffer_spec: None,
280        buffer_done: false,
281        copied: false,
282        failed: false,
283        ready: false,
284        y_invert: false,
285        shm_buffer: None,
286    });
287    let _ = conn.flush();
288    loop {
289        app.maybe_issue_copy(qh)?;
290        queue
291            .blocking_dispatch(app)
292            .map_err(|e| format!("dispatch capture frame: {e}"))?;
293        if app
294            .active
295            .as_ref()
296            .is_some_and(|active| active.failed || active.ready)
297        {
298            break;
299        }
300    }
301    let active = app
302        .active
303        .take()
304        .ok_or("capture state missing after dispatch")?;
305    if active.failed {
306        return Err(format!(
307            "screencopy failed for output {:?}",
308            active.output.name
309        ));
310    }
311    active.into_captured()
312}
313
314fn clamp_crop_to_output_bounds(
315    crop: CaptureCrop,
316    outputs: &[CaptureOutputInfo],
317) -> Result<CaptureCrop, String> {
318    let min_x = outputs.iter().map(|output| output.x).min().unwrap_or(0);
319    let min_y = outputs.iter().map(|output| output.y).min().unwrap_or(0);
320    let max_x = outputs
321        .iter()
322        .map(|output| output.x.saturating_add(output.width.max(0)))
323        .max()
324        .unwrap_or(0);
325    let max_y = outputs
326        .iter()
327        .map(|output| output.y.saturating_add(output.height.max(0)))
328        .max()
329        .unwrap_or(0);
330
331    let x0 = crop.x.max(min_x);
332    let y0 = crop.y.max(min_y);
333    let x1 = crop.x.saturating_add(crop.w.max(0)).min(max_x);
334    let y1 = crop.y.saturating_add(crop.h.max(0)).min(max_y);
335    let w = x1.saturating_sub(x0);
336    let h = y1.saturating_sub(y0);
337    if w <= 0 || h <= 0 {
338        return Err(format!(
339            "crop rect empty after clamping: ({},{}) {}x{} within ({},{})-({},{})",
340            crop.x, crop.y, crop.w, crop.h, min_x, min_y, max_x, max_y
341        ));
342    }
343
344    Ok(CaptureCrop { x: x0, y: y0, w, h })
345}
346
347fn output_intersects_crop(info: &CaptureOutputInfo, crop: CaptureCrop) -> bool {
348    let output_x1 = info.x.saturating_add(info.width.max(0));
349    let output_y1 = info.y.saturating_add(info.height.max(0));
350    let crop_x1 = crop.x.saturating_add(crop.w.max(0));
351    let crop_y1 = crop.y.saturating_add(crop.h.max(0));
352    info.width > 0
353        && info.height > 0
354        && info.x < crop_x1
355        && output_x1 > crop.x
356        && info.y < crop_y1
357        && output_y1 > crop.y
358}
359
360fn render_logical_crop(
361    captures: &[CapturedOutput],
362    crop: CaptureCrop,
363) -> Result<RgbaImage, String> {
364    let mut image = RgbaImage::from_pixel(
365        crop.w.max(1) as u32,
366        crop.h.max(1) as u32,
367        Rgba([0, 0, 0, 0]),
368    );
369    for capture in captures {
370        blit_capture_crop(&mut image, capture, crop)?;
371    }
372    Ok(image)
373}
374
375fn blit_capture_crop(
376    image: &mut RgbaImage,
377    capture: &CapturedOutput,
378    crop: CaptureCrop,
379) -> Result<(), String> {
380    if !output_intersects_crop(&capture.info, crop) {
381        return Ok(());
382    }
383
384    let has_alpha = match capture.format {
385        wl_shm::Format::Argb8888 => true,
386        wl_shm::Format::Xrgb8888 => false,
387        other => return Err(format!("unsupported screencopy wl_shm format {:?}", other)),
388    };
389
390    let logical_w = capture.info.width.max(1);
391    let logical_h = capture.info.height.max(1);
392    let physical_w = capture.width.max(1);
393    let physical_h = capture.height.max(1);
394    let x0 = crop.x.max(capture.info.x);
395    let y0 = crop.y.max(capture.info.y);
396    let x1 = crop
397        .x
398        .saturating_add(crop.w)
399        .min(capture.info.x.saturating_add(capture.info.width));
400    let y1 = crop
401        .y
402        .saturating_add(crop.h)
403        .min(capture.info.y.saturating_add(capture.info.height));
404
405    for y in y0..y1 {
406        let local_y = y - capture.info.y;
407        let mut src_y = map_logical_to_physical(local_y, logical_h, physical_h);
408        if capture.y_invert {
409            src_y = physical_h - 1 - src_y;
410        }
411        let dst_y = (y - crop.y) as u32;
412        for x in x0..x1 {
413            let local_x = x - capture.info.x;
414            let src_x = map_logical_to_physical(local_x, logical_w, physical_w);
415            let dst_x = (x - crop.x) as u32;
416            let offset = (src_y * capture.stride + src_x * 4) as usize;
417            let pixel = capture
418                .bytes
419                .get(offset..offset + 4)
420                .ok_or_else(|| format!("capture buffer too small at offset {offset}"))?;
421            let rgba = [
422                pixel[2],
423                pixel[1],
424                pixel[0],
425                if has_alpha { pixel[3] } else { 255 },
426            ];
427            image.put_pixel(dst_x, dst_y, Rgba(rgba));
428        }
429    }
430
431    Ok(())
432}
433
434fn map_logical_to_physical(logical_offset: i32, logical_len: i32, physical_len: i32) -> i32 {
435    if logical_len <= 1 || physical_len <= 1 {
436        return 0;
437    }
438
439    (((logical_offset as i64) * (physical_len as i64)) / (logical_len as i64))
440        .clamp(0, (physical_len - 1) as i64) as i32
441}
442
443fn save_rgba_png(image: &RgbaImage, out_path: &Path) -> Result<(), String> {
444    ensure_parent_dir(out_path)?;
445    let file =
446        File::create(out_path).map_err(|e| format!("create screenshot {out_path:?}: {e}"))?;
447    let encoder = PngEncoder::new_with_quality(file, CompressionType::Fast, FilterType::Adaptive);
448    encoder
449        .write_image(
450            image.as_raw(),
451            image.width(),
452            image.height(),
453            ExtendedColorType::Rgba8,
454        )
455        .map_err(|e| format!("save screenshot: {e}"))
456}
457
458fn stitch_logical_desktop(captures: &[CapturedOutput]) -> Result<RgbaImage, String> {
459    let min_x = captures.iter().map(|cap| cap.info.x).min().unwrap_or(0);
460    let min_y = captures.iter().map(|cap| cap.info.y).min().unwrap_or(0);
461    let max_x = captures
462        .iter()
463        .map(|cap| cap.info.x + cap.info.width)
464        .max()
465        .unwrap_or(0);
466    let max_y = captures
467        .iter()
468        .map(|cap| cap.info.y + cap.info.height)
469        .max()
470        .unwrap_or(0);
471    let desktop_w = (max_x - min_x).max(1) as u32;
472    let desktop_h = (max_y - min_y).max(1) as u32;
473    let mut desktop = RgbaImage::from_pixel(desktop_w, desktop_h, Rgba([0, 0, 0, 0]));
474    for cap in captures {
475        let image = captured_output_to_image(cap)?;
476        imageops::overlay(
477            &mut desktop,
478            &image,
479            (cap.info.x - min_x) as i64,
480            (cap.info.y - min_y) as i64,
481        );
482    }
483    Ok(desktop)
484}
485
486fn captured_output_to_image(cap: &CapturedOutput) -> Result<RgbaImage, String> {
487    let mut output = RgbaImage::new(cap.width.max(1) as u32, cap.height.max(1) as u32);
488    for y in 0..cap.height.max(0) {
489        let src_y = if cap.y_invert { cap.height - 1 - y } else { y };
490        for x in 0..cap.width.max(0) {
491            let off = (src_y * cap.stride + x * 4) as usize;
492            let b = cap.bytes.get(off).copied().unwrap_or(0);
493            let g = cap.bytes.get(off + 1).copied().unwrap_or(0);
494            let r = cap.bytes.get(off + 2).copied().unwrap_or(0);
495            let a = match cap.format {
496                wl_shm::Format::Argb8888 => cap.bytes.get(off + 3).copied().unwrap_or(255),
497                wl_shm::Format::Xrgb8888 => 255,
498                other => return Err(format!("unsupported screencopy wl_shm format {:?}", other)),
499            };
500            output.put_pixel(x as u32, y as u32, Rgba([r, g, b, a]));
501        }
502    }
503    Ok(output)
504}
505
506#[cfg(test)]
507mod tests {
508    use super::{
509        CaptureOutputInfo, CapturedOutput, blit_capture_crop, clamp_crop_to_output_bounds,
510        render_logical_crop,
511    };
512    use crate::capture::CaptureCrop;
513    use image::RgbaImage;
514    use wayland_client::protocol::wl_shm;
515
516    fn bgra(rgba: [u8; 4]) -> [u8; 4] {
517        [rgba[2], rgba[1], rgba[0], rgba[3]]
518    }
519
520    #[test]
521    fn clamp_crop_to_output_bounds_handles_negative_desktop_coordinates() {
522        let outputs = vec![
523            CaptureOutputInfo {
524                name: Some("left".into()),
525                x: -1920,
526                y: 0,
527                width: 1920,
528                height: 1080,
529                scale: 1,
530            },
531            CaptureOutputInfo {
532                name: Some("center".into()),
533                x: 0,
534                y: 0,
535                width: 1920,
536                height: 1080,
537                scale: 1,
538            },
539        ];
540
541        let crop = clamp_crop_to_output_bounds(
542            CaptureCrop {
543                x: -1940,
544                y: -10,
545                w: 50,
546                h: 30,
547            },
548            &outputs,
549        )
550        .expect("clamped crop");
551
552        assert_eq!(
553            crop,
554            CaptureCrop {
555                x: -1920,
556                y: 0,
557                w: 30,
558                h: 20
559            }
560        );
561    }
562
563    #[test]
564    fn render_logical_crop_blits_only_the_requested_overlap() {
565        let bytes = [
566            bgra([255, 0, 0, 255]),
567            bgra([0, 255, 0, 255]),
568            bgra([0, 0, 255, 255]),
569            bgra([255, 255, 255, 255]),
570        ]
571        .into_iter()
572        .flatten()
573        .collect::<Vec<_>>();
574        let capture = CapturedOutput {
575            info: CaptureOutputInfo {
576                name: Some("primary".into()),
577                x: 10,
578                y: 20,
579                width: 2,
580                height: 2,
581                scale: 1,
582            },
583            width: 2,
584            height: 2,
585            stride: 8,
586            format: wl_shm::Format::Argb8888,
587            y_invert: false,
588            bytes,
589        };
590
591        let image = render_logical_crop(
592            &[capture],
593            CaptureCrop {
594                x: 11,
595                y: 20,
596                w: 1,
597                h: 2,
598            },
599        )
600        .expect("rendered crop");
601
602        assert_eq!(image.dimensions(), (1, 2));
603        assert_eq!(image.get_pixel(0, 0).0, [0, 255, 0, 255]);
604        assert_eq!(image.get_pixel(0, 1).0, [255, 255, 255, 255]);
605    }
606
607    #[test]
608    fn blit_capture_crop_maps_physical_pixels_back_to_logical_resolution() {
609        let bytes = [
610            bgra([255, 0, 0, 255]),
611            bgra([255, 0, 0, 255]),
612            bgra([0, 255, 0, 255]),
613            bgra([0, 255, 0, 255]),
614        ]
615        .into_iter()
616        .flatten()
617        .collect::<Vec<_>>();
618        let capture = CapturedOutput {
619            info: CaptureOutputInfo {
620                name: Some("scaled".into()),
621                x: 0,
622                y: 0,
623                width: 2,
624                height: 1,
625                scale: 2,
626            },
627            width: 4,
628            height: 1,
629            stride: 16,
630            format: wl_shm::Format::Argb8888,
631            y_invert: false,
632            bytes,
633        };
634        let mut image = RgbaImage::from_pixel(2, 1, image::Rgba([0, 0, 0, 0]));
635
636        blit_capture_crop(
637            &mut image,
638            &capture,
639            CaptureCrop {
640                x: 0,
641                y: 0,
642                w: 2,
643                h: 1,
644            },
645        )
646        .expect("scaled blit");
647
648        assert_eq!(image.get_pixel(0, 0).0, [255, 0, 0, 255]);
649        assert_eq!(image.get_pixel(1, 0).0, [0, 255, 0, 255]);
650    }
651}
652
653impl ProvidesRegistryState for CaptureApp {
654    fn registry(&mut self) -> &mut RegistryState {
655        &mut self.registry_state
656    }
657    registry_handlers![OutputState];
658}
659impl OutputHandler for CaptureApp {
660    fn output_state(&mut self) -> &mut OutputState {
661        &mut self.output_state
662    }
663
664    fn new_output(&mut self, _: &Connection, _: &QueueHandle<Self>, _: wl_output::WlOutput) {}
665
666    fn update_output(&mut self, _: &Connection, _: &QueueHandle<Self>, _: wl_output::WlOutput) {}
667
668    fn output_destroyed(&mut self, _: &Connection, _: &QueueHandle<Self>, _: wl_output::WlOutput) {}
669}
670delegate_registry!(CaptureApp);
671delegate_output!(CaptureApp);
672
673impl Dispatch<wl_shm::WlShm, ()> for CaptureApp {
674    fn event(
675        _: &mut Self,
676        _: &wl_shm::WlShm,
677        _: wl_shm::Event,
678        _: &(),
679        _: &Connection,
680        _: &QueueHandle<Self>,
681    ) {
682    }
683}
684
685impl Dispatch<wl_shm_pool::WlShmPool, ()> for CaptureApp {
686    fn event(
687        _: &mut Self,
688        _: &wl_shm_pool::WlShmPool,
689        _: wl_shm_pool::Event,
690        _: &(),
691        _: &Connection,
692        _: &QueueHandle<Self>,
693    ) {
694    }
695}
696
697impl Dispatch<wl_buffer::WlBuffer, ()> for CaptureApp {
698    fn event(
699        _: &mut Self,
700        _: &wl_buffer::WlBuffer,
701        _: wl_buffer::Event,
702        _: &(),
703        _: &Connection,
704        _: &QueueHandle<Self>,
705    ) {
706    }
707}
708
709impl Dispatch<zwlr_screencopy_manager_v1::ZwlrScreencopyManagerV1, ()> for CaptureApp {
710    fn event(
711        _: &mut Self,
712        _: &zwlr_screencopy_manager_v1::ZwlrScreencopyManagerV1,
713        _: zwlr_screencopy_manager_v1::Event,
714        _: &(),
715        _: &Connection,
716        _: &QueueHandle<Self>,
717    ) {
718    }
719}
720
721impl Dispatch<zwlr_screencopy_frame_v1::ZwlrScreencopyFrameV1, ()> for CaptureApp {
722    fn event(
723        state: &mut Self,
724        proxy: &zwlr_screencopy_frame_v1::ZwlrScreencopyFrameV1,
725        event: zwlr_screencopy_frame_v1::Event,
726        _: &(),
727        _: &Connection,
728        qh: &QueueHandle<Self>,
729    ) {
730        let Some(active) = state.active.as_mut() else {
731            return;
732        };
733        if active.frame.id() != proxy.id() {
734            return;
735        }
736        match event {
737            zwlr_screencopy_frame_v1::Event::Buffer {
738                format,
739                width,
740                height,
741                stride,
742            } => {
743                let WEnum::Value(format) = format else {
744                    active.failed = true;
745                    return;
746                };
747                active.buffer_spec = Some(BufferSpec {
748                    format,
749                    width: width as i32,
750                    height: height as i32,
751                    stride: stride as i32,
752                });
753                let _ = state.maybe_issue_copy(qh);
754            }
755            zwlr_screencopy_frame_v1::Event::LinuxDmabuf { .. } => {}
756            zwlr_screencopy_frame_v1::Event::BufferDone => {
757                active.buffer_done = true;
758                let _ = state.maybe_issue_copy(qh);
759            }
760            zwlr_screencopy_frame_v1::Event::Flags { flags } => {
761                active.y_invert = matches!(flags, WEnum::Value(value) if value.contains(zwlr_screencopy_frame_v1::Flags::YInvert));
762            }
763            zwlr_screencopy_frame_v1::Event::Ready { .. } => {
764                active.ready = true;
765            }
766            zwlr_screencopy_frame_v1::Event::Failed => {
767                active.failed = true;
768            }
769            _ => {}
770        }
771    }
772}