#![cfg(target_os = "linux")]

use std::cell::RefCell;
use std::collections::BTreeMap;
use std::error::Error;
use std::ffi::c_void;
use std::fs::File;
use std::io::ErrorKind;
use std::io::Read;
use std::io::Write;
use std::num::NonZeroU32;
use std::os::fd::{AsFd, AsRawFd, FromRawFd, OwnedFd, RawFd};
use std::ptr::NonNull;
use std::rc::Rc;
use std::time::{Duration, Instant};

use raw_window_handle::{
    DisplayHandle, HandleError, HasDisplayHandle, HasWindowHandle, RawDisplayHandle,
    RawWindowHandle, WaylandDisplayHandle, WaylandWindowHandle, WindowHandle,
};
use ruin_runtime::channel::mpsc;
use ruin_runtime::{
    TimeoutHandle, WorkerHandle, clear_timeout, queue_future, set_timeout, spawn_worker,
};
use ruin_ui::{
    CursorIcon, KeyboardEvent, KeyboardEventKind, KeyboardKey, KeyboardModifiers, PlatformEndpoint,
    PlatformEvent, PlatformRequest, PlatformRuntime, Point, PointerButton, PointerEvent,
    PointerEventKind, SceneSnapshot, UiRuntime, UiSize, WindowConfigured, WindowId,
    WindowLifecycle, WindowSpec, WindowUpdate,
};
use ruin_ui_renderer_wgpu::{RenderError, WgpuSceneRenderer};
use tracing::Level;
use tracing::{debug, trace};
use wayland_client::globals::{GlobalListContents, registry_queue_init};
use wayland_client::protocol::{
    wl_callback, wl_compositor, wl_data_device, wl_data_device_manager, wl_data_offer,
    wl_data_source, wl_keyboard, wl_pointer, wl_registry, wl_seat, wl_surface,
};
use wayland_client::{
    Connection, Dispatch, Proxy, QueueHandle, WEnum, delegate_noop, event_created_child,
};
use wayland_protocols::wp::cursor_shape::v1::client::{
    wp_cursor_shape_device_v1, wp_cursor_shape_manager_v1,
};
use wayland_protocols::wp::primary_selection::zv1::client::{
    zwp_primary_selection_device_manager_v1, zwp_primary_selection_device_v1,
    zwp_primary_selection_offer_v1, zwp_primary_selection_source_v1,
};
use wayland_protocols::xdg::decoration::zv1::client::{
    zxdg_decoration_manager_v1, zxdg_toplevel_decoration_v1,
};
use wayland_protocols::xdg::shell::client::{xdg_surface, xdg_toplevel, xdg_wm_base};
use xkbcommon::xkb;

#[derive(Clone)]
pub struct WaylandSurfaceTarget {
    connection: Connection,
    surface: wl_surface::WlSurface,
}

impl HasDisplayHandle for WaylandSurfaceTarget {
    fn display_handle(&self) -> Result<DisplayHandle<'_>, HandleError> {
        let ptr = NonNull::new(self.connection.backend().display_ptr().cast::<c_void>())
            .ok_or(HandleError::Unavailable)?;
        let raw = RawDisplayHandle::Wayland(WaylandDisplayHandle::new(ptr));
        Ok(unsafe { DisplayHandle::borrow_raw(raw) })
    }
}

impl HasWindowHandle for WaylandSurfaceTarget {
    fn window_handle(&self) -> Result<WindowHandle<'_>, HandleError> {
        let ptr = NonNull::new(self.surface.id().as_ptr().cast::<c_void>())
            .ok_or(HandleError::Unavailable)?;
        let raw = RawWindowHandle::Wayland(WaylandWindowHandle::new(ptr));
        Ok(unsafe { WindowHandle::borrow_raw(raw) })
    }
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct FrameRequest {
    pub width: u32,
    pub height: u32,
    pub resized: bool,
}

#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub struct WaitOutcome {
    pub dispatched: usize,
    pub external_ready: bool,
}

pub struct WaylandWindow {
    event_queue: wayland_client::EventQueue<State>,
    surface_target: WaylandSurfaceTarget,
    state: State,
}

struct WindowWorkerHandle {
    command_tx: mpsc::UnboundedSender<WindowWorkerCommand>,
    _worker: WorkerHandle,
}

struct WindowRecord {
    spec: WindowSpec,
    lifecycle: WindowLifecycle,
    latest_scene: Option<SceneSnapshot>,
    worker: Option<WindowWorkerHandle>,
}

struct WindowWorkerState {
    window_id: WindowId,
    spec: WindowSpec,
    window: WaylandWindow,
    renderer: WgpuSceneRenderer,
    latest_scene: Option<SceneSnapshot>,
    opened_emitted: bool,
    close_requested_emitted: bool,
    closed_emitted: bool,
    shutdown_requested: bool,
    pending_viewport: Option<UiSize>,
    viewport_request_in_flight: Option<UiSize>,
    event_tx: mpsc::UnboundedSender<PlatformEvent>,
    internal_tx: mpsc::UnboundedSender<InternalBackendEvent>,
    /// Pending keyboard-repeat timeout scheduled via [`ruin_runtime::set_timeout`].
    keyboard_repeat_timer: Option<TimeoutHandle>,
    /// When the current pending viewport was first requested; used to measure end-to-end
    /// resize latency in the "resize complete" log event.
    pending_viewport_since: Option<Instant>,
    /// Swapchain size that should be applied just before the next `renderer.render()`.
    /// Set on every `frame.resized` event; cleared once applied.  Deferring the resize
    /// to render-time means rapid configures (A→B→C) cause only one swapchain recreation
    /// instead of one per configure.
    pending_swapchain_size: Option<(u32, u32)>,
}

enum WindowWorkerCommand {
    ReplaceScene(SceneSnapshot),
    SetClipboardText(String),
    RequestClipboardText,
    SetPrimarySelectionText(String),
    RequestPrimarySelectionText,
    SetCursorIcon(CursorIcon),
    ApplySpec(WindowSpec),
    Shutdown,
}

enum InternalBackendEvent {
    Closed { window_id: WindowId },
}

struct State {
    running: bool,
    fixed_size_request: Option<(u32, u32)>,
    _connection: Connection,
    _compositor: wl_compositor::WlCompositor,
    _surface: wl_surface::WlSurface,
    _xdg_surface: xdg_surface::XdgSurface,
    _toplevel: xdg_toplevel::XdgToplevel,
    _wm_base: xdg_wm_base::XdgWmBase,
    _seat: wl_seat::WlSeat,
    clipboard_manager: Option<wl_data_device_manager::WlDataDeviceManager>,
    clipboard_device: Option<wl_data_device::WlDataDevice>,
    cursor_shape_manager: Option<wp_cursor_shape_manager_v1::WpCursorShapeManagerV1>,
    cursor_shape_device: Option<wp_cursor_shape_device_v1::WpCursorShapeDeviceV1>,
    /// Server-side decoration handle.  Kept alive so the compositor maintains decorations.
    _decoration: Option<zxdg_toplevel_decoration_v1::ZxdgToplevelDecorationV1>,
    primary_selection_manager:
        Option<zwp_primary_selection_device_manager_v1::ZwpPrimarySelectionDeviceManagerV1>,
    primary_selection_device: Option<zwp_primary_selection_device_v1::ZwpPrimarySelectionDeviceV1>,
    qh: QueueHandle<State>,
    pointer: Option<wl_pointer::WlPointer>,
    keyboard: Option<wl_keyboard::WlKeyboard>,
    keyboard_focused: bool,
    xkb_context: xkb::Context,
    xkb_keymap: Option<xkb::Keymap>,
    xkb_state: Option<xkb::State>,
    current_size: (u32, u32),
    configured: bool,
    pending_size: Option<(u32, u32)>,
    needs_redraw: bool,
    frame_callback: Option<wl_callback::WlCallback>,
    pointer_position: Option<Point>,
    pending_pointer_events: Vec<PointerEvent>,
    pending_keyboard_events: Vec<KeyboardEvent>,
    keyboard_modifiers: KeyboardModifiers,
    keyboard_repeat_rate: i32,
    keyboard_repeat_delay: Duration,
    keyboard_repeat: Option<KeyboardRepeatState>,
    clipboard_source: Option<wl_data_source::WlDataSource>,
    clipboard_text: Option<String>,
    clipboard_offer: Option<wl_data_offer::WlDataOffer>,
    clipboard_offer_mime_types: Vec<String>,
    primary_selection_source: Option<zwp_primary_selection_source_v1::ZwpPrimarySelectionSourceV1>,
    primary_selection_text: Option<String>,
    primary_selection_offer: Option<zwp_primary_selection_offer_v1::ZwpPrimarySelectionOfferV1>,
    primary_selection_offer_mime_types: Vec<String>,
    last_selection_serial: Option<u32>,
    last_pointer_enter_serial: Option<u32>,
    cursor_icon: CursorIcon,
}

#[derive(Clone, Debug)]
struct KeyboardRepeatState {
    keycode: u32,
    next_at: Instant,
    interval: Duration,
}

impl State {
    fn request_redraw(&mut self) {
        self.needs_redraw = true;
    }

    fn queue_ready_keyboard_repeats(&mut self) {
        let Some(repeat) = self.keyboard_repeat.as_mut() else {
            return;
        };
        let Some(xkb_state) = self.xkb_state.as_ref() else {
            return;
        };
        let now = Instant::now();
        while now >= repeat.next_at {
            let keycode = xkb::Keycode::new(repeat.keycode + 8);
            let text = keyboard_text_for_xkb(xkb_state, keycode);
            let key = keyboard_key_from_xkb(xkb_state.key_get_one_sym(keycode), text.as_deref());
            self.pending_keyboard_events.push(KeyboardEvent::new(
                repeat.keycode,
                KeyboardEventKind::Pressed,
                key,
                self.keyboard_modifiers,
                text,
            ));
            repeat.next_at += repeat.interval;
        }
    }
}

