Files
ruin/lib/ui_platform_wayland/src/lib.rs
2026-03-21 01:55:06 -04:00

1988 lines
74 KiB
Rust

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<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>,
}
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<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>,
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 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 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<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;
}
pub fn apply_spec(&mut self, spec: &WindowSpec) -> Result<(), Box<dyn Error>> {
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() {
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::<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 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<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);
}
}
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>();
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<RefCell<WindowWorkerState>>) {
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<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
);
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));
}
_ => {}
}
}
}
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;
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 {
if state.frame_callback.as_ref() == Some(callback) {
state.frame_callback = None;
}
state.request_redraw();
}
}
}
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"
);
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);
}