Early UI work.

This commit is contained in:
2026-03-20 16:46:18 -04:00
parent 9ab1167fef
commit 39ede248cf
15 changed files with 3560 additions and 1 deletions

View File

@@ -0,0 +1,11 @@
[package]
name = "ruin_ui_platform_wayland"
version = "0.1.0"
edition = "2024"
[dependencies]
raw-window-handle = "0.6"
ruin_ui = { path = "../ui" }
wayland-backend = { version = "0.3", features = ["client_system"] }
wayland-client = "0.31"
wayland-protocols = { version = "0.32", features = ["client"] }

View File

@@ -0,0 +1,271 @@
use std::error::Error;
use std::ffi::c_void;
use std::num::NonZeroU32;
use std::ptr::NonNull;
use raw_window_handle::{
DisplayHandle, HandleError, HasDisplayHandle, HasWindowHandle, RawDisplayHandle,
RawWindowHandle, WaylandDisplayHandle, WaylandWindowHandle, WindowHandle,
};
use ruin_ui::{UiSize, WindowSpec};
use wayland_client::globals::{GlobalListContents, registry_queue_init};
use wayland_client::protocol::{wl_compositor, wl_registry, wl_surface};
use wayland_client::{Connection, Dispatch, Proxy, QueueHandle, delegate_noop};
use wayland_protocols::xdg::shell::client::{xdg_surface, xdg_toplevel, xdg_wm_base};
#[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,
}
pub struct WaylandWindow {
event_queue: wayland_client::EventQueue<State>,
surface_target: WaylandSurfaceTarget,
state: State,
}
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,
current_size: (u32, u32),
configured: bool,
pending_size: Option<(u32, u32)>,
needs_redraw: bool,
}
impl State {
fn request_redraw(&mut self) {
self.needs_redraw = true;
}
}
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 wm_base: xdg_wm_base::XdgWmBase = globals.bind(&qh, 1..=6, ())?;
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,
current_size: (initial_width, initial_height),
configured: false,
pending_size: None,
needs_redraw: false,
},
})
}
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 request_redraw(&mut self) {
self.state.request_redraw();
}
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
}
}
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);
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<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);
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);
}