fn fixed_size_request_for_spec(spec: &WindowSpec) -> Option<(u32, u32)> {
    if spec.resizable {
        return None;
    }
    let size = spec
        .requested_inner_size
        .or(spec.min_inner_size)
        .or(spec.max_inner_size)
        .unwrap_or_else(|| UiSize::new(800.0, 500.0));
    Some((
        size.width.max(1.0).round() as u32,
        size.height.max(1.0).round() as u32,
    ))
}

fn wayland_cursor_shape(icon: CursorIcon) -> wp_cursor_shape_device_v1::Shape {
    match icon {
        CursorIcon::Default => wp_cursor_shape_device_v1::Shape::Default,
        CursorIcon::Pointer => wp_cursor_shape_device_v1::Shape::Pointer,
        CursorIcon::Text => wp_cursor_shape_device_v1::Shape::Text,
    }
}

fn apply_cursor_icon(state: &mut State) {
    let Some(device) = state.cursor_shape_device.as_ref() else {
        return;
    };
    let Some(serial) = state.last_pointer_enter_serial else {
        return;
    };
    device.set_shape(serial, wayland_cursor_shape(state.cursor_icon));
    let _ = state._connection.flush();
}

fn keyboard_modifiers_from_xkb(state: &xkb::State) -> KeyboardModifiers {
    KeyboardModifiers {
        shift: state.mod_name_is_active(xkb::MOD_NAME_SHIFT, xkb::STATE_MODS_EFFECTIVE),
        control: state.mod_name_is_active(xkb::MOD_NAME_CTRL, xkb::STATE_MODS_EFFECTIVE),
        alt: state.mod_name_is_active(xkb::MOD_NAME_ALT, xkb::STATE_MODS_EFFECTIVE),
        super_key: state.mod_name_is_active(xkb::MOD_NAME_LOGO, xkb::STATE_MODS_EFFECTIVE),
    }
}

fn preferred_plain_text_mime(mime_types: &[String]) -> Option<String> {
    mime_types
        .iter()
        .find(|mime| mime.as_str() == "text/plain;charset=utf-8")
        .or_else(|| mime_types.iter().find(|mime| mime.as_str() == "text/plain"))
        .cloned()
}

fn keyboard_text_for_xkb(state: &xkb::State, keycode: xkb::Keycode) -> Option<String> {
    let text = state.key_get_utf8(keycode);
    if text.is_empty() || text.chars().any(char::is_control) {
        return None;
    }
    Some(text)
}

fn keyboard_key_from_xkb(keysym: xkb::Keysym, text: Option<&str>) -> KeyboardKey {
    match xkb::keysym_get_name(keysym).as_str() {
        "BackSpace" => KeyboardKey::Backspace,
        "Delete" => KeyboardKey::Delete,
        "Return" => KeyboardKey::Enter,
        "Tab" => KeyboardKey::Tab,
        "Escape" => KeyboardKey::Escape,
        "Left" => KeyboardKey::ArrowLeft,
        "Right" => KeyboardKey::ArrowRight,
        "Up" => KeyboardKey::ArrowUp,
        "Down" => KeyboardKey::ArrowDown,
        "Home" => KeyboardKey::Home,
        "End" => KeyboardKey::End,
        _ => text
            .filter(|text| !text.is_empty())
            .map(str::to_owned)
            .or_else(|| {
                let utf8 = xkb::keysym_to_utf8(keysym);
                (!utf8.is_empty() && !utf8.chars().any(char::is_control)).then_some(utf8)
            })
            .map(KeyboardKey::Character)
            .unwrap_or(KeyboardKey::Unknown),
    }
}

impl WaylandWindow {
    pub fn open(spec: WindowSpec) -> Result<Self, Box<dyn Error>> {
        let connection = Connection::connect_to_env()?;
        let (globals, event_queue) = registry_queue_init::<State>(&connection)?;
        let qh = event_queue.handle();

        let compositor: wl_compositor::WlCompositor = globals.bind(&qh, 4..=6, ())?;
        let seat: wl_seat::WlSeat = globals.bind(&qh, 1..=9, ())?;
        let wm_base: xdg_wm_base::XdgWmBase = globals.bind(&qh, 1..=6, ())?;
        let clipboard_manager = globals.bind(&qh, 1..=3, ()).ok();
        let clipboard_device = clipboard_manager.as_ref().map(
            |manager: &wl_data_device_manager::WlDataDeviceManager| {
                manager.get_data_device(&seat, &qh, ())
            },
        );
        let cursor_shape_manager = globals.bind(&qh, 1..=2, ()).ok();
        let primary_selection_manager = globals.bind(&qh, 1..=1, ()).ok();
        let primary_selection_device = primary_selection_manager.as_ref().map(
            |manager: &zwp_primary_selection_device_manager_v1::ZwpPrimarySelectionDeviceManagerV1| {
                manager.get_device(&seat, &qh, ())
            },
        );
        let decoration_manager: Option<zxdg_decoration_manager_v1::ZxdgDecorationManagerV1> =
            globals.bind(&qh, 1..=1, ()).ok();
        let surface = compositor.create_surface(&qh, ());
        let xdg_surface = wm_base.get_xdg_surface(&surface, &qh, ());
        let toplevel = xdg_surface.get_toplevel(&qh, ());
        toplevel.set_title(spec.title.clone());
        if let Some(app_id) = spec.app_id.as_ref() {
            toplevel.set_app_id(app_id.clone());
        }
        let decoration = decoration_manager.as_ref().map(|dm| {
            let deco = dm.get_toplevel_decoration(&toplevel, &qh, ());
            deco.set_mode(zxdg_toplevel_decoration_v1::Mode::ServerSide);
            deco
        });

        apply_size_constraints(&toplevel, &spec);
        if spec.maximized {
            toplevel.set_maximized();
        }
        if spec.fullscreen {
            toplevel.set_fullscreen(None);
        }

        surface.commit();
        connection.flush()?;

        let initial_size = spec
            .requested_inner_size
            .unwrap_or_else(|| UiSize::new(800.0, 500.0));
        let initial_width = initial_size.width.max(1.0).round() as u32;
        let initial_height = initial_size.height.max(1.0).round() as u32;
        let surface_target = WaylandSurfaceTarget {
            connection: connection.clone(),
            surface: surface.clone(),
        };

        Ok(Self {
            event_queue,
            surface_target,
            state: State {
                running: true,
                fixed_size_request: fixed_size_request_for_spec(&spec),
                _connection: connection,
                _compositor: compositor,
                _surface: surface,
                _xdg_surface: xdg_surface,
                _toplevel: toplevel,
                _wm_base: wm_base,
                _seat: seat,
                clipboard_manager,
                clipboard_device,
                cursor_shape_manager,
                cursor_shape_device: None,
                primary_selection_manager,
                primary_selection_device,
                qh,
                pointer: None,
                keyboard: None,
                keyboard_focused: false,
                xkb_context: xkb::Context::new(xkb::CONTEXT_NO_FLAGS),
                xkb_keymap: None,
                xkb_state: None,
                current_size: (initial_width, initial_height),
                configured: false,
                pending_size: None,
                needs_redraw: false,
                frame_callback: None,
                pointer_position: None,
                pending_pointer_events: Vec::new(),
                pending_keyboard_events: Vec::new(),
                keyboard_modifiers: KeyboardModifiers::default(),
                keyboard_repeat_rate: 25,
                keyboard_repeat_delay: Duration::from_millis(500),
                keyboard_repeat: None,
                clipboard_source: None,
                clipboard_text: None,
                clipboard_offer: None,
                clipboard_offer_mime_types: Vec::new(),
                primary_selection_source: None,
                primary_selection_text: None,
                primary_selection_offer: None,
                primary_selection_offer_mime_types: Vec::new(),
                last_selection_serial: None,
                last_pointer_enter_serial: None,
                cursor_icon: CursorIcon::Default,
                _decoration: decoration,
            },
        })
    }

    pub fn surface_target(&self) -> WaylandSurfaceTarget {
        self.surface_target.clone()
    }

    pub fn is_running(&self) -> bool {
        self.state.running
    }

    pub fn dispatch(&mut self) -> Result<(), Box<dyn Error>> {
        self.event_queue.blocking_dispatch(&mut self.state)?;
        Ok(())
    }

    pub fn dispatch_pending(&mut self) -> Result<usize, Box<dyn Error>> {
        let dispatched = self.event_queue.dispatch_pending(&mut self.state)?;
        self.event_queue.flush()?;
        Ok(dispatched)
    }

    pub fn wait_for_events(&mut self, timeout: Duration) -> Result<usize, Box<dyn Error>> {
        Ok(self.wait_for_events_or_fd(None, timeout)?.dispatched)
    }

    pub fn poll_fd(&self) -> i32 {
        self.state._connection.as_fd().as_raw_fd()
    }

    pub fn dispatch_ready(&mut self) -> Result<usize, Box<dyn Error>> {
        self.event_queue.flush()?;

        let dispatched = self.event_queue.dispatch_pending(&mut self.state)?;
        if dispatched > 0 {
            return Ok(dispatched);
        }

        let Some(read_guard) = self.event_queue.prepare_read() else {
            return Ok(self.event_queue.dispatch_pending(&mut self.state)?);
        };

        match read_guard.read() {
            Ok(_) => {}
            Err(wayland_client::backend::WaylandError::Io(e))
                if e.kind() == ErrorKind::WouldBlock =>
            {
                return Ok(0);
            }
            Err(error) => return Err(Box::new(error)),
        }

        Ok(self.event_queue.dispatch_pending(&mut self.state)?)
    }

