More text improvements, performance enhancements, input handling, text selection, wl cursors
This commit is contained in:
@@ -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"] }
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user