Skip to main content

blit_compositor/
imp.rs

1//! Headless Wayland compositor using `wayland-server` directly.
2//!
3//! Handles
4//! wl_compositor, wl_subcompositor, xdg_shell, wl_shm, wl_seat,
5//! wl_output, and zwp_linux_dmabuf_v1.  Pixel data is read on every
6//! commit and sent to the server via `CompositorEvent::SurfaceCommit`.
7
8use crate::positioner::PositionerGeometry;
9use std::collections::HashMap;
10use std::os::fd::{AsFd, AsRawFd, FromRawFd, OwnedFd};
11use std::sync::Arc;
12use std::sync::atomic::{AtomicBool, Ordering};
13use std::sync::mpsc;
14
15use calloop::generic::Generic;
16use calloop::{EventLoop, Interest, LoopSignal, PostAction};
17use wayland_protocols::wp::cursor_shape::v1::server::wp_cursor_shape_device_v1::{
18    self, WpCursorShapeDeviceV1,
19};
20use wayland_protocols::wp::cursor_shape::v1::server::wp_cursor_shape_manager_v1::{
21    self, WpCursorShapeManagerV1,
22};
23use wayland_protocols::wp::fractional_scale::v1::server::wp_fractional_scale_manager_v1::{
24    self, WpFractionalScaleManagerV1,
25};
26use wayland_protocols::wp::fractional_scale::v1::server::wp_fractional_scale_v1::WpFractionalScaleV1;
27use wayland_protocols::wp::presentation_time::server::wp_presentation::{
28    self, WpPresentation,
29};
30use wayland_protocols::wp::presentation_time::server::wp_presentation_feedback::{
31    Kind as WpPresentationFeedbackKind, WpPresentationFeedback,
32};
33use wayland_protocols::wp::linux_dmabuf::zv1::server::zwp_linux_buffer_params_v1::{
34    self, ZwpLinuxBufferParamsV1,
35};
36use wayland_protocols::wp::linux_dmabuf::zv1::server::zwp_linux_dmabuf_v1::{
37    self, ZwpLinuxDmabufV1,
38};
39use wayland_protocols::wp::pointer_constraints::zv1::server::zwp_confined_pointer_v1::ZwpConfinedPointerV1;
40use wayland_protocols::wp::pointer_constraints::zv1::server::zwp_locked_pointer_v1::ZwpLockedPointerV1;
41use wayland_protocols::wp::pointer_constraints::zv1::server::zwp_pointer_constraints_v1::{
42    self, ZwpPointerConstraintsV1,
43};
44use wayland_protocols::wp::primary_selection::zv1::server::zwp_primary_selection_device_manager_v1::{
45    self, ZwpPrimarySelectionDeviceManagerV1,
46};
47use wayland_protocols::wp::primary_selection::zv1::server::zwp_primary_selection_device_v1::{
48    self, ZwpPrimarySelectionDeviceV1,
49};
50use wayland_protocols::wp::primary_selection::zv1::server::zwp_primary_selection_offer_v1::{
51    self, ZwpPrimarySelectionOfferV1,
52};
53use wayland_protocols::wp::primary_selection::zv1::server::zwp_primary_selection_source_v1::{
54    self, ZwpPrimarySelectionSourceV1,
55};
56use wayland_protocols::wp::relative_pointer::zv1::server::zwp_relative_pointer_manager_v1::{
57    self, ZwpRelativePointerManagerV1,
58};
59use wayland_protocols::wp::relative_pointer::zv1::server::zwp_relative_pointer_v1::ZwpRelativePointerV1;
60use wayland_protocols::wp::text_input::zv3::server::zwp_text_input_manager_v3::{
61    self, ZwpTextInputManagerV3,
62};
63use wayland_protocols::wp::text_input::zv3::server::zwp_text_input_v3::{
64    self, ZwpTextInputV3,
65};
66use wayland_protocols::wp::viewporter::server::wp_viewport::WpViewport;
67use wayland_protocols::wp::viewporter::server::wp_viewporter::{self, WpViewporter};
68use wayland_protocols::xdg::activation::v1::server::xdg_activation_token_v1::{
69    self, XdgActivationTokenV1,
70};
71use wayland_protocols::xdg::activation::v1::server::xdg_activation_v1::{
72    self, XdgActivationV1,
73};
74use wayland_protocols::xdg::decoration::zv1::server::zxdg_decoration_manager_v1::{
75    self, ZxdgDecorationManagerV1,
76};
77use wayland_protocols::xdg::decoration::zv1::server::zxdg_toplevel_decoration_v1::{
78    self, ZxdgToplevelDecorationV1,
79};
80use wayland_protocols::xdg::shell::server::xdg_popup::{self, XdgPopup};
81use wayland_protocols::xdg::shell::server::xdg_positioner::XdgPositioner;
82use wayland_protocols::xdg::shell::server::xdg_surface::{self, XdgSurface};
83use wayland_protocols::xdg::shell::server::xdg_toplevel::{self, XdgToplevel};
84use wayland_protocols::xdg::shell::server::xdg_wm_base::{self, XdgWmBase};
85use wayland_server::protocol::wl_buffer::WlBuffer;
86use wayland_server::protocol::wl_callback::WlCallback;
87use wayland_server::protocol::wl_compositor::WlCompositor;
88use wayland_server::protocol::wl_data_device::{self, WlDataDevice};
89use wayland_server::protocol::wl_data_device_manager::{self, WlDataDeviceManager};
90use wayland_server::protocol::wl_data_offer::{self, WlDataOffer};
91use wayland_server::protocol::wl_data_source::{self, WlDataSource};
92use wayland_server::protocol::wl_keyboard::{self, WlKeyboard};
93use wayland_server::protocol::wl_output::{self, WlOutput};
94use wayland_server::protocol::wl_pointer::{self, WlPointer};
95use wayland_server::protocol::wl_region::WlRegion;
96use wayland_server::protocol::wl_seat::{self, WlSeat};
97use wayland_server::protocol::wl_shm::{self, WlShm};
98use wayland_server::protocol::wl_shm_pool::WlShmPool;
99use wayland_server::protocol::wl_subcompositor::WlSubcompositor;
100use wayland_server::protocol::wl_subsurface::WlSubsurface;
101use wayland_server::protocol::wl_surface::WlSurface;
102use wayland_server::backend::ObjectId;
103use wayland_server::{
104    Client, DataInit, Dispatch, Display, DisplayHandle, GlobalDispatch, New, Resource,
105};
106
107// ---------------------------------------------------------------------------
108// Public types (re-exported from lib.rs)
109// ---------------------------------------------------------------------------
110
111/// Pixel data in its native format, avoiding unnecessary colorspace conversions.
112#[derive(Clone)]
113pub enum PixelData {
114    Bgra(Arc<Vec<u8>>),
115    Rgba(Arc<Vec<u8>>),
116    Nv12 {
117        data: Arc<Vec<u8>>,
118        y_stride: usize,
119        uv_stride: usize,
120    },
121    DmaBuf {
122        fd: Arc<OwnedFd>,
123        fourcc: u32,
124        modifier: u64,
125        stride: u32,
126        offset: u32,
127    },
128    /// VA-API surface ready for VPP/encode — zero-copy path.
129    /// The surface was allocated by VA-API, exported as DMA-BUF for EGL
130    /// rendering, and is identified by its VASurfaceID.  The `va_display`
131    /// is an opaque pointer to the shared VADisplay.
132    VaSurface {
133        surface_id: u32,
134        va_display: usize, // *mut c_void as usize for Send+Sync
135        /// Keep the DMA-BUF fd alive so the EGL image remains valid.
136        _fd: Arc<OwnedFd>,
137    },
138}
139
140#[derive(Clone)]
141pub struct PixelLayer {
142    pub x: i32,
143    pub y: i32,
144    pub width: u32,
145    pub height: u32,
146    pub pixels: PixelData,
147}
148
149/// A DMA-BUF fd exported from a VA-API surface for use as a GPU
150/// renderer output target.  The compositor renders into the EGL FBO
151/// backed by this fd; the encoder references the VA-API surface by ID.
152pub struct ExternalOutputBuffer {
153    pub fd: Arc<OwnedFd>,
154    pub fourcc: u32,
155    pub modifier: u64,
156    pub stride: u32,
157    pub offset: u32,
158    pub width: u32,
159    pub height: u32,
160    pub va_surface_id: u32,
161    pub va_display: usize,
162}
163
164pub mod drm_fourcc {
165    pub const ARGB8888: u32 = u32::from_le_bytes(*b"AR24");
166    pub const XRGB8888: u32 = u32::from_le_bytes(*b"XR24");
167    pub const ABGR8888: u32 = u32::from_le_bytes(*b"AB24");
168    pub const XBGR8888: u32 = u32::from_le_bytes(*b"XB24");
169    pub const NV12: u32 = u32::from_le_bytes(*b"NV12");
170}
171
172impl PixelData {
173    pub fn to_rgba(&self, width: u32, height: u32) -> Vec<u8> {
174        let w = width as usize;
175        let h = height as usize;
176        match self {
177            PixelData::Rgba(data) => data.as_ref().clone(),
178            PixelData::Bgra(data) => {
179                let mut rgba = Vec::with_capacity(w * h * 4);
180                for px in data.chunks_exact(4) {
181                    rgba.extend_from_slice(&[px[2], px[1], px[0], px[3]]);
182                }
183                rgba
184            }
185            PixelData::Nv12 {
186                data,
187                y_stride,
188                uv_stride,
189            } => {
190                let y_plane_size = *y_stride * h;
191                let uv_h = h.div_ceil(2);
192                let uv_plane_size = *uv_stride * uv_h;
193                if data.len() < y_plane_size + uv_plane_size {
194                    return Vec::new();
195                }
196                let y_plane = &data[..y_plane_size];
197                let uv_plane = &data[y_plane_size..];
198                let mut rgba = Vec::with_capacity(w * h * 4);
199                for row in 0..h {
200                    for col in 0..w {
201                        let y = y_plane[row * y_stride + col];
202                        let uv_idx = (row / 2) * uv_stride + (col / 2) * 2;
203                        if uv_idx + 1 >= uv_plane.len() {
204                            rgba.extend_from_slice(&[0, 0, 0, 255]);
205                            continue;
206                        }
207                        let u = uv_plane[uv_idx];
208                        let v = uv_plane[uv_idx + 1];
209                        let [r, g, b] = yuv420_to_rgb(y, u, v);
210                        rgba.extend_from_slice(&[r, g, b, 255]);
211                    }
212                }
213                rgba
214            }
215            PixelData::DmaBuf {
216                fd,
217                fourcc,
218                stride,
219                offset,
220                ..
221            } => {
222                let raw = fd.as_raw_fd();
223                let stride_usize = *stride as usize;
224                let plane_offset = *offset as usize;
225                let map_size = plane_offset + stride_usize * h;
226                if map_size == 0 {
227                    return Vec::new();
228                }
229                // Best-effort DMA-BUF sync: try a non-blocking poll to see
230                // if the implicit GPU fence is signaled.  If it is, bracket
231                // the read with SYNC_START/SYNC_END for cache coherency.
232                // If poll fails (fd doesn't support it, e.g. Vulkan WSI) or
233                // the fence isn't ready yet, skip the sync and read anyway —
234                // a slightly stale frame is far better than a black surface.
235                const DMA_BUF_SYNC_READ: u64 = 1;
236                const DMA_BUF_SYNC_START: u64 = 0;
237                const DMA_BUF_SYNC_END: u64 = 4;
238                const DMA_BUF_IOCTL_SYNC: libc::c_ulong = 0x40086200;
239                let did_sync = {
240                    let mut pfd = libc::pollfd {
241                        fd: raw,
242                        events: libc::POLLIN,
243                        revents: 0,
244                    };
245                    let ready = unsafe { libc::poll(&mut pfd, 1, 0) };
246                    if ready > 0 {
247                        let s: u64 = DMA_BUF_SYNC_START | DMA_BUF_SYNC_READ;
248                        unsafe { libc::ioctl(raw, DMA_BUF_IOCTL_SYNC as _, &s) };
249                        true
250                    } else {
251                        false
252                    }
253                };
254                let ptr = unsafe {
255                    libc::mmap(
256                        std::ptr::null_mut(),
257                        map_size,
258                        libc::PROT_READ,
259                        libc::MAP_SHARED,
260                        raw,
261                        0,
262                    )
263                };
264                if ptr == libc::MAP_FAILED {
265                    if did_sync {
266                        let s: u64 = DMA_BUF_SYNC_END | DMA_BUF_SYNC_READ;
267                        unsafe { libc::ioctl(raw, DMA_BUF_IOCTL_SYNC as _, &s) };
268                    }
269                    return Vec::new();
270                }
271                let slice = unsafe { std::slice::from_raw_parts(ptr as *const u8, map_size) };
272                let row_bytes = w * 4;
273                let mut pixels = Vec::with_capacity(w * h * 4);
274                for row in 0..h {
275                    let start = plane_offset + row * stride_usize;
276                    if start + row_bytes <= slice.len() {
277                        pixels.extend_from_slice(&slice[start..start + row_bytes]);
278                    }
279                }
280                let is_bgr_mem = matches!(*fourcc, drm_fourcc::ARGB8888 | drm_fourcc::XRGB8888);
281                let force_alpha = matches!(*fourcc, drm_fourcc::XRGB8888 | drm_fourcc::XBGR8888);
282                for px in pixels.chunks_exact_mut(4) {
283                    if is_bgr_mem {
284                        px.swap(0, 2);
285                    }
286                    if force_alpha {
287                        px[3] = 255;
288                    }
289                }
290                unsafe { libc::munmap(ptr, map_size) };
291                if did_sync {
292                    let s: u64 = DMA_BUF_SYNC_END | DMA_BUF_SYNC_READ;
293                    unsafe { libc::ioctl(raw, DMA_BUF_IOCTL_SYNC as _, &s) };
294                }
295                pixels
296            }
297            PixelData::VaSurface { .. } => Vec::new(),
298        }
299    }
300
301    pub fn to_bgra_vec(&self, width: u32, height: u32) -> Vec<u8> {
302        match self {
303            PixelData::Bgra(v) => v.as_ref().to_vec(),
304            PixelData::Rgba(v) => {
305                let mut bgra = v.as_ref().to_vec();
306                for px in bgra.chunks_exact_mut(4) {
307                    px.swap(0, 2);
308                }
309                bgra
310            }
311            PixelData::DmaBuf {
312                fd,
313                stride,
314                fourcc,
315                offset,
316                ..
317            } => {
318                let raw = fd.as_raw_fd();
319                let s = *stride as usize;
320                let h = height as usize;
321                let w = width as usize;
322                let off = *offset as usize;
323                let map_size = off + s * h;
324                if map_size == 0 {
325                    return Vec::new();
326                }
327                const DMA_BUF_SYNC_READ: u64 = 1;
328                const DMA_BUF_SYNC_START: u64 = 0;
329                const DMA_BUF_SYNC_END: u64 = 4;
330                const DMA_BUF_IOCTL_SYNC: libc::c_ulong = 0x40086200;
331                let sync_start: u64 = DMA_BUF_SYNC_START | DMA_BUF_SYNC_READ;
332                unsafe { libc::ioctl(raw, DMA_BUF_IOCTL_SYNC as _, &sync_start) };
333                let ptr = unsafe {
334                    libc::mmap(
335                        std::ptr::null_mut(),
336                        map_size,
337                        libc::PROT_READ,
338                        libc::MAP_SHARED,
339                        raw,
340                        0,
341                    )
342                };
343                if ptr == libc::MAP_FAILED {
344                    return Vec::new();
345                }
346                let slice = unsafe { std::slice::from_raw_parts(ptr as *const u8, map_size) };
347                let row_bytes = w * 4;
348                let is_bgr = matches!(fourcc, 0x34325241 | 0x34325258);
349                let mut bgra = Vec::with_capacity(w * h * 4);
350                for row in 0..h {
351                    let row_start = off + row * s;
352                    let src = &slice[row_start..row_start + row_bytes.min(slice.len() - row_start)];
353                    if is_bgr {
354                        bgra.extend_from_slice(src);
355                    } else {
356                        for px in src.chunks_exact(4) {
357                            bgra.extend_from_slice(&[px[2], px[1], px[0], px[3]]);
358                        }
359                    }
360                }
361                let sync_end: u64 = DMA_BUF_SYNC_END | DMA_BUF_SYNC_READ;
362                unsafe {
363                    libc::munmap(ptr, map_size);
364                    libc::ioctl(raw, DMA_BUF_IOCTL_SYNC as _, &sync_end);
365                }
366                bgra
367            }
368            _ => Vec::new(),
369        }
370    }
371
372    pub fn is_empty(&self) -> bool {
373        match self {
374            PixelData::Bgra(v) | PixelData::Rgba(v) => v.is_empty(),
375            PixelData::Nv12 { data, .. } => data.is_empty(),
376            PixelData::DmaBuf { .. } | PixelData::VaSurface { .. } => false,
377        }
378    }
379
380    pub fn is_dmabuf(&self) -> bool {
381        matches!(self, PixelData::DmaBuf { .. })
382    }
383
384    pub fn is_va_surface(&self) -> bool {
385        matches!(self, PixelData::VaSurface { .. })
386    }
387}
388
389#[derive(Clone)]
390pub enum CursorImage {
391    Named(String),
392    Custom {
393        hotspot_x: u16,
394        hotspot_y: u16,
395        width: u16,
396        height: u16,
397        rgba: Vec<u8>,
398    },
399    Hidden,
400}
401
402pub enum CompositorEvent {
403    SurfaceCreated {
404        surface_id: u16,
405        title: String,
406        app_id: String,
407        parent_id: u16,
408        width: u16,
409        height: u16,
410    },
411    SurfaceDestroyed {
412        surface_id: u16,
413    },
414    SurfaceCommit {
415        surface_id: u16,
416        width: u32,
417        height: u32,
418        pixels: PixelData,
419    },
420    SurfaceTitle {
421        surface_id: u16,
422        title: String,
423    },
424    SurfaceAppId {
425        surface_id: u16,
426        app_id: String,
427    },
428    SurfaceResized {
429        surface_id: u16,
430        width: u16,
431        height: u16,
432    },
433    ClipboardContent {
434        mime_type: String,
435        data: Vec<u8>,
436    },
437    SurfaceCursor {
438        surface_id: u16,
439        cursor: CursorImage,
440    },
441}
442
443pub enum CompositorCommand {
444    KeyInput {
445        surface_id: u16,
446        keycode: u32,
447        pressed: bool,
448    },
449    PointerMotion {
450        surface_id: u16,
451        x: f64,
452        y: f64,
453    },
454    PointerButton {
455        surface_id: u16,
456        button: u32,
457        pressed: bool,
458    },
459    PointerAxis {
460        surface_id: u16,
461        axis: u8,
462        value: f64,
463    },
464    SurfaceResize {
465        surface_id: u16,
466        width: u16,
467        height: u16,
468        scale_120: u16,
469    },
470    SurfaceFocus {
471        surface_id: u16,
472    },
473    SurfaceClose {
474        surface_id: u16,
475    },
476    ClipboardOffer {
477        mime_type: String,
478        data: Vec<u8>,
479    },
480    Capture {
481        surface_id: u16,
482        scale_120: u16,
483        reply: mpsc::SyncSender<Option<(u32, u32, Vec<u8>)>>,
484    },
485    RequestFrame {
486        surface_id: u16,
487    },
488    ReleaseKeys {
489        keycodes: Vec<u32>,
490    },
491    /// List available clipboard MIME types.
492    ClipboardListMimes {
493        reply: mpsc::SyncSender<Vec<String>>,
494    },
495    /// Read clipboard content for a specific MIME type.
496    ClipboardGet {
497        mime_type: String,
498        reply: mpsc::SyncSender<Option<Vec<u8>>>,
499    },
500    /// Set externally-allocated DMA-BUF fds as GPU renderer output targets.
501    SetExternalOutputBuffers {
502        buffers: Vec<ExternalOutputBuffer>,
503    },
504    /// Synthesize text input as key press/release sequences.
505    TextInput {
506        text: String,
507    },
508    Shutdown,
509}
510
511// ---------------------------------------------------------------------------
512// Internal state
513// ---------------------------------------------------------------------------
514
515/// Per-wl_surface state.  `pub(crate)` so render.rs can access fields.
516pub(crate) struct Surface {
517    pub surface_id: u16,
518    pub wl_surface: WlSurface,
519
520    // pending state
521    pending_buffer: Option<WlBuffer>,
522    pending_buffer_scale: i32,
523    pending_damage: bool,
524    pending_frame_callbacks: Vec<WlCallback>,
525    pending_presentation_feedbacks: Vec<WpPresentationFeedback>,
526    pending_opaque: bool,
527
528    // committed state
529    pub buffer_scale: i32,
530    pub is_opaque: bool,
531
532    // subsurface
533    pub parent_surface_id: Option<ObjectId>,
534    pending_subsurface_position: Option<(i32, i32)>,
535    pub subsurface_position: (i32, i32),
536    pub children: Vec<ObjectId>,
537
538    // xdg
539    xdg_surface: Option<XdgSurface>,
540    xdg_toplevel: Option<XdgToplevel>,
541    xdg_popup: Option<XdgPopup>,
542    pub xdg_geometry: Option<(i32, i32, i32, i32)>,
543
544    title: String,
545    app_id: String,
546
547    // viewport
548    pending_viewport_destination: Option<(i32, i32)>,
549    /// Committed viewport destination (logical size declared by client via
550    /// `wp_viewport.set_destination`).  Used by fractional-scale-aware clients
551    /// (e.g. Chromium) that render at physical resolution with `buffer_scale=1`
552    /// and rely on the viewport to declare the logical surface size.
553    pub viewport_destination: Option<(i32, i32)>,
554
555    is_cursor: bool,
556    cursor_hotspot: (i32, i32),
557}
558
559struct ShmPool {
560    resource: WlShmPool,
561    fd: OwnedFd,
562    size: usize,
563    mmap_ptr: *mut u8,
564}
565
566impl ShmPool {
567    fn new(resource: WlShmPool, fd: OwnedFd, size: i32) -> Self {
568        let sz = size.max(0) as usize;
569        let ptr = if sz > 0 {
570            unsafe {
571                libc::mmap(
572                    std::ptr::null_mut(),
573                    sz,
574                    libc::PROT_READ,
575                    libc::MAP_SHARED,
576                    fd.as_raw_fd(),
577                    0,
578                )
579            }
580        } else {
581            libc::MAP_FAILED
582        };
583        ShmPool {
584            resource,
585            fd,
586            size: sz,
587            mmap_ptr: if ptr == libc::MAP_FAILED {
588                std::ptr::null_mut()
589            } else {
590                ptr as *mut u8
591            },
592        }
593    }
594
595    fn resize(&mut self, new_size: i32) {
596        let new_sz = new_size.max(0) as usize;
597        if new_sz <= self.size {
598            return;
599        }
600        if !self.mmap_ptr.is_null() {
601            unsafe {
602                libc::munmap(self.mmap_ptr as *mut _, self.size);
603            }
604        }
605        let ptr = unsafe {
606            libc::mmap(
607                std::ptr::null_mut(),
608                new_sz,
609                libc::PROT_READ,
610                libc::MAP_SHARED,
611                self.fd.as_raw_fd(),
612                0,
613            )
614        };
615        self.mmap_ptr = if ptr == libc::MAP_FAILED {
616            std::ptr::null_mut()
617        } else {
618            ptr as *mut u8
619        };
620        self.size = new_sz;
621    }
622
623    fn read_buffer(
624        &self,
625        offset: i32,
626        width: i32,
627        height: i32,
628        stride: i32,
629        format: wl_shm::Format,
630    ) -> Option<(u32, u32, PixelData)> {
631        if self.mmap_ptr.is_null() {
632            return None;
633        }
634        let w = width as u32;
635        let h = height as u32;
636        let s = stride as usize;
637        let off = offset as usize;
638        let row_bytes = w as usize * 4;
639        let needed = off + s * (h as usize).saturating_sub(1) + row_bytes;
640        if needed > self.size {
641            return None;
642        }
643        let mut bgra = if s == row_bytes && off == 0 {
644            let total = row_bytes * h as usize;
645            unsafe { std::slice::from_raw_parts(self.mmap_ptr, total) }.to_vec()
646        } else {
647            let mut packed = Vec::with_capacity(row_bytes * h as usize);
648            for row in 0..h as usize {
649                let src = unsafe {
650                    std::slice::from_raw_parts(self.mmap_ptr.add(off + row * s), row_bytes)
651                };
652                packed.extend_from_slice(src);
653            }
654            packed
655        };
656        if matches!(format, wl_shm::Format::Xrgb8888 | wl_shm::Format::Xbgr8888) {
657            for px in bgra.chunks_exact_mut(4) {
658                px[3] = 255;
659            }
660        }
661        if matches!(format, wl_shm::Format::Abgr8888 | wl_shm::Format::Xbgr8888) {
662            Some((w, h, PixelData::Rgba(Arc::new(bgra))))
663        } else {
664            Some((w, h, PixelData::Bgra(Arc::new(bgra))))
665        }
666    }
667}
668
669impl Drop for ShmPool {
670    fn drop(&mut self) {
671        if !self.mmap_ptr.is_null() {
672            unsafe {
673                libc::munmap(self.mmap_ptr as *mut _, self.size);
674            }
675        }
676    }
677}
678
679unsafe impl Send for ShmPool {}
680
681struct ShmBufferData {
682    pool_id: ObjectId,
683    offset: i32,
684    width: i32,
685    height: i32,
686    stride: i32,
687    format: wl_shm::Format,
688}
689
690struct DmaBufBufferData {
691    width: i32,
692    height: i32,
693    fourcc: u32,
694    modifier: u64,
695    planes: Vec<DmaBufPlane>,
696}
697
698struct DmaBufPlane {
699    fd: OwnedFd,
700    offset: u32,
701    stride: u32,
702}
703
704struct DmaBufParamsPending {
705    resource: ZwpLinuxBufferParamsV1,
706    planes: Vec<DmaBufPlane>,
707    modifier: u64,
708}
709
710struct ClientState;
711struct XdgSurfaceData {
712    wl_surface_id: ObjectId,
713}
714struct XdgToplevelData {
715    wl_surface_id: ObjectId,
716}
717struct XdgPopupData {
718    wl_surface_id: ObjectId,
719}
720struct SubsurfaceData {
721    wl_surface_id: ObjectId,
722    parent_surface_id: ObjectId,
723}
724
725// -- Clipboard / data device data types --
726
727struct DataSourceData {
728    mime_types: std::sync::Mutex<Vec<String>>,
729}
730
731struct DataOfferData {
732    /// If `true`, the offer represents external (browser/CLI) clipboard data
733    /// stored in `Compositor::external_clipboard`.  Otherwise it is backed by
734    /// a Wayland `wl_data_source`.
735    external: bool,
736}
737
738/// Stored state for the external (browser/CLI) clipboard selection.
739struct ExternalClipboard {
740    mime_type: String,
741    data: Vec<u8>,
742}
743
744struct PrimarySourceData {
745    mime_types: std::sync::Mutex<Vec<String>>,
746}
747struct PrimaryOfferData {
748    external: bool,
749}
750
751// -- Activation token data --
752struct ActivationTokenData {
753    serial: u32,
754}
755
756struct PositionerState {
757    resource: XdgPositioner,
758    geometry: PositionerGeometry,
759}
760
761// ---------------------------------------------------------------------------
762// US-QWERTY character → evdev keycode mapping
763// ---------------------------------------------------------------------------
764
765/// Map an ASCII character to its evdev keycode under a US-QWERTY layout.
766/// Returns `(keycode, needs_shift)`, or `None` for characters not on the
767/// layout (non-ASCII, control chars other than \t/\n).
768fn char_to_keycode(ch: char) -> Option<(u32, bool)> {
769    const KEY_1: u32 = 2;
770    const KEY_2: u32 = 3;
771    const KEY_3: u32 = 4;
772    const KEY_4: u32 = 5;
773    const KEY_5: u32 = 6;
774    const KEY_6: u32 = 7;
775    const KEY_7: u32 = 8;
776    const KEY_8: u32 = 9;
777    const KEY_9: u32 = 10;
778    const KEY_0: u32 = 11;
779    const KEY_MINUS: u32 = 12;
780    const KEY_EQUAL: u32 = 13;
781    const KEY_TAB: u32 = 15;
782    const KEY_Q: u32 = 16;
783    const KEY_W: u32 = 17;
784    const KEY_E: u32 = 18;
785    const KEY_R: u32 = 19;
786    const KEY_T: u32 = 20;
787    const KEY_Y: u32 = 21;
788    const KEY_U: u32 = 22;
789    const KEY_I: u32 = 23;
790    const KEY_O: u32 = 24;
791    const KEY_P: u32 = 25;
792    const KEY_LEFTBRACE: u32 = 26;
793    const KEY_RIGHTBRACE: u32 = 27;
794    const KEY_ENTER: u32 = 28;
795    const KEY_A: u32 = 30;
796    const KEY_S: u32 = 31;
797    const KEY_D: u32 = 32;
798    const KEY_F: u32 = 33;
799    const KEY_G: u32 = 34;
800    const KEY_H: u32 = 35;
801    const KEY_J: u32 = 36;
802    const KEY_K: u32 = 37;
803    const KEY_L: u32 = 38;
804    const KEY_SEMICOLON: u32 = 39;
805    const KEY_APOSTROPHE: u32 = 40;
806    const KEY_GRAVE: u32 = 41;
807    const KEY_BACKSLASH: u32 = 43;
808    const KEY_Z: u32 = 44;
809    const KEY_X: u32 = 45;
810    const KEY_C: u32 = 46;
811    const KEY_V: u32 = 47;
812    const KEY_B: u32 = 48;
813    const KEY_N: u32 = 49;
814    const KEY_M: u32 = 50;
815    const KEY_COMMA: u32 = 51;
816    const KEY_DOT: u32 = 52;
817    const KEY_SLASH: u32 = 53;
818    const KEY_SPACE: u32 = 57;
819
820    fn letter_kc(ch: char) -> u32 {
821        match ch {
822            'a' => KEY_A,
823            'b' => KEY_B,
824            'c' => KEY_C,
825            'd' => KEY_D,
826            'e' => KEY_E,
827            'f' => KEY_F,
828            'g' => KEY_G,
829            'h' => KEY_H,
830            'i' => KEY_I,
831            'j' => KEY_J,
832            'k' => KEY_K,
833            'l' => KEY_L,
834            'm' => KEY_M,
835            'n' => KEY_N,
836            'o' => KEY_O,
837            'p' => KEY_P,
838            'q' => KEY_Q,
839            'r' => KEY_R,
840            's' => KEY_S,
841            't' => KEY_T,
842            'u' => KEY_U,
843            'v' => KEY_V,
844            'w' => KEY_W,
845            'x' => KEY_X,
846            'y' => KEY_Y,
847            'z' => KEY_Z,
848            _ => KEY_SPACE,
849        }
850    }
851
852    let (kc, shift) = match ch {
853        'a'..='z' => (letter_kc(ch), false),
854        'A'..='Z' => (letter_kc(ch.to_ascii_lowercase()), true),
855        '0' => (KEY_0, false),
856        '1'..='9' => (KEY_1 + (ch as u32 - '1' as u32), false),
857        ' ' => (KEY_SPACE, false),
858        '-' => (KEY_MINUS, false),
859        '=' => (KEY_EQUAL, false),
860        '[' => (KEY_LEFTBRACE, false),
861        ']' => (KEY_RIGHTBRACE, false),
862        ';' => (KEY_SEMICOLON, false),
863        '\'' => (KEY_APOSTROPHE, false),
864        ',' => (KEY_COMMA, false),
865        '.' => (KEY_DOT, false),
866        '/' => (KEY_SLASH, false),
867        '\\' => (KEY_BACKSLASH, false),
868        '`' => (KEY_GRAVE, false),
869        '\t' => (KEY_TAB, false),
870        '\n' => (KEY_ENTER, false),
871        '!' => (KEY_1, true),
872        '@' => (KEY_2, true),
873        '#' => (KEY_3, true),
874        '$' => (KEY_4, true),
875        '%' => (KEY_5, true),
876        '^' => (KEY_6, true),
877        '&' => (KEY_7, true),
878        '*' => (KEY_8, true),
879        '(' => (KEY_9, true),
880        ')' => (KEY_0, true),
881        '_' => (KEY_MINUS, true),
882        '+' => (KEY_EQUAL, true),
883        '{' => (KEY_LEFTBRACE, true),
884        '}' => (KEY_RIGHTBRACE, true),
885        ':' => (KEY_SEMICOLON, true),
886        '"' => (KEY_APOSTROPHE, true),
887        '<' => (KEY_COMMA, true),
888        '>' => (KEY_DOT, true),
889        '?' => (KEY_SLASH, true),
890        '|' => (KEY_BACKSLASH, true),
891        '~' => (KEY_GRAVE, true),
892        _ => return None,
893    };
894    Some((kc, shift))
895}
896
897// ---------------------------------------------------------------------------
898// XKB modifier state tracking
899// ---------------------------------------------------------------------------
900
901/// Bitmask values matching the `modifier_map` in us-qwerty.xkb.
902const MOD_SHIFT: u32 = 1 << 0;
903const MOD_LOCK: u32 = 1 << 1;
904const MOD_CONTROL: u32 = 1 << 2;
905const MOD_MOD1: u32 = 1 << 3; // Alt
906const MOD_MOD4: u32 = 1 << 6; // Super / Meta
907
908/// Return the XKB modifier bit for an evdev keycode, or 0 if the key is
909/// not a modifier.
910fn keycode_to_mod(keycode: u32) -> u32 {
911    match keycode {
912        42 | 54 => MOD_SHIFT,   // ShiftLeft, ShiftRight
913        58 => MOD_LOCK,         // CapsLock (toggled, handled separately)
914        29 | 97 => MOD_CONTROL, // ControlLeft, ControlRight
915        56 | 100 => MOD_MOD1,   // AltLeft, AltRight
916        125 | 126 => MOD_MOD4,  // MetaLeft, MetaRight
917        _ => 0,
918    }
919}
920
921/// Per-object state for a `zwp_text_input_v3` resource.
922struct TextInputState {
923    resource: ZwpTextInputV3,
924    /// Whether the client has sent `enable` (text input is active).
925    enabled: bool,
926}
927
928/// Main compositor state.
929struct Compositor {
930    display_handle: DisplayHandle,
931    surfaces: HashMap<ObjectId, Surface>,
932    toplevel_surface_ids: HashMap<u16, ObjectId>,
933    next_surface_id: u16,
934    shm_pools: HashMap<ObjectId, ShmPool>,
935    pixel_cache: HashMap<ObjectId, (u32, u32, i32, bool, PixelData)>,
936    dmabuf_params: HashMap<ObjectId, DmaBufParamsPending>,
937    vulkan_renderer: Option<super::vulkan_render::VulkanRenderer>,
938    output_width: i32,
939    output_height: i32,
940    /// Output scale in 1/120th units (wp_fractional_scale_v1 convention).
941    /// 120 = 1×, 180 = 1.5×, 240 = 2×.  Derived from the browser's
942    /// devicePixelRatio sent via C2S_SURFACE_RESIZE.
943    output_scale_120: u16,
944    outputs: Vec<WlOutput>,
945    keyboards: Vec<WlKeyboard>,
946    pointers: Vec<WlPointer>,
947    keyboard_keymap_data: Vec<u8>,
948    /// Currently depressed (held down) XKB modifier mask.
949    mods_depressed: u32,
950    /// CapsLock locked modifier mask (toggled on/off by CapsLock key).
951    mods_locked: u32,
952    serial: u32,
953    event_tx: mpsc::Sender<CompositorEvent>,
954    event_notify: Arc<dyn Fn() + Send + Sync>,
955    loop_signal: LoopSignal,
956    /// (phys_w, phys_h, log_w, log_h, pixels)
957    pending_commits: HashMap<u16, (u32, u32, u32, u32, PixelData)>,
958    focused_surface_id: u16,
959    /// The wl_surface ObjectId the pointer is currently over (None = none).
960    pointer_entered_id: Option<ObjectId>,
961    /// Set after output scale change; triggers keyboard leave/re-enter
962    /// on the next surface commit so clients have time to process the
963    /// reconfigure before receiving new input events.
964    pending_kb_reenter: bool,
965    verbose: bool,
966    shutdown: Arc<AtomicBool>,
967    /// Track last reported size per toplevel surface_id to detect changes.
968    /// Per-toplevel: (composited_w, composited_h, logical_w, logical_h).
969    /// Used for pointer coordinate mapping (browser→Wayland).
970    last_reported_size: HashMap<u16, (u32, u32, u32, u32)>,
971    /// Per-toplevel configured size.  Each surface can live in a
972    /// differently-sized BSP pane, so we need to track sizes individually
973    /// rather than relying on the single `output_width`/`output_height`.
974    surface_sizes: HashMap<u16, (i32, i32)>,
975    /// Pending positioner geometry, keyed by XdgPositioner protocol id.
976    positioners: HashMap<ObjectId, PositionerState>,
977    /// Active wp_fractional_scale_v1 objects.  When `output_scale_120`
978    /// changes we send `preferred_scale` to every entry.
979    fractional_scales: Vec<WpFractionalScaleV1>,
980
981    // -- Clipboard --
982    /// Active wl_data_device objects (one per seat binding).
983    data_devices: Vec<WlDataDevice>,
984    /// The wl_data_source that currently owns the clipboard selection (if any).
985    /// Cleared when the source is destroyed or replaced.
986    selection_source: Option<WlDataSource>,
987    /// External clipboard data offered from the browser or CLI.
988    external_clipboard: Option<ExternalClipboard>,
989
990    // -- Primary selection --
991    primary_devices: Vec<ZwpPrimarySelectionDeviceV1>,
992    primary_source: Option<ZwpPrimarySelectionSourceV1>,
993    external_primary: Option<ExternalClipboard>,
994
995    // -- Relative pointer --
996    relative_pointers: Vec<ZwpRelativePointerV1>,
997
998    // -- Text input --
999    /// Active zwp_text_input_v3 objects.  When the compositor receives
1000    /// composed text from the browser it delivers it via `commit_string`
1001    /// + `done` to the text_input object belonging to the focused surface.
1002    text_inputs: Vec<TextInputState>,
1003    /// Serial counter for `zwp_text_input_v3.done` events.  Incremented on
1004    /// every `done` event sent by the compositor.
1005    #[expect(dead_code)]
1006    text_input_serial: u32,
1007
1008    // -- Activation --
1009    next_activation_token: u32,
1010}
1011
1012impl Compositor {
1013    fn next_serial(&mut self) -> u32 {
1014        self.serial = self.serial.wrapping_add(1);
1015        self.serial
1016    }
1017
1018    /// Update internal modifier state from a key event and send
1019    /// `wl_keyboard.modifiers` to all keyboards belonging to the focused
1020    /// surface's client.  Many Wayland clients (GTK, Chromium) rely on this
1021    /// event rather than tracking modifiers from raw key events.
1022    fn update_and_send_modifiers(&mut self, keycode: u32, pressed: bool) {
1023        let m = keycode_to_mod(keycode);
1024        if m == 0 {
1025            return;
1026        }
1027        if keycode == 58 {
1028            // CapsLock toggles mods_locked on press.
1029            if pressed {
1030                self.mods_locked ^= MOD_LOCK;
1031            }
1032        } else if pressed {
1033            self.mods_depressed |= m;
1034        } else {
1035            self.mods_depressed &= !m;
1036        }
1037        let serial = self.next_serial();
1038        let focused_wl = self
1039            .toplevel_surface_ids
1040            .get(&self.focused_surface_id)
1041            .and_then(|root_id| self.surfaces.get(root_id))
1042            .map(|s| s.wl_surface.clone());
1043        for kb in &self.keyboards {
1044            if let Some(ref wl) = focused_wl
1045                && same_client(kb, wl)
1046            {
1047                kb.modifiers(serial, self.mods_depressed, 0, self.mods_locked, 0);
1048            }
1049        }
1050    }
1051
1052    /// Switch keyboard (and text_input) focus from the current surface to
1053    /// `new_surface_id`.  Sends `wl_keyboard.leave` to the old surface's
1054    /// client and `wl_keyboard.enter` to the new surface's client, which is
1055    /// required by the Wayland protocol when focus changes between clients.
1056    fn set_keyboard_focus(&mut self, new_surface_id: u16) {
1057        let old_id = self.focused_surface_id;
1058        if old_id == new_surface_id {
1059            // Focus unchanged — still send enter so the client gets the
1060            // event (e.g. first toplevel), but skip leave.
1061            self.focused_surface_id = new_surface_id;
1062            if let Some(root_id) = self.toplevel_surface_ids.get(&new_surface_id)
1063                && let Some(wl_surface) = self.surfaces.get(root_id).map(|s| s.wl_surface.clone())
1064            {
1065                let serial = self.next_serial();
1066                for kb in &self.keyboards {
1067                    if same_client(kb, &wl_surface) {
1068                        kb.enter(serial, &wl_surface, vec![]);
1069                    }
1070                }
1071                for ti in &self.text_inputs {
1072                    if same_client(&ti.resource, &wl_surface) {
1073                        ti.resource.enter(&wl_surface);
1074                    }
1075                }
1076            }
1077            return;
1078        }
1079
1080        // Leave the old surface.
1081        if old_id != 0
1082            && let Some(old_root) = self.toplevel_surface_ids.get(&old_id)
1083            && let Some(old_wl) = self.surfaces.get(old_root).map(|s| s.wl_surface.clone())
1084        {
1085            let serial = self.next_serial();
1086            for kb in &self.keyboards {
1087                if same_client(kb, &old_wl) {
1088                    kb.leave(serial, &old_wl);
1089                }
1090            }
1091            for ti in &self.text_inputs {
1092                if same_client(&ti.resource, &old_wl) {
1093                    ti.resource.leave(&old_wl);
1094                }
1095            }
1096        }
1097
1098        self.focused_surface_id = new_surface_id;
1099
1100        // Enter the new surface.
1101        if let Some(root_id) = self.toplevel_surface_ids.get(&new_surface_id)
1102            && let Some(wl_surface) = self.surfaces.get(root_id).map(|s| s.wl_surface.clone())
1103        {
1104            let serial = self.next_serial();
1105            for kb in &self.keyboards {
1106                if same_client(kb, &wl_surface) {
1107                    kb.enter(serial, &wl_surface, vec![]);
1108                }
1109            }
1110            for ti in &self.text_inputs {
1111                if same_client(&ti.resource, &wl_surface) {
1112                    ti.resource.enter(&wl_surface);
1113                }
1114            }
1115        }
1116    }
1117
1118    fn allocate_surface_id(&mut self) -> u16 {
1119        let mut id = self.next_surface_id;
1120        let start = id;
1121        loop {
1122            if !self.toplevel_surface_ids.contains_key(&id) {
1123                break;
1124            }
1125            id = id.wrapping_add(1);
1126            if id == 0 {
1127                id = 1;
1128            }
1129            if id == start {
1130                break;
1131            }
1132        }
1133        self.next_surface_id = id.wrapping_add(1);
1134        if self.next_surface_id == 0 {
1135            self.next_surface_id = 1;
1136        }
1137        id
1138    }
1139
1140    fn flush_pending_commits(&mut self) {
1141        for (surface_id, (width, height, log_w, log_h, pixels)) in self.pending_commits.drain() {
1142            // Check for size change.
1143            let prev = self.last_reported_size.get(&surface_id).copied();
1144            if prev.is_none() || prev.map(|(pw, ph, _, _)| (pw, ph)) != Some((width, height)) {
1145                self.last_reported_size
1146                    .insert(surface_id, (width, height, log_w, log_h));
1147                let _ = self.event_tx.send(CompositorEvent::SurfaceResized {
1148                    surface_id,
1149                    width: width as u16,
1150                    height: height as u16,
1151                });
1152            }
1153            let _ = self.event_tx.send(CompositorEvent::SurfaceCommit {
1154                surface_id,
1155                width,
1156                height,
1157                pixels,
1158            });
1159        }
1160        (self.event_notify)();
1161    }
1162
1163    fn read_shm_buffer(&self, buffer: &WlBuffer) -> Option<(u32, u32, PixelData)> {
1164        let data = buffer.data::<ShmBufferData>()?;
1165        let pool = self.shm_pools.get(&data.pool_id)?;
1166        pool.read_buffer(
1167            data.offset,
1168            data.width,
1169            data.height,
1170            data.stride,
1171            data.format,
1172        )
1173    }
1174
1175    fn read_dmabuf_buffer(&self, buffer: &WlBuffer) -> Option<(u32, u32, PixelData)> {
1176        let data = buffer.data::<DmaBufBufferData>()?;
1177        let width = data.width as u32;
1178        let height = data.height as u32;
1179        if width == 0 || height == 0 || data.planes.is_empty() {
1180            return None;
1181        }
1182        let plane = &data.planes[0];
1183        if matches!(
1184            data.fourcc,
1185            drm_fourcc::ARGB8888
1186                | drm_fourcc::XRGB8888
1187                | drm_fourcc::ABGR8888
1188                | drm_fourcc::XBGR8888
1189        ) {
1190            // Check if this is a DRM GEM fd (importable by VA-API) or an
1191            // anonymous /dmabuf heap fd (Vulkan WSI, needs CPU mmap).
1192            use std::os::fd::AsRawFd;
1193            let raw_fd = plane.fd.as_raw_fd();
1194            let _is_drm = {
1195                let mut link_buf = [0u8; 256];
1196                let path = format!("/proc/self/fd/{raw_fd}\0");
1197                let n = unsafe {
1198                    libc::readlink(
1199                        path.as_ptr() as *const _,
1200                        link_buf.as_mut_ptr() as *mut _,
1201                        255,
1202                    )
1203                };
1204                n > 0 && link_buf[..n as usize].starts_with(b"/dev/dri/")
1205            };
1206
1207            // Always dup the fd — the encoder handles both DRM GEM and
1208            // anonymous /dmabuf fds.  For /dmabuf fds, the encoder falls
1209            // back to CPU mmap internally.
1210            let owned = plane.fd.try_clone().ok()?;
1211            return Some((
1212                width,
1213                height,
1214                PixelData::DmaBuf {
1215                    fd: Arc::new(owned),
1216                    fourcc: data.fourcc,
1217                    modifier: data.modifier,
1218                    stride: plane.stride,
1219                    offset: plane.offset,
1220                },
1221            ));
1222        }
1223        None
1224    }
1225
1226    fn read_buffer(&self, buffer: &WlBuffer) -> Option<(u32, u32, PixelData)> {
1227        self.read_shm_buffer(buffer)
1228            .or_else(|| self.read_dmabuf_buffer(buffer))
1229    }
1230
1231    fn handle_surface_commit(&mut self, surface_id: &ObjectId) {
1232        let (root_id, toplevel_sid) = self.find_toplevel_root(surface_id);
1233
1234        // Always consume the pending buffer so the client gets a release
1235        // event.  Skipping this (e.g. when the surface has no toplevel
1236        // role yet) leaks a buffer from the client's pool on every attach,
1237        // eventually starving it and causing a hang.
1238        let had_buffer = self
1239            .surfaces
1240            .get(surface_id)
1241            .is_some_and(|s| s.pending_buffer.is_some());
1242        self.apply_pending_state(surface_id);
1243
1244        let toplevel_sid = match toplevel_sid {
1245            Some(sid) => sid,
1246            None => {
1247                // No toplevel yet — fire any pending frame callbacks so
1248                // the client doesn't stall.
1249                self.fire_surface_frame_callbacks(surface_id);
1250                let _ = self.display_handle.flush_clients();
1251                return;
1252            }
1253        };
1254
1255        // Composite at the output scale so HiDPI clients are rendered
1256        // at full resolution.  Use the browser's requested size as the
1257        // target so the frame fits the canvas without letterboxing.
1258        let s120 = self.output_scale_120;
1259        let target_phys = self.surface_sizes.get(&toplevel_sid).map(|&(lw, lh)| {
1260            let pw = super::render::to_physical(lw as u32, s120 as u32);
1261            let ph = super::render::to_physical(lh as u32, s120 as u32);
1262            (pw, ph)
1263        });
1264        let composited = if let Some(ref mut vk) = self.vulkan_renderer {
1265            vk.render_tree_sized(
1266                &root_id,
1267                &self.surfaces,
1268                &self.pixel_cache,
1269                s120,
1270                target_phys,
1271            )
1272        } else {
1273            None
1274        };
1275        let gpu_ok = composited.is_some();
1276        let composited = composited.or_else(|| {
1277            super::render::cpu_composite_from_cache(
1278                &root_id,
1279                &self.surfaces,
1280                &self.pixel_cache,
1281                s120,
1282            )
1283        });
1284
1285        if let Some((w, h, ref pixels)) = composited
1286            && !pixels.is_empty()
1287        {
1288            let kind = match pixels {
1289                PixelData::Bgra(_) => "bgra",
1290                PixelData::Rgba(_) => "rgba",
1291                PixelData::Nv12 { .. } => "nv12",
1292                PixelData::VaSurface { .. } => "va-surface",
1293                PixelData::DmaBuf { fd, .. } => {
1294                    use std::os::fd::AsRawFd;
1295                    let raw = fd.as_raw_fd();
1296                    let mut lb = [0u8; 128];
1297                    let p = format!("/proc/self/fd/{raw}\0");
1298                    let n = unsafe {
1299                        libc::readlink(p.as_ptr() as *const _, lb.as_mut_ptr() as *mut _, 127)
1300                    };
1301                    if n > 0 && lb[..n as usize].starts_with(b"/dev/dri/") {
1302                        "dmabuf-drm"
1303                    } else {
1304                        "dmabuf-anon"
1305                    }
1306                }
1307            };
1308            if self.verbose {
1309                static LC: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
1310                let lc = LC.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
1311                if lc < 3 || lc.is_multiple_of(1000) {
1312                    eprintln!("[pending #{lc}] {w}x{h} gpu={gpu_ok} kind={kind}");
1313                }
1314            }
1315            // Determine the logical size for pointer coordinate mapping.
1316            // The composited frame's physical dimensions (w, h) must pair
1317            // with a logical size that preserves the true DPR ratio so the
1318            // PointerMotion handler can convert browser pixel coords back
1319            // to Wayland logical coords correctly.  The simplest correct
1320            // approach is to derive logical size directly from the physical
1321            // size and the output scale — this works regardless of whether
1322            // the frame came from the Vulkan renderer (which targets the
1323            // browser's requested size) or the CPU renderer (which targets
1324            // the xdg_geometry content area).  The PointerMotion handler
1325            // separately adds the xdg_geometry offset to translate from
1326            // composited-frame space into surface-tree space, so the
1327            // logical size here should represent the full composited frame,
1328            // not just the xdg_geometry window extents.
1329            let s120_u32 = (s120 as u32).max(120);
1330            let log_w = (w * 120).div_ceil(s120_u32);
1331            let log_h = (h * 120).div_ceil(s120_u32);
1332            self.pending_commits
1333                .insert(toplevel_sid, (w, h, log_w, log_h, composited.unwrap().2));
1334        }
1335
1336        // Always fire frame callbacks after processing a commit, so
1337        // clients can continue their render loop.  Without this, clients
1338        // stall when the server doesn't send RequestFrame (e.g. during
1339        // resize or when no subscribers are connected).
1340        self.fire_frame_callbacks_for_toplevel(toplevel_sid);
1341
1342        // After an output scale change, re-send keyboard leave/enter on
1343        // the first commit so clients (especially Firefox) resume input
1344        // processing.  Deferred to here so the client has processed the
1345        // reconfigure before we re-enter.
1346        if self.pending_kb_reenter {
1347            self.pending_kb_reenter = false;
1348            let root_ids: Vec<ObjectId> = self.toplevel_surface_ids.values().cloned().collect();
1349            for root_id in root_ids {
1350                let wl = self.surfaces.get(&root_id).map(|s| s.wl_surface.clone());
1351                if let Some(wl) = wl {
1352                    let serial = self.next_serial();
1353                    for kb in &self.keyboards {
1354                        if same_client(kb, &wl) {
1355                            kb.leave(serial, &wl);
1356                        }
1357                    }
1358                    let serial = self.next_serial();
1359                    for kb in &self.keyboards {
1360                        if same_client(kb, &wl) {
1361                            kb.enter(serial, &wl, vec![]);
1362                        }
1363                    }
1364                }
1365            }
1366            let _ = self.display_handle.flush_clients();
1367        }
1368
1369        if self.verbose {
1370            let cache_entries = self.pixel_cache.len();
1371            let has_pending = self.pending_commits.contains_key(&toplevel_sid);
1372            static COMMIT_COUNT: std::sync::atomic::AtomicU64 =
1373                std::sync::atomic::AtomicU64::new(0);
1374            let n = COMMIT_COUNT.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
1375            if n < 5 || n.is_multiple_of(1000) {
1376                eprintln!(
1377                    "[commit #{n}] sid={surface_id:?} root={root_id:?} cache={cache_entries} pending={has_pending} buf={had_buffer}",
1378                );
1379            }
1380        }
1381    }
1382
1383    /// Compute the absolute position of a surface within its toplevel by
1384    /// walking up the parent chain and summing `subsurface_position` offsets.
1385    /// The toplevel root itself has position (0, 0).
1386    fn surface_absolute_position(&self, surface_id: &ObjectId) -> (i32, i32) {
1387        let mut x = 0i32;
1388        let mut y = 0i32;
1389        let mut current = surface_id.clone();
1390        while let Some(surf) = self.surfaces.get(&current) {
1391            x += surf.subsurface_position.0;
1392            y += surf.subsurface_position.1;
1393            match surf.parent_surface_id {
1394                Some(ref parent) => current = parent.clone(),
1395                None => break,
1396            }
1397        }
1398        (x, y)
1399    }
1400
1401    fn find_toplevel_root(&self, surface_id: &ObjectId) -> (ObjectId, Option<u16>) {
1402        let mut current = surface_id.clone();
1403        loop {
1404            match self.surfaces.get(&current) {
1405                Some(surf) => {
1406                    if let Some(ref parent) = surf.parent_surface_id {
1407                        current = parent.clone();
1408                    } else {
1409                        return (
1410                            current,
1411                            if surf.surface_id > 0 {
1412                                Some(surf.surface_id)
1413                            } else {
1414                                None
1415                            },
1416                        );
1417                    }
1418                }
1419                None => return (current, None),
1420            }
1421        }
1422    }
1423
1424    fn collect_surface_tree(&self, root_id: &ObjectId) -> Vec<ObjectId> {
1425        let mut result = Vec::new();
1426        self.collect_tree_recursive(root_id, &mut result);
1427        result
1428    }
1429
1430    fn collect_tree_recursive(&self, surface_id: &ObjectId, result: &mut Vec<ObjectId>) {
1431        result.push(surface_id.clone());
1432        if let Some(surf) = self.surfaces.get(surface_id) {
1433            for child_id in &surf.children {
1434                self.collect_tree_recursive(child_id, result);
1435            }
1436        }
1437    }
1438
1439    /// Walk the surface tree rooted at `root_id` and return the topmost
1440    /// surface whose pixel bounds contain (`x`, `y`).  Returns
1441    /// `(wl_surface, local_x, local_y)` with coordinates relative to the
1442    /// hit surface.  Falls back to the root surface when nothing else matches.
1443    fn hit_test_surface_at(
1444        &self,
1445        root_id: &ObjectId,
1446        x: f64,
1447        y: f64,
1448    ) -> Option<(WlSurface, f64, f64)> {
1449        self.hit_test_recursive(root_id, x, y, 0, 0).or_else(|| {
1450            // Fallback: return the root surface with the original coords.
1451            self.surfaces
1452                .get(root_id)
1453                .map(|s| (s.wl_surface.clone(), x, y))
1454        })
1455    }
1456
1457    fn hit_test_recursive(
1458        &self,
1459        surface_id: &ObjectId,
1460        x: f64,
1461        y: f64,
1462        offset_x: i32,
1463        offset_y: i32,
1464    ) -> Option<(WlSurface, f64, f64)> {
1465        let surf = self.surfaces.get(surface_id)?;
1466        let sx = offset_x + surf.subsurface_position.0;
1467        let sy = offset_y + surf.subsurface_position.1;
1468
1469        // Children are ordered back-to-front; iterate in reverse for topmost.
1470        for child_id in surf.children.iter().rev() {
1471            if let Some(hit) = self.hit_test_recursive(child_id, x, y, sx, sy) {
1472                return Some(hit);
1473            }
1474        }
1475
1476        // Check this surface's bounds (logical coordinates).
1477        if let Some(&(w, h, scale, _, _)) = self.pixel_cache.get(surface_id) {
1478            let s = scale.max(1) as f64;
1479            // Prefer viewport destination for logical size (fractional-scale
1480            // clients set buffer_scale=1 and declare logical size via viewport).
1481            let (lw, lh) = surf
1482                .viewport_destination
1483                .filter(|&(dw, dh)| dw > 0 && dh > 0)
1484                .map(|(dw, dh)| (dw as f64, dh as f64))
1485                .unwrap_or((w as f64 / s, h as f64 / s));
1486            let lx = x - sx as f64;
1487            let ly = y - sy as f64;
1488            if lx >= 0.0 && ly >= 0.0 && lx < lw && ly < lh {
1489                return Some((surf.wl_surface.clone(), lx, ly));
1490            }
1491        }
1492        None
1493    }
1494
1495    /// Apply double-buffered pending state and consume the pending buffer.
1496    ///
1497    /// DMA-BUF content is eagerly snapshot into CPU memory so the
1498    /// pixel_cache holds a stable copy.  Without this, sibling surfaces
1499    /// whose buffers were released in previous frames would be read from
1500    /// DMA-BUF fds that the client is actively rewriting — producing
1501    /// black rectangles wherever the client's GPU has already cleared
1502    /// the old content.
1503    fn apply_pending_state(&mut self, surface_id: &ObjectId) {
1504        let (buffer, scale, is_opaque) = {
1505            let Some(surf) = self.surfaces.get_mut(surface_id) else {
1506                return;
1507            };
1508            let buffer = surf.pending_buffer.take();
1509            let scale = surf.pending_buffer_scale;
1510            surf.buffer_scale = scale;
1511            surf.viewport_destination = surf.pending_viewport_destination;
1512            surf.is_opaque = surf.pending_opaque;
1513            surf.pending_damage = false;
1514            if let Some(pos) = surf.pending_subsurface_position.take() {
1515                surf.subsurface_position = pos;
1516            }
1517            (buffer, scale, surf.is_opaque)
1518        };
1519        let Some(buf) = buffer else { return };
1520        if let Some((w, h, pixels)) = self.read_buffer(&buf) {
1521            // For DMA-BUFs, eagerly read pixel data into a CPU-side
1522            // snapshot.  The pixel_cache may hold entries from surfaces
1523            // that committed many frames ago — those DMA-BUF fds
1524            // reference buffers the client has since reused and is
1525            // actively overwriting.  Snapshotting here means every
1526            // cache entry is a frozen copy from commit time, immune
1527            // to client-side GPU clears.
1528            let pixels = if pixels.is_dmabuf() {
1529                let rgba = pixels.to_rgba(w, h);
1530                if !rgba.is_empty() {
1531                    PixelData::Rgba(std::sync::Arc::new(rgba))
1532                } else {
1533                    // Fence not ready — keep the DMA-BUF reference as
1534                    // a best-effort fallback; the Vulkan renderer may
1535                    // still be able to import it directly.
1536                    pixels
1537                }
1538            } else {
1539                pixels
1540            };
1541            self.pixel_cache
1542                .insert(surface_id.clone(), (w, h, scale, is_opaque, pixels));
1543        }
1544        buf.release();
1545    }
1546
1547    fn fire_surface_frame_callbacks(&mut self, surface_id: &ObjectId) {
1548        let (callbacks, feedbacks) = {
1549            let Some(surf) = self.surfaces.get_mut(surface_id) else {
1550                return;
1551            };
1552            (
1553                std::mem::take(&mut surf.pending_frame_callbacks),
1554                std::mem::take(&mut surf.pending_presentation_feedbacks),
1555            )
1556        };
1557        let time = elapsed_ms();
1558        for cb in callbacks {
1559            cb.done(time);
1560        }
1561        if !feedbacks.is_empty() {
1562            let (sec, nsec) = monotonic_timespec();
1563            // Send sync_output for each feedback, then presented().
1564            // refresh=0 means unknown (headless, no real display).
1565            for fb in feedbacks {
1566                for output in &self.outputs {
1567                    if same_client(&fb, output) {
1568                        fb.sync_output(output);
1569                    }
1570                }
1571                fb.presented(
1572                    (sec >> 32) as u32,
1573                    sec as u32,
1574                    nsec as u32,
1575                    0, // refresh: unknown (headless)
1576                    0, // seq_hi
1577                    0, // seq_lo
1578                    WpPresentationFeedbackKind::empty(),
1579                );
1580            }
1581        }
1582    }
1583
1584    /// Remove surfaces whose underlying `WlSurface` is no longer alive.
1585    /// This handles the case where a Wayland client process exits or crashes
1586    /// without explicitly destroying its surfaces — `dispatch_clients()`
1587    /// marks the resources as dead, and we clean up here.
1588    fn cleanup_dead_surfaces(&mut self) {
1589        // Purge stale protocol objects from disconnected clients.
1590        self.fractional_scales.retain(|fs| fs.is_alive());
1591        self.outputs.retain(|o| o.is_alive());
1592        self.keyboards.retain(|k| k.is_alive());
1593        self.pointers.retain(|p| p.is_alive());
1594        self.data_devices.retain(|d| d.is_alive());
1595        self.primary_devices.retain(|d| d.is_alive());
1596        self.relative_pointers.retain(|p| p.is_alive());
1597        self.text_inputs.retain(|ti| ti.resource.is_alive());
1598        self.shm_pools.retain(|_, p| p.resource.is_alive());
1599        self.dmabuf_params.retain(|_, p| p.resource.is_alive());
1600        self.positioners.retain(|_, p| p.resource.is_alive());
1601
1602        let dead: Vec<ObjectId> = self
1603            .surfaces
1604            .iter()
1605            .filter(|(_, surf)| !surf.wl_surface.is_alive())
1606            .map(|(id, _)| id.clone())
1607            .collect();
1608
1609        for proto_id in &dead {
1610            self.pixel_cache.remove(proto_id);
1611            if let Some(surf) = self.surfaces.remove(proto_id) {
1612                // Discard any pending presentation feedbacks — the surface
1613                // died before the frame was ever presented.
1614                for fb in surf.pending_presentation_feedbacks {
1615                    fb.discarded();
1616                }
1617                if let Some(ref parent_id) = surf.parent_surface_id
1618                    && let Some(parent) = self.surfaces.get_mut(parent_id)
1619                {
1620                    parent.children.retain(|c| c != proto_id);
1621                }
1622                if surf.surface_id > 0 {
1623                    self.toplevel_surface_ids.remove(&surf.surface_id);
1624                    self.last_reported_size.remove(&surf.surface_id);
1625                    self.surface_sizes.remove(&surf.surface_id);
1626                    let _ = self.event_tx.send(CompositorEvent::SurfaceDestroyed {
1627                        surface_id: surf.surface_id,
1628                    });
1629                    (self.event_notify)();
1630                }
1631            }
1632        }
1633    }
1634
1635    fn fire_frame_callbacks_for_toplevel(&mut self, toplevel_sid: u16) {
1636        let Some(root_id) = self.toplevel_surface_ids.get(&toplevel_sid).cloned() else {
1637            return;
1638        };
1639        let tree = self.collect_surface_tree(&root_id);
1640        for sid in &tree {
1641            self.fire_surface_frame_callbacks(sid);
1642        }
1643        let _ = self.display_handle.flush_clients();
1644    }
1645
1646    fn handle_cursor_commit(&mut self, surface_id: &ObjectId) {
1647        self.apply_pending_state(surface_id);
1648        let hotspot = self
1649            .surfaces
1650            .get(surface_id)
1651            .map(|s| s.cursor_hotspot)
1652            .unwrap_or((0, 0));
1653        if let Some((w, h, _scale, _opaque, pixels)) = self.pixel_cache.get(surface_id) {
1654            let rgba = pixels.to_rgba(*w, *h);
1655            if !rgba.is_empty() {
1656                let _ = self.event_tx.send(CompositorEvent::SurfaceCursor {
1657                    surface_id: self.focused_surface_id,
1658                    cursor: CursorImage::Custom {
1659                        hotspot_x: hotspot.0 as u16,
1660                        hotspot_y: hotspot.1 as u16,
1661                        width: *w as u16,
1662                        height: *h as u16,
1663                        rgba,
1664                    },
1665                });
1666            }
1667        }
1668        self.fire_surface_frame_callbacks(surface_id);
1669        let _ = self.display_handle.flush_clients();
1670    }
1671
1672    fn handle_command(&mut self, cmd: CompositorCommand) {
1673        match cmd {
1674            CompositorCommand::KeyInput {
1675                surface_id: _,
1676                keycode,
1677                pressed,
1678            } => {
1679                let serial = self.next_serial();
1680                let time = elapsed_ms();
1681                let state = if pressed {
1682                    wl_keyboard::KeyState::Pressed
1683                } else {
1684                    wl_keyboard::KeyState::Released
1685                };
1686                let focused_wl = self
1687                    .toplevel_surface_ids
1688                    .get(&self.focused_surface_id)
1689                    .and_then(|root_id| self.surfaces.get(root_id))
1690                    .map(|s| s.wl_surface.clone());
1691                for kb in &self.keyboards {
1692                    if let Some(ref wl) = focused_wl
1693                        && same_client(kb, wl)
1694                    {
1695                        kb.key(serial, time, keycode, state);
1696                    }
1697                }
1698                // Send wl_keyboard.modifiers if this key changed modifier
1699                // state.  Many Wayland clients (GTK, Chromium, Qt) rely on
1700                // this event rather than computing modifiers from raw key
1701                // events.
1702                self.update_and_send_modifiers(keycode, pressed);
1703                let _ = self.display_handle.flush_clients();
1704            }
1705            CompositorCommand::TextInput { text } => {
1706                let focused_wl = self
1707                    .toplevel_surface_ids
1708                    .get(&self.focused_surface_id)
1709                    .and_then(|root_id| self.surfaces.get(root_id))
1710                    .map(|s| s.wl_surface.clone());
1711                let Some(focused_wl) = focused_wl else { return };
1712
1713                // Synthesise evdev key sequences for ASCII
1714                // characters that exist on the US-QWERTY layout.
1715                const KEY_LEFTSHIFT: u32 = 42;
1716                for ch in text.chars() {
1717                    if let Some((kc, need_shift)) = char_to_keycode(ch) {
1718                        let time = elapsed_ms();
1719                        if need_shift {
1720                            let serial = self.next_serial();
1721                            for kb in &self.keyboards {
1722                                if same_client(kb, &focused_wl) {
1723                                    kb.key(
1724                                        serial,
1725                                        time,
1726                                        KEY_LEFTSHIFT,
1727                                        wl_keyboard::KeyState::Pressed,
1728                                    );
1729                                }
1730                            }
1731                            self.update_and_send_modifiers(KEY_LEFTSHIFT, true);
1732                        }
1733                        let serial = self.next_serial();
1734                        for kb in &self.keyboards {
1735                            if same_client(kb, &focused_wl) {
1736                                kb.key(serial, time, kc, wl_keyboard::KeyState::Pressed);
1737                            }
1738                        }
1739                        let serial = self.next_serial();
1740                        for kb in &self.keyboards {
1741                            if same_client(kb, &focused_wl) {
1742                                kb.key(serial, time, kc, wl_keyboard::KeyState::Released);
1743                            }
1744                        }
1745                        if need_shift {
1746                            let serial = self.next_serial();
1747                            for kb in &self.keyboards {
1748                                if same_client(kb, &focused_wl) {
1749                                    kb.key(
1750                                        serial,
1751                                        time,
1752                                        KEY_LEFTSHIFT,
1753                                        wl_keyboard::KeyState::Released,
1754                                    );
1755                                }
1756                            }
1757                            self.update_and_send_modifiers(KEY_LEFTSHIFT, false);
1758                        }
1759                    }
1760                    // Non-ASCII characters without a text_input_v3 path
1761                    // are silently dropped.
1762                }
1763                let _ = self.display_handle.flush_clients();
1764            }
1765            CompositorCommand::PointerMotion { surface_id, x, y } => {
1766                let time = elapsed_ms();
1767                // The browser sends coordinates in the composited frame's
1768                // physical pixel space.  Convert to logical (surface-local)
1769                // coordinates using the actual composited-to-logical ratio
1770                // for this surface.
1771                let (mut x, mut y) =
1772                    if let Some(&(cw, ch, lw, lh)) = self.last_reported_size.get(&surface_id) {
1773                        let sx = if cw > 0 { lw as f64 / cw as f64 } else { 1.0 };
1774                        let sy = if ch > 0 { lh as f64 / ch as f64 } else { 1.0 };
1775                        (x * sx, y * sy)
1776                    } else {
1777                        (x, y)
1778                    };
1779                // The composited frame is cropped to xdg_geometry (if set),
1780                // so the browser's (0,0) corresponds to (geo_x, geo_y) in the
1781                // surface tree.  Offset accordingly.
1782                if let Some((gx, gy, _, _)) = self
1783                    .toplevel_surface_ids
1784                    .get(&surface_id)
1785                    .and_then(|rid| self.surfaces.get(rid))
1786                    .and_then(|s| s.xdg_geometry)
1787                {
1788                    x += gx as f64;
1789                    y += gy as f64;
1790                }
1791                // Hit-test the surface tree to find the actual target
1792                // (may be a subsurface or popup rather than the root).
1793                let target_wl = self
1794                    .toplevel_surface_ids
1795                    .get(&surface_id)
1796                    .and_then(|root_id| self.hit_test_surface_at(root_id, x, y))
1797                    .map(|(wl_surface, lx, ly)| (wl_surface.id(), wl_surface, lx, ly));
1798
1799                static PTR_DBG: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
1800                let pn = PTR_DBG.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
1801                if pn < 5 || pn.is_multiple_of(500) {
1802                    let root = self.toplevel_surface_ids.get(&surface_id).cloned();
1803                    let lrs = self.last_reported_size.get(&surface_id).copied();
1804                    eprintln!(
1805                        "[pointer #{pn}] sid={surface_id} logical=({x:.1},{y:.1}) lrs={lrs:?} root={root:?} hit={:?}",
1806                        target_wl.as_ref().map(|(pid, _, lx, ly)| format!(
1807                            "proto={pid:?} local=({lx:.1},{ly:.1})"
1808                        ))
1809                    );
1810                }
1811                if let Some((proto_id, wl_surface, lx, ly)) = target_wl {
1812                    if self.pointer_entered_id.as_ref() != Some(&proto_id) {
1813                        let serial = self.next_serial();
1814                        let matching_ptrs = self
1815                            .pointers
1816                            .iter()
1817                            .filter(|p| same_client(*p, &wl_surface))
1818                            .count();
1819                        eprintln!(
1820                            "[pointer-enter] proto={proto_id:?} matching_ptrs={matching_ptrs} total_ptrs={}",
1821                            self.pointers.len()
1822                        );
1823                        // Leave old surface.
1824                        if self.pointer_entered_id.is_some() {
1825                            let old_wl = self
1826                                .surfaces
1827                                .values()
1828                                .find(|s| Some(s.wl_surface.id()) == self.pointer_entered_id)
1829                                .map(|s| s.wl_surface.clone());
1830                            if let Some(old_wl) = old_wl {
1831                                for ptr in &self.pointers {
1832                                    if same_client(ptr, &old_wl) {
1833                                        ptr.leave(serial, &old_wl);
1834                                    }
1835                                }
1836                            }
1837                        }
1838                        for ptr in &self.pointers {
1839                            if same_client(ptr, &wl_surface) {
1840                                ptr.enter(serial, &wl_surface, lx, ly);
1841                            }
1842                        }
1843                        self.pointer_entered_id = Some(proto_id);
1844                    }
1845                    for ptr in &self.pointers {
1846                        if same_client(ptr, &wl_surface) {
1847                            ptr.motion(time, lx, ly);
1848                            ptr.frame();
1849                        }
1850                    }
1851                }
1852                // When no surface is hit, don't send motion events —
1853                // there is no valid surface-local coordinate to report.
1854                let _ = self.display_handle.flush_clients();
1855            }
1856            CompositorCommand::PointerButton {
1857                surface_id: _,
1858                button,
1859                pressed,
1860            } => {
1861                let serial = self.next_serial();
1862                let time = elapsed_ms();
1863                let state = if pressed {
1864                    wl_pointer::ButtonState::Pressed
1865                } else {
1866                    wl_pointer::ButtonState::Released
1867                };
1868                let focused_wl = self
1869                    .surfaces
1870                    .values()
1871                    .find(|s| Some(s.wl_surface.id()) == self.pointer_entered_id)
1872                    .map(|s| s.wl_surface.clone());
1873                for ptr in &self.pointers {
1874                    if let Some(ref wl) = focused_wl
1875                        && same_client(ptr, wl)
1876                    {
1877                        ptr.button(serial, time, button, state);
1878                        ptr.frame();
1879                    }
1880                }
1881                let _ = self.display_handle.flush_clients();
1882            }
1883            CompositorCommand::PointerAxis {
1884                surface_id: _,
1885                axis,
1886                value,
1887            } => {
1888                let time = elapsed_ms();
1889                let wl_axis = if axis == 0 {
1890                    wl_pointer::Axis::VerticalScroll
1891                } else {
1892                    wl_pointer::Axis::HorizontalScroll
1893                };
1894                let focused_wl = self
1895                    .surfaces
1896                    .values()
1897                    .find(|s| Some(s.wl_surface.id()) == self.pointer_entered_id)
1898                    .map(|s| s.wl_surface.clone());
1899                for ptr in &self.pointers {
1900                    if let Some(ref wl) = focused_wl
1901                        && same_client(ptr, wl)
1902                    {
1903                        ptr.axis(time, wl_axis, value);
1904                        ptr.frame();
1905                    }
1906                }
1907                let _ = self.display_handle.flush_clients();
1908            }
1909            CompositorCommand::SurfaceResize {
1910                surface_id,
1911                width,
1912                height,
1913                scale_120,
1914            } => {
1915                // The browser sends physical pixels (cssW × DPR).  Convert
1916                // to logical (CSS) pixels for use in Wayland configures.
1917                let s_in = (scale_120 as i32).max(120);
1918                let w = (width as i32) * 120 / s_in;
1919                let h = (height as i32) * 120 / s_in;
1920                self.surface_sizes.insert(surface_id, (w, h));
1921
1922                // Track whether output properties changed so we can batch
1923                // all events before a single output.done().
1924                let mut output_changed = false;
1925
1926                // Update output scale (in 1/120th units) from the browser DPR.
1927                if scale_120 > 0 && scale_120 != self.output_scale_120 {
1928                    self.output_scale_120 = scale_120;
1929                    output_changed = true;
1930                }
1931
1932                let s120 = self.output_scale_120 as i32;
1933
1934                // Recompute output dimensions from scratch (start from 0,0)
1935                // so the output can shrink when surfaces get smaller or are
1936                // destroyed.  The previous fold started from (output_width,
1937                // output_height) which meant dimensions could only grow.
1938                let (max_w, max_h) = self
1939                    .surface_sizes
1940                    .values()
1941                    .fold((0i32, 0i32), |(mw, mh), &(sw, sh)| (mw.max(sw), mh.max(sh)));
1942                // Clamp to a sensible minimum so the output is never 0×0.
1943                let max_w = max_w.max(1);
1944                let max_h = max_h.max(1);
1945                if max_w != self.output_width || max_h != self.output_height {
1946                    self.output_width = max_w;
1947                    self.output_height = max_h;
1948                    output_changed = true;
1949                }
1950
1951                // When any output property changed, re-send the full
1952                // sequence so clients see it as a display configuration
1953                // change: geometry → mode → scale → fractional_scale → done.
1954                if output_changed {
1955                    let int_scale = ((s120) + 119) / 120;
1956                    for output in &self.outputs {
1957                        output.geometry(
1958                            0,
1959                            0,
1960                            0,
1961                            0,
1962                            wl_output::Subpixel::None,
1963                            "blit".to_string(),
1964                            "virtual".to_string(),
1965                            wl_output::Transform::Normal,
1966                        );
1967                        // mode() takes physical pixels: logical × scale.
1968                        let mode_w = self.output_width * s120 / 120;
1969                        let mode_h = self.output_height * s120 / 120;
1970                        output.mode(
1971                            wl_output::Mode::Current | wl_output::Mode::Preferred,
1972                            mode_w,
1973                            mode_h,
1974                            60_000,
1975                        );
1976                        if output.version() >= 2 {
1977                            output.scale(int_scale);
1978                        }
1979                    }
1980                    for fs in &self.fractional_scales {
1981                        fs.preferred_scale(s120 as u32);
1982                    }
1983                }
1984
1985                // Single output.done() after all property changes, so the
1986                // client sees scale + mode atomically before the configure.
1987                if output_changed {
1988                    for output in &self.outputs {
1989                        if output.version() >= 2 {
1990                            output.done();
1991                        }
1992                    }
1993                }
1994
1995                let states = xdg_toplevel_states(&[
1996                    xdg_toplevel::State::Activated,
1997                    xdg_toplevel::State::Maximized,
1998                ]);
1999
2000                if output_changed {
2001                    // When output scale or dimensions changed, every
2002                    // toplevel needs a new configure so it re-renders at
2003                    // the correct density / size.
2004                    for (&sid, root_id) in &self.toplevel_surface_ids {
2005                        let (lw, lh) = self.surface_sizes.get(&sid).copied().unwrap_or((w, h));
2006                        if let Some(surf) = self.surfaces.get(root_id) {
2007                            if let Some(ref tl) = surf.xdg_toplevel {
2008                                tl.configure(lw, lh, states.clone());
2009                            }
2010                            if let Some(ref xs) = surf.xdg_surface {
2011                                let serial = self.serial.wrapping_add(1);
2012                                self.serial = serial;
2013                                xs.configure(serial);
2014                            }
2015                        }
2016                    }
2017                    // Fire frame callbacks so all clients repaint at new
2018                    // scale.
2019                    let all_sids: Vec<u16> = self.toplevel_surface_ids.keys().copied().collect();
2020                    for sid in all_sids {
2021                        self.fire_frame_callbacks_for_toplevel(sid);
2022                    }
2023
2024                    // Reset pointer/keyboard state — scale change
2025                    // invalidates coordinate mappings.
2026                    self.pointer_entered_id = None;
2027                    self.pending_kb_reenter = true;
2028                } else {
2029                    // Only the target surface changed size — configure just
2030                    // that one.  This avoids disturbing other surfaces'
2031                    // frame callback / render cycle, which would race with
2032                    // the server's RequestFrame mechanism and stall them.
2033                    if let Some(root_id) = self.toplevel_surface_ids.get(&surface_id)
2034                        && let Some(surf) = self.surfaces.get(root_id)
2035                    {
2036                        if let Some(ref tl) = surf.xdg_toplevel {
2037                            tl.configure(w, h, states);
2038                        }
2039                        if let Some(ref xs) = surf.xdg_surface {
2040                            let serial = self.serial.wrapping_add(1);
2041                            self.serial = serial;
2042                            xs.configure(serial);
2043                        }
2044                    }
2045                    self.fire_frame_callbacks_for_toplevel(surface_id);
2046                }
2047                let _ = self.display_handle.flush_clients();
2048            }
2049            CompositorCommand::SurfaceFocus { surface_id } => {
2050                self.set_keyboard_focus(surface_id);
2051                let _ = self.display_handle.flush_clients();
2052            }
2053            CompositorCommand::SurfaceClose { surface_id } => {
2054                if let Some(root_id) = self.toplevel_surface_ids.get(&surface_id)
2055                    && let Some(surf) = self.surfaces.get(root_id)
2056                    && let Some(ref tl) = surf.xdg_toplevel
2057                {
2058                    tl.close();
2059                }
2060                let _ = self.display_handle.flush_clients();
2061            }
2062            CompositorCommand::ClipboardOffer { mime_type, data } => {
2063                self.external_clipboard = Some(ExternalClipboard { mime_type, data });
2064                // Invalidate the Wayland-side selection — external takes over.
2065                self.selection_source = None;
2066                self.offer_external_clipboard();
2067            }
2068            CompositorCommand::Capture {
2069                surface_id,
2070                scale_120,
2071                reply,
2072            } => {
2073                // Use the capture-specific scale if provided, otherwise
2074                // fall back to the current output scale.
2075                let cap_s120 = if scale_120 > 0 {
2076                    scale_120
2077                } else {
2078                    self.output_scale_120
2079                };
2080                let result = if let Some(root_id) = self.toplevel_surface_ids.get(&surface_id) {
2081                    super::render::cpu_composite_from_cache(
2082                        root_id,
2083                        &self.surfaces,
2084                        &self.pixel_cache,
2085                        cap_s120,
2086                    )
2087                    .map(|(w, h, pixels)| {
2088                        let rgba = pixels.to_rgba(w, h);
2089                        (w, h, rgba)
2090                    })
2091                } else {
2092                    None
2093                };
2094                let _ = reply.send(result);
2095            }
2096            CompositorCommand::RequestFrame { surface_id } => {
2097                self.fire_frame_callbacks_for_toplevel(surface_id);
2098            }
2099            CompositorCommand::ReleaseKeys { keycodes } => {
2100                let time = elapsed_ms();
2101                let focused_wl = self
2102                    .toplevel_surface_ids
2103                    .get(&self.focused_surface_id)
2104                    .and_then(|root_id| self.surfaces.get(root_id))
2105                    .map(|s| s.wl_surface.clone());
2106                for keycode in &keycodes {
2107                    let serial = self.next_serial();
2108                    for kb in &self.keyboards {
2109                        if let Some(ref wl) = focused_wl
2110                            && same_client(kb, wl)
2111                        {
2112                            kb.key(serial, time, *keycode, wl_keyboard::KeyState::Released);
2113                        }
2114                    }
2115                }
2116                // Update modifier state for any released modifier keys.
2117                for keycode in &keycodes {
2118                    self.update_and_send_modifiers(*keycode, false);
2119                }
2120                let _ = self.display_handle.flush_clients();
2121            }
2122            CompositorCommand::ClipboardListMimes { reply } => {
2123                let mimes = self.collect_clipboard_mime_types();
2124                let _ = reply.send(mimes);
2125            }
2126            CompositorCommand::ClipboardGet { mime_type, reply } => {
2127                let data = self.get_clipboard_content(&mime_type);
2128                let _ = reply.send(data);
2129            }
2130            CompositorCommand::SetExternalOutputBuffers { buffers } => {
2131                if let Some(ref mut vk) = self.vulkan_renderer {
2132                    vk.set_external_output_buffers(buffers);
2133                }
2134            }
2135            CompositorCommand::Shutdown => {
2136                self.shutdown.store(true, Ordering::Relaxed);
2137                self.loop_signal.stop();
2138            }
2139        }
2140    }
2141}
2142
2143impl Compositor {
2144    /// Collect all MIME types available on the current clipboard.
2145    fn collect_clipboard_mime_types(&self) -> Vec<String> {
2146        // If a Wayland app owns the selection, use its MIME types.
2147        if let Some(ref src) = self.selection_source {
2148            let data = src.data::<DataSourceData>().unwrap();
2149            return data.mime_types.lock().unwrap().clone();
2150        }
2151        // Otherwise use the external (browser/CLI) clipboard.
2152        if let Some(ref cb) = self.external_clipboard
2153            && !cb.mime_type.is_empty()
2154        {
2155            let mut mimes = vec![cb.mime_type.clone()];
2156            // Add standard text aliases.
2157            if cb.mime_type.starts_with("text/plain") {
2158                if cb.mime_type != "text/plain" {
2159                    mimes.push("text/plain".to_string());
2160                }
2161                if cb.mime_type != "text/plain;charset=utf-8" {
2162                    mimes.push("text/plain;charset=utf-8".to_string());
2163                }
2164                mimes.push("UTF8_STRING".to_string());
2165            }
2166            return mimes;
2167        }
2168        Vec::new()
2169    }
2170
2171    /// Get clipboard content for a specific MIME type.
2172    fn get_clipboard_content(&mut self, mime_type: &str) -> Option<Vec<u8>> {
2173        // If external clipboard matches, return its data directly.
2174        if let Some(ref cb) = self.external_clipboard
2175            && self.selection_source.is_none()
2176        {
2177            // External clipboard is active.
2178            let matches = cb.mime_type == mime_type
2179                || (cb.mime_type.starts_with("text/plain")
2180                    && (mime_type == "text/plain"
2181                        || mime_type == "text/plain;charset=utf-8"
2182                        || mime_type == "UTF8_STRING"));
2183            if matches {
2184                return Some(cb.data.clone());
2185            }
2186            return None;
2187        }
2188        // If a Wayland app owns the selection, read from it via pipe.
2189        if let Some(src) = self.selection_source.clone() {
2190            return self.read_data_source_sync(&src, mime_type);
2191        }
2192        None
2193    }
2194
2195    /// Synchronously read data from a Wayland data source via pipe.
2196    fn read_data_source_sync(&mut self, source: &WlDataSource, mime_type: &str) -> Option<Vec<u8>> {
2197        let mut fds = [0i32; 2];
2198        if unsafe { libc::pipe(fds.as_mut_ptr()) } != 0 {
2199            return None;
2200        }
2201        let read_fd = unsafe { OwnedFd::from_raw_fd(fds[0]) };
2202        let write_fd = unsafe { OwnedFd::from_raw_fd(fds[1]) };
2203        source.send(mime_type.to_string(), write_fd.as_fd());
2204        let _ = self.display_handle.flush_clients();
2205        drop(write_fd); // close write end so read gets EOF
2206        // Non-blocking read with a modest limit.
2207        unsafe {
2208            libc::fcntl(read_fd.as_raw_fd(), libc::F_SETFL, libc::O_NONBLOCK);
2209        }
2210        std::thread::sleep(std::time::Duration::from_millis(5));
2211        let mut buf = Vec::new();
2212        let mut tmp = [0u8; 8192];
2213        loop {
2214            let n = unsafe {
2215                libc::read(
2216                    read_fd.as_raw_fd(),
2217                    tmp.as_mut_ptr() as *mut libc::c_void,
2218                    tmp.len(),
2219                )
2220            };
2221            if n <= 0 {
2222                break;
2223            }
2224            buf.extend_from_slice(&tmp[..n as usize]);
2225            if buf.len() > 1024 * 1024 {
2226                break; // 1 MiB cap
2227            }
2228        }
2229        if buf.is_empty() { None } else { Some(buf) }
2230    }
2231}
2232
2233// ---------------------------------------------------------------------------
2234// Helpers
2235// ---------------------------------------------------------------------------
2236
2237/// Read `CLOCK_MONOTONIC` and return `(tv_sec, tv_nsec)`.
2238fn monotonic_timespec() -> (i64, i64) {
2239    let mut ts = libc::timespec {
2240        tv_sec: 0,
2241        tv_nsec: 0,
2242    };
2243    // SAFETY: clock_gettime with CLOCK_MONOTONIC is always valid.
2244    unsafe { libc::clock_gettime(libc::CLOCK_MONOTONIC, &mut ts) };
2245    (ts.tv_sec, ts.tv_nsec)
2246}
2247
2248fn elapsed_ms() -> u32 {
2249    // Use CLOCK_MONOTONIC directly so the timestamp matches what Wayland
2250    // clients (especially Chromium/Brave) expect for frame-latency
2251    // calculations.  The previous implementation measured from an arbitrary
2252    // epoch which caused Chromium to report negative frame latency.
2253    let (sec, nsec) = monotonic_timespec();
2254    (sec as u32)
2255        .wrapping_mul(1000)
2256        .wrapping_add(nsec as u32 / 1_000_000)
2257}
2258
2259/// Returns true when two Wayland resources belong to the same still-connected client.
2260fn same_client<R1: Resource, R2: Resource>(a: &R1, b: &R2) -> bool {
2261    match (a.client(), b.client()) {
2262        (Some(ca), Some(cb)) => ca.id() == cb.id(),
2263        _ => false,
2264    }
2265}
2266
2267fn yuv420_to_rgb(y: u8, u: u8, v: u8) -> [u8; 3] {
2268    let y = (y as i32 - 16).max(0);
2269    let u = u as i32 - 128;
2270    let v = v as i32 - 128;
2271    let r = ((298 * y + 409 * v + 128) >> 8).clamp(0, 255) as u8;
2272    let g = ((298 * y - 100 * u - 208 * v + 128) >> 8).clamp(0, 255) as u8;
2273    let b = ((298 * y + 516 * u + 128) >> 8).clamp(0, 255) as u8;
2274    [r, g, b]
2275}
2276
2277/// Encode xdg_toplevel states as the raw byte array expected by the protocol.
2278fn xdg_toplevel_states(states: &[xdg_toplevel::State]) -> Vec<u8> {
2279    let mut bytes = Vec::with_capacity(states.len() * 4);
2280    for state in states {
2281        bytes.extend_from_slice(&(*state as u32).to_ne_bytes());
2282    }
2283    bytes
2284}
2285
2286fn create_keymap_fd(keymap_data: &[u8]) -> Option<OwnedFd> {
2287    use std::io::Write;
2288    let name = c"blit-keymap";
2289    let raw_fd = unsafe { libc::memfd_create(name.as_ptr(), libc::MFD_CLOEXEC) };
2290    if raw_fd < 0 {
2291        return None;
2292    }
2293    let fd = unsafe { OwnedFd::from_raw_fd(raw_fd) };
2294    let mut file = std::fs::File::from(fd);
2295    file.write_all(keymap_data).ok()?;
2296    Some(file.into())
2297}
2298
2299// ---------------------------------------------------------------------------
2300// Protocol dispatch implementations
2301// ---------------------------------------------------------------------------
2302
2303// -- wl_compositor --
2304
2305impl GlobalDispatch<WlCompositor, ()> for Compositor {
2306    fn bind(
2307        _state: &mut Self,
2308        _handle: &DisplayHandle,
2309        _client: &Client,
2310        resource: New<WlCompositor>,
2311        _data: &(),
2312        data_init: &mut DataInit<'_, Self>,
2313    ) {
2314        data_init.init(resource, ());
2315    }
2316}
2317
2318impl Dispatch<WlCompositor, ()> for Compositor {
2319    fn request(
2320        state: &mut Self,
2321        _client: &Client,
2322        _resource: &WlCompositor,
2323        request: <WlCompositor as Resource>::Request,
2324        _data: &(),
2325        _dh: &DisplayHandle,
2326        data_init: &mut DataInit<'_, Self>,
2327    ) {
2328        use wayland_server::protocol::wl_compositor::Request;
2329        match request {
2330            Request::CreateSurface { id } => {
2331                let surface = data_init.init(id, ());
2332                let proto_id = surface.id();
2333                state.surfaces.insert(
2334                    proto_id,
2335                    Surface {
2336                        surface_id: 0,
2337                        wl_surface: surface,
2338                        pending_buffer: None,
2339                        pending_buffer_scale: 1,
2340                        pending_damage: false,
2341                        pending_frame_callbacks: Vec::new(),
2342                        pending_presentation_feedbacks: Vec::new(),
2343                        pending_opaque: false,
2344                        buffer_scale: 1,
2345                        is_opaque: false,
2346                        parent_surface_id: None,
2347                        pending_subsurface_position: None,
2348                        subsurface_position: (0, 0),
2349                        children: Vec::new(),
2350                        xdg_surface: None,
2351                        xdg_toplevel: None,
2352                        xdg_popup: None,
2353                        xdg_geometry: None,
2354                        title: String::new(),
2355                        app_id: String::new(),
2356                        pending_viewport_destination: None,
2357                        viewport_destination: None,
2358                        is_cursor: false,
2359                        cursor_hotspot: (0, 0),
2360                    },
2361                );
2362            }
2363            Request::CreateRegion { id } => {
2364                data_init.init(id, ());
2365            }
2366            _ => {}
2367        }
2368    }
2369}
2370
2371// -- wl_surface --
2372
2373impl Dispatch<WlSurface, ()> for Compositor {
2374    fn request(
2375        state: &mut Self,
2376        _client: &Client,
2377        resource: &WlSurface,
2378        request: <WlSurface as Resource>::Request,
2379        _data: &(),
2380        _dh: &DisplayHandle,
2381        data_init: &mut DataInit<'_, Self>,
2382    ) {
2383        use wayland_server::protocol::wl_surface::Request;
2384        let sid = resource.id();
2385        match request {
2386            Request::Attach { buffer, x: _, y: _ } => {
2387                if let Some(surf) = state.surfaces.get_mut(&sid) {
2388                    surf.pending_buffer = buffer;
2389                }
2390            }
2391            Request::Damage { .. } | Request::DamageBuffer { .. } => {
2392                if let Some(surf) = state.surfaces.get_mut(&sid) {
2393                    surf.pending_damage = true;
2394                }
2395            }
2396            Request::Frame { callback } => {
2397                let cb = data_init.init(callback, ());
2398                if let Some(surf) = state.surfaces.get_mut(&sid) {
2399                    surf.pending_frame_callbacks.push(cb);
2400                }
2401            }
2402            Request::SetBufferScale { scale } => {
2403                if let Some(surf) = state.surfaces.get_mut(&sid) {
2404                    surf.pending_buffer_scale = scale;
2405                }
2406            }
2407            Request::SetOpaqueRegion { region: _ } => {
2408                if let Some(surf) = state.surfaces.get_mut(&sid) {
2409                    surf.pending_opaque = true;
2410                }
2411            }
2412            Request::SetInputRegion { .. } => {}
2413            Request::Commit => {
2414                let is_cursor = state.surfaces.get(&sid).is_some_and(|s| s.is_cursor);
2415                if is_cursor {
2416                    state.handle_cursor_commit(&sid);
2417                } else {
2418                    state.handle_surface_commit(&sid);
2419                }
2420            }
2421            Request::SetBufferTransform { .. } => {}
2422            Request::Offset { .. } => {}
2423            Request::Destroy => {
2424                state.pixel_cache.remove(&sid);
2425                if let Some(parent_id) = state
2426                    .surfaces
2427                    .get(&sid)
2428                    .and_then(|s| s.parent_surface_id.clone())
2429                    && let Some(parent) = state.surfaces.get_mut(&parent_id)
2430                {
2431                    parent.children.retain(|c| *c != sid);
2432                }
2433                if let Some(surf) = state.surfaces.remove(&sid) {
2434                    for fb in surf.pending_presentation_feedbacks {
2435                        fb.discarded();
2436                    }
2437                    if surf.surface_id > 0 {
2438                        state.toplevel_surface_ids.remove(&surf.surface_id);
2439                        state.last_reported_size.remove(&surf.surface_id);
2440                        state.surface_sizes.remove(&surf.surface_id);
2441                        let _ = state.event_tx.send(CompositorEvent::SurfaceDestroyed {
2442                            surface_id: surf.surface_id,
2443                        });
2444                        (state.event_notify)();
2445                    }
2446                }
2447            }
2448            _ => {}
2449        }
2450    }
2451}
2452
2453// -- wl_callback --
2454impl Dispatch<WlCallback, ()> for Compositor {
2455    fn request(
2456        _: &mut Self,
2457        _: &Client,
2458        _: &WlCallback,
2459        _: <WlCallback as Resource>::Request,
2460        _: &(),
2461        _: &DisplayHandle,
2462        _: &mut DataInit<'_, Self>,
2463    ) {
2464    }
2465}
2466
2467// -- wp_presentation --
2468impl GlobalDispatch<WpPresentation, ()> for Compositor {
2469    fn bind(
2470        _: &mut Self,
2471        _: &DisplayHandle,
2472        _: &Client,
2473        resource: New<WpPresentation>,
2474        _: &(),
2475        data_init: &mut DataInit<'_, Self>,
2476    ) {
2477        let pres = data_init.init(resource, ());
2478        // Tell the client we use CLOCK_MONOTONIC for presentation timestamps.
2479        pres.clock_id(libc::CLOCK_MONOTONIC as u32);
2480    }
2481}
2482
2483impl Dispatch<WpPresentation, ()> for Compositor {
2484    fn request(
2485        state: &mut Self,
2486        _: &Client,
2487        _: &WpPresentation,
2488        request: <WpPresentation as Resource>::Request,
2489        _: &(),
2490        _: &DisplayHandle,
2491        data_init: &mut DataInit<'_, Self>,
2492    ) {
2493        use wp_presentation::Request;
2494        match request {
2495            Request::Feedback { surface, callback } => {
2496                let fb = data_init.init(callback, ());
2497                let sid = surface.id();
2498                if let Some(surf) = state.surfaces.get_mut(&sid) {
2499                    surf.pending_presentation_feedbacks.push(fb);
2500                }
2501            }
2502            Request::Destroy => {}
2503            _ => {}
2504        }
2505    }
2506}
2507
2508// -- wp_presentation_feedback (no client requests) --
2509impl Dispatch<WpPresentationFeedback, ()> for Compositor {
2510    fn request(
2511        _: &mut Self,
2512        _: &Client,
2513        _: &WpPresentationFeedback,
2514        _: <WpPresentationFeedback as Resource>::Request,
2515        _: &(),
2516        _: &DisplayHandle,
2517        _: &mut DataInit<'_, Self>,
2518    ) {
2519    }
2520}
2521
2522// -- wl_region --
2523impl Dispatch<WlRegion, ()> for Compositor {
2524    fn request(
2525        _: &mut Self,
2526        _: &Client,
2527        _: &WlRegion,
2528        _: <WlRegion as Resource>::Request,
2529        _: &(),
2530        _: &DisplayHandle,
2531        _: &mut DataInit<'_, Self>,
2532    ) {
2533    }
2534}
2535
2536// -- wl_subcompositor --
2537impl GlobalDispatch<WlSubcompositor, ()> for Compositor {
2538    fn bind(
2539        _: &mut Self,
2540        _: &DisplayHandle,
2541        _: &Client,
2542        resource: New<WlSubcompositor>,
2543        _: &(),
2544        data_init: &mut DataInit<'_, Self>,
2545    ) {
2546        data_init.init(resource, ());
2547    }
2548}
2549
2550impl Dispatch<WlSubcompositor, ()> for Compositor {
2551    fn request(
2552        state: &mut Self,
2553        _: &Client,
2554        _: &WlSubcompositor,
2555        request: <WlSubcompositor as Resource>::Request,
2556        _: &(),
2557        _: &DisplayHandle,
2558        data_init: &mut DataInit<'_, Self>,
2559    ) {
2560        use wayland_server::protocol::wl_subcompositor::Request;
2561        match request {
2562            Request::GetSubsurface {
2563                id,
2564                surface,
2565                parent,
2566            } => {
2567                let child_id = surface.id();
2568                let parent_id = parent.id();
2569                data_init.init(
2570                    id,
2571                    SubsurfaceData {
2572                        wl_surface_id: child_id.clone(),
2573                        parent_surface_id: parent_id.clone(),
2574                    },
2575                );
2576                if let Some(surf) = state.surfaces.get_mut(&child_id) {
2577                    surf.parent_surface_id = Some(parent_id.clone());
2578                }
2579                if let Some(parent_surf) = state.surfaces.get_mut(&parent_id)
2580                    && !parent_surf.children.contains(&child_id)
2581                {
2582                    parent_surf.children.push(child_id);
2583                }
2584            }
2585            Request::Destroy => {}
2586            _ => {}
2587        }
2588    }
2589}
2590
2591// -- wl_subsurface --
2592impl Dispatch<WlSubsurface, SubsurfaceData> for Compositor {
2593    fn request(
2594        state: &mut Self,
2595        _: &Client,
2596        _: &WlSubsurface,
2597        request: <WlSubsurface as Resource>::Request,
2598        data: &SubsurfaceData,
2599        _: &DisplayHandle,
2600        _: &mut DataInit<'_, Self>,
2601    ) {
2602        use wayland_server::protocol::wl_subsurface::Request;
2603        match request {
2604            Request::SetPosition { x, y } => {
2605                if let Some(surf) = state.surfaces.get_mut(&data.wl_surface_id) {
2606                    surf.pending_subsurface_position = Some((x, y));
2607                }
2608            }
2609            Request::PlaceAbove { sibling } => {
2610                let sibling_id = sibling.id();
2611                if let Some(parent) = state.surfaces.get_mut(&data.parent_surface_id) {
2612                    let child_id = &data.wl_surface_id;
2613                    parent.children.retain(|c| c != child_id);
2614                    let pos = parent
2615                        .children
2616                        .iter()
2617                        .position(|c| *c == sibling_id)
2618                        .map(|p| p + 1)
2619                        .unwrap_or(parent.children.len());
2620                    parent.children.insert(pos, child_id.clone());
2621                }
2622            }
2623            Request::PlaceBelow { sibling } => {
2624                let sibling_id = sibling.id();
2625                if let Some(parent) = state.surfaces.get_mut(&data.parent_surface_id) {
2626                    let child_id = &data.wl_surface_id;
2627                    parent.children.retain(|c| c != child_id);
2628                    let pos = parent
2629                        .children
2630                        .iter()
2631                        .position(|c| *c == sibling_id)
2632                        .unwrap_or(0);
2633                    parent.children.insert(pos, child_id.clone());
2634                }
2635            }
2636            Request::SetSync | Request::SetDesync => {}
2637            Request::Destroy => {
2638                let child_id = &data.wl_surface_id;
2639                if let Some(parent) = state.surfaces.get_mut(&data.parent_surface_id) {
2640                    parent.children.retain(|c| c != child_id);
2641                }
2642                if let Some(surf) = state.surfaces.get_mut(child_id) {
2643                    surf.parent_surface_id = None;
2644                }
2645            }
2646            _ => {}
2647        }
2648    }
2649}
2650
2651// -- xdg_wm_base --
2652impl GlobalDispatch<XdgWmBase, ()> for Compositor {
2653    fn bind(
2654        _: &mut Self,
2655        _: &DisplayHandle,
2656        _: &Client,
2657        resource: New<XdgWmBase>,
2658        _: &(),
2659        data_init: &mut DataInit<'_, Self>,
2660    ) {
2661        data_init.init(resource, ());
2662    }
2663}
2664
2665impl Dispatch<XdgWmBase, ()> for Compositor {
2666    fn request(
2667        state: &mut Self,
2668        _: &Client,
2669        _: &XdgWmBase,
2670        request: <XdgWmBase as Resource>::Request,
2671        _: &(),
2672        _: &DisplayHandle,
2673        data_init: &mut DataInit<'_, Self>,
2674    ) {
2675        use xdg_wm_base::Request;
2676        match request {
2677            Request::GetXdgSurface { id, surface } => {
2678                let wl_surface_id = surface.id();
2679                let xdg_surface = data_init.init(
2680                    id,
2681                    XdgSurfaceData {
2682                        wl_surface_id: wl_surface_id.clone(),
2683                    },
2684                );
2685                if let Some(surf) = state.surfaces.get_mut(&wl_surface_id) {
2686                    surf.xdg_surface = Some(xdg_surface);
2687                }
2688            }
2689            Request::CreatePositioner { id } => {
2690                let positioner = data_init.init(id, ());
2691                let pos_id = positioner.id();
2692                state.positioners.insert(
2693                    pos_id,
2694                    PositionerState {
2695                        resource: positioner,
2696                        geometry: PositionerGeometry {
2697                            size: (0, 0),
2698                            anchor_rect: (0, 0, 0, 0),
2699                            anchor: 0,
2700                            gravity: 0,
2701                            constraint_adjustment: 0,
2702                            offset: (0, 0),
2703                        },
2704                    },
2705                );
2706            }
2707            Request::Pong { .. } => {}
2708            Request::Destroy => {}
2709            _ => {}
2710        }
2711    }
2712}
2713
2714// -- xdg_surface --
2715impl Dispatch<XdgSurface, XdgSurfaceData> for Compositor {
2716    fn request(
2717        state: &mut Self,
2718        _: &Client,
2719        resource: &XdgSurface,
2720        request: <XdgSurface as Resource>::Request,
2721        data: &XdgSurfaceData,
2722        _: &DisplayHandle,
2723        data_init: &mut DataInit<'_, Self>,
2724    ) {
2725        use xdg_surface::Request;
2726        match request {
2727            Request::GetToplevel { id } => {
2728                let toplevel = data_init.init(
2729                    id,
2730                    XdgToplevelData {
2731                        wl_surface_id: data.wl_surface_id.clone(),
2732                    },
2733                );
2734                let surface_id = state.allocate_surface_id();
2735                if let Some(surf) = state.surfaces.get_mut(&data.wl_surface_id) {
2736                    surf.xdg_toplevel = Some(toplevel.clone());
2737                    surf.surface_id = surface_id;
2738                }
2739                state
2740                    .toplevel_surface_ids
2741                    .insert(surface_id, data.wl_surface_id.clone());
2742
2743                // Use a per-surface size if one was already configured
2744                // (e.g. the browser sent C2S_SURFACE_RESIZE before the
2745                // toplevel was created), otherwise fall back to the global
2746                // output dimensions.  surface_sizes stores logical pixels.
2747                let (cw, ch) = state
2748                    .surface_sizes
2749                    .get(&surface_id)
2750                    .copied()
2751                    .unwrap_or((state.output_width, state.output_height));
2752                let states = xdg_toplevel_states(&[
2753                    xdg_toplevel::State::Activated,
2754                    xdg_toplevel::State::Maximized,
2755                ]);
2756                toplevel.configure(cw, ch, states);
2757                let serial = state.next_serial();
2758                resource.configure(serial);
2759
2760                // Keyboard focus — sends leave to the previously focused
2761                // surface's client before entering the new one.
2762                state.set_keyboard_focus(surface_id);
2763                // Tell the client which output its surface is on so it can
2764                // determine scale and start rendering.
2765                if let Some(surf) = state.surfaces.get(&data.wl_surface_id) {
2766                    for output in &state.outputs {
2767                        if same_client(output, &surf.wl_surface) {
2768                            surf.wl_surface.enter(output);
2769                        }
2770                    }
2771                }
2772                let _ = state.display_handle.flush_clients();
2773
2774                let _ = state.event_tx.send(CompositorEvent::SurfaceCreated {
2775                    surface_id,
2776                    title: String::new(),
2777                    app_id: String::new(),
2778                    parent_id: 0,
2779                    width: 0,
2780                    height: 0,
2781                });
2782                (state.event_notify)();
2783                if state.verbose {
2784                    eprintln!("[compositor] new_toplevel sid={surface_id}");
2785                }
2786            }
2787            Request::GetPopup {
2788                id,
2789                parent,
2790                positioner,
2791            } => {
2792                let popup = data_init.init(
2793                    id,
2794                    XdgPopupData {
2795                        wl_surface_id: data.wl_surface_id.clone(),
2796                    },
2797                );
2798
2799                // Parent relationship: make the popup a child of the parent
2800                // surface so it is composited into the same toplevel frame.
2801                let parent_wl_id: Option<ObjectId> = parent
2802                    .as_ref()
2803                    .and_then(|p| p.data::<XdgSurfaceData>())
2804                    .map(|d| d.wl_surface_id.clone());
2805
2806                // The xdg-shell protocol specifies popup positions relative
2807                // to the parent's *window geometry*, not its surface origin.
2808                // Fetch the parent's geometry offset so we can convert
2809                // between window-geometry space and surface-tree space.
2810                let parent_geom_offset = parent_wl_id
2811                    .as_ref()
2812                    .and_then(|pid| state.surfaces.get(pid))
2813                    .and_then(|s| s.xdg_geometry)
2814                    .map(|(gx, gy, _, _)| (gx, gy))
2815                    .unwrap_or((0, 0));
2816
2817                // Compute the parent's absolute position within the toplevel
2818                // and the logical output bounds for constraint adjustment.
2819                // Add the geometry offset so parent_abs represents the
2820                // window-geometry origin in surface-tree coordinates.
2821                let parent_abs = parent_wl_id
2822                    .as_ref()
2823                    .map(|pid| {
2824                        let abs = state.surface_absolute_position(pid);
2825                        (abs.0 + parent_geom_offset.0, abs.1 + parent_geom_offset.1)
2826                    })
2827                    .unwrap_or((0, 0));
2828                // Use the client's actual surface size for popup bounds,
2829                // not the configured size (client may not have resized yet).
2830                let (_, toplevel_root) = parent_wl_id
2831                    .as_ref()
2832                    .map(|pid| state.find_toplevel_root(pid))
2833                    .unwrap_or_else(|| {
2834                        // Dummy root — no parent.
2835                        (data.wl_surface_id.clone(), None)
2836                    });
2837                let bounds = toplevel_root
2838                    .and_then(|_| {
2839                        let root_wl_id = parent_wl_id.as_ref().map(|pid| {
2840                            let (rid, _) = state.find_toplevel_root(pid);
2841                            rid
2842                        })?;
2843                        let surf = state.surfaces.get(&root_wl_id)?;
2844                        if let Some((gx, gy, gw, gh)) = surf.xdg_geometry
2845                            && gw > 0
2846                            && gh > 0
2847                        {
2848                            return Some((gx, gy, gw, gh));
2849                        }
2850
2851                        // Fall back to the client's actual logical surface
2852                        // size when window geometry is unavailable.
2853                        let (w, h, scale, _, _) = state.pixel_cache.get(&root_wl_id)?;
2854                        let s = (*scale).max(1);
2855                        let (lw, lh) = surf
2856                            .viewport_destination
2857                            .filter(|&(dw, dh)| dw > 0 && dh > 0)
2858                            .unwrap_or((*w as i32 / s, *h as i32 / s));
2859                        Some((0, 0, lw, lh))
2860                    })
2861                    .unwrap_or((0, 0, state.output_width, state.output_height));
2862
2863                eprintln!(
2864                    "[popup] parent_abs={parent_abs:?} bounds={bounds:?} parent_wl={parent_wl_id:?} geom_off={parent_geom_offset:?}"
2865                );
2866                // Compute geometry from positioner with constraint adjustment.
2867                let pos_id = positioner.id();
2868                let (px, py, pw, ph) = state
2869                    .positioners
2870                    .get(&pos_id)
2871                    .map(|p| p.geometry.compute_position(parent_abs, bounds))
2872                    .unwrap_or((0, 0, 200, 200));
2873                eprintln!("[popup] result=({px},{py},{pw},{ph})");
2874
2875                if let Some(surf) = state.surfaces.get_mut(&data.wl_surface_id) {
2876                    surf.xdg_popup = Some(popup.clone());
2877                    surf.parent_surface_id = parent_wl_id.clone();
2878                    // Convert from window-geometry-relative to surface-
2879                    // relative coords so the popup composites correctly.
2880                    // The rendering crops to xdg_geometry, so the popup
2881                    // must be offset by the parent's geometry origin.
2882                    surf.subsurface_position =
2883                        (parent_geom_offset.0 + px, parent_geom_offset.1 + py);
2884                }
2885                if let Some(ref parent_id) = parent_wl_id
2886                    && let Some(parent_surf) = state.surfaces.get_mut(parent_id)
2887                    && !parent_surf.children.contains(&data.wl_surface_id)
2888                {
2889                    parent_surf.children.push(data.wl_surface_id.clone());
2890                }
2891
2892                popup.configure(px, py, pw, ph);
2893                let serial = state.next_serial();
2894                resource.configure(serial);
2895                let _ = state.display_handle.flush_clients();
2896            }
2897            Request::SetWindowGeometry {
2898                x,
2899                y,
2900                width,
2901                height,
2902            } => {
2903                if let Some(surf) = state.surfaces.get_mut(&data.wl_surface_id) {
2904                    // For popup surfaces, adjust subsurface_position to
2905                    // account for the popup's own geometry offset.  The
2906                    // xdg-shell protocol positions the popup's *geometry*
2907                    // (not its surface origin) relative to the parent's
2908                    // geometry.  Without this adjustment, CSD shadows or
2909                    // borders around the popup cause the visible content
2910                    // to shift by (gx, gy).
2911                    if surf.xdg_popup.is_some() {
2912                        let (old_gx, old_gy) = surf
2913                            .xdg_geometry
2914                            .map(|(gx, gy, _, _)| (gx, gy))
2915                            .unwrap_or((0, 0));
2916                        surf.subsurface_position.0 += old_gx - x;
2917                        surf.subsurface_position.1 += old_gy - y;
2918                    }
2919                    surf.xdg_geometry = Some((x, y, width, height));
2920                }
2921            }
2922            Request::AckConfigure { .. } => {}
2923            Request::Destroy => {}
2924            _ => {}
2925        }
2926    }
2927}
2928
2929// -- xdg_toplevel --
2930impl Dispatch<XdgToplevel, XdgToplevelData> for Compositor {
2931    fn request(
2932        state: &mut Self,
2933        _: &Client,
2934        _: &XdgToplevel,
2935        request: <XdgToplevel as Resource>::Request,
2936        data: &XdgToplevelData,
2937        _: &DisplayHandle,
2938        _: &mut DataInit<'_, Self>,
2939    ) {
2940        use xdg_toplevel::Request;
2941        match request {
2942            Request::SetTitle { title } => {
2943                if let Some(surf) = state.surfaces.get_mut(&data.wl_surface_id)
2944                    && surf.title != title
2945                {
2946                    surf.title = title.clone();
2947                    if surf.surface_id > 0 {
2948                        let _ = state.event_tx.send(CompositorEvent::SurfaceTitle {
2949                            surface_id: surf.surface_id,
2950                            title,
2951                        });
2952                        (state.event_notify)();
2953                    }
2954                }
2955            }
2956            Request::SetAppId { app_id } => {
2957                if let Some(surf) = state.surfaces.get_mut(&data.wl_surface_id)
2958                    && surf.app_id != app_id
2959                {
2960                    surf.app_id = app_id.clone();
2961                    if surf.surface_id > 0 {
2962                        let _ = state.event_tx.send(CompositorEvent::SurfaceAppId {
2963                            surface_id: surf.surface_id,
2964                            app_id,
2965                        });
2966                        (state.event_notify)();
2967                    }
2968                }
2969            }
2970            Request::Destroy => {
2971                let wl_surface_id = &data.wl_surface_id;
2972                state.pixel_cache.remove(wl_surface_id);
2973                if let Some(surf) = state.surfaces.get_mut(wl_surface_id) {
2974                    let sid = surf.surface_id;
2975                    surf.xdg_toplevel = None;
2976                    if sid > 0 {
2977                        state.toplevel_surface_ids.remove(&sid);
2978                        state.last_reported_size.remove(&sid);
2979                        state.surface_sizes.remove(&sid);
2980                        let _ = state
2981                            .event_tx
2982                            .send(CompositorEvent::SurfaceDestroyed { surface_id: sid });
2983                        (state.event_notify)();
2984                        surf.surface_id = 0;
2985                    }
2986                }
2987            }
2988            _ => {}
2989        }
2990    }
2991}
2992
2993// -- xdg_popup --
2994impl Dispatch<XdgPopup, XdgPopupData> for Compositor {
2995    fn request(
2996        state: &mut Self,
2997        _: &Client,
2998        _: &XdgPopup,
2999        request: <XdgPopup as Resource>::Request,
3000        data: &XdgPopupData,
3001        _: &DisplayHandle,
3002        _: &mut DataInit<'_, Self>,
3003    ) {
3004        use xdg_popup::Request;
3005        if let Request::Destroy = request {
3006            // Remove from parent's children list.
3007            if let Some(parent_id) = state
3008                .surfaces
3009                .get(&data.wl_surface_id)
3010                .and_then(|s| s.parent_surface_id.clone())
3011                && let Some(parent) = state.surfaces.get_mut(&parent_id)
3012            {
3013                parent.children.retain(|c| *c != data.wl_surface_id);
3014            }
3015            if let Some(surf) = state.surfaces.get_mut(&data.wl_surface_id) {
3016                surf.xdg_popup = None;
3017                surf.parent_surface_id = None;
3018            }
3019        }
3020    }
3021}
3022
3023// -- xdg_positioner --
3024use wayland_protocols::xdg::shell::server::xdg_positioner;
3025impl Dispatch<XdgPositioner, ()> for Compositor {
3026    fn request(
3027        state: &mut Self,
3028        _: &Client,
3029        resource: &XdgPositioner,
3030        request: <XdgPositioner as Resource>::Request,
3031        _: &(),
3032        _: &DisplayHandle,
3033        _: &mut DataInit<'_, Self>,
3034    ) {
3035        use xdg_positioner::Request;
3036        let pos_id = resource.id();
3037        let Some(pos) = state.positioners.get_mut(&pos_id) else {
3038            return;
3039        };
3040        match request {
3041            Request::SetSize { width, height } => {
3042                pos.geometry.size = (width, height);
3043            }
3044            Request::SetAnchorRect {
3045                x,
3046                y,
3047                width,
3048                height,
3049            } => {
3050                pos.geometry.anchor_rect = (x, y, width, height);
3051            }
3052            Request::SetAnchor {
3053                anchor: wayland_server::WEnum::Value(v),
3054            } => {
3055                pos.geometry.anchor = v as u32;
3056            }
3057            Request::SetGravity {
3058                gravity: wayland_server::WEnum::Value(v),
3059            } => {
3060                pos.geometry.gravity = v as u32;
3061            }
3062            Request::SetOffset { x, y } => {
3063                pos.geometry.offset = (x, y);
3064            }
3065            Request::SetConstraintAdjustment {
3066                constraint_adjustment,
3067            } => {
3068                pos.geometry.constraint_adjustment = constraint_adjustment.into();
3069            }
3070            Request::Destroy => {
3071                state.positioners.remove(&pos_id);
3072            }
3073            _ => {}
3074        }
3075    }
3076}
3077
3078// -- xdg_decoration --
3079impl GlobalDispatch<ZxdgDecorationManagerV1, ()> for Compositor {
3080    fn bind(
3081        _: &mut Self,
3082        _: &DisplayHandle,
3083        _: &Client,
3084        resource: New<ZxdgDecorationManagerV1>,
3085        _: &(),
3086        data_init: &mut DataInit<'_, Self>,
3087    ) {
3088        data_init.init(resource, ());
3089    }
3090}
3091
3092impl Dispatch<ZxdgDecorationManagerV1, ()> for Compositor {
3093    fn request(
3094        _: &mut Self,
3095        _: &Client,
3096        _: &ZxdgDecorationManagerV1,
3097        request: <ZxdgDecorationManagerV1 as Resource>::Request,
3098        _: &(),
3099        _: &DisplayHandle,
3100        data_init: &mut DataInit<'_, Self>,
3101    ) {
3102        use zxdg_decoration_manager_v1::Request;
3103        match request {
3104            Request::GetToplevelDecoration { id, toplevel: _ } => {
3105                let decoration = data_init.init(id, ());
3106                // Always request server-side (i.e. no) decorations.
3107                decoration.configure(zxdg_toplevel_decoration_v1::Mode::ServerSide);
3108            }
3109            Request::Destroy => {}
3110            _ => {}
3111        }
3112    }
3113}
3114
3115impl Dispatch<ZxdgToplevelDecorationV1, ()> for Compositor {
3116    fn request(
3117        _: &mut Self,
3118        _: &Client,
3119        resource: &ZxdgToplevelDecorationV1,
3120        request: <ZxdgToplevelDecorationV1 as Resource>::Request,
3121        _: &(),
3122        _: &DisplayHandle,
3123        _: &mut DataInit<'_, Self>,
3124    ) {
3125        use zxdg_toplevel_decoration_v1::Request;
3126        match request {
3127            Request::SetMode { .. } | Request::UnsetMode => {
3128                resource.configure(zxdg_toplevel_decoration_v1::Mode::ServerSide);
3129            }
3130            Request::Destroy => {}
3131            _ => {}
3132        }
3133    }
3134}
3135
3136// -- wl_shm --
3137impl GlobalDispatch<WlShm, ()> for Compositor {
3138    fn bind(
3139        _: &mut Self,
3140        _: &DisplayHandle,
3141        _: &Client,
3142        resource: New<WlShm>,
3143        _: &(),
3144        data_init: &mut DataInit<'_, Self>,
3145    ) {
3146        let shm = data_init.init(resource, ());
3147        shm.format(wl_shm::Format::Argb8888);
3148        shm.format(wl_shm::Format::Xrgb8888);
3149        shm.format(wl_shm::Format::Abgr8888);
3150        shm.format(wl_shm::Format::Xbgr8888);
3151    }
3152}
3153
3154impl Dispatch<WlShm, ()> for Compositor {
3155    fn request(
3156        state: &mut Self,
3157        _: &Client,
3158        _: &WlShm,
3159        request: <WlShm as Resource>::Request,
3160        _: &(),
3161        _: &DisplayHandle,
3162        data_init: &mut DataInit<'_, Self>,
3163    ) {
3164        use wayland_server::protocol::wl_shm::Request;
3165        if let Request::CreatePool { id, fd, size } = request {
3166            let pool = data_init.init(id, ());
3167            let pool_id = pool.id();
3168            state
3169                .shm_pools
3170                .insert(pool_id, ShmPool::new(pool, fd, size));
3171        }
3172    }
3173}
3174
3175// -- wl_shm_pool --
3176impl Dispatch<WlShmPool, ()> for Compositor {
3177    fn request(
3178        state: &mut Self,
3179        _: &Client,
3180        resource: &WlShmPool,
3181        request: <WlShmPool as Resource>::Request,
3182        _: &(),
3183        _: &DisplayHandle,
3184        data_init: &mut DataInit<'_, Self>,
3185    ) {
3186        use wayland_server::protocol::wl_shm_pool::Request;
3187        let pool_id = resource.id();
3188        match request {
3189            Request::CreateBuffer {
3190                id,
3191                offset,
3192                width,
3193                height,
3194                stride,
3195                format,
3196            } => {
3197                // format comes as WEnum<Format>, extract the known value.
3198                let fmt = match format {
3199                    wayland_server::WEnum::Value(f) => f,
3200                    _ => wl_shm::Format::Argb8888, // fallback
3201                };
3202                data_init.init(
3203                    id,
3204                    ShmBufferData {
3205                        pool_id: pool_id.clone(),
3206                        offset,
3207                        width,
3208                        height,
3209                        stride,
3210                        format: fmt,
3211                    },
3212                );
3213            }
3214            Request::Resize { size } => {
3215                if let Some(pool) = state.shm_pools.get_mut(&pool_id) {
3216                    pool.resize(size);
3217                }
3218            }
3219            Request::Destroy => {
3220                state.shm_pools.remove(&pool_id);
3221            }
3222            _ => {}
3223        }
3224    }
3225}
3226
3227// -- wl_buffer (SHM) --
3228impl Dispatch<WlBuffer, ShmBufferData> for Compositor {
3229    fn request(
3230        _: &mut Self,
3231        _: &Client,
3232        _: &WlBuffer,
3233        _: <WlBuffer as Resource>::Request,
3234        _: &ShmBufferData,
3235        _: &DisplayHandle,
3236        _: &mut DataInit<'_, Self>,
3237    ) {
3238    }
3239}
3240
3241// -- wl_buffer (DMA-BUF) --
3242impl Dispatch<WlBuffer, DmaBufBufferData> for Compositor {
3243    fn request(
3244        _: &mut Self,
3245        _: &Client,
3246        _: &WlBuffer,
3247        _: <WlBuffer as Resource>::Request,
3248        _: &DmaBufBufferData,
3249        _: &DisplayHandle,
3250        _: &mut DataInit<'_, Self>,
3251    ) {
3252    }
3253}
3254
3255// -- wl_output --
3256impl GlobalDispatch<WlOutput, ()> for Compositor {
3257    fn bind(
3258        state: &mut Self,
3259        _: &DisplayHandle,
3260        _: &Client,
3261        resource: New<WlOutput>,
3262        _: &(),
3263        data_init: &mut DataInit<'_, Self>,
3264    ) {
3265        let output = data_init.init(resource, ());
3266        output.geometry(
3267            0,
3268            0,
3269            0,
3270            0,
3271            wl_output::Subpixel::Unknown,
3272            "Virtual".to_string(),
3273            "Headless".to_string(),
3274            wl_output::Transform::Normal,
3275        );
3276        let s120 = state.output_scale_120 as i32;
3277        let mode_w = state.output_width * s120 / 120;
3278        let mode_h = state.output_height * s120 / 120;
3279        output.mode(
3280            wl_output::Mode::Current | wl_output::Mode::Preferred,
3281            mode_w,
3282            mode_h,
3283            60_000,
3284        );
3285        if output.version() >= 2 {
3286            output.scale(((state.output_scale_120 as i32) + 119) / 120);
3287        }
3288        if output.version() >= 2 {
3289            output.done();
3290        }
3291        state.outputs.push(output);
3292    }
3293}
3294
3295impl Dispatch<WlOutput, ()> for Compositor {
3296    fn request(
3297        state: &mut Self,
3298        _: &Client,
3299        resource: &WlOutput,
3300        request: <WlOutput as Resource>::Request,
3301        _: &(),
3302        _: &DisplayHandle,
3303        _: &mut DataInit<'_, Self>,
3304    ) {
3305        use wayland_server::protocol::wl_output::Request;
3306        if let Request::Release = request {
3307            state.outputs.retain(|o| o.id() != resource.id());
3308        }
3309    }
3310}
3311
3312// -- wl_seat --
3313impl GlobalDispatch<WlSeat, ()> for Compositor {
3314    fn bind(
3315        _: &mut Self,
3316        _: &DisplayHandle,
3317        _: &Client,
3318        resource: New<WlSeat>,
3319        _: &(),
3320        data_init: &mut DataInit<'_, Self>,
3321    ) {
3322        let seat = data_init.init(resource, ());
3323        seat.capabilities(wl_seat::Capability::Keyboard | wl_seat::Capability::Pointer);
3324        if seat.version() >= 2 {
3325            seat.name("headless".to_string());
3326        }
3327    }
3328}
3329
3330impl Dispatch<WlSeat, ()> for Compositor {
3331    fn request(
3332        state: &mut Self,
3333        _: &Client,
3334        _: &WlSeat,
3335        request: <WlSeat as Resource>::Request,
3336        _: &(),
3337        _: &DisplayHandle,
3338        data_init: &mut DataInit<'_, Self>,
3339    ) {
3340        use wayland_server::protocol::wl_seat::Request;
3341        match request {
3342            Request::GetKeyboard { id } => {
3343                let kb = data_init.init(id, ());
3344                if let Some(fd) = create_keymap_fd(&state.keyboard_keymap_data) {
3345                    kb.keymap(
3346                        wl_keyboard::KeymapFormat::XkbV1,
3347                        fd.as_fd(),
3348                        state.keyboard_keymap_data.len() as u32,
3349                    );
3350                }
3351                if kb.version() >= 4 {
3352                    kb.repeat_info(25, 200);
3353                }
3354                state.keyboards.push(kb);
3355            }
3356            Request::GetPointer { id } => {
3357                let ptr = data_init.init(id, ());
3358                state.pointers.push(ptr);
3359            }
3360            Request::GetTouch { id } => {
3361                data_init.init(id, ());
3362            }
3363            Request::Release => {}
3364            _ => {}
3365        }
3366    }
3367}
3368
3369// -- wl_keyboard --
3370impl Dispatch<WlKeyboard, ()> for Compositor {
3371    fn request(
3372        state: &mut Self,
3373        _: &Client,
3374        resource: &WlKeyboard,
3375        request: <WlKeyboard as Resource>::Request,
3376        _: &(),
3377        _: &DisplayHandle,
3378        _: &mut DataInit<'_, Self>,
3379    ) {
3380        if let wl_keyboard::Request::Release = request {
3381            state.keyboards.retain(|k| k.id() != resource.id());
3382        }
3383    }
3384}
3385
3386// -- wl_pointer --
3387impl Dispatch<WlPointer, ()> for Compositor {
3388    fn request(
3389        state: &mut Self,
3390        _: &Client,
3391        resource: &WlPointer,
3392        request: <WlPointer as Resource>::Request,
3393        _: &(),
3394        _: &DisplayHandle,
3395        _: &mut DataInit<'_, Self>,
3396    ) {
3397        use wl_pointer::Request;
3398        match request {
3399            Request::SetCursor {
3400                serial: _,
3401                surface,
3402                hotspot_x,
3403                hotspot_y,
3404            } => {
3405                if let Some(surface) = surface {
3406                    let sid = surface.id();
3407                    if let Some(surf) = state.surfaces.get_mut(&sid) {
3408                        surf.is_cursor = true;
3409                        surf.cursor_hotspot = (hotspot_x, hotspot_y);
3410                    }
3411                } else {
3412                    let _ = state.event_tx.send(CompositorEvent::SurfaceCursor {
3413                        surface_id: state.focused_surface_id,
3414                        cursor: CursorImage::Hidden,
3415                    });
3416                }
3417            }
3418            Request::Release => {
3419                state.pointers.retain(|p| p.id() != resource.id());
3420            }
3421            _ => {}
3422        }
3423    }
3424}
3425
3426// -- wl_touch (stub) --
3427impl Dispatch<wayland_server::protocol::wl_touch::WlTouch, ()> for Compositor {
3428    fn request(
3429        _: &mut Self,
3430        _: &Client,
3431        _: &wayland_server::protocol::wl_touch::WlTouch,
3432        _: <wayland_server::protocol::wl_touch::WlTouch as Resource>::Request,
3433        _: &(),
3434        _: &DisplayHandle,
3435        _: &mut DataInit<'_, Self>,
3436    ) {
3437    }
3438}
3439
3440// -- zwp_linux_dmabuf_v1 --
3441impl GlobalDispatch<ZwpLinuxDmabufV1, ()> for Compositor {
3442    fn bind(
3443        _: &mut Self,
3444        _: &DisplayHandle,
3445        _: &Client,
3446        resource: New<ZwpLinuxDmabufV1>,
3447        _: &(),
3448        data_init: &mut DataInit<'_, Self>,
3449    ) {
3450        let dmabuf = data_init.init(resource, ());
3451        if dmabuf.version() >= 3 {
3452            // MOD_LINEAR = 0, MOD_INVALID = 0x00ffffffffffffff
3453            let formats: &[(u32, u32, u32)] = &[
3454                (drm_fourcc::ARGB8888, 0, 0),
3455                (drm_fourcc::XRGB8888, 0, 0),
3456                (drm_fourcc::ABGR8888, 0, 0),
3457                (drm_fourcc::XBGR8888, 0, 0),
3458                (drm_fourcc::ARGB8888, 0x00ffffff, 0xffffffff),
3459                (drm_fourcc::XRGB8888, 0x00ffffff, 0xffffffff),
3460                (drm_fourcc::ABGR8888, 0x00ffffff, 0xffffffff),
3461                (drm_fourcc::XBGR8888, 0x00ffffff, 0xffffffff),
3462            ];
3463            for &(fmt, mod_hi, mod_lo) in formats {
3464                dmabuf.modifier(fmt, mod_hi, mod_lo);
3465            }
3466        } else {
3467            dmabuf.format(drm_fourcc::ARGB8888);
3468            dmabuf.format(drm_fourcc::XRGB8888);
3469            dmabuf.format(drm_fourcc::ABGR8888);
3470            dmabuf.format(drm_fourcc::XBGR8888);
3471        }
3472    }
3473}
3474
3475impl Dispatch<ZwpLinuxDmabufV1, ()> for Compositor {
3476    fn request(
3477        _: &mut Self,
3478        _: &Client,
3479        _: &ZwpLinuxDmabufV1,
3480        request: <ZwpLinuxDmabufV1 as Resource>::Request,
3481        _: &(),
3482        _: &DisplayHandle,
3483        data_init: &mut DataInit<'_, Self>,
3484    ) {
3485        use zwp_linux_dmabuf_v1::Request;
3486        match request {
3487            Request::CreateParams { params_id } => {
3488                data_init.init(params_id, ());
3489            }
3490            Request::Destroy => {}
3491            _ => {}
3492        }
3493    }
3494}
3495
3496// -- zwp_linux_buffer_params_v1 --
3497impl Dispatch<ZwpLinuxBufferParamsV1, ()> for Compositor {
3498    fn request(
3499        state: &mut Self,
3500        client: &Client,
3501        resource: &ZwpLinuxBufferParamsV1,
3502        request: <ZwpLinuxBufferParamsV1 as Resource>::Request,
3503        _: &(),
3504        dh: &DisplayHandle,
3505        data_init: &mut DataInit<'_, Self>,
3506    ) {
3507        use zwp_linux_buffer_params_v1::Request;
3508        let params_id = resource.id();
3509        match request {
3510            Request::Add {
3511                fd,
3512                plane_idx: _,
3513                offset,
3514                stride,
3515                modifier_hi,
3516                modifier_lo,
3517            } => {
3518                let modifier = ((modifier_hi as u64) << 32) | (modifier_lo as u64);
3519                let entry = state
3520                    .dmabuf_params
3521                    .entry(params_id.clone())
3522                    .or_insert_with(|| DmaBufParamsPending {
3523                        resource: resource.clone(),
3524                        planes: Vec::new(),
3525                        modifier,
3526                    });
3527                entry.modifier = modifier;
3528                entry.planes.push(DmaBufPlane { fd, offset, stride });
3529            }
3530            Request::Create {
3531                width,
3532                height,
3533                format,
3534                flags: _,
3535            } => {
3536                let pending = state.dmabuf_params.remove(&params_id);
3537                let (planes, modifier) = match pending {
3538                    Some(p) => (p.planes, p.modifier),
3539                    None => {
3540                        resource.failed();
3541                        return;
3542                    }
3543                };
3544                match client.create_resource::<WlBuffer, DmaBufBufferData, Compositor>(
3545                    dh,
3546                    1,
3547                    DmaBufBufferData {
3548                        width,
3549                        height,
3550                        fourcc: format,
3551                        modifier,
3552                        planes,
3553                    },
3554                ) {
3555                    Ok(buffer) => resource.created(&buffer),
3556                    Err(_) => resource.failed(),
3557                }
3558            }
3559            Request::CreateImmed {
3560                buffer_id,
3561                width,
3562                height,
3563                format,
3564                flags: _,
3565            } => {
3566                let (planes, modifier) = state
3567                    .dmabuf_params
3568                    .remove(&params_id)
3569                    .map(|p| (p.planes, p.modifier))
3570                    .unwrap_or_default();
3571                data_init.init(
3572                    buffer_id,
3573                    DmaBufBufferData {
3574                        width,
3575                        height,
3576                        fourcc: format,
3577                        modifier,
3578                        planes,
3579                    },
3580                );
3581            }
3582            Request::Destroy => {
3583                state.dmabuf_params.remove(&params_id);
3584            }
3585            _ => {}
3586        }
3587    }
3588}
3589
3590// -- wp_fractional_scale_manager_v1 --
3591impl GlobalDispatch<WpFractionalScaleManagerV1, ()> for Compositor {
3592    fn bind(
3593        _: &mut Self,
3594        _: &DisplayHandle,
3595        _: &Client,
3596        resource: New<WpFractionalScaleManagerV1>,
3597        _: &(),
3598        data_init: &mut DataInit<'_, Self>,
3599    ) {
3600        data_init.init(resource, ());
3601    }
3602}
3603
3604impl Dispatch<WpFractionalScaleManagerV1, ()> for Compositor {
3605    fn request(
3606        state: &mut Self,
3607        _: &Client,
3608        _: &WpFractionalScaleManagerV1,
3609        request: <WpFractionalScaleManagerV1 as Resource>::Request,
3610        _: &(),
3611        _: &DisplayHandle,
3612        data_init: &mut DataInit<'_, Self>,
3613    ) {
3614        use wp_fractional_scale_manager_v1::Request;
3615        match request {
3616            Request::GetFractionalScale { id, surface: _ } => {
3617                let fs = data_init.init(id, ());
3618                // Send the current preferred scale immediately.
3619                fs.preferred_scale(state.output_scale_120 as u32);
3620                state.fractional_scales.push(fs);
3621            }
3622            Request::Destroy => {}
3623            _ => {}
3624        }
3625    }
3626}
3627
3628// -- wp_fractional_scale_v1 --
3629impl Dispatch<WpFractionalScaleV1, ()> for Compositor {
3630    fn request(
3631        state: &mut Self,
3632        _: &Client,
3633        resource: &WpFractionalScaleV1,
3634        _: <WpFractionalScaleV1 as Resource>::Request,
3635        _: &(),
3636        _: &DisplayHandle,
3637        _: &mut DataInit<'_, Self>,
3638    ) {
3639        // Only request is Destroy.
3640        state
3641            .fractional_scales
3642            .retain(|fs| fs.id() != resource.id());
3643    }
3644}
3645
3646// -- wp_viewporter --
3647impl GlobalDispatch<WpViewporter, ()> for Compositor {
3648    fn bind(
3649        _: &mut Self,
3650        _: &DisplayHandle,
3651        _: &Client,
3652        resource: New<WpViewporter>,
3653        _: &(),
3654        data_init: &mut DataInit<'_, Self>,
3655    ) {
3656        data_init.init(resource, ());
3657    }
3658}
3659
3660impl Dispatch<WpViewporter, ()> for Compositor {
3661    fn request(
3662        _: &mut Self,
3663        _: &Client,
3664        _: &WpViewporter,
3665        request: <WpViewporter as Resource>::Request,
3666        _: &(),
3667        _: &DisplayHandle,
3668        data_init: &mut DataInit<'_, Self>,
3669    ) {
3670        use wp_viewporter::Request;
3671        match request {
3672            Request::GetViewport { id, surface } => {
3673                // Associate the viewport with the surface's ObjectId so
3674                // SetDestination can update the right Surface.
3675                let obj_id = surface.id();
3676                data_init.init(id, obj_id);
3677            }
3678            Request::Destroy => {}
3679            _ => {}
3680        }
3681    }
3682}
3683
3684// -- wp_viewport --
3685impl Dispatch<WpViewport, ObjectId> for Compositor {
3686    fn request(
3687        state: &mut Self,
3688        _: &Client,
3689        _: &WpViewport,
3690        request: <WpViewport as Resource>::Request,
3691        surface_obj_id: &ObjectId,
3692        _: &DisplayHandle,
3693        _: &mut DataInit<'_, Self>,
3694    ) {
3695        use wayland_protocols::wp::viewporter::server::wp_viewport::Request;
3696        match request {
3697            Request::SetDestination { width, height } => {
3698                if let Some(surf) = state.surfaces.get_mut(surface_obj_id) {
3699                    // width/height of -1 means unset (revert to buffer size).
3700                    if width > 0 && height > 0 {
3701                        surf.pending_viewport_destination = Some((width, height));
3702                    } else {
3703                        surf.pending_viewport_destination = None;
3704                    }
3705                }
3706            }
3707            Request::SetSource { .. } => {
3708                // Source crop — not needed for headless compositor.
3709            }
3710            Request::Destroy => {}
3711            _ => {}
3712        }
3713    }
3714}
3715
3716// =========================================================================
3717// NEW PROTOCOLS
3718// =========================================================================
3719
3720// -- wl_data_device_manager (clipboard) --
3721
3722impl GlobalDispatch<WlDataDeviceManager, ()> for Compositor {
3723    fn bind(
3724        _: &mut Self,
3725        _: &DisplayHandle,
3726        _: &Client,
3727        resource: New<WlDataDeviceManager>,
3728        _: &(),
3729        data_init: &mut DataInit<'_, Self>,
3730    ) {
3731        data_init.init(resource, ());
3732    }
3733}
3734
3735impl Dispatch<WlDataDeviceManager, ()> for Compositor {
3736    fn request(
3737        state: &mut Self,
3738        _: &Client,
3739        _: &WlDataDeviceManager,
3740        request: <WlDataDeviceManager as Resource>::Request,
3741        _: &(),
3742        _: &DisplayHandle,
3743        data_init: &mut DataInit<'_, Self>,
3744    ) {
3745        use wl_data_device_manager::Request;
3746        match request {
3747            Request::CreateDataSource { id } => {
3748                data_init.init(
3749                    id,
3750                    DataSourceData {
3751                        mime_types: std::sync::Mutex::new(Vec::new()),
3752                    },
3753                );
3754            }
3755            Request::GetDataDevice { id, seat: _ } => {
3756                let dd = data_init.init(id, ());
3757                state.data_devices.push(dd);
3758            }
3759            _ => {}
3760        }
3761    }
3762}
3763
3764impl Dispatch<WlDataSource, DataSourceData> for Compositor {
3765    fn request(
3766        _: &mut Self,
3767        _: &Client,
3768        _: &WlDataSource,
3769        request: <WlDataSource as Resource>::Request,
3770        data: &DataSourceData,
3771        _: &DisplayHandle,
3772        _: &mut DataInit<'_, Self>,
3773    ) {
3774        use wl_data_source::Request;
3775        match request {
3776            Request::Offer { mime_type } => {
3777                data.mime_types.lock().unwrap().push(mime_type);
3778            }
3779            Request::Destroy => {}
3780            _ => {} // SetActions — DnD, ignored
3781        }
3782    }
3783
3784    fn destroyed(
3785        state: &mut Self,
3786        _: wayland_server::backend::ClientId,
3787        resource: &WlDataSource,
3788        _: &DataSourceData,
3789    ) {
3790        if state
3791            .selection_source
3792            .as_ref()
3793            .is_some_and(|s| s.id() == resource.id())
3794        {
3795            state.selection_source = None;
3796        }
3797    }
3798}
3799
3800impl Dispatch<WlDataDevice, ()> for Compositor {
3801    fn request(
3802        state: &mut Self,
3803        _: &Client,
3804        _: &WlDataDevice,
3805        request: <WlDataDevice as Resource>::Request,
3806        _: &(),
3807        _: &DisplayHandle,
3808        _: &mut DataInit<'_, Self>,
3809    ) {
3810        use wl_data_device::Request;
3811        match request {
3812            Request::SetSelection { source, serial: _ } => {
3813                state.selection_source = source.clone();
3814                // Try to read text content and emit an event.
3815                if let Some(ref src) = source {
3816                    let data = src.data::<DataSourceData>().unwrap();
3817                    let mimes = data.mime_types.lock().unwrap();
3818                    let text_mime = mimes
3819                        .iter()
3820                        .find(|m| {
3821                            m.as_str() == "text/plain;charset=utf-8"
3822                                || m.as_str() == "text/plain"
3823                                || m.as_str() == "UTF8_STRING"
3824                        })
3825                        .cloned();
3826                    drop(mimes);
3827                    if let Some(mime) = text_mime {
3828                        state.read_data_source_and_emit(src, &mime);
3829                    }
3830                }
3831            }
3832            Request::Release => {}
3833            _ => {} // StartDrag — ignored
3834        }
3835    }
3836
3837    fn destroyed(
3838        state: &mut Self,
3839        _: wayland_server::backend::ClientId,
3840        resource: &WlDataDevice,
3841        _: &(),
3842    ) {
3843        state.data_devices.retain(|d| d.id() != resource.id());
3844    }
3845}
3846
3847impl Dispatch<WlDataOffer, DataOfferData> for Compositor {
3848    fn request(
3849        state: &mut Self,
3850        _: &Client,
3851        _: &WlDataOffer,
3852        request: <WlDataOffer as Resource>::Request,
3853        data: &DataOfferData,
3854        _: &DisplayHandle,
3855        _: &mut DataInit<'_, Self>,
3856    ) {
3857        use wl_data_offer::Request;
3858        match request {
3859            Request::Receive { mime_type, fd } => {
3860                if data.external {
3861                    // Write external clipboard data to the fd.
3862                    if let Some(ref cb) = state.external_clipboard
3863                        && (cb.mime_type == mime_type
3864                            || mime_type == "text/plain"
3865                            || mime_type == "text/plain;charset=utf-8"
3866                            || mime_type == "UTF8_STRING")
3867                    {
3868                        use std::io::Write;
3869                        let mut f = std::fs::File::from(fd);
3870                        let _ = f.write_all(&cb.data);
3871                    }
3872                } else if let Some(ref src) = state.selection_source {
3873                    // Forward to the Wayland data source.
3874                    src.send(mime_type, fd.as_fd());
3875                }
3876            }
3877            Request::Destroy => {}
3878            _ => {} // Accept, Finish, SetActions — DnD
3879        }
3880    }
3881}
3882
3883impl Compositor {
3884    /// Create a pipe, ask the data source to write into it, read the result,
3885    /// and emit a `ClipboardContent` event.
3886    fn read_data_source_and_emit(&mut self, source: &WlDataSource, mime_type: &str) {
3887        let mut fds = [0i32; 2];
3888        if unsafe { libc::pipe(fds.as_mut_ptr()) } != 0 {
3889            return;
3890        }
3891        let read_fd = unsafe { OwnedFd::from_raw_fd(fds[0]) };
3892        let write_fd = unsafe { OwnedFd::from_raw_fd(fds[1]) };
3893        source.send(mime_type.to_string(), write_fd.as_fd());
3894        let _ = self.display_handle.flush_clients();
3895        // Non-blocking read with a modest limit.
3896        unsafe {
3897            libc::fcntl(read_fd.as_raw_fd(), libc::F_SETFL, libc::O_NONBLOCK);
3898        }
3899        // Give the client a moment to write.
3900        std::thread::sleep(std::time::Duration::from_millis(5));
3901        let mut buf = Vec::new();
3902        let mut tmp = [0u8; 8192];
3903        loop {
3904            let n = unsafe {
3905                libc::read(
3906                    read_fd.as_raw_fd(),
3907                    tmp.as_mut_ptr() as *mut libc::c_void,
3908                    tmp.len(),
3909                )
3910            };
3911            if n <= 0 {
3912                break;
3913            }
3914            buf.extend_from_slice(&tmp[..n as usize]);
3915            if buf.len() > 1024 * 1024 {
3916                break; // 1 MiB cap
3917            }
3918        }
3919        if !buf.is_empty() {
3920            let _ = self.event_tx.send(CompositorEvent::ClipboardContent {
3921                mime_type: mime_type.to_string(),
3922                data: buf,
3923            });
3924            (self.event_notify)();
3925        }
3926    }
3927
3928    /// Push external clipboard to all connected wl_data_device objects.
3929    fn offer_external_clipboard(&mut self) {
3930        let Some(ref cb) = self.external_clipboard else {
3931            return;
3932        };
3933        let mime = cb.mime_type.clone();
3934        for dd in &self.data_devices {
3935            if let Some(client) = dd.client() {
3936                let offer = client
3937                    .create_resource::<WlDataOffer, DataOfferData, Compositor>(
3938                        &self.display_handle,
3939                        dd.version(),
3940                        DataOfferData { external: true },
3941                    )
3942                    .unwrap();
3943                dd.data_offer(&offer);
3944                offer.offer(mime.clone());
3945                // Offer standard text aliases.
3946                if mime.starts_with("text/plain") {
3947                    if mime != "text/plain" {
3948                        offer.offer("text/plain".to_string());
3949                    }
3950                    if mime != "text/plain;charset=utf-8" {
3951                        offer.offer("text/plain;charset=utf-8".to_string());
3952                    }
3953                    offer.offer("UTF8_STRING".to_string());
3954                }
3955                dd.selection(Some(&offer));
3956            }
3957        }
3958        let _ = self.display_handle.flush_clients();
3959    }
3960}
3961
3962// -- zwp_primary_selection --
3963
3964impl GlobalDispatch<ZwpPrimarySelectionDeviceManagerV1, ()> for Compositor {
3965    fn bind(
3966        _: &mut Self,
3967        _: &DisplayHandle,
3968        _: &Client,
3969        resource: New<ZwpPrimarySelectionDeviceManagerV1>,
3970        _: &(),
3971        data_init: &mut DataInit<'_, Self>,
3972    ) {
3973        data_init.init(resource, ());
3974    }
3975}
3976
3977impl Dispatch<ZwpPrimarySelectionDeviceManagerV1, ()> for Compositor {
3978    fn request(
3979        state: &mut Self,
3980        _: &Client,
3981        _: &ZwpPrimarySelectionDeviceManagerV1,
3982        request: <ZwpPrimarySelectionDeviceManagerV1 as Resource>::Request,
3983        _: &(),
3984        _: &DisplayHandle,
3985        data_init: &mut DataInit<'_, Self>,
3986    ) {
3987        use zwp_primary_selection_device_manager_v1::Request;
3988        match request {
3989            Request::CreateSource { id } => {
3990                data_init.init(
3991                    id,
3992                    PrimarySourceData {
3993                        mime_types: std::sync::Mutex::new(Vec::new()),
3994                    },
3995                );
3996            }
3997            Request::GetDevice { id, seat: _ } => {
3998                let pd = data_init.init(id, ());
3999                state.primary_devices.push(pd);
4000            }
4001            Request::Destroy => {}
4002            _ => {}
4003        }
4004    }
4005}
4006
4007impl Dispatch<ZwpPrimarySelectionSourceV1, PrimarySourceData> for Compositor {
4008    fn request(
4009        _: &mut Self,
4010        _: &Client,
4011        _: &ZwpPrimarySelectionSourceV1,
4012        request: <ZwpPrimarySelectionSourceV1 as Resource>::Request,
4013        data: &PrimarySourceData,
4014        _: &DisplayHandle,
4015        _: &mut DataInit<'_, Self>,
4016    ) {
4017        use zwp_primary_selection_source_v1::Request;
4018        match request {
4019            Request::Offer { mime_type } => {
4020                data.mime_types.lock().unwrap().push(mime_type);
4021            }
4022            Request::Destroy => {}
4023            _ => {}
4024        }
4025    }
4026
4027    fn destroyed(
4028        state: &mut Self,
4029        _: wayland_server::backend::ClientId,
4030        resource: &ZwpPrimarySelectionSourceV1,
4031        _: &PrimarySourceData,
4032    ) {
4033        if state
4034            .primary_source
4035            .as_ref()
4036            .is_some_and(|s| s.id() == resource.id())
4037        {
4038            state.primary_source = None;
4039        }
4040    }
4041}
4042
4043impl Dispatch<ZwpPrimarySelectionDeviceV1, ()> for Compositor {
4044    fn request(
4045        state: &mut Self,
4046        _: &Client,
4047        _: &ZwpPrimarySelectionDeviceV1,
4048        request: <ZwpPrimarySelectionDeviceV1 as Resource>::Request,
4049        _: &(),
4050        _: &DisplayHandle,
4051        _: &mut DataInit<'_, Self>,
4052    ) {
4053        use zwp_primary_selection_device_v1::Request;
4054        match request {
4055            Request::SetSelection { source, serial: _ } => {
4056                state.primary_source = source;
4057            }
4058            Request::Destroy => {}
4059            _ => {}
4060        }
4061    }
4062
4063    fn destroyed(
4064        state: &mut Self,
4065        _: wayland_server::backend::ClientId,
4066        resource: &ZwpPrimarySelectionDeviceV1,
4067        _: &(),
4068    ) {
4069        state.primary_devices.retain(|d| d.id() != resource.id());
4070    }
4071}
4072
4073impl Dispatch<ZwpPrimarySelectionOfferV1, PrimaryOfferData> for Compositor {
4074    fn request(
4075        state: &mut Self,
4076        _: &Client,
4077        _: &ZwpPrimarySelectionOfferV1,
4078        request: <ZwpPrimarySelectionOfferV1 as Resource>::Request,
4079        data: &PrimaryOfferData,
4080        _: &DisplayHandle,
4081        _: &mut DataInit<'_, Self>,
4082    ) {
4083        use zwp_primary_selection_offer_v1::Request;
4084        match request {
4085            Request::Receive { mime_type, fd } => {
4086                if data.external {
4087                    if let Some(ref cb) = state.external_primary {
4088                        use std::io::Write;
4089                        let mut f = std::fs::File::from(fd);
4090                        let _ = f.write_all(&cb.data);
4091                        let _ = mime_type; // accepted regardless
4092                    }
4093                } else if let Some(ref src) = state.primary_source {
4094                    src.send(mime_type, fd.as_fd());
4095                }
4096            }
4097            Request::Destroy => {}
4098            _ => {}
4099        }
4100    }
4101}
4102
4103// -- zwp_pointer_constraints_v1 --
4104
4105impl GlobalDispatch<ZwpPointerConstraintsV1, ()> for Compositor {
4106    fn bind(
4107        _: &mut Self,
4108        _: &DisplayHandle,
4109        _: &Client,
4110        resource: New<ZwpPointerConstraintsV1>,
4111        _: &(),
4112        data_init: &mut DataInit<'_, Self>,
4113    ) {
4114        data_init.init(resource, ());
4115    }
4116}
4117
4118impl Dispatch<ZwpPointerConstraintsV1, ()> for Compositor {
4119    fn request(
4120        _: &mut Self,
4121        _: &Client,
4122        _: &ZwpPointerConstraintsV1,
4123        request: <ZwpPointerConstraintsV1 as Resource>::Request,
4124        _: &(),
4125        _: &DisplayHandle,
4126        data_init: &mut DataInit<'_, Self>,
4127    ) {
4128        use zwp_pointer_constraints_v1::Request;
4129        match request {
4130            Request::LockPointer {
4131                id,
4132                surface: _,
4133                pointer: _,
4134                region: _,
4135                lifetime: _,
4136            } => {
4137                let lp = data_init.init(id, ());
4138                // Immediately grant the lock (headless — no physical pointer to contest).
4139                lp.locked();
4140            }
4141            Request::ConfinePointer {
4142                id,
4143                surface: _,
4144                pointer: _,
4145                region: _,
4146                lifetime: _,
4147            } => {
4148                let cp = data_init.init(id, ());
4149                cp.confined();
4150            }
4151            Request::Destroy => {}
4152            _ => {}
4153        }
4154    }
4155}
4156
4157impl Dispatch<ZwpLockedPointerV1, ()> for Compositor {
4158    fn request(
4159        _: &mut Self,
4160        _: &Client,
4161        _: &ZwpLockedPointerV1,
4162        _: <ZwpLockedPointerV1 as Resource>::Request,
4163        _: &(),
4164        _: &DisplayHandle,
4165        _: &mut DataInit<'_, Self>,
4166    ) {
4167        // SetCursorPositionHint, SetRegion, Destroy — no-ops for headless.
4168    }
4169}
4170
4171impl Dispatch<ZwpConfinedPointerV1, ()> for Compositor {
4172    fn request(
4173        _: &mut Self,
4174        _: &Client,
4175        _: &ZwpConfinedPointerV1,
4176        _: <ZwpConfinedPointerV1 as Resource>::Request,
4177        _: &(),
4178        _: &DisplayHandle,
4179        _: &mut DataInit<'_, Self>,
4180    ) {
4181        // SetRegion, Destroy — no-ops for headless.
4182    }
4183}
4184
4185// -- zwp_relative_pointer_manager_v1 --
4186
4187impl GlobalDispatch<ZwpRelativePointerManagerV1, ()> for Compositor {
4188    fn bind(
4189        _: &mut Self,
4190        _: &DisplayHandle,
4191        _: &Client,
4192        resource: New<ZwpRelativePointerManagerV1>,
4193        _: &(),
4194        data_init: &mut DataInit<'_, Self>,
4195    ) {
4196        data_init.init(resource, ());
4197    }
4198}
4199
4200impl Dispatch<ZwpRelativePointerManagerV1, ()> for Compositor {
4201    fn request(
4202        state: &mut Self,
4203        _: &Client,
4204        _: &ZwpRelativePointerManagerV1,
4205        request: <ZwpRelativePointerManagerV1 as Resource>::Request,
4206        _: &(),
4207        _: &DisplayHandle,
4208        data_init: &mut DataInit<'_, Self>,
4209    ) {
4210        use zwp_relative_pointer_manager_v1::Request;
4211        match request {
4212            Request::GetRelativePointer { id, pointer: _ } => {
4213                let rp = data_init.init(id, ());
4214                state.relative_pointers.push(rp);
4215            }
4216            Request::Destroy => {}
4217            _ => {}
4218        }
4219    }
4220}
4221
4222impl Dispatch<ZwpRelativePointerV1, ()> for Compositor {
4223    fn request(
4224        state: &mut Self,
4225        _: &Client,
4226        resource: &ZwpRelativePointerV1,
4227        _: <ZwpRelativePointerV1 as Resource>::Request,
4228        _: &(),
4229        _: &DisplayHandle,
4230        _: &mut DataInit<'_, Self>,
4231    ) {
4232        // Only request is Destroy.
4233        state
4234            .relative_pointers
4235            .retain(|rp| rp.id() != resource.id());
4236    }
4237}
4238
4239// -- zwp_text_input_v3 --
4240
4241impl GlobalDispatch<ZwpTextInputManagerV3, ()> for Compositor {
4242    fn bind(
4243        _: &mut Self,
4244        _: &DisplayHandle,
4245        _: &Client,
4246        resource: New<ZwpTextInputManagerV3>,
4247        _: &(),
4248        data_init: &mut DataInit<'_, Self>,
4249    ) {
4250        data_init.init(resource, ());
4251    }
4252}
4253
4254impl Dispatch<ZwpTextInputManagerV3, ()> for Compositor {
4255    fn request(
4256        state: &mut Self,
4257        _: &Client,
4258        _: &ZwpTextInputManagerV3,
4259        request: <ZwpTextInputManagerV3 as Resource>::Request,
4260        _: &(),
4261        _: &DisplayHandle,
4262        data_init: &mut DataInit<'_, Self>,
4263    ) {
4264        use zwp_text_input_manager_v3::Request;
4265        match request {
4266            Request::GetTextInput { id, seat: _ } => {
4267                let ti = data_init.init(id, ());
4268                state.text_inputs.push(TextInputState {
4269                    resource: ti,
4270                    enabled: false,
4271                });
4272            }
4273            Request::Destroy => {}
4274            _ => {}
4275        }
4276    }
4277}
4278
4279impl Dispatch<ZwpTextInputV3, ()> for Compositor {
4280    fn request(
4281        state: &mut Self,
4282        _: &Client,
4283        resource: &ZwpTextInputV3,
4284        request: <ZwpTextInputV3 as Resource>::Request,
4285        _: &(),
4286        _: &DisplayHandle,
4287        _: &mut DataInit<'_, Self>,
4288    ) {
4289        use zwp_text_input_v3::Request;
4290        match request {
4291            Request::Enable => {
4292                if let Some(ti) = state
4293                    .text_inputs
4294                    .iter_mut()
4295                    .find(|t| t.resource.id() == resource.id())
4296                {
4297                    ti.enabled = true;
4298                }
4299            }
4300            Request::Disable => {
4301                if let Some(ti) = state
4302                    .text_inputs
4303                    .iter_mut()
4304                    .find(|t| t.resource.id() == resource.id())
4305                {
4306                    ti.enabled = false;
4307                }
4308            }
4309            Request::Commit => {
4310                // Client acknowledges our last done; nothing to do.
4311            }
4312            Request::Destroy => {
4313                state
4314                    .text_inputs
4315                    .retain(|t| t.resource.id() != resource.id());
4316            }
4317            // SetSurroundingText, SetTextChangeCause, SetContentType,
4318            // SetCursorRectangle — informational; ignored for now.
4319            _ => {}
4320        }
4321    }
4322}
4323
4324// -- xdg_activation_v1 --
4325
4326impl GlobalDispatch<XdgActivationV1, ()> for Compositor {
4327    fn bind(
4328        _: &mut Self,
4329        _: &DisplayHandle,
4330        _: &Client,
4331        resource: New<XdgActivationV1>,
4332        _: &(),
4333        data_init: &mut DataInit<'_, Self>,
4334    ) {
4335        data_init.init(resource, ());
4336    }
4337}
4338
4339impl Dispatch<XdgActivationV1, ()> for Compositor {
4340    fn request(
4341        state: &mut Self,
4342        _: &Client,
4343        _: &XdgActivationV1,
4344        request: <XdgActivationV1 as Resource>::Request,
4345        _: &(),
4346        _: &DisplayHandle,
4347        data_init: &mut DataInit<'_, Self>,
4348    ) {
4349        use xdg_activation_v1::Request;
4350        match request {
4351            Request::GetActivationToken { id } => {
4352                let serial = state.next_activation_token;
4353                state.next_activation_token = serial.wrapping_add(1);
4354                data_init.init(id, ActivationTokenData { serial });
4355            }
4356            Request::Activate {
4357                token: _,
4358                surface: _,
4359            } => {
4360                // In a headless compositor, activation requests are always
4361                // granted (focus is managed externally by the browser/CLI).
4362            }
4363            Request::Destroy => {}
4364            _ => {}
4365        }
4366    }
4367}
4368
4369impl Dispatch<XdgActivationTokenV1, ActivationTokenData> for Compositor {
4370    fn request(
4371        _: &mut Self,
4372        _: &Client,
4373        resource: &XdgActivationTokenV1,
4374        request: <XdgActivationTokenV1 as Resource>::Request,
4375        data: &ActivationTokenData,
4376        _: &DisplayHandle,
4377        _: &mut DataInit<'_, Self>,
4378    ) {
4379        use xdg_activation_token_v1::Request;
4380        match request {
4381            Request::Commit => {
4382                // Issue a token immediately — the headless compositor doesn't
4383                // need to validate app_id / surface / serial.
4384                resource.done(format!("blit-token-{}", data.serial));
4385            }
4386            Request::SetSerial { .. } | Request::SetAppId { .. } | Request::SetSurface { .. } => {}
4387            Request::Destroy => {}
4388            _ => {}
4389        }
4390    }
4391}
4392
4393// -- wp_cursor_shape_manager_v1 --
4394
4395impl GlobalDispatch<WpCursorShapeManagerV1, ()> for Compositor {
4396    fn bind(
4397        _: &mut Self,
4398        _: &DisplayHandle,
4399        _: &Client,
4400        resource: New<WpCursorShapeManagerV1>,
4401        _: &(),
4402        data_init: &mut DataInit<'_, Self>,
4403    ) {
4404        data_init.init(resource, ());
4405    }
4406}
4407
4408impl Dispatch<WpCursorShapeManagerV1, ()> for Compositor {
4409    fn request(
4410        _: &mut Self,
4411        _: &Client,
4412        _: &WpCursorShapeManagerV1,
4413        request: <WpCursorShapeManagerV1 as Resource>::Request,
4414        _: &(),
4415        _: &DisplayHandle,
4416        data_init: &mut DataInit<'_, Self>,
4417    ) {
4418        use wp_cursor_shape_manager_v1::Request;
4419        match request {
4420            Request::GetPointer {
4421                cursor_shape_device,
4422                pointer: _,
4423            } => {
4424                data_init.init(cursor_shape_device, ());
4425            }
4426            Request::GetTabletToolV2 {
4427                cursor_shape_device,
4428                tablet_tool: _,
4429            } => {
4430                data_init.init(cursor_shape_device, ());
4431            }
4432            Request::Destroy => {}
4433            _ => {}
4434        }
4435    }
4436}
4437
4438impl Dispatch<WpCursorShapeDeviceV1, ()> for Compositor {
4439    fn request(
4440        state: &mut Self,
4441        _: &Client,
4442        _: &WpCursorShapeDeviceV1,
4443        request: <WpCursorShapeDeviceV1 as Resource>::Request,
4444        _: &(),
4445        _: &DisplayHandle,
4446        _: &mut DataInit<'_, Self>,
4447    ) {
4448        use wp_cursor_shape_device_v1::Request;
4449        match request {
4450            Request::SetShape { serial: _, shape } => {
4451                use wayland_server::WEnum;
4452                use wp_cursor_shape_device_v1::Shape;
4453                let name = match shape {
4454                    WEnum::Value(Shape::Default) => "default",
4455                    WEnum::Value(Shape::ContextMenu) => "context-menu",
4456                    WEnum::Value(Shape::Help) => "help",
4457                    WEnum::Value(Shape::Pointer) => "pointer",
4458                    WEnum::Value(Shape::Progress) => "progress",
4459                    WEnum::Value(Shape::Wait) => "wait",
4460                    WEnum::Value(Shape::Cell) => "cell",
4461                    WEnum::Value(Shape::Crosshair) => "crosshair",
4462                    WEnum::Value(Shape::Text) => "text",
4463                    WEnum::Value(Shape::VerticalText) => "vertical-text",
4464                    WEnum::Value(Shape::Alias) => "alias",
4465                    WEnum::Value(Shape::Copy) => "copy",
4466                    WEnum::Value(Shape::Move) => "move",
4467                    WEnum::Value(Shape::NoDrop) => "no-drop",
4468                    WEnum::Value(Shape::NotAllowed) => "not-allowed",
4469                    WEnum::Value(Shape::Grab) => "grab",
4470                    WEnum::Value(Shape::Grabbing) => "grabbing",
4471                    WEnum::Value(Shape::EResize) => "e-resize",
4472                    WEnum::Value(Shape::NResize) => "n-resize",
4473                    WEnum::Value(Shape::NeResize) => "ne-resize",
4474                    WEnum::Value(Shape::NwResize) => "nw-resize",
4475                    WEnum::Value(Shape::SResize) => "s-resize",
4476                    WEnum::Value(Shape::SeResize) => "se-resize",
4477                    WEnum::Value(Shape::SwResize) => "sw-resize",
4478                    WEnum::Value(Shape::WResize) => "w-resize",
4479                    WEnum::Value(Shape::EwResize) => "ew-resize",
4480                    WEnum::Value(Shape::NsResize) => "ns-resize",
4481                    WEnum::Value(Shape::NeswResize) => "nesw-resize",
4482                    WEnum::Value(Shape::NwseResize) => "nwse-resize",
4483                    WEnum::Value(Shape::ColResize) => "col-resize",
4484                    WEnum::Value(Shape::RowResize) => "row-resize",
4485                    WEnum::Value(Shape::AllScroll) => "all-scroll",
4486                    WEnum::Value(Shape::ZoomIn) => "zoom-in",
4487                    WEnum::Value(Shape::ZoomOut) => "zoom-out",
4488                    _ => "default",
4489                };
4490                let _ = state.event_tx.send(CompositorEvent::SurfaceCursor {
4491                    surface_id: state.focused_surface_id,
4492                    cursor: CursorImage::Named(name.to_string()),
4493                });
4494                (state.event_notify)();
4495            }
4496            Request::Destroy => {}
4497            _ => {}
4498        }
4499    }
4500}
4501
4502// -- Client data --
4503impl wayland_server::backend::ClientData for ClientState {
4504    fn initialized(&self, _: wayland_server::backend::ClientId) {}
4505    fn disconnected(
4506        &self,
4507        _: wayland_server::backend::ClientId,
4508        _: wayland_server::backend::DisconnectReason,
4509    ) {
4510    }
4511}
4512
4513// ---------------------------------------------------------------------------
4514// Public API
4515// ---------------------------------------------------------------------------
4516
4517pub struct CompositorHandle {
4518    pub event_rx: mpsc::Receiver<CompositorEvent>,
4519    pub command_tx: mpsc::Sender<CompositorCommand>,
4520    pub socket_name: String,
4521    pub thread: std::thread::JoinHandle<()>,
4522    pub shutdown: Arc<AtomicBool>,
4523    loop_signal: LoopSignal,
4524}
4525
4526impl CompositorHandle {
4527    pub fn wake(&self) {
4528        self.loop_signal.wakeup();
4529    }
4530}
4531
4532pub fn spawn_compositor(
4533    verbose: bool,
4534    event_notify: Arc<dyn Fn() + Send + Sync>,
4535    gpu_device: &str,
4536) -> CompositorHandle {
4537    let _gpu_device = gpu_device.to_string();
4538    let (event_tx, event_rx) = mpsc::channel();
4539    let (command_tx, command_rx) = mpsc::channel();
4540    let (socket_tx, socket_rx) = mpsc::sync_channel(1);
4541    let (signal_tx, signal_rx) = mpsc::sync_channel::<LoopSignal>(1);
4542    let shutdown = Arc::new(AtomicBool::new(false));
4543    let shutdown_clone = shutdown.clone();
4544
4545    let runtime_dir = std::env::var_os("XDG_RUNTIME_DIR")
4546        .map(std::path::PathBuf::from)
4547        .filter(|p| {
4548            let probe = p.join(".blit-probe");
4549            if std::fs::write(&probe, b"").is_ok() {
4550                let _ = std::fs::remove_file(&probe);
4551                true
4552            } else {
4553                false
4554            }
4555        })
4556        .unwrap_or_else(std::env::temp_dir);
4557
4558    let runtime_dir_clone = runtime_dir.clone();
4559    let thread = std::thread::Builder::new()
4560        .name("compositor".into())
4561        .spawn(move || {
4562            unsafe { std::env::set_var("XDG_RUNTIME_DIR", &runtime_dir_clone) };
4563            let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
4564                run_compositor(
4565                    event_tx,
4566                    command_rx,
4567                    socket_tx,
4568                    signal_tx,
4569                    event_notify,
4570                    shutdown_clone,
4571                    verbose,
4572                    _gpu_device,
4573                );
4574            }));
4575            if let Err(e) = result {
4576                let msg = if let Some(s) = e.downcast_ref::<&str>() {
4577                    s.to_string()
4578                } else if let Some(s) = e.downcast_ref::<String>() {
4579                    s.clone()
4580                } else {
4581                    "unknown panic".to_string()
4582                };
4583                eprintln!("[compositor] PANIC: {msg}");
4584            }
4585        })
4586        .expect("failed to spawn compositor thread");
4587
4588    let socket_name = socket_rx.recv().expect("compositor failed to start");
4589    let socket_name = runtime_dir
4590        .join(&socket_name)
4591        .to_string_lossy()
4592        .into_owned();
4593    let loop_signal = signal_rx
4594        .recv()
4595        .expect("compositor failed to send loop signal");
4596
4597    CompositorHandle {
4598        event_rx,
4599        command_tx,
4600        socket_name,
4601        thread,
4602        shutdown,
4603        loop_signal,
4604    }
4605}
4606
4607#[allow(clippy::too_many_arguments)]
4608fn run_compositor(
4609    event_tx: mpsc::Sender<CompositorEvent>,
4610    command_rx: mpsc::Receiver<CompositorCommand>,
4611    socket_tx: mpsc::SyncSender<String>,
4612    signal_tx: mpsc::SyncSender<LoopSignal>,
4613    event_notify: Arc<dyn Fn() + Send + Sync>,
4614    shutdown: Arc<AtomicBool>,
4615    verbose: bool,
4616    gpu_device: String,
4617) {
4618    let mut event_loop: EventLoop<Compositor> =
4619        EventLoop::try_new().expect("failed to create event loop");
4620    let loop_signal = event_loop.get_signal();
4621
4622    let display: Display<Compositor> = Display::new().expect("failed to create display");
4623    let dh = display.handle();
4624
4625    // Create globals.
4626    dh.create_global::<Compositor, WlCompositor, ()>(6, ());
4627    dh.create_global::<Compositor, WlSubcompositor, ()>(1, ());
4628    dh.create_global::<Compositor, XdgWmBase, ()>(6, ());
4629    dh.create_global::<Compositor, WlShm, ()>(1, ());
4630    dh.create_global::<Compositor, WlOutput, ()>(4, ());
4631    dh.create_global::<Compositor, WlSeat, ()>(9, ());
4632    dh.create_global::<Compositor, ZwpLinuxDmabufV1, ()>(3, ());
4633    dh.create_global::<Compositor, WpViewporter, ()>(1, ());
4634    dh.create_global::<Compositor, WpFractionalScaleManagerV1, ()>(1, ());
4635    dh.create_global::<Compositor, ZxdgDecorationManagerV1, ()>(1, ());
4636    dh.create_global::<Compositor, WlDataDeviceManager, ()>(3, ());
4637    dh.create_global::<Compositor, ZwpPointerConstraintsV1, ()>(1, ());
4638    dh.create_global::<Compositor, ZwpRelativePointerManagerV1, ()>(1, ());
4639    dh.create_global::<Compositor, XdgActivationV1, ()>(1, ());
4640    dh.create_global::<Compositor, WpCursorShapeManagerV1, ()>(1, ());
4641    dh.create_global::<Compositor, ZwpPrimarySelectionDeviceManagerV1, ()>(1, ());
4642    dh.create_global::<Compositor, WpPresentation, ()>(1, ());
4643    dh.create_global::<Compositor, ZwpTextInputManagerV3, ()>(1, ());
4644
4645    // XKB keymap.
4646    let keymap_string = include_str!("../data/us-qwerty.xkb");
4647    let mut keymap_data = keymap_string.as_bytes().to_vec();
4648    keymap_data.push(0); // null-terminate
4649
4650    // Listening socket.
4651    let listening_socket = wayland_server::ListeningSocket::bind_auto("wayland", 0..33)
4652        .unwrap_or_else(|e| {
4653            let dir = std::env::var("XDG_RUNTIME_DIR").unwrap_or_else(|_| "(unset)".into());
4654            panic!("failed to create wayland socket in XDG_RUNTIME_DIR={dir}: {e}\nhint: ensure the directory exists and is writable by the current user");
4655        });
4656    let socket_name = listening_socket
4657        .socket_name()
4658        .unwrap()
4659        .to_string_lossy()
4660        .into_owned();
4661    socket_tx.send(socket_name).unwrap();
4662    let _ = signal_tx.send(loop_signal.clone());
4663
4664    let mut compositor = Compositor {
4665        display_handle: dh,
4666        surfaces: HashMap::new(),
4667        toplevel_surface_ids: HashMap::new(),
4668        next_surface_id: 1,
4669        shm_pools: HashMap::new(),
4670        pixel_cache: HashMap::new(),
4671        dmabuf_params: HashMap::new(),
4672        vulkan_renderer: {
4673            eprintln!("[compositor] trying Vulkan renderer for {gpu_device}");
4674            let r = super::vulkan_render::VulkanRenderer::try_new(&gpu_device);
4675            eprintln!("[compositor] Vulkan renderer: {}", r.is_some());
4676            r
4677        },
4678        output_width: 1920,
4679        output_height: 1080,
4680        output_scale_120: 120,
4681        outputs: Vec::new(),
4682        keyboards: Vec::new(),
4683        pointers: Vec::new(),
4684        keyboard_keymap_data: keymap_data,
4685        mods_depressed: 0,
4686        mods_locked: 0,
4687        serial: 0,
4688        event_tx,
4689        event_notify,
4690        loop_signal: loop_signal.clone(),
4691        pending_commits: HashMap::new(),
4692        focused_surface_id: 0,
4693        pointer_entered_id: None,
4694        pending_kb_reenter: false,
4695        verbose,
4696        shutdown: shutdown.clone(),
4697        last_reported_size: HashMap::new(),
4698        surface_sizes: HashMap::new(),
4699        positioners: HashMap::new(),
4700        fractional_scales: Vec::new(),
4701        data_devices: Vec::new(),
4702        selection_source: None,
4703        external_clipboard: None,
4704        primary_devices: Vec::new(),
4705        primary_source: None,
4706        external_primary: None,
4707        relative_pointers: Vec::new(),
4708        text_inputs: Vec::new(),
4709        text_input_serial: 0,
4710        next_activation_token: 1,
4711    };
4712
4713    let handle = event_loop.handle();
4714
4715    // Insert display fd source.
4716    let display_source = Generic::new(display, Interest::READ, calloop::Mode::Level);
4717    handle
4718        .insert_source(display_source, |_, display, state| {
4719            let d = unsafe { display.get_mut() };
4720            if let Err(e) = d.dispatch_clients(state)
4721                && state.verbose
4722            {
4723                eprintln!("[compositor] dispatch_clients error: {e}");
4724            }
4725            state.cleanup_dead_surfaces();
4726            if let Err(e) = d.flush_clients()
4727                && state.verbose
4728            {
4729                eprintln!("[compositor] flush_clients error: {e}");
4730            }
4731            Ok(PostAction::Continue)
4732        })
4733        .expect("failed to insert display source");
4734
4735    // Insert listening socket.
4736    let socket_source = Generic::new(listening_socket, Interest::READ, calloop::Mode::Level);
4737    handle
4738        .insert_source(socket_source, |_, socket, state| {
4739            let ls = unsafe { socket.get_mut() };
4740            if let Some(client_stream) = ls.accept().ok().flatten()
4741                && let Err(e) = state
4742                    .display_handle
4743                    .insert_client(client_stream, Arc::new(ClientState))
4744                && state.verbose
4745            {
4746                eprintln!("[compositor] insert_client error: {e}");
4747            }
4748            Ok(PostAction::Continue)
4749        })
4750        .expect("failed to insert listening socket");
4751
4752    if verbose {
4753        eprintln!("[compositor] entering event loop");
4754    }
4755
4756    while !shutdown.load(Ordering::Relaxed) {
4757        // Process commands.
4758        while let Ok(cmd) = command_rx.try_recv() {
4759            match cmd {
4760                CompositorCommand::Shutdown => {
4761                    shutdown.store(true, Ordering::Relaxed);
4762                    return;
4763                }
4764                other => compositor.handle_command(other),
4765            }
4766        }
4767
4768        if let Err(e) =
4769            event_loop.dispatch(Some(std::time::Duration::from_secs(1)), &mut compositor)
4770            && verbose
4771        {
4772            eprintln!("[compositor] event loop error: {e}");
4773        }
4774
4775        if !compositor.pending_commits.is_empty() {
4776            compositor.flush_pending_commits();
4777        }
4778
4779        if let Err(e) = compositor.display_handle.flush_clients()
4780            && verbose
4781        {
4782            eprintln!("[compositor] flush error: {e}");
4783        }
4784    }
4785
4786    if verbose {
4787        eprintln!("[compositor] event loop exited");
4788    }
4789}