    pub fn wait_for_events_or_fd(
        &mut self,
        external_fd: Option<i32>,
        timeout: Duration,
    ) -> Result<WaitOutcome, Box<dyn Error>> {
        self.event_queue.flush()?;

        let dispatched = self.event_queue.dispatch_pending(&mut self.state)?;
        if dispatched > 0 {
            return Ok(WaitOutcome {
                dispatched,
                external_ready: false,
            });
        }

        let Some(read_guard) = self.event_queue.prepare_read() else {
            return Ok(WaitOutcome {
                dispatched: self.event_queue.dispatch_pending(&mut self.state)?,
                external_ready: false,
            });
        };
        let fd = read_guard.connection_fd();
        let mut pollfds = [
            libc::pollfd {
                fd: fd.as_raw_fd(),
                events: libc::POLLIN | libc::POLLERR,
                revents: 0,
            },
            libc::pollfd {
                fd: external_fd.unwrap_or(-1),
                events: libc::POLLIN | libc::POLLERR,
                revents: 0,
            },
        ];
        let pollfd_count = if external_fd.is_some() { 2 } else { 1 };
        let timeout_ms = timeout.as_millis().min(i32::MAX as u128) as i32;
        let ready = loop {
            let ready = unsafe { libc::poll(pollfds.as_mut_ptr(), pollfd_count, timeout_ms) };
            if ready >= 0 {
                break ready;
            }
            let error = std::io::Error::last_os_error();
            if error.kind() == ErrorKind::Interrupted {
                continue;
            }
            return Err(Box::new(error));
        };

        if ready == 0 {
            drop(read_guard);
            return Ok(WaitOutcome::default());
        }
        let external_ready =
            external_fd.is_some() && (pollfds[1].revents & (libc::POLLIN | libc::POLLERR)) != 0;
        let wayland_ready = (pollfds[0].revents & (libc::POLLIN | libc::POLLERR)) != 0;
        if !wayland_ready {
            drop(read_guard);
            return Ok(WaitOutcome {
                dispatched: 0,
                external_ready,
            });
        }

        match read_guard.read() {
            Ok(_) => {}
            Err(wayland_client::backend::WaylandError::Io(e))
                if e.kind() == ErrorKind::WouldBlock =>
            {
                return Ok(WaitOutcome {
                    dispatched: 0,
                    external_ready,
                });
            }
            Err(error) => return Err(Box::new(error)),
        }

        Ok(WaitOutcome {
            dispatched: self.event_queue.dispatch_pending(&mut self.state)?,
            external_ready,
        })
    }

    pub fn request_redraw(&mut self) {
        self.state.request_redraw();
    }

    pub fn set_clipboard_text(&mut self, text: impl Into<String>) -> Result<(), Box<dyn Error>> {
        let text = text.into();
        let Some(clipboard_manager) = self.state.clipboard_manager.as_ref() else {
            debug!(
                target: "ruin_ui_platform_wayland::clipboard",
                "Wayland compositor does not expose wl_data_device_manager; skipping clipboard copy"
            );
            return Ok(());
        };
        let Some(clipboard_device) = self.state.clipboard_device.as_ref() else {
            debug!(
                target: "ruin_ui_platform_wayland::clipboard",
                "Wayland seat does not expose a clipboard data device; skipping clipboard copy"
            );
            return Ok(());
        };
        let Some(serial) = self.state.last_selection_serial else {
            return Err(Box::new(std::io::Error::other(
                "clipboard copy requires a recent input serial",
            )));
        };

        let source = clipboard_manager.create_data_source(&self.state.qh, ());
        source.offer("text/plain;charset=utf-8".to_owned());
        source.offer("text/plain".to_owned());
        clipboard_device.set_selection(Some(&source), serial);
        self.state.clipboard_source = Some(source);
        self.state.clipboard_text = Some(text);
        self.state._connection.flush()?;
        Ok(())
    }

    pub fn read_clipboard_text(&mut self) -> Result<Option<String>, Box<dyn Error>> {
        let preferred_mime = preferred_plain_text_mime(&self.state.clipboard_offer_mime_types);
        let Some(mime_type) = preferred_mime else {
            return Ok(self.state.clipboard_text.clone());
        };
        let Some(offer) = self.state.clipboard_offer.as_ref() else {
            return Ok(self.state.clipboard_text.clone());
        };

        let mut pipe_fds = [0; 2];
        let pipe_result = unsafe { libc::pipe2(pipe_fds.as_mut_ptr(), libc::O_CLOEXEC) };
        if pipe_result != 0 {
            return Err(Box::new(std::io::Error::last_os_error()));
        }

        let read_fd = unsafe { OwnedFd::from_raw_fd(pipe_fds[0]) };
        let write_fd = unsafe { OwnedFd::from_raw_fd(pipe_fds[1]) };
        offer.receive(mime_type, write_fd.as_fd());
        self.state._connection.flush()?;
        drop(write_fd);
        let mut file = File::from(read_fd);
        let mut text = String::new();
        file.read_to_string(&mut text)?;
        Ok((!text.is_empty()).then_some(text))
    }

    pub fn set_primary_selection_text(
        &mut self,
        text: impl Into<String>,
    ) -> Result<(), Box<dyn Error>> {
        let text = text.into();
        let Some(primary_selection_manager) = self.state.primary_selection_manager.as_ref() else {
            debug!(
                target: "ruin_ui_platform_wayland::clipboard",
                "Wayland compositor does not expose primary selection; skipping copy"
            );
            return Ok(());
        };
        let Some(primary_selection_device) = self.state.primary_selection_device.as_ref() else {
            debug!(
                target: "ruin_ui_platform_wayland::clipboard",
                "Wayland seat does not expose a primary selection device; skipping copy"
            );
            return Ok(());
        };
        let Some(serial) = self.state.last_selection_serial else {
            return Err(Box::new(std::io::Error::other(
                "primary selection copy requires a recent input serial",
            )));
        };

        let source = primary_selection_manager.create_source(&self.state.qh, ());
        source.offer("text/plain;charset=utf-8".to_owned());
        source.offer("text/plain".to_owned());
        primary_selection_device.set_selection(Some(&source), serial);
        self.state.primary_selection_source = Some(source);
        self.state.primary_selection_text = Some(text);
        self.state._connection.flush()?;
        Ok(())
    }

    pub fn read_primary_selection_text(&mut self) -> Result<Option<String>, Box<dyn Error>> {
        let preferred_mime =
            preferred_plain_text_mime(&self.state.primary_selection_offer_mime_types);
        let Some(mime_type) = preferred_mime else {
            return Ok(self.state.primary_selection_text.clone());
        };
        let Some(offer) = self.state.primary_selection_offer.as_ref() else {
            return Ok(self.state.primary_selection_text.clone());
        };

        let mut pipe_fds = [0; 2];
        let pipe_result = unsafe { libc::pipe2(pipe_fds.as_mut_ptr(), libc::O_CLOEXEC) };
        if pipe_result != 0 {
            return Err(Box::new(std::io::Error::last_os_error()));
        }

        let read_fd = unsafe { OwnedFd::from_raw_fd(pipe_fds[0]) };
        let write_fd = unsafe { OwnedFd::from_raw_fd(pipe_fds[1]) };
        offer.receive(mime_type, write_fd.as_fd());
        self.state._connection.flush()?;
        drop(write_fd);
        let mut file = File::from(read_fd);
        let mut text = String::new();
        file.read_to_string(&mut text)?;
        Ok((!text.is_empty()).then_some(text))
    }

    pub fn set_cursor_icon(&mut self, cursor: CursorIcon) -> Result<(), Box<dyn Error>> {
        self.state.cursor_icon = cursor;
        apply_cursor_icon(&mut self.state);
        self.state._connection.flush()?;
        Ok(())
    }

    fn presentation_ready(&self) -> bool {
        self.state.frame_callback.is_none()
    }

    fn arm_frame_callback(&mut self) {
        if self.state.frame_callback.is_none() {
            self.state.frame_callback = Some(self.state._surface.frame(&self.state.qh, ()));
        }
    }

    fn clear_frame_callback(&mut self) {
        self.state.frame_callback = None;
    }

    fn flush_connection(&mut self) -> Result<(), Box<dyn Error>> {
        self.state._connection.flush()?;
        Ok(())
    }

    pub fn apply_spec(&mut self, spec: &WindowSpec) -> Result<(), Box<dyn Error>> {
        self.state.fixed_size_request = fixed_size_request_for_spec(spec);
        self.state._toplevel.set_title(spec.title.clone());
        if let Some(app_id) = spec.app_id.as_ref() {
            self.state._toplevel.set_app_id(app_id.clone());
        }
        apply_size_constraints(&self.state._toplevel, spec);
        if spec.maximized {
            self.state._toplevel.set_maximized();
        } else {
            self.state._toplevel.unset_maximized();
        }
        if spec.fullscreen {
            self.state._toplevel.set_fullscreen(None);
        } else {
            self.state._toplevel.unset_fullscreen();
        }
        self.state._surface.commit();
        self.state._connection.flush()?;
        Ok(())
    }

    pub fn drain_pointer_events(&mut self) -> Vec<PointerEvent> {
        std::mem::take(&mut self.state.pending_pointer_events)
    }

    pub fn drain_keyboard_events(&mut self) -> Vec<KeyboardEvent> {
        self.state.queue_ready_keyboard_repeats();
        std::mem::take(&mut self.state.pending_keyboard_events)
    }

    pub fn prepare_frame(&mut self) -> Option<FrameRequest> {
        if !self.state.configured {
            return None;
        }

        if let Some((width, height)) = self.state.pending_size.take() {
            trace!(
                target: "ruin_ui_platform_wayland::resize",
                width,
                height,
                previous_width = self.state.current_size.0,
                previous_height = self.state.current_size.1,
                "prepare_frame observed pending size change"
            );
            self.state.current_size = (width, height);
            self.state.needs_redraw = false;
            return Some(FrameRequest {
                width,
                height,
                resized: true,
            });
        }

        if self.state.needs_redraw {
            trace!(
                target: "ruin_ui_platform_wayland::resize",
                width = self.state.current_size.0,
                height = self.state.current_size.1,
                "prepare_frame servicing redraw without size change"
            );
            self.state.needs_redraw = false;
            return Some(FrameRequest {
                width: self.state.current_size.0,
                height: self.state.current_size.1,
                resized: false,
            });
        }

        None
    }
}

