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