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}; 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::{WorkerHandle, queue_future, queue_task, 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::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, HandleError> { let ptr = NonNull::new(self.connection.backend().display_ptr().cast::()) .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, HandleError> { let ptr = NonNull::new(self.surface.id().as_ptr().cast::()) .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, surface_target: WaylandSurfaceTarget, state: State, } struct WindowWorkerHandle { command_tx: mpsc::UnboundedSender, _worker: WorkerHandle, } struct WindowRecord { spec: WindowSpec, lifecycle: WindowLifecycle, latest_scene: Option, worker: Option, } struct WindowWorkerState { window_id: WindowId, spec: WindowSpec, window: WaylandWindow, renderer: WgpuSceneRenderer, latest_scene: Option, opened_emitted: bool, close_requested_emitted: bool, closed_emitted: bool, shutdown_requested: bool, pending_viewport: Option, viewport_request_in_flight: Option, event_tx: mpsc::UnboundedSender, internal_tx: mpsc::UnboundedSender, } 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, _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, clipboard_device: Option, cursor_shape_manager: Option, cursor_shape_device: Option, primary_selection_manager: Option, primary_selection_device: Option, qh: QueueHandle, pointer: Option, keyboard: Option, keyboard_focused: bool, xkb_context: xkb::Context, xkb_keymap: Option, xkb_state: Option, current_size: (u32, u32), configured: bool, pending_size: Option<(u32, u32)>, needs_redraw: bool, frame_callback: Option, pointer_position: Option, pending_pointer_events: Vec, pending_keyboard_events: Vec, keyboard_modifiers: KeyboardModifiers, keyboard_repeat_rate: i32, keyboard_repeat_delay: Duration, keyboard_repeat: Option, clipboard_source: Option, clipboard_text: Option, clipboard_offer: Option, clipboard_offer_mime_types: Vec, primary_selection_source: Option, primary_selection_text: Option, primary_selection_offer: Option, primary_selection_offer_mime_types: Vec, last_selection_serial: Option, last_pointer_enter_serial: Option, 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 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 { 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 { 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> { let connection = Connection::connect_to_env()?; let (globals, event_queue) = registry_queue_init::(&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 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()); } 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, _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, }, }) } 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> { self.event_queue.blocking_dispatch(&mut self.state)?; Ok(()) } pub fn dispatch_pending(&mut self) -> Result> { 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> { 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> { 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, timeout: Duration, ) -> Result> { 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) -> Result<(), Box> { 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, Box> { 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, ) -> Result<(), Box> { 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, Box> { 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> { 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; } pub fn apply_spec(&mut self, spec: &WindowSpec) -> Result<(), Box> { 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 { std::mem::take(&mut self.state.pending_pointer_events) } pub fn drain_keyboard_events(&mut self) -> Vec { self.state.queue_ready_keyboard_repeats(); std::mem::take(&mut self.state.pending_keyboard_events) } pub fn prepare_frame(&mut self) -> Option { if !self.state.configured { return None; } if let Some((width, height)) = self.state.pending_size.take() { self.state.current_size = (width, height); self.state.needs_redraw = false; return Some(FrameRequest { width, height, resized: true, }); } if self.state.needs_redraw { 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::(); 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, windows: BTreeMap, } fn handle_create_window( state: &Rc>, internal_tx: &mpsc::UnboundedSender, 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>, internal_tx: &mpsc::UnboundedSender, 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>, 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 mut command_tx = None; { 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()); if let Some(worker) = record.worker.as_ref() { command_tx = Some(worker.command_tx.clone()); } } if let Some(command_tx) = command_tx { let _ = command_tx.send(WindowWorkerCommand::ReplaceScene(scene)); } } fn handle_set_primary_selection_text( state: &Rc>, 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>, 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>, 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>, 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>, 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>) { let workers: Vec> = 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); } } fn spawn_window_worker( window_id: WindowId, spec: WindowSpec, latest_scene: Option, event_tx: mpsc::UnboundedSender, internal_tx: mpsc::UnboundedSender, ) -> WindowWorkerHandle { let (command_tx, mut command_rx) = mpsc::unbounded_channel::(); 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; } }; 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, ) { 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, })); 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; break; } } } } }); queue_task({ let state = Rc::clone(&state); move || pump_window_worker(state) }); }, || {}, ); WindowWorkerHandle { command_tx, _worker: worker, } } fn pump_window_worker(state: Rc>) { let mut reschedule = true; { let mut state_ref = state.borrow_mut(); if state_ref.shutdown_requested || state_ref .window .wait_for_events(Duration::from_millis(16)) .is_err() { emit_window_closed(&mut state_ref, false); reschedule = false; } else { 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, }); } if !state_ref.window.is_running() { emit_window_closed(&mut state_ref, true); reschedule = false; } else 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.renderer.resize(frame.width, frame.height); state_ref.window.request_redraw(); state_ref.pending_viewport = Some(current_viewport); if state_ref .latest_scene .as_ref() .is_none_or(|scene| scene.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; } } 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 !state_ref.window.presentation_ready() { // Wait for the compositor frame callback before attempting another present. } else if scene.logical_size != current_viewport { 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" ); state_ref.pending_viewport = Some(current_viewport); let mut preview_scene = scene.clone(); preview_scene.logical_size = current_viewport; state_ref.window.arm_frame_callback(); if state_ref.renderer.render(&preview_scene).is_ok() { trace!( target: "ruin_ui_platform_wayland::scene", window_id = state_ref.window_id.raw(), scene_version = scene.version, width = current_viewport.width, height = current_viewport.height, "presented scaled preview scene for current viewport" ); let _ = state_ref.event_tx.send(PlatformEvent::FramePresented { window_id: state_ref.window_id, scene_version: scene.version, item_count: scene.item_count(), }); finish_presented_viewport_request(&mut state_ref, scene.logical_size); } else { state_ref.window.clear_frame_callback(); } } else { state_ref.window.arm_frame_callback(); match state_ref.renderer.render(scene) { Ok(()) => { 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, "presented matching 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(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); } } } if reschedule { queue_task(move || pump_window_worker(state)); } } 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>, event: PlatformEvent) { let _ = state.borrow().events.send(event); } impl Dispatch for State { fn event( _state: &mut Self, _proxy: &wl_registry::WlRegistry, _event: wl_registry::Event, _data: &GlobalListContents, _conn: &Connection, _qh: &QueueHandle, ) { } } 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 ); impl Dispatch for State { fn event( state: &mut Self, seat: &wl_seat::WlSeat, event: wl_seat::Event, _data: &(), _conn: &Connection, qh: &QueueHandle, ) { 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 for State { fn event( state: &mut Self, _pointer: &wl_pointer::WlPointer, event: wl_pointer::Event, _data: &(), _conn: &Connection, _qh: &QueueHandle, ) { 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)); } _ => {} } } } impl Dispatch for State { fn event( state: &mut Self, _keyboard: &wl_keyboard::WlKeyboard, event: wl_keyboard::Event, _data: &(), _conn: &Connection, _qh: &QueueHandle, ) { 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 for State { fn event( state: &mut Self, _data_device: &wl_data_device::WlDataDevice, event: wl_data_device::Event, _data: &(), _conn: &Connection, _qh: &QueueHandle, ) { 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 for State { fn event( state: &mut Self, offer: &wl_data_offer::WlDataOffer, event: wl_data_offer::Event, _data: &(), _conn: &Connection, _qh: &QueueHandle, ) { 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 for State { fn event( state: &mut Self, data_source: &wl_data_source::WlDataSource, event: wl_data_source::Event, _data: &(), _conn: &Connection, _qh: &QueueHandle, ) { 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 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, ) { 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 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, ) { 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 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, ) { 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 for State { fn event( _state: &mut Self, wm_base: &xdg_wm_base::XdgWmBase, event: xdg_wm_base::Event, _data: &(), _conn: &Connection, _qh: &QueueHandle, ) { if let xdg_wm_base::Event::Ping { serial } = event { wm_base.pong(serial); } } } impl Dispatch for State { fn event( state: &mut Self, xdg_surface: &xdg_surface::XdgSurface, event: xdg_surface::Event, _data: &(), _conn: &Connection, _qh: &QueueHandle, ) { if let xdg_surface::Event::Configure { serial } = event { xdg_surface.ack_configure(serial); state.configured = true; state.request_redraw(); } } } impl Dispatch for State { fn event( state: &mut Self, callback: &wl_callback::WlCallback, event: wl_callback::Event, _data: &(), _conn: &Connection, _qh: &QueueHandle, ) { if let wl_callback::Event::Done { .. } = event { if state.frame_callback.as_ref() == Some(callback) { state.frame_callback = None; } state.request_redraw(); } } } impl Dispatch for State { fn event( state: &mut Self, _proxy: &xdg_toplevel::XdgToplevel, event: xdg_toplevel::Event, _data: &(), _conn: &Connection, _qh: &QueueHandle, ) { 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" ); state.pending_size = Some((width, height)); 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); }