pub fn start_wayland_platform() -> PlatformRuntime {
    PlatformRuntime::custom(|endpoint| spawn_worker(move || run_wayland_platform(endpoint), || {}))
}

pub fn start_wayland_ui() -> UiRuntime {
    UiRuntime::from_platform(start_wayland_platform())
}

fn run_wayland_platform(mut endpoint: PlatformEndpoint) {
    let state = Rc::new(RefCell::new(WaylandBackendState {
        events: endpoint.events.clone(),
        windows: BTreeMap::new(),
    }));
    let (internal_tx, mut internal_rx) = mpsc::unbounded_channel::<InternalBackendEvent>();

    queue_future({
        let state = Rc::clone(&state);
        let internal_tx = internal_tx.clone();
        async move {
            while let Some(request) = endpoint.commands.recv().await {
                match request {
                    PlatformRequest::CreateWindow { window_id, spec } => {
                        handle_create_window(&state, &internal_tx, window_id, spec);
                    }
                    PlatformRequest::UpdateWindow { window_id, update } => {
                        handle_update_window(&state, &internal_tx, window_id, update);
                    }
                    PlatformRequest::ReplaceScene { window_id, scene } => {
                        handle_replace_scene(&state, window_id, scene);
                    }
                    PlatformRequest::SetClipboardText { window_id, text } => {
                        handle_set_clipboard_text(&state, window_id, text);
                    }
                    PlatformRequest::RequestClipboardText { window_id } => {
                        handle_request_clipboard_text(&state, window_id);
                    }
                    PlatformRequest::SetPrimarySelectionText { window_id, text } => {
                        handle_set_primary_selection_text(&state, window_id, text);
                    }
                    PlatformRequest::RequestPrimarySelectionText { window_id } => {
                        handle_request_primary_selection_text(&state, window_id);
                    }
                    PlatformRequest::SetCursorIcon { window_id, cursor } => {
                        handle_set_cursor_icon(&state, window_id, cursor);
                    }
                    PlatformRequest::EmitCloseRequested { window_id } => {
                        emit_wayland_event(&state, PlatformEvent::CloseRequested { window_id });
                    }
                    PlatformRequest::EmitPointerEvent { window_id, event } => {
                        emit_wayland_event(&state, PlatformEvent::Pointer { window_id, event });
                    }
                    PlatformRequest::EmitKeyboardEvent { window_id, event } => {
                        emit_wayland_event(&state, PlatformEvent::Keyboard { window_id, event });
                    }
                    PlatformRequest::EmitWake { window_id, token } => {
                        emit_wayland_event(&state, PlatformEvent::Wake { window_id, token });
                    }
                    PlatformRequest::Shutdown => {
                        shutdown_wayland_backend(&state);
                        break;
                    }
                }
            }
        }
    });

    queue_future({
        let state = Rc::clone(&state);
        async move {
            while let Some(event) = internal_rx.recv().await {
                let InternalBackendEvent::Closed { window_id } = event;
                if let Some(record) = state.borrow_mut().windows.get_mut(&window_id) {
                    record.worker = None;
                    record.lifecycle = WindowLifecycle::LogicalClosed;
                    record.spec.open = false;
                }
            }
        }
    });
}

struct WaylandBackendState {
    events: mpsc::UnboundedSender<PlatformEvent>,
    windows: BTreeMap<WindowId, WindowRecord>,
}

fn handle_create_window(
    state: &Rc<RefCell<WaylandBackendState>>,
    internal_tx: &mpsc::UnboundedSender<InternalBackendEvent>,
    window_id: WindowId,
    spec: WindowSpec,
) {
    let mut record = WindowRecord {
        spec: spec.clone(),
        lifecycle: WindowLifecycle::LogicalClosed,
        latest_scene: None,
        worker: None,
    };
    if spec.open {
        record.worker = Some(spawn_window_worker(
            window_id,
            spec.clone(),
            None,
            state.borrow().events.clone(),
            internal_tx.clone(),
        ));
        record.lifecycle = WindowLifecycle::Opening;
    }
    state.borrow_mut().windows.insert(window_id, record);
}

fn handle_update_window(
    state: &Rc<RefCell<WaylandBackendState>>,
    internal_tx: &mpsc::UnboundedSender<InternalBackendEvent>,
    window_id: WindowId,
    update: WindowUpdate,
) {
    let mut spawn_spec = None;
    let mut shutdown_worker = None;
    let mut apply_spec = None;
    {
        let mut state_ref = state.borrow_mut();
        let Some(record) = state_ref.windows.get_mut(&window_id) else {
            return;
        };
        let was_open = record.spec.open;
        update.apply_to(&mut record.spec);
        match (was_open, record.spec.open) {
            (false, true) => {
                record.lifecycle = WindowLifecycle::Opening;
                spawn_spec = Some((record.spec.clone(), record.latest_scene.clone()));
            }
            (true, false) => {
                shutdown_worker = record.worker.take();
                record.lifecycle = WindowLifecycle::Closing;
            }
            _ => {
                if let Some(worker) = record.worker.as_ref() {
                    apply_spec = Some((worker.command_tx.clone(), record.spec.clone()));
                }
            }
        }
    }

    if let Some(worker) = shutdown_worker {
        let _ = worker.command_tx.send(WindowWorkerCommand::Shutdown);
        return;
    }

    if let Some((spec, latest_scene)) = spawn_spec {
        let worker = spawn_window_worker(
            window_id,
            spec,
            latest_scene,
            state.borrow().events.clone(),
            internal_tx.clone(),
        );
        if let Some(record) = state.borrow_mut().windows.get_mut(&window_id) {
            record.worker = Some(worker);
        }
        return;
    }

    if let Some((command_tx, spec)) = apply_spec {
        let _ = command_tx.send(WindowWorkerCommand::ApplySpec(spec));
    }
}

fn handle_replace_scene(
    state: &Rc<RefCell<WaylandBackendState>>,
    window_id: WindowId,
    scene: SceneSnapshot,
) {
    trace!(
        target: "ruin_ui_platform_wayland::scene",
        window_id = window_id.raw(),
        scene_version = scene.version,
        width = scene.logical_size.width,
        height = scene.logical_size.height,
        "received scene replacement"
    );
    let command_tx = {
        let mut state_ref = state.borrow_mut();
        let Some(record) = state_ref.windows.get_mut(&window_id) else {
            return;
        };
        record.latest_scene = Some(scene.clone());
        record.worker.as_ref().map(|w| w.command_tx.clone())
    };
    if let Some(command_tx) = command_tx {
        // send() uses MSG_RING to wake the command loop on the worker thread; the command
        // loop then writes to the wakeup pipe to notify the pump.
        let _ = command_tx.send(WindowWorkerCommand::ReplaceScene(scene));
    }
}

fn handle_set_primary_selection_text(
    state: &Rc<RefCell<WaylandBackendState>>,
    window_id: WindowId,
    text: String,
) {
    let command_tx = state.borrow().windows.get(&window_id).and_then(|record| {
        record
            .worker
            .as_ref()
            .map(|worker| worker.command_tx.clone())
    });
    if let Some(command_tx) = command_tx {
        let _ = command_tx.send(WindowWorkerCommand::SetPrimarySelectionText(text));
    }
}

fn handle_set_clipboard_text(
    state: &Rc<RefCell<WaylandBackendState>>,
    window_id: WindowId,
    text: String,
) {
    let command_tx = state.borrow().windows.get(&window_id).and_then(|record| {
        record
            .worker
            .as_ref()
            .map(|worker| worker.command_tx.clone())
    });
    if let Some(command_tx) = command_tx {
        let _ = command_tx.send(WindowWorkerCommand::SetClipboardText(text));
    }
}

fn handle_request_primary_selection_text(
    state: &Rc<RefCell<WaylandBackendState>>,
    window_id: WindowId,
) {
    if let Some(command_tx) = state
        .borrow()
        .windows
        .get(&window_id)
        .and_then(|record| record.worker.as_ref())
        .map(|worker| worker.command_tx.clone())
    {
        let _ = command_tx.send(WindowWorkerCommand::RequestPrimarySelectionText);
    }
}

fn handle_request_clipboard_text(state: &Rc<RefCell<WaylandBackendState>>, window_id: WindowId) {
    if let Some(command_tx) = state
        .borrow()
        .windows
        .get(&window_id)
        .and_then(|record| record.worker.as_ref())
        .map(|worker| worker.command_tx.clone())
    {
        let _ = command_tx.send(WindowWorkerCommand::RequestClipboardText);
    }
}

fn handle_set_cursor_icon(
    state: &Rc<RefCell<WaylandBackendState>>,
    window_id: WindowId,
    cursor: CursorIcon,
) {
    let command_tx = {
        let state_ref = state.borrow();
        state_ref
            .windows
            .get(&window_id)
            .and_then(|record| record.worker.as_ref())
            .map(|worker| worker.command_tx.clone())
    };
    if let Some(command_tx) = command_tx {
        let _ = command_tx.send(WindowWorkerCommand::SetCursorIcon(cursor));
    }
}

fn shutdown_wayland_backend(state: &Rc<RefCell<WaylandBackendState>>) {
    let workers: Vec<mpsc::UnboundedSender<WindowWorkerCommand>> = state
        .borrow()
        .windows
        .values()
        .filter_map(|record| {
            record
                .worker
                .as_ref()
                .map(|worker| worker.command_tx.clone())
        })
        .collect();
    for command_tx in workers {
        let _ = command_tx.send(WindowWorkerCommand::Shutdown);
    }
}

/// Writes one byte to the pump wakeup pipe (non-blocking, ignores errors).
fn write_wakeup(fd: RawFd) {
    let b: u8 = 1;
    let _ = unsafe { libc::write(fd, &b as *const u8 as *const libc::c_void, 1) };
}

/// Drains all pending bytes from the pump wakeup pipe (non-blocking).
fn drain_wakeup(fd: RawFd) {
    let mut buf = [0u8; 64];
    let _ = unsafe { libc::read(fd, buf.as_mut_ptr() as *mut libc::c_void, 64) };
}

