More text improvements, performance enhancements, input handling, text selection, wl cursors

This commit is contained in:
2026-03-20 22:24:29 -04:00
parent d79a3bb728
commit 423df4ae1f
15 changed files with 2458 additions and 265 deletions

View File

@@ -8,6 +8,8 @@ libc = "0.2"
raw-window-handle = "0.6"
ruin_runtime = { package = "ruin-runtime", path = "../runtime" }
ruin_ui = { path = "../ui" }
ruin_ui_renderer_wgpu = { path = "../ui_renderer_wgpu" }
tracing = "0.1"
wayland-backend = { version = "0.3", features = ["client_system"] }
wayland-client = "0.31"
wayland-protocols = { version = "0.32", features = ["client"] }
wayland-protocols = { version = "0.32", features = ["client", "staging", "unstable"] }

View File

@@ -1,19 +1,43 @@
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::Write;
use std::num::NonZeroU32;
use std::os::fd::{AsFd, AsRawFd};
use std::ptr::NonNull;
use std::rc::Rc;
use std::time::Duration;
use raw_window_handle::{
DisplayHandle, HandleError, HasDisplayHandle, HasWindowHandle, RawDisplayHandle,
RawWindowHandle, WaylandDisplayHandle, WaylandWindowHandle, WindowHandle,
};
use ruin_ui::{Point, PointerButton, PointerEvent, PointerEventKind, UiSize, WindowSpec};
use ruin_runtime::channel::mpsc;
use ruin_runtime::{WorkerHandle, queue_future, queue_task, spawn_worker};
use ruin_ui::{
CursorIcon, 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::{debug, trace};
use wayland_client::globals::{GlobalListContents, registry_queue_init};
use wayland_client::protocol::{wl_compositor, wl_pointer, wl_registry, wl_seat, wl_surface};
use wayland_client::{Connection, Dispatch, Proxy, QueueHandle, WEnum, delegate_noop};
use wayland_client::protocol::{
wl_callback, wl_compositor, 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};
#[derive(Clone)]
@@ -59,6 +83,46 @@ pub struct WaylandWindow {
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),
SetPrimarySelectionText(String),
SetCursorIcon(CursorIcon),
ApplySpec(WindowSpec),
Shutdown,
}
enum InternalBackendEvent {
Closed { window_id: WindowId },
}
struct State {
running: bool,
_connection: Connection,
@@ -68,13 +132,25 @@ struct State {
_toplevel: xdg_toplevel::XdgToplevel,
_wm_base: xdg_wm_base::XdgWmBase,
_seat: wl_seat::WlSeat,
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>,
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>,
primary_selection_source: Option<zwp_primary_selection_source_v1::ZwpPrimarySelectionSourceV1>,
primary_selection_text: Option<String>,
last_selection_serial: Option<u32>,
last_pointer_enter_serial: Option<u32>,
cursor_icon: CursorIcon,
}
impl State {
@@ -83,6 +159,25 @@ impl State {
}
}
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();
}
impl WaylandWindow {
pub fn open(spec: WindowSpec) -> Result<Self, Box<dyn Error>> {
let connection = Connection::connect_to_env()?;
@@ -92,6 +187,13 @@ impl WaylandWindow {
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 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, ());
@@ -133,13 +235,24 @@ impl WaylandWindow {
_toplevel: toplevel,
_wm_base: wm_base,
_seat: seat,
cursor_shape_manager,
cursor_shape_device: None,
primary_selection_manager,
primary_selection_device,
qh,
pointer: 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(),
primary_selection_source: None,
primary_selection_text: None,
last_selection_serial: None,
last_pointer_enter_serial: None,
cursor_icon: CursorIcon::Default,
},
})
}
@@ -282,6 +395,83 @@ impl WaylandWindow {
self.state.request_redraw();
}
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 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)
}
@@ -314,6 +504,568 @@ impl WaylandWindow {
}
}
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::SetPrimarySelectionText { window_id, text } => {
handle_set_primary_selection_text(&state, window_id, text);
}
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::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_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::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::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,
});
}
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,
@@ -328,6 +1080,13 @@ impl Dispatch<wl_registry::WlRegistry, GlobalListContents> for State {
delegate_noop!(State: ignore wl_compositor::WlCompositor);
delegate_noop!(State: ignore wl_surface::WlSurface);
delegate_noop!(State: ignore wp_cursor_shape_manager_v1::WpCursorShapeManagerV1);
delegate_noop!(State: ignore wp_cursor_shape_device_v1::WpCursorShapeDeviceV1);
delegate_noop!(
State: ignore
zwp_primary_selection_device_manager_v1::ZwpPrimarySelectionDeviceManagerV1
);
delegate_noop!(State: ignore zwp_primary_selection_offer_v1::ZwpPrimarySelectionOfferV1);
impl Dispatch<wl_seat::WlSeat, ()> for State {
fn event(
@@ -344,11 +1103,18 @@ impl Dispatch<wl_seat::WlSeat, ()> for State {
};
if capabilities.contains(wl_seat::Capability::Pointer) {
if state.pointer.is_none() {
state.pointer = Some(seat.get_pointer(qh, ()));
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;
}
}
}
@@ -365,11 +1131,22 @@ impl Dispatch<wl_pointer::WlPointer, ()> for State {
) {
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 {
wl_pointer::Event::Motion {
surface_x,
surface_y,
..
@@ -390,8 +1167,10 @@ impl Dispatch<wl_pointer::WlPointer, ()> for State {
PointerEventKind::LeaveWindow,
));
state.pointer_position = None;
state.last_pointer_enter_serial = None;
}
wl_pointer::Event::Button {
serial,
button,
state: button_state,
..
@@ -402,6 +1181,7 @@ impl Dispatch<wl_pointer::WlPointer, ()> for State {
if button != 0x110 {
return;
}
state.last_selection_serial = Some(serial);
let kind = match button_state {
WEnum::Value(wl_pointer::ButtonState::Pressed) => PointerEventKind::Down {
button: PointerButton::Primary,
@@ -420,6 +1200,53 @@ impl Dispatch<wl_pointer::WlPointer, ()> for State {
}
}
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>,
) {
}
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_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,
@@ -452,6 +1279,24 @@ impl Dispatch<xdg_surface::XdgSurface, ()> for State {
}
}
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,
@@ -476,6 +1321,12 @@ impl Dispatch<xdg_toplevel::XdgToplevel, ()> for State {
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();
}