1988 lines
74 KiB
Rust
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);
|
|
}
|