fn spawn_window_worker(
    window_id: WindowId,
    spec: WindowSpec,
    latest_scene: Option<SceneSnapshot>,
    event_tx: mpsc::UnboundedSender<PlatformEvent>,
    internal_tx: mpsc::UnboundedSender<InternalBackendEvent>,
) -> WindowWorkerHandle {
    let (command_tx, mut command_rx) = mpsc::unbounded_channel::<WindowWorkerCommand>();

    // Create a wakeup pipe (non-blocking, close-on-exec) used to drive the async pump.
    // Three sources write to the write end:
    //   1. The Wayland-fd watcher task — when the compositor buffers new events.
    //   2. The command loop — after processing each window command.
    //   3. Keyboard-repeat timer callbacks scheduled via set_timeout.
    let mut pipe_fds = [0i32; 2];
    let r = unsafe { libc::pipe2(pipe_fds.as_mut_ptr(), libc::O_NONBLOCK | libc::O_CLOEXEC) };
    assert_eq!(r, 0, "pipe2 failed");
    let pipe_read_fd = pipe_fds[0];
    let pipe_write_fd = pipe_fds[1];

    let worker = spawn_worker(
        move || {
            let window = match WaylandWindow::open(spec.clone()) {
                Ok(window) => window,
                Err(_) => {
                    let _ = event_tx.send(PlatformEvent::Closed { window_id });
                    let _ = internal_tx.send(InternalBackendEvent::Closed { window_id });
                    return;
                }
            };
            // Extract the Wayland socket fd before moving `window` into state.
            let wayland_fd = window.poll_fd();
            let renderer = match WgpuSceneRenderer::new(
                window.surface_target(),
                spec.requested_inner_size
                    .unwrap_or_else(|| UiSize::new(800.0, 500.0))
                    .width as u32,
                spec.requested_inner_size
                    .unwrap_or_else(|| UiSize::new(800.0, 500.0))
                    .height as u32,
                spec.base_color,
            ) {
                Ok(renderer) => renderer,
                Err(_) => {
                    let _ = event_tx.send(PlatformEvent::Closed { window_id });
                    let _ = internal_tx.send(InternalBackendEvent::Closed { window_id });
                    return;
                }
            };
            let state = Rc::new(RefCell::new(WindowWorkerState {
                window_id,
                spec: spec.clone(),
                window,
                renderer,
                latest_scene,
                opened_emitted: false,
                close_requested_emitted: false,
                closed_emitted: false,
                shutdown_requested: false,
                pending_viewport: None,
                viewport_request_in_flight: None,
                event_tx,
                internal_tx,
                keyboard_repeat_timer: None,
                pending_viewport_since: None,
                pending_swapchain_size: None,
            }));

            // Task 1: Wayland-fd watcher.  Waits for the compositor to buffer events (frame
            // callbacks, input, configure, etc.) then pokes the pump via the wakeup pipe.
            // When the socket errors or hangs up the watcher exits silently; the pump will
            // detect the closure on its next iteration.
            queue_future(async move {
                loop {
                    if ruin_runtime::fd::wait_readable(wayland_fd).await.is_err() {
                        break;
                    }
                    write_wakeup(pipe_write_fd);
                }
            });

            // Task 2: command loop.  Receives window commands from the platform thread via
            // the mpsc channel (which uses MSG_RING for cross-thread delivery).  After each
            // command, pokes the pump so it can act on the updated state immediately.
            queue_future({
                let state = Rc::clone(&state);
                async move {
                    while let Some(command) = command_rx.recv().await {
                        match command {
                            WindowWorkerCommand::ReplaceScene(scene) => {
                                let mut state_ref = state.borrow_mut();
                                trace!(
                                    target: "ruin_ui_platform_wayland::scene",
                                    window_id = state_ref.window_id.raw(),
                                    scene_version = scene.version,
                                    width = scene.logical_size.width,
                                    height = scene.logical_size.height,
                                    "worker accepted scene"
                                );
                                state_ref.latest_scene = Some(scene);
                                state_ref.window.request_redraw();
                            }
                            WindowWorkerCommand::SetClipboardText(text) => {
                                let mut state_ref = state.borrow_mut();
                                if let Err(error) = state_ref.window.set_clipboard_text(text) {
                                    debug!(
                                        target: "ruin_ui_platform_wayland::clipboard",
                                        window_id = state_ref.window_id.raw(),
                                        error = %error,
                                        "failed to set clipboard text"
                                    );
                                }
                            }
                            WindowWorkerCommand::RequestClipboardText => {
                                let mut state_ref = state.borrow_mut();
                                match state_ref.window.read_clipboard_text() {
                                    Ok(Some(text)) => {
                                        let _ =
                                            state_ref.event_tx.send(PlatformEvent::ClipboardText {
                                                window_id: state_ref.window_id,
                                                text,
                                            });
                                    }
                                    Ok(None) => {}
                                    Err(error) => {
                                        debug!(
                                            target: "ruin_ui_platform_wayland::clipboard",
                                            window_id = state_ref.window_id.raw(),
                                            error = %error,
                                            "failed to read clipboard text"
                                        );
                                    }
                                }
                            }
                            WindowWorkerCommand::SetPrimarySelectionText(text) => {
                                let mut state_ref = state.borrow_mut();
                                if let Err(error) =
                                    state_ref.window.set_primary_selection_text(text)
                                {
                                    debug!(
                                        target: "ruin_ui_platform_wayland::clipboard",
                                        window_id = state_ref.window_id.raw(),
                                        error = %error,
                                        "failed to set primary selection text"
                                    );
                                }
                            }
                            WindowWorkerCommand::RequestPrimarySelectionText => {
                                let mut state_ref = state.borrow_mut();
                                match state_ref.window.read_primary_selection_text() {
                                    Ok(Some(text)) => {
                                        let _ = state_ref.event_tx.send(
                                            PlatformEvent::PrimarySelectionText {
                                                window_id: state_ref.window_id,
                                                text,
                                            },
                                        );
                                    }
                                    Ok(None) => {}
                                    Err(error) => {
                                        debug!(
                                            target: "ruin_ui_platform_wayland::clipboard",
                                            window_id = state_ref.window_id.raw(),
                                            error = %error,
                                            "failed to read primary selection text"
                                        );
                                    }
                                }
                            }
                            WindowWorkerCommand::SetCursorIcon(cursor) => {
                                let mut state_ref = state.borrow_mut();
                                if let Err(error) = state_ref.window.set_cursor_icon(cursor) {
                                    debug!(
                                        target: "ruin_ui_platform_wayland::cursor",
                                        window_id = state_ref.window_id.raw(),
                                        error = %error,
                                        "failed to set cursor icon"
                                    );
                                }
                            }
                            WindowWorkerCommand::ApplySpec(spec) => {
                                let mut state_ref = state.borrow_mut();
                                state_ref.spec = spec.clone();
                                if state_ref.window.apply_spec(&spec).is_ok() {
                                    state_ref.window.request_redraw();
                                }
                            }
                            WindowWorkerCommand::Shutdown => {
                                state.borrow_mut().shutdown_requested = true;
                                // Poke the pump so it sees shutdown_requested.
                                write_wakeup(pipe_write_fd);
                                break;
                            }
                        }
                        // Poke the pump so it acts on the updated state without waiting for the
                        // next Wayland event.
                        write_wakeup(pipe_write_fd);
                    }
                }
            });

            // Task 3: async pump.  Sleeps on the wakeup pipe read end; woken by Task 1
            // (Wayland events), Task 2 (commands), or keyboard-repeat timers.  On each wake,
            // dispatches Wayland events non-blockingly and renders if ready.
            queue_future({
                let state = Rc::clone(&state);
                async move {
                    loop {
                        // Consume all pending wakeup bytes before doing work.
                        drain_wakeup(pipe_read_fd);

                        let mut keep_running = true;
                        {
                            let mut state_ref = state.borrow_mut();

                            // Non-blocking: read the Wayland socket and dispatch buffered events.
                            match state_ref.window.dispatch_ready() {
                                Ok(_) => {}
                                Err(_) => {
                                    emit_window_closed(&mut state_ref, false);
                                    keep_running = false;
                                }
                            }

                            if keep_running {
                                for event in state_ref.window.drain_pointer_events() {
                                    let _ = state_ref.event_tx.send(PlatformEvent::Pointer {
                                        window_id: state_ref.window_id,
                                        event,
                                    });
                                }
                                for event in state_ref.window.drain_keyboard_events() {
                                    tracing::trace!(
                                        target: "ruin_ui_platform_wayland::event_bridge",
                                        window_id = state_ref.window_id.raw(),
                                        keycode = event.keycode,
                                        ?event.kind,
                                        ?event.key,
                                        text = event.text.as_deref().unwrap_or(""),
                                        "forwarding keyboard event to UI runtime"
                                    );
                                    let _ = state_ref.event_tx.send(PlatformEvent::Keyboard {
                                        window_id: state_ref.window_id,
                                        event,
                                    });
                                }

                                let user_closed = !state_ref.shutdown_requested;
                                if state_ref.shutdown_requested || !state_ref.window.is_running() {
                                    emit_window_closed(&mut state_ref, user_closed);
                                    keep_running = false;
                                }
                            }

                            if keep_running {
                                if let Some(frame) = state_ref.window.prepare_frame() {
                                    let current_viewport =
                                        UiSize::new(frame.width as f32, frame.height as f32);
                                    if frame.resized {
                                        debug!(
                                            target: "ruin_ui_platform_wayland::resize",
                                            window_id = state_ref.window_id.raw(),
                                            width = current_viewport.width,
                                            height = current_viewport.height,
                                            "worker observed resized frame"
                                        );
                                        state_ref.pending_viewport = Some(current_viewport);
                                        state_ref
                                            .pending_viewport_since
                                            .get_or_insert_with(Instant::now);
                                        // Emit configure BEFORE touching the GPU so the app
                                        // thread starts layout immediately in parallel.
                                        // The actual swapchain recreation is deferred to just
                                        // before the next renderer.render() call so rapid
                                        // configures only pay one recreation.
                                        if state_ref
                                            .latest_scene
                                            .as_ref()
                                            .is_none_or(|s| s.logical_size != current_viewport)
                                            || state_ref.viewport_request_in_flight.is_some()
                                        {
                                            maybe_request_pending_viewport(&mut state_ref);
                                        } else {
                                            state_ref.pending_viewport = None;
                                        }
                                        state_ref.pending_swapchain_size =
                                            Some((frame.width, frame.height));
                                        state_ref.window.request_redraw();
                                    }
                                    if !state_ref.opened_emitted {
                                        state_ref.opened_emitted = true;
                                        let _ = state_ref.event_tx.send(PlatformEvent::Opened {
                                            window_id: state_ref.window_id,
                                        });
                                    }
                                    let scene = state_ref.latest_scene.clone();
                                    if let Some(scene) = scene.as_ref() {
                                        if scene.logical_size != current_viewport {
                                            // Size mismatch: hold the last committed buffer and
                                            // wait for the app to produce a correctly-sized scene.
                                            // Clear any pending frame callback so we can render
                                            // the new scene as soon as it arrives.
                                            state_ref.window.clear_frame_callback();
                                            debug!(
                                                target: "ruin_ui_platform_wayland::resize",
                                                window_id = state_ref.window_id.raw(),
                                                scene_version = scene.version,
                                                scene_width = scene.logical_size.width,
                                                scene_height = scene.logical_size.height,
                                                viewport_width = current_viewport.width,
                                                viewport_height = current_viewport.height,
                                                "scene size does not match current viewport; holding last buffer"
                                            );
                                            state_ref.pending_viewport = Some(current_viewport);
                                            state_ref
                                                .pending_viewport_since
                                                .get_or_insert_with(Instant::now);
                                            // Deadlock prevention: if the app responded to the
                                            // configure we sent (scene matches in_flight) but the
                                            // viewport has since moved on, clear the in_flight
                                            // marker so we can immediately send the next configure.
                                            // Without this, viewport_request_in_flight stays set
                                            // to the old size forever and the app is never told
                                            // about the new viewport.
                                            if state_ref.viewport_request_in_flight
                                                == Some(scene.logical_size)
                                            {
                                                state_ref.viewport_request_in_flight = None;
                                                maybe_request_pending_viewport(&mut state_ref);
                                            }
                                            state_ref.window.request_redraw();
                                        } else if !state_ref.window.presentation_ready() {
                                            // Correct scene is ready but the compositor
                                            // hasn't signalled vsync yet.  Wait for it.
                                            state_ref.window.request_redraw();
                                        } else {
                                            // Correct scene: apply any deferred swapchain resize
                                            // before rendering.  Deferring from frame.resized means
                                            // rapid configures pay one recreation instead of one
                                            // per event.
                                            if let Some((w, h)) =
                                                state_ref.pending_swapchain_size.take()
                                            {
                                                let t_resize = std::time::Instant::now();
                                                state_ref.renderer.resize(w, h);
                                                let resize_gpu_us = t_resize.elapsed().as_micros();
                                                debug!(
                                                    target: "ruin_ui_platform_wayland::perf",
                                                    window_id = state_ref.window_id.raw(),
                                                    width = w,
                                                    height = h,
                                                    resize_gpu_us,
                                                    "renderer swapchain resized (deferred)"
                                                );
                                            }
                                            state_ref.window.arm_frame_callback();
                                            let t_render = std::time::Instant::now();
                                            match state_ref.renderer.render(scene) {
                                                Ok(()) => {
                                                    let render_gpu_us =
                                                        t_render.elapsed().as_micros();
                                                    debug!(
                                                        target: "ruin_ui_platform_wayland::perf",
                                                        window_id = state_ref.window_id.raw(),
                                                        scene_version = scene.version,
                                                        render_gpu_us,
                                                        "renderer.render() complete"
                                                    );
                                                    match state_ref.window.flush_connection() {
                                                        Ok(()) => {
                                                            let resize_lag_us = state_ref
                                                                .pending_viewport_since
                                                                .take()
                                                                .map(|t| t.elapsed().as_micros());
                                                            debug!(
                                                                target: "ruin_ui_platform_wayland::resize",
                                                                window_id = state_ref.window_id.raw(),
                                                                scene_version = scene.version,
                                                                width = scene.logical_size.width,
                                                                height = scene.logical_size.height,
                                                                resize_lag_us,
                                                                "resize complete: presented correct scene"
                                                            );
                                                            finish_presented_viewport_request(
                                                                &mut state_ref,
                                                                scene.logical_size,
                                                            );
                                                            let _ = state_ref.event_tx.send(
                                                                PlatformEvent::FramePresented {
                                                                    window_id: state_ref.window_id,
                                                                    scene_version: scene.version,
                                                                    item_count: scene.item_count(),
                                                                },
                                                            );
                                                        }
                                                        Err(error) => {
                                                            debug!(
                                                                target: "ruin_ui_platform_wayland::scene",
                                                                window_id = state_ref.window_id.raw(),
                                                                error = %error,
                                                                "failed to flush presented scene"
                                                            );
                                                            state_ref.window.clear_frame_callback();
                                                            state_ref.window.request_redraw();
                                                        }
                                                    }
                                                }
                                                Err(RenderError::Lost | RenderError::Outdated) => {
                                                    state_ref.window.clear_frame_callback();
                                                    state_ref
                                                        .renderer
                                                        .resize(frame.width, frame.height);
                                                    state_ref.window.request_redraw();
                                                }
                                                Err(
                                                    RenderError::Timeout
                                                    | RenderError::Occluded
                                                    | RenderError::Validation,
                                                ) => {
                                                    state_ref.window.clear_frame_callback();
                                                    state_ref.window.request_redraw();
                                                }
                                            }
                                        }
                                    }
                                }
                                if state_ref.viewport_request_in_flight.is_none()
                                    && state_ref.pending_viewport.is_some()
                                {
                                    maybe_request_pending_viewport(&mut state_ref);
                                }

                                // Re-arm the keyboard-repeat timer so the pump wakes at the right
                                // time even when no Wayland events or commands are in flight.
                                if let Some(timer) = state_ref.keyboard_repeat_timer.take() {
                                    clear_timeout(&timer);
                                }
                                if let Some(repeat) =
                                    state_ref.window.state.keyboard_repeat.as_ref()
                                {
                                    let delay =
                                        repeat.next_at.saturating_duration_since(Instant::now());
                                    state_ref.keyboard_repeat_timer =
                                        Some(set_timeout(delay, move || {
                                            write_wakeup(pipe_write_fd)
                                        }));
                                }
                            }
                        }

                        if !keep_running {
                            unsafe {
                                libc::close(pipe_read_fd);
                                libc::close(pipe_write_fd);
                            }
                            break;
                        }

                        // Yield until the Wayland watcher, command loop, or a keyboard-repeat
                        // timer writes to the wakeup pipe.
                        ruin_runtime::fd::wait_readable(pipe_read_fd).await.ok();
                    }
                }
            });
        },
        || {},
    );
    WindowWorkerHandle {
        command_tx,
        _worker: worker,
    }
}

