use crate::core::ObjectId;
use crate::core::PlatformFamily;
use crate::event::EventLoop;
use crate::platform::types::{
DropEvent, Platform, PlatformCapabilities, WidgetTriggerEvent, WidgetTriggerKind,
};
use crate::platform::wayland::types::{WaylandHandleKind, WaylandPlatform};
#[cfg(all(feature = "wayland-native", target_os = "linux"))]
use wayland_client as wl_client;
#[cfg(all(feature = "wayland-native", target_os = "linux"))]
use wayland_protocols as wl_protocols;
impl Platform for WaylandPlatform {
fn as_any(&self) -> &dyn std::any::Any {
self
}
fn backend_name(&self) -> &'static str {
"wayland"
}
fn family(&self) -> PlatformFamily {
PlatformFamily::Desktop
}
fn total_memory_mb(&self) -> Option<u64> {
crate::platform::os_probes::total_memory_mb()
}
fn is_on_battery(&self) -> bool {
crate::platform::os_probes::is_on_battery()
}
fn process_memory_utilization(&self) -> Option<f32> {
crate::platform::os_probes::process_memory_utilization()
}
fn process_cpu_utilization(&self) -> Option<f32> {
crate::platform::os_probes::process_cpu_utilization()
}
fn spawn_print_job(&self, job_file: &std::path::Path) -> Result<(), String> {
crate::platform::os_probes::spawn_print_job(job_file)
}
fn has_print_support(&self) -> bool {
crate::platform::types::unix_print_clients_available()
}
fn capabilities(&self) -> PlatformCapabilities {
PlatformCapabilities {
dpi_scaling: true,
ime: true,
accessibility: true,
native_menu: false,
typed_widget_trigger: true,
}
}
fn dpi_scale_factor(&self) -> f32 {
if let Ok(scale_str) = std::env::var("GDK_SCALE") {
if let Ok(scale) = scale_str.parse::<f32>() {
if scale > 0.0 {
return scale;
}
}
}
if let Ok(scale_str) = std::env::var("QT_SCALE_FACTOR") {
if let Ok(scale) = scale_str.parse::<f32>() {
if scale > 0.0 {
return scale;
}
}
}
if let Ok(scale_str) = std::env::var("RUST_WIDGETS_DPI_SCALE") {
if let Ok(scale) = scale_str.parse::<f32>() {
if scale > 0.0 {
return scale;
}
}
}
#[cfg(all(feature = "wayland-native", target_os = "linux"))]
{
if let Ok(guard) = self.native_session.lock() {
if let Some(ref session) = *guard {
let scale = session.state.dpi_scale;
if scale > 0.0 {
return scale;
}
}
}
}
1.0
}
fn init(&self) {
self.runtime.initialized.store(true, std::sync::atomic::Ordering::SeqCst);
log::info!("[wayland] Platform initialized (state-only backend).");
}
fn run(&self) {
self.runtime.running.store(true, std::sync::atomic::Ordering::SeqCst);
let wayland_display =
std::env::var("WAYLAND_DISPLAY").unwrap_or_else(|_| String::from("wayland-0"));
let xdg_session =
std::env::var("XDG_SESSION_TYPE").unwrap_or_else(|_| String::from("unknown"));
log::info!(
"[wayland] Platform running: XDG_SESSION_TYPE={}, WAYLAND_DISPLAY={}",
xdg_session,
wayland_display
);
let mut event_loop = EventLoop::new();
#[cfg(all(feature = "wayland-native", not(alloc_frugal), target_os = "linux"))]
if let Some(pump) = create_event_loop_pump() {
event_loop.set_native_pump(pump);
}
event_loop.start();
#[cfg(all(feature = "wayland-native", not(alloc_frugal), target_os = "linux"))]
{
self.run_native_event_loop();
}
#[cfg(not(all(feature = "wayland-native", not(alloc_frugal), target_os = "linux")))]
while self.runtime.running.load(std::sync::atomic::Ordering::SeqCst) {
std::thread::sleep(std::time::Duration::from_millis(16));
}
}
fn quit(&self) {
self.runtime.running.store(false, std::sync::atomic::Ordering::SeqCst);
log::info!("[wayland] Platform quit.");
}
fn destroy_widget(&self, widget_id: ObjectId) -> bool {
if let Ok(mut menus) = self.menus.lock() {
menus.attached_menu_bar.remove(&widget_id);
menus.attached_menu_bar.retain(|_, bar| *bar != widget_id);
menus.menu_children.remove(&widget_id);
for children in menus.menu_children.values_mut() {
children.retain(|child| *child != widget_id);
}
menus.pending_menu_events.retain(|queued| *queued != widget_id);
menus.pending_widget_events.retain(|event| event.widget_id != widget_id);
} else {
log::error!("[wayland] destroy_widget: menu mutex poisoned");
}
self.state.destroy_widget(widget_id)
}
fn create_window(&self, title: &str, x: i32, y: i32, width: u32, height: u32) -> ObjectId {
#[cfg(all(feature = "wayland-native", target_os = "linux"))]
if let Some(id) = self.try_create_native_window(title, x, y, width, height) {
return id;
}
self.insert_widget(WaylandHandleKind::Window, title, x, y, width, height)
}
fn attach_menu_bar_to_window(&self, window: ObjectId, menu_bar: ObjectId) -> bool {
if !matches!(self.state.kind_of(window), Some(WaylandHandleKind::Window)) {
return false;
}
if !matches!(self.state.kind_of(menu_bar), Some(WaylandHandleKind::MenuBar)) {
return false;
}
if let Ok(mut menus) = self.menus.lock() {
menus.attached_menu_bar.insert(window, menu_bar);
true
} else {
log::error!("[wayland] attach_menu_bar_to_window: mutex poisoned");
false
}
}
fn menu_add_item(&self, parent_menu: ObjectId, text: &str, shortcut: Option<&str>) -> ObjectId {
if !matches!(self.state.kind_of(parent_menu), Some(WaylandHandleKind::Menu)) {
return 0;
}
let display = if let Some(shortcut) = shortcut {
format!("{}\t{}", text, shortcut)
} else {
text.to_string()
};
let id = self.insert_widget(WaylandHandleKind::MenuItem, &display, 0, 0, 0, 0);
if let Ok(mut menus) = self.menus.lock() {
menus.menu_children.entry(parent_menu).or_default().push(id);
}
id
}
fn poll_menu_triggered(&self) -> Option<ObjectId> {
if let Ok(mut menus) = self.menus.lock() {
menus.pending_menu_events.pop_front()
} else {
log::error!("[wayland] poll_menu_triggered: mutex poisoned");
None
}
}
fn inject_menu_trigger(&self, menu_item_id: ObjectId) -> bool {
if !matches!(self.state.kind_of(menu_item_id), Some(WaylandHandleKind::MenuItem)) {
return false;
}
if let Ok(mut menus) = self.menus.lock() {
menus.pending_menu_events.push_back(menu_item_id);
true
} else {
log::error!("[wayland] inject_menu_trigger: mutex poisoned");
false
}
}
fn poll_widget_triggered(&self) -> Option<ObjectId> {
if let Ok(mut menus) = self.menus.lock() {
menus.pending_widget_events.pop_front().map(|e| e.widget_id)
} else {
log::error!("[wayland] poll_widget_triggered: mutex poisoned");
None
}
}
fn poll_widget_trigger_event(&self) -> Option<WidgetTriggerEvent> {
if let Ok(mut menus) = self.menus.lock() {
menus.pending_widget_events.pop_front()
} else {
log::error!("[wayland] poll_widget_trigger_event: mutex poisoned");
None
}
}
fn inject_widget_trigger_event(&self, widget_id: ObjectId, kind: WidgetTriggerKind) -> bool {
if !self.state.contains_widget(widget_id) {
return false;
}
if let Ok(mut menus) = self.menus.lock() {
menus.pending_widget_events.push_back(WidgetTriggerEvent { widget_id, kind });
true
} else {
log::error!("[wayland] inject_widget_trigger_event: mutex poisoned");
false
}
}
fn show_widget(&self, widget_id: ObjectId) {
self.state.set_visible(widget_id, true);
}
fn hide_widget(&self, widget_id: ObjectId) {
self.state.set_visible(widget_id, false);
}
fn set_widget_geometry(&self, widget_id: ObjectId, x: i32, y: i32, width: u32, height: u32) {
self.state.set_geometry(widget_id, x, y, width, height);
}
fn set_widget_text(&self, widget_id: ObjectId, text: &str) {
self.state.set_text(widget_id, text);
}
fn get_widget_text(&self, widget_id: ObjectId) -> String {
self.state.text(widget_id)
}
fn set_widget_enabled(&self, widget_id: ObjectId, enabled: bool) {
self.state.set_enabled(widget_id, enabled);
}
fn is_widget_enabled(&self, widget_id: ObjectId) -> bool {
self.state.enabled(widget_id)
}
fn set_widget_visible(&self, widget_id: ObjectId, visible: bool) {
self.state.set_visible(widget_id, visible);
}
fn is_widget_visible(&self, widget_id: ObjectId) -> bool {
self.state.visible(widget_id)
}
fn set_widget_ime_enabled(&self, widget_id: ObjectId, enabled: bool) -> bool {
self.state.set_ime_enabled(widget_id, enabled)
}
fn is_widget_ime_enabled(&self, widget_id: ObjectId) -> bool {
self.state.ime_enabled(widget_id)
}
fn set_widget_accessibility_name(&self, widget_id: ObjectId, name: &str) -> bool {
self.state.set_accessibility_name(widget_id, name)
}
fn get_widget_accessibility_name(&self, widget_id: ObjectId) -> String {
self.state.accessibility_name(widget_id)
}
fn set_clipboard_text(&self, text: &str) -> bool {
self.state.set_clipboard_text(text)
}
fn get_clipboard_text(&self) -> String {
self.state.clipboard_text()
}
fn begin_drag(&self, source_widget_id: ObjectId, mime: &str, payload: &[u8]) -> bool {
self.state.begin_drag(source_widget_id, mime, payload)
}
fn poll_drop_event(&self) -> Option<DropEvent> {
self.state.pop_drop_event()
}
fn inject_drop_event(&self, event: DropEvent) -> bool {
self.state.inject_drop_event(event)
}
}
#[cfg(all(feature = "wayland-native", target_os = "linux"))]
impl WaylandPlatform {
#[cfg(all(feature = "wayland-native", not(alloc_frugal), target_os = "linux"))]
fn run_native_event_loop(&self) {
use std::os::fd::AsRawFd;
const IDLE_TIMEOUT_MS: i32 = 50;
log::info!("[wayland] Entering native fd-based event loop");
loop {
if !self.runtime.running.load(std::sync::atomic::Ordering::SeqCst) {
break;
}
let mut guard = self.native_session.lock().unwrap();
let Some(session) = guard.as_mut() else {
drop(guard);
std::thread::sleep(std::time::Duration::from_millis(16));
continue;
};
if let Err(e) = session.event_queue.flush() {
log::warn!("[wayland] flush failed: {e}");
}
match session.event_queue.prepare_read() {
Some(read_guard) => {
let fd = read_guard.connection_fd().as_raw_fd();
let mut pollfd = libc::pollfd { fd, events: libc::POLLIN, revents: 0 };
let rc = unsafe { libc::poll(&mut pollfd, 1, IDLE_TIMEOUT_MS) };
if rc > 0 {
if let Err(e) = read_guard.read() {
log::warn!("[wayland] read failed: {e}");
}
if let Err(e) = session.event_queue.dispatch_pending(&mut session.state) {
log::warn!("[wayland] dispatch failed: {e}");
}
} else if rc < 0 {
let err = std::io::Error::last_os_error();
log::warn!("[wayland] poll failed: {err}");
std::thread::sleep(std::time::Duration::from_millis(16));
}
}
None => {
let _ = session.event_queue.dispatch_pending(&mut session.state);
}
}
}
log::info!("[wayland] Native event loop exited");
}
#[cfg(not(alloc_frugal))]
pub(crate) fn dispatch_native_events(&self) {
let mut guard = self.native_session.lock().unwrap();
if let Some(ref mut session) = *guard {
let _ = session.event_queue.dispatch_pending(&mut session.state);
}
}
fn try_create_native_window(
&self,
title: &str,
x: i32,
y: i32,
width: u32,
height: u32,
) -> Option<ObjectId> {
fn connect_wayland() -> Option<WaylandSession> {
let conn = wl_client::Connection::connect_to_env()
.inspect_err(|e| {
log::error!("[wayland] Failed to connect to Wayland display: {}", e)
})
.ok()?;
let display = conn.display();
let mut event_queue = conn.new_event_queue();
let qh = event_queue.handle();
let _registry = display.get_registry(&qh, ());
let mut state = WaylandSessionState {
compositor: None,
xdg_wm_base: None,
wl_output: None,
dpi_scale: 1.0,
};
event_queue
.roundtrip(&mut state)
.inspect_err(|e| log::error!("[wayland] Registry roundtrip failed: {}", e))
.ok()?;
if state.compositor.is_none() {
log::error!("[wayland] wl_compositor global not available");
return None;
}
if state.xdg_wm_base.is_none() {
log::error!("[wayland] xdg_wm_base global not available");
return None;
}
log::info!("[wayland] Connected to display; compositor and xdg_wm_base bound.");
Some(WaylandSession { conn, event_queue, state })
}
let mut guard = self.native_session.lock().unwrap();
if guard.is_none() {
*guard = connect_wayland();
}
let session = guard.as_mut()?;
let compositor = session.state.compositor.as_ref()?;
let xdg_wm_base = session.state.xdg_wm_base.as_ref()?;
let qh = session.event_queue.handle();
let surface = compositor.create_surface(&qh, ());
let xdg_surface = xdg_wm_base.get_xdg_surface(&surface, &qh, ());
let toplevel = xdg_surface.get_toplevel(&qh, ());
toplevel.set_title(title.to_string());
toplevel.set_app_id("rust_widgets".to_string());
if width > 0 && height > 0 {
toplevel.set_min_size(width as i32, height as i32);
}
surface.commit();
session.event_queue.roundtrip(&mut session.state).ok()?;
log::info!("[wayland] Created xdg_toplevel '{}' ({}x{})", title, width, height);
let id = self.insert_widget(WaylandHandleKind::Window, title, x, y, width, height);
log::info!("[wayland] Window {} registered with state backend", id);
Some(id)
}
}
#[cfg(all(feature = "wayland-native", target_os = "linux"))]
pub(crate) struct WaylandSessionState {
pub(crate) compositor: Option<wl_client::protocol::wl_compositor::WlCompositor>,
pub(crate) xdg_wm_base: Option<wl_protocols::xdg::shell::client::xdg_wm_base::XdgWmBase>,
pub(crate) wl_output: Option<wl_client::protocol::wl_output::WlOutput>,
pub(crate) dpi_scale: f32,
}
#[cfg(all(feature = "wayland-native", target_os = "linux"))]
pub(crate) struct WaylandSession {
#[allow(dead_code)]
pub(crate) conn: wl_client::Connection,
pub(crate) event_queue: wl_client::EventQueue<WaylandSessionState>,
pub(crate) state: WaylandSessionState,
}
#[cfg(all(feature = "wayland-native", not(alloc_frugal), target_os = "linux"))]
pub fn create_event_loop_pump() -> Option<Box<dyn Fn() + Send + Sync>> {
let platform = crate::platform::runtime::get_platform();
let wayland = platform.as_any().downcast_ref::<WaylandPlatform>()?;
Some(Box::new(move || {
wayland.dispatch_native_events();
}))
}
#[cfg(all(feature = "wayland-native", target_os = "linux"))]
impl wl_client::Dispatch<wl_client::protocol::wl_registry::WlRegistry, ()> for WaylandSessionState {
fn event(
state: &mut Self,
registry: &wl_client::protocol::wl_registry::WlRegistry,
event: wl_client::protocol::wl_registry::Event,
_data: &(),
_conn: &wl_client::Connection,
_qh: &wl_client::QueueHandle<WaylandSessionState>,
) {
use wl_client::protocol::wl_registry::Event;
if let Event::Global { name, interface, version } = event {
log::debug!(
"[wayland] Registry global: interface='{}', version={}",
interface,
version
);
match interface.as_str() {
"wl_compositor" => {
let comp = registry
.bind::<wl_client::protocol::wl_compositor::WlCompositor, _, _>(
name,
version.min(4),
_qh,
(),
);
state.compositor = Some(comp);
log::info!("[wayland] Bound wl_compositor (v{})", version);
}
"xdg_wm_base" => {
let xdg = registry
.bind::<wl_protocols::xdg::shell::client::xdg_wm_base::XdgWmBase, _, _>(
name,
version.min(6),
_qh,
(),
);
state.xdg_wm_base = Some(xdg);
log::info!("[wayland] Bound xdg_wm_base (v{})", version);
}
"wl_output" => {
let output = registry.bind::<wl_client::protocol::wl_output::WlOutput, _, _>(
name,
version.min(4),
_qh,
(),
);
state.wl_output = Some(output);
log::info!("[wayland] Bound wl_output (v{})", version);
}
_ => { }
}
}
}
}
#[cfg(all(feature = "wayland-native", target_os = "linux"))]
impl wl_client::Dispatch<wl_client::protocol::wl_compositor::WlCompositor, ()>
for WaylandSessionState
{
fn event(
_state: &mut Self,
_proxy: &wl_client::protocol::wl_compositor::WlCompositor,
_event: wl_client::protocol::wl_compositor::Event,
_data: &(),
_conn: &wl_client::Connection,
_qh: &wl_client::QueueHandle<WaylandSessionState>,
) {
}
}
#[cfg(all(feature = "wayland-native", target_os = "linux"))]
impl wl_client::Dispatch<wl_client::protocol::wl_output::WlOutput, ()> for WaylandSessionState {
fn event(
state: &mut Self,
_proxy: &wl_client::protocol::wl_output::WlOutput,
event: wl_client::protocol::wl_output::Event,
_data: &(),
_conn: &wl_client::Connection,
_qh: &wl_client::QueueHandle<WaylandSessionState>,
) {
use wl_client::protocol::wl_output::Event;
match event {
Event::Scale { factor } => {
state.dpi_scale = factor as f32;
log::info!("[wayland] wl_output scale factor: {} (DPI: {})", factor, factor * 96);
}
Event::Done => {
log::trace!("[wayland] wl_output done");
}
Event::Geometry { .. }
| Event::Mode { .. }
| Event::Name { .. }
| Event::Description { .. } => {
log::trace!("[wayland] wl_output event: {:?}", event);
}
_ => {
log::trace!("[wayland] wl_output unhandled event: {:?}", event);
}
}
}
}
#[cfg(all(feature = "wayland-native", target_os = "linux"))]
impl wl_client::Dispatch<wl_client::protocol::wl_surface::WlSurface, ()> for WaylandSessionState {
fn event(
_state: &mut Self,
_proxy: &wl_client::protocol::wl_surface::WlSurface,
_event: wl_client::protocol::wl_surface::Event,
_data: &(),
_conn: &wl_client::Connection,
_qh: &wl_client::QueueHandle<WaylandSessionState>,
) {
log::trace!("[wayland] wl_surface event: {:?}", _event);
}
}
#[cfg(all(feature = "wayland-native", target_os = "linux"))]
impl wl_client::Dispatch<wl_protocols::xdg::shell::client::xdg_wm_base::XdgWmBase, ()>
for WaylandSessionState
{
fn event(
_state: &mut Self,
proxy: &wl_protocols::xdg::shell::client::xdg_wm_base::XdgWmBase,
event: <wl_protocols::xdg::shell::client::xdg_wm_base::XdgWmBase as wl_client::Proxy>::Event,
_data: &(),
_conn: &wl_client::Connection,
_qh: &wl_client::QueueHandle<WaylandSessionState>,
) {
use wl_protocols::xdg::shell::client::xdg_wm_base::Event;
if let Event::Ping { serial } = event {
proxy.pong(serial);
log::trace!("[wayland] xdg_wm_base ping/pong (serial={})", serial);
}
}
}
#[cfg(all(feature = "wayland-native", target_os = "linux"))]
impl wl_client::Dispatch<wl_protocols::xdg::shell::client::xdg_surface::XdgSurface, ()>
for WaylandSessionState
{
fn event(
_state: &mut Self,
proxy: &wl_protocols::xdg::shell::client::xdg_surface::XdgSurface,
event: <wl_protocols::xdg::shell::client::xdg_surface::XdgSurface as wl_client::Proxy>::Event,
_data: &(),
_conn: &wl_client::Connection,
_qh: &wl_client::QueueHandle<WaylandSessionState>,
) {
use wl_protocols::xdg::shell::client::xdg_surface::Event;
if let Event::Configure { serial } = event {
proxy.ack_configure(serial);
log::trace!("[wayland] xdg_surface configure ack (serial={})", serial);
}
}
}
#[cfg(all(feature = "wayland-native", target_os = "linux"))]
impl wl_client::Dispatch<wl_protocols::xdg::shell::client::xdg_toplevel::XdgToplevel, ()>
for WaylandSessionState
{
fn event(
_state: &mut Self,
_proxy: &wl_protocols::xdg::shell::client::xdg_toplevel::XdgToplevel,
event: <wl_protocols::xdg::shell::client::xdg_toplevel::XdgToplevel as wl_client::Proxy>::Event,
_data: &(),
_conn: &wl_client::Connection,
_qh: &wl_client::QueueHandle<WaylandSessionState>,
) {
use wl_protocols::xdg::shell::client::xdg_toplevel::Event;
match event {
Event::Configure { width, height, states } => {
log::trace!(
"[wayland] xdg_toplevel configure: {}x{}, states={:?}",
width,
height,
states
);
}
Event::Close => {
log::info!("[wayland] xdg_toplevel close requested");
}
Event::ConfigureBounds { .. } | Event::WmCapabilities { .. } => {
log::trace!("[wayland] xdg_toplevel event: {:?}", event);
}
_ => {
log::trace!("[wayland] xdg_toplevel unhandled event: {:?}", event);
}
}
}
}