fn emit_window_closed(state: &mut WindowWorkerState, emit_close_requested: bool) {
    if emit_close_requested && !state.close_requested_emitted {
        state.close_requested_emitted = true;
        let _ = state.event_tx.send(PlatformEvent::CloseRequested {
            window_id: state.window_id,
        });
    }
    if state.closed_emitted {
        return;
    }
    state.closed_emitted = true;
    let _ = state.event_tx.send(PlatformEvent::Closed {
        window_id: state.window_id,
    });
    let _ = state.internal_tx.send(InternalBackendEvent::Closed {
        window_id: state.window_id,
    });
}

fn emit_window_configured(state: &WindowWorkerState, viewport: UiSize) {
    debug!(
        target: "ruin_ui_platform_wayland::resize",
        window_id = state.window_id.raw(),
        width = viewport.width,
        height = viewport.height,
        "emitting configured event to UI"
    );
    let _ = state.event_tx.send(PlatformEvent::Configured {
        window_id: state.window_id,
        configuration: WindowConfigured {
            actual_inner_size: viewport,
            scale_factor: 1.0,
            visible: state.spec.visible,
            maximized: state.spec.maximized,
            fullscreen: state.spec.fullscreen,
        },
    });
}

fn maybe_request_pending_viewport(state: &mut WindowWorkerState) {
    if state.viewport_request_in_flight.is_some() {
        return;
    }
    let Some(pending_viewport) = state.pending_viewport else {
        return;
    };
    state.viewport_request_in_flight = Some(pending_viewport);
    emit_window_configured(state, pending_viewport);
}

fn finish_presented_viewport_request(state: &mut WindowWorkerState, presented_viewport: UiSize) {
    if state.pending_viewport == Some(presented_viewport) {
        state.pending_viewport = None;
    }
    if state.viewport_request_in_flight == Some(presented_viewport) {
        state.viewport_request_in_flight = None;
    }
    if state.pending_viewport != Some(presented_viewport) {
        maybe_request_pending_viewport(state);
    }
}

fn emit_wayland_event(state: &Rc<RefCell<WaylandBackendState>>, event: PlatformEvent) {
    let _ = state.borrow().events.send(event);
}

impl Dispatch<wl_registry::WlRegistry, GlobalListContents> for State {
    fn event(
        _state: &mut Self,
        _proxy: &wl_registry::WlRegistry,
        _event: wl_registry::Event,
        _data: &GlobalListContents,
        _conn: &Connection,
        _qh: &QueueHandle<Self>,
    ) {
    }
}

delegate_noop!(State: ignore wl_compositor::WlCompositor);
delegate_noop!(State: ignore wl_surface::WlSurface);
delegate_noop!(State: ignore wl_data_device_manager::WlDataDeviceManager);
delegate_noop!(State: ignore wp_cursor_shape_manager_v1::WpCursorShapeManagerV1);
delegate_noop!(State: ignore wp_cursor_shape_device_v1::WpCursorShapeDeviceV1);
delegate_noop!(
    State: ignore
        zwp_primary_selection_device_manager_v1::ZwpPrimarySelectionDeviceManagerV1
);
delegate_noop!(State: ignore zxdg_decoration_manager_v1::ZxdgDecorationManagerV1);
delegate_noop!(State: ignore zxdg_toplevel_decoration_v1::ZxdgToplevelDecorationV1);
impl Dispatch<wl_seat::WlSeat, ()> for State {
    fn event(
        state: &mut Self,
        seat: &wl_seat::WlSeat,
        event: wl_seat::Event,
        _data: &(),
        _conn: &Connection,
        qh: &QueueHandle<Self>,
    ) {
        if let wl_seat::Event::Capabilities { capabilities } = event {
            let WEnum::Value(capabilities) = capabilities else {
                return;
            };
            if capabilities.contains(wl_seat::Capability::Pointer) {
                if state.pointer.is_none() {
                    let pointer = seat.get_pointer(qh, ());
                    state.cursor_shape_device = state
                        .cursor_shape_manager
                        .as_ref()
                        .map(|manager| manager.get_pointer(&pointer, qh, ()));
                    state.pointer = Some(pointer);
                }
            } else {
                state.pointer = None;
                state.cursor_shape_device = None;
                state.pointer_position = None;
                state.last_pointer_enter_serial = None;
            }
            if capabilities.contains(wl_seat::Capability::Keyboard) {
                if state.keyboard.is_none() {
                    state.keyboard = Some(seat.get_keyboard(qh, ()));
                }
            } else {
                state.keyboard = None;
                state.xkb_keymap = None;
                state.xkb_state = None;
                state.keyboard_modifiers = KeyboardModifiers::default();
                state.keyboard_repeat = None;
            }
        }
    }
}

impl Dispatch<wl_pointer::WlPointer, ()> for State {
    fn event(
        state: &mut Self,
        _pointer: &wl_pointer::WlPointer,
        event: wl_pointer::Event,
        _data: &(),
        _conn: &Connection,
        _qh: &QueueHandle<Self>,
    ) {
        match event {
            wl_pointer::Event::Enter {
                serial,
                surface_x,
                surface_y,
                ..
            } => {
                let position = Point::new(surface_x as f32, surface_y as f32);
                state.pointer_position = Some(position);
                state.last_pointer_enter_serial = Some(serial);
                apply_cursor_icon(state);
                state.pending_pointer_events.push(PointerEvent::new(
                    0,
                    position,
                    PointerEventKind::Move,
                ));
            }
            wl_pointer::Event::Motion {
                surface_x,
                surface_y,
                ..
            } => {
                let position = Point::new(surface_x as f32, surface_y as f32);
                state.pointer_position = Some(position);
                state.pending_pointer_events.push(PointerEvent::new(
                    0,
                    position,
                    PointerEventKind::Move,
                ));
            }
            wl_pointer::Event::Leave { .. } => {
                let position = state.pointer_position.unwrap_or(Point::new(-1.0, -1.0));
                state.pending_pointer_events.push(PointerEvent::new(
                    0,
                    position,
                    PointerEventKind::LeaveWindow,
                ));
                state.pointer_position = None;
                state.last_pointer_enter_serial = None;
            }
            wl_pointer::Event::Button {
                serial,
                button,
                state: button_state,
                ..
            } => {
                let Some(position) = state.pointer_position else {
                    return;
                };
                let button = match button {
                    0x110 => PointerButton::Primary,
                    0x112 => PointerButton::Middle,
                    _ => return,
                };
                if button == PointerButton::Primary {
                    state.last_selection_serial = Some(serial);
                }
                let kind = match button_state {
                    WEnum::Value(wl_pointer::ButtonState::Pressed) => {
                        PointerEventKind::Down { button }
                    }
                    WEnum::Value(wl_pointer::ButtonState::Released) => {
                        PointerEventKind::Up { button }
                    }
                    WEnum::Value(_) | WEnum::Unknown(_) => return,
                };
                state
                    .pending_pointer_events
                    .push(PointerEvent::new(0, position, kind));
            }
            wl_pointer::Event::Axis { axis, value, .. } => {
                let Some(position) = state.pointer_position else {
                    return;
                };
                let delta = match axis {
                    WEnum::Value(wl_pointer::Axis::VerticalScroll) => Point::new(0.0, value as f32),
                    WEnum::Value(wl_pointer::Axis::HorizontalScroll) => {
                        Point::new(value as f32, 0.0)
                    }
                    WEnum::Value(_) | WEnum::Unknown(_) => return,
                };
                state.pending_pointer_events.push(PointerEvent::new(
                    0,
                    position,
                    PointerEventKind::Scroll { delta },
                ));
            }
            _ => {}
        }
    }
}

impl Dispatch<wl_keyboard::WlKeyboard, ()> for State {
    fn event(
        state: &mut Self,
        _keyboard: &wl_keyboard::WlKeyboard,
        event: wl_keyboard::Event,
        _data: &(),
        _conn: &Connection,
        _qh: &QueueHandle<Self>,
    ) {
        tracing::event!(
            target: "ruin_ui_platform_wayland::keyboard",
            Level::INFO,
            ?event,
            "received keyboard event"
        );
        match event {
            wl_keyboard::Event::Keymap { format, fd, size } => {
                let WEnum::Value(wl_keyboard::KeymapFormat::XkbV1) = format else {
                    tracing::info!(
                        target: "ruin_ui_platform_wayland::keyboard",
                        ?format,
                        "ignored unsupported keymap format"
                    );
                    return;
                };
                let Ok(size) = usize::try_from(size) else {
                    tracing::info!(
                        target: "ruin_ui_platform_wayland::keyboard",
                        size,
                        "ignored keymap with invalid size"
                    );
                    return;
                };
                let keymap = match unsafe {
                    xkb::Keymap::new_from_fd(
                        &state.xkb_context,
                        fd,
                        size,
                        xkb::KEYMAP_FORMAT_TEXT_V1,
                        xkb::KEYMAP_COMPILE_NO_FLAGS,
                    )
                } {
                    Ok(Some(keymap)) => keymap,
                    Ok(None) => {
                        tracing::info!(
                            target: "ruin_ui_platform_wayland::keyboard",
                            "failed to compile XKB keymap"
                        );
                        return;
                    }
                    Err(error) => {
                        tracing::info!(
                            target: "ruin_ui_platform_wayland::keyboard",
                            size,
                            error = %error,
                            "failed to map compositor keymap fd"
                        );
                        return;
                    }
                };
                state.xkb_state = Some(xkb::State::new(&keymap));
                state.xkb_keymap = Some(keymap);
                state.keyboard_modifiers = KeyboardModifiers::default();
                state.keyboard_repeat = None;
                tracing::info!(
                    target: "ruin_ui_platform_wayland::keyboard",
                    "installed XKB keymap"
                );
            }
            wl_keyboard::Event::Enter { .. } => {
                state.keyboard_focused = true;
                state.keyboard_repeat = None;
            }
            wl_keyboard::Event::Leave { .. } => {
                state.keyboard_focused = false;
                state.keyboard_modifiers = KeyboardModifiers::default();
                state.keyboard_repeat = None;
            }
            wl_keyboard::Event::RepeatInfo { rate, delay } => {
                state.keyboard_repeat_rate = rate;
                state.keyboard_repeat_delay = Duration::from_millis(delay.max(0) as u64);
                if rate <= 0 {
                    state.keyboard_repeat = None;
                }
            }
            wl_keyboard::Event::Modifiers {
                mods_depressed,
                mods_latched,
                mods_locked,
                group,
                ..
            } => {
                let Some(xkb_state) = state.xkb_state.as_mut() else {
                    return;
                };
                xkb_state.update_mask(mods_depressed, mods_latched, mods_locked, 0, 0, group);
                state.keyboard_modifiers = keyboard_modifiers_from_xkb(xkb_state);
            }
            wl_keyboard::Event::Key {
                serial,
                key,
                state: key_state,
                ..
            } => {
                if !state.keyboard_focused {
                    tracing::info!(
                        target: "ruin_ui_platform_wayland::keyboard",
                        key,
                        ?key_state,
                        "dropping key because keyboard focus is not active"
                    );
                    return;
                }
                let Some(xkb_state) = state.xkb_state.as_mut() else {
                    tracing::info!(
                        target: "ruin_ui_platform_wayland::keyboard",
                        key,
                        ?key_state,
                        "dropping key because XKB state is not initialized"
                    );
                    return;
                };
                let (kind, direction) = match key_state {
                    WEnum::Value(wl_keyboard::KeyState::Pressed) => {
                        (KeyboardEventKind::Pressed, xkb::KeyDirection::Down)
                    }
                    WEnum::Value(wl_keyboard::KeyState::Released) => {
                        (KeyboardEventKind::Released, xkb::KeyDirection::Up)
                    }
                    WEnum::Value(_) | WEnum::Unknown(_) => return,
                };
                if matches!(kind, KeyboardEventKind::Pressed) {
                    state.last_selection_serial = Some(serial);
                }
                let keycode = xkb::Keycode::new(key + 8);
                xkb_state.update_key(keycode, direction);
                state.keyboard_modifiers = keyboard_modifiers_from_xkb(xkb_state);
                let text = matches!(kind, KeyboardEventKind::Pressed)
                    .then(|| keyboard_text_for_xkb(xkb_state, keycode))
                    .flatten();
                let logical_key =
                    keyboard_key_from_xkb(xkb_state.key_get_one_sym(keycode), text.as_deref());
                state.pending_keyboard_events.push(KeyboardEvent::new(
                    key,
                    kind,
                    logical_key.clone(),
                    state.keyboard_modifiers,
                    text,
                ));
                tracing::info!(
                    target: "ruin_ui_platform_wayland::keyboard",
                    keycode = key,
                    ?kind,
                    ?logical_key,
                    modifiers = ?state.keyboard_modifiers,
                    queued = state.pending_keyboard_events.len(),
                    "queued translated keyboard event"
                );
                if kind == KeyboardEventKind::Pressed
                    && state.keyboard_repeat_rate > 0
                    && state
                        .xkb_keymap
                        .as_ref()
                        .is_some_and(|keymap| keymap.key_repeats(keycode))
                {
                    state.keyboard_repeat = Some(KeyboardRepeatState {
                        keycode: key,
                        next_at: Instant::now() + state.keyboard_repeat_delay,
                        interval: Duration::from_secs_f64(
                            1.0 / f64::from(state.keyboard_repeat_rate),
                        ),
                    });
                } else if state
                    .keyboard_repeat
                    .as_ref()
                    .is_some_and(|repeat| repeat.keycode == key)
                {
                    state.keyboard_repeat = None;
                }
            }
            _ => {}
        }
    }
}

impl Dispatch<wl_data_device::WlDataDevice, ()> for State {
    fn event(
        state: &mut Self,
        _data_device: &wl_data_device::WlDataDevice,
        event: wl_data_device::Event,
        _data: &(),
        _conn: &Connection,
        _qh: &QueueHandle<Self>,
    ) {
        if let wl_data_device::Event::Selection { id } = event {
            state.clipboard_offer = id;
            state.clipboard_offer_mime_types.clear();
        }
    }

    event_created_child!(State, wl_data_device::WlDataDevice, [
        wl_data_device::EVT_DATA_OFFER_OPCODE => (wl_data_offer::WlDataOffer, ())
    ]);
}

impl Dispatch<wl_data_offer::WlDataOffer, ()> for State {
    fn event(
        state: &mut Self,
        offer: &wl_data_offer::WlDataOffer,
        event: wl_data_offer::Event,
        _data: &(),
        _conn: &Connection,
        _qh: &QueueHandle<Self>,
    ) {
        if let wl_data_offer::Event::Offer { mime_type } = event
            && state.clipboard_offer.as_ref() == Some(offer)
        {
            state.clipboard_offer_mime_types.push(mime_type);
        }
    }
}

impl Dispatch<wl_data_source::WlDataSource, ()> for State {
    fn event(
        state: &mut Self,
        data_source: &wl_data_source::WlDataSource,
        event: wl_data_source::Event,
        _data: &(),
        _conn: &Connection,
        _qh: &QueueHandle<Self>,
    ) {
        match event {
            wl_data_source::Event::Send { mime_type, fd } => {
                if mime_type == "text/plain" || mime_type == "text/plain;charset=utf-8" {
                    let mut file = File::from(fd);
                    if let Some(text) = state.clipboard_text.as_deref() {
                        let _ = file.write_all(text.as_bytes());
                    }
                }
            }
            wl_data_source::Event::Cancelled => {
                if state.clipboard_source.as_ref() == Some(data_source) {
                    state.clipboard_source = None;
                    state.clipboard_text = None;
                }
            }
            _ => {}
        }
    }
}

impl Dispatch<zwp_primary_selection_device_v1::ZwpPrimarySelectionDeviceV1, ()> for State {
    fn event(
        state: &mut Self,
        _data_device: &zwp_primary_selection_device_v1::ZwpPrimarySelectionDeviceV1,
        event: zwp_primary_selection_device_v1::Event,
        _data: &(),
        _conn: &Connection,
        _qh: &QueueHandle<Self>,
    ) {
        if let zwp_primary_selection_device_v1::Event::Selection { id } = event {
            state.primary_selection_offer = id;
            state.primary_selection_offer_mime_types.clear();
        }
    }

    event_created_child!(State, zwp_primary_selection_device_v1::ZwpPrimarySelectionDeviceV1, [
        zwp_primary_selection_device_v1::EVT_DATA_OFFER_OPCODE
            => (zwp_primary_selection_offer_v1::ZwpPrimarySelectionOfferV1, ())
    ]);
}

impl Dispatch<zwp_primary_selection_offer_v1::ZwpPrimarySelectionOfferV1, ()> for State {
    fn event(
        state: &mut Self,
        offer: &zwp_primary_selection_offer_v1::ZwpPrimarySelectionOfferV1,
        event: zwp_primary_selection_offer_v1::Event,
        _data: &(),
        _conn: &Connection,
        _qh: &QueueHandle<Self>,
    ) {
        if let zwp_primary_selection_offer_v1::Event::Offer { mime_type } = event
            && state.primary_selection_offer.as_ref() == Some(offer)
        {
            state.primary_selection_offer_mime_types.push(mime_type);
        }
    }
}

impl Dispatch<zwp_primary_selection_source_v1::ZwpPrimarySelectionSourceV1, ()> for State {
    fn event(
        state: &mut Self,
        data_source: &zwp_primary_selection_source_v1::ZwpPrimarySelectionSourceV1,
        event: zwp_primary_selection_source_v1::Event,
        _data: &(),
        _conn: &Connection,
        _qh: &QueueHandle<Self>,
    ) {
        match event {
            zwp_primary_selection_source_v1::Event::Send { mime_type, fd } => {
                if mime_type == "text/plain" || mime_type == "text/plain;charset=utf-8" {
                    let mut file = File::from(fd);
                    if let Some(text) = state.primary_selection_text.as_deref() {
                        let _ = file.write_all(text.as_bytes());
                    }
                    let _ = file.flush();
                }
            }
            zwp_primary_selection_source_v1::Event::Cancelled => {
                if state.primary_selection_source.as_ref() == Some(data_source) {
                    state.primary_selection_source = None;
                    state.primary_selection_text = None;
                }
            }
            _ => {}
        }
    }
}

impl Dispatch<xdg_wm_base::XdgWmBase, ()> for State {
    fn event(
        _state: &mut Self,
        wm_base: &xdg_wm_base::XdgWmBase,
        event: xdg_wm_base::Event,
        _data: &(),
        _conn: &Connection,
        _qh: &QueueHandle<Self>,
    ) {
        if let xdg_wm_base::Event::Ping { serial } = event {
            wm_base.pong(serial);
        }
    }
}

impl Dispatch<xdg_surface::XdgSurface, ()> for State {
    fn event(
        state: &mut Self,
        xdg_surface: &xdg_surface::XdgSurface,
        event: xdg_surface::Event,
        _data: &(),
        _conn: &Connection,
        _qh: &QueueHandle<Self>,
    ) {
        if let xdg_surface::Event::Configure { serial } = event {
            xdg_surface.ack_configure(serial);
            state.configured = true;
            trace!(
                target: "ruin_ui_platform_wayland::resize",
                serial,
                current_width = state.current_size.0,
                current_height = state.current_size.1,
                has_pending_size = state.pending_size.is_some(),
                "received xdg_surface configure"
            );
            state.request_redraw();
        }
    }
}

impl Dispatch<wl_callback::WlCallback, ()> for State {
    fn event(
        state: &mut Self,
        callback: &wl_callback::WlCallback,
        event: wl_callback::Event,
        _data: &(),
        _conn: &Connection,
        _qh: &QueueHandle<Self>,
    ) {
        if let wl_callback::Event::Done { .. } = event
            && state.frame_callback.as_ref() == Some(callback)
        {
            state.frame_callback = None;
        }
    }
}

impl Dispatch<xdg_toplevel::XdgToplevel, ()> for State {
    fn event(
        state: &mut Self,
        _proxy: &xdg_toplevel::XdgToplevel,
        event: xdg_toplevel::Event,
        _data: &(),
        _conn: &Connection,
        _qh: &QueueHandle<Self>,
    ) {
        match event {
            xdg_toplevel::Event::Close => {
                state.running = false;
            }
            xdg_toplevel::Event::Configure {
                width,
                height,
                states: _,
            } => {
                let width = NonZeroU32::new(width as u32)
                    .map(NonZeroU32::get)
                    .unwrap_or(state.current_size.0);
                let height = NonZeroU32::new(height as u32)
                    .map(NonZeroU32::get)
                    .unwrap_or(state.current_size.1);
                trace!(
                    target: "ruin_ui_platform_wayland::resize",
                    width,
                    height,
                    "received Wayland toplevel configure"
                );
                let next_size = (width, height);
                if let Some(expected_size) = state.fixed_size_request {
                    if next_size != expected_size {
                        trace!(
                            target: "ruin_ui_platform_wayland::resize",
                            width,
                            height,
                            expected_width = expected_size.0,
                            expected_height = expected_size.1,
                            "ignoring stale toplevel configure for fixed-size window"
                        );
                        state.request_redraw();
                        return;
                    }
                }
                let size_changed =
                    next_size != state.current_size && state.pending_size != Some(next_size);
                trace!(
                    target: "ruin_ui_platform_wayland::resize",
                    width,
                    height,
                    current_width = state.current_size.0,
                    current_height = state.current_size.1,
                    pending_width = state.pending_size.map(|size| size.0),
                    pending_height = state.pending_size.map(|size| size.1),
                    size_changed,
                    "processed Wayland toplevel configure"
                );
                if size_changed {
                    state.pending_size = Some(next_size);
                }
                state.request_redraw();
            }
            _ => {}
        }
    }
}

fn apply_size_constraints(toplevel: &xdg_toplevel::XdgToplevel, spec: &WindowSpec) {
    if spec.resizable {
        let min = spec.min_inner_size.unwrap_or_else(|| UiSize::new(0.0, 0.0));
        let max = spec.max_inner_size.unwrap_or_else(|| UiSize::new(0.0, 0.0));
        toplevel.set_min_size(min.width.round() as i32, min.height.round() as i32);
        toplevel.set_max_size(max.width.round() as i32, max.height.round() as i32);
        return;
    }

    let fixed = spec
        .requested_inner_size
        .or(spec.min_inner_size)
        .or(spec.max_inner_size)
        .unwrap_or_else(|| UiSize::new(800.0, 500.0));
    let width = fixed.width.max(1.0).round() as i32;
    let height = fixed.height.max(1.0).round() as i32;
    toplevel.set_min_size(width, height);
    toplevel.set_max_size(width, height);
}
