mod data;
mod data_device;
mod fd;
pub mod ffi;
mod input;
mod layer;
mod popup;
pub mod protocol;
pub(crate) use crate::linux::{keymap, xkb};
#[cfg(feature = "wayland-e2e")]
pub mod e2e;
use core::ffi::{CStr, c_int, c_void};
use core::ptr::{self, NonNull};
use core::time::Duration;
use std::collections::VecDeque;
use std::ffi::CString;
use crcbl_core::input::{DeviceId, Keysym, Modifiers, ScrollDelta};
use crcbl_core::{EventTime, KeyCode, Pool, SurfaceTarget};
use crate::{
ClipboardContent, ClipboardOffer, ClipboardRequestId, CloseReply, CursorIcon, DisplayMode,
LogicalSize, MimeType, MonitorId, MonitorInfo, PhysicalPoint, PhysicalRect, PointerMode,
ReceivedMime, Shell, ShellBackend, ShellCaps, ShellError, ShellEvent, SizeConstraints,
WindowConfiguration, WindowDesc, WindowId, WindowRole, WindowState,
};
use data::{Delivery, HeldRead, Resolution, Transfer};
use ffi::{Lib, WlArgument, WlDisplay, WlMessage, WlProxy};
use protocol::fractional_scale::{wp_fractional_scale_manager_v1, wp_fractional_scale_v1};
use protocol::layer_shell::{zwlr_layer_shell_v1, zwlr_layer_surface_v1};
use protocol::pointer_constraints::{zwp_locked_pointer_v1, zwp_pointer_constraints_v1};
use protocol::relative_pointer::{zwp_relative_pointer_manager_v1, zwp_relative_pointer_v1};
use protocol::viewporter::{wp_viewport, wp_viewporter};
use protocol::wayland::{
wl_compositor, wl_data_device, wl_data_device_manager, wl_data_offer, wl_data_source,
wl_keyboard, wl_output, wl_pointer, wl_registry, wl_seat, wl_surface,
};
use protocol::xdg_decoration::{zxdg_decoration_manager_v1, zxdg_toplevel_decoration_v1};
use protocol::xdg_output::{zxdg_output_manager_v1, zxdg_output_v1};
use protocol::xdg_shell::{xdg_popup, xdg_surface, xdg_toplevel, xdg_wm_base};
const COMPOSITOR_VERSION: u32 = 4;
const WM_BASE_VERSION: u32 = 3;
const OUTPUT_VERSION: u32 = 4;
const SEAT_VERSION: u32 = 8;
const XDG_OUTPUT_VERSION: u32 = 3;
const DECORATION_VERSION: u32 = 1;
const DATA_DEVICE_VERSION: u32 = 3;
const FRACTIONAL_SCALE_DENOMINATOR: f64 = 120.0;
const TRANSFER_POLL: Duration = Duration::from_millis(50);
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
struct TimeBase {
epoch_nanos: u64,
}
impl TimeBase {
fn now() -> Self {
Self {
epoch_nanos: ffi::monotonic_nanos(),
}
}
fn align(&mut self, elapsed: Duration) {
let now = ffi::monotonic_nanos();
self.epoch_nanos =
now.saturating_sub(u64::try_from(elapsed.as_nanos()).unwrap_or(u64::MAX));
}
fn rebase(epoch_nanos: u64, now_nanos: u64, wayland_millis: u32) -> EventTime {
const WRAP: u64 = 1 << 32;
let now_millis = now_nanos / 1_000_000;
let current = (now_millis & !(WRAP - 1)) | u64::from(wayland_millis);
let full = [
current.checked_sub(WRAP),
Some(current),
current.checked_add(WRAP),
]
.into_iter()
.flatten()
.min_by_key(|candidate| candidate.abs_diff(now_millis))
.expect("the current wrap is always a candidate");
let epoch_millis = epoch_nanos / 1_000_000;
EventTime::from_millis(full.saturating_sub(epoch_millis))
}
fn event_time(self, wayland_millis: u32) -> EventTime {
Self::rebase(self.epoch_nanos, ffi::monotonic_nanos(), wayland_millis)
}
fn event_time_micros(self, micros: u64) -> EventTime {
EventTime::from_micros(micros.saturating_sub(self.epoch_nanos / 1_000))
}
fn event_time_now(self) -> EventTime {
self.event_time_nanos(ffi::monotonic_nanos())
}
fn event_time_nanos(self, nanos: u64) -> EventTime {
EventTime::from_duration(Duration::from_nanos(nanos.saturating_sub(self.epoch_nanos)))
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum ObjectKind {
Registry,
WmBase,
Output,
XdgOutput,
Surface,
XdgSurface,
XdgToplevel,
XdgPopup,
LayerSurface,
Decoration,
FractionalScale,
Seat,
Pointer,
Keyboard,
RelativePointer,
DataDevice,
DataOffer,
DataSource,
Ignored,
}
#[derive(Clone, Debug)]
enum RawEvent {
Global {
name: u32,
interface: String,
version: u32,
},
GlobalRemove {
name: u32,
},
Ping {
serial: u32,
},
OutputGeometry {
output: usize,
x: i32,
y: i32,
},
OutputMode {
output: usize,
flags: u32,
width: i32,
height: i32,
refresh: i32,
},
OutputScale {
output: usize,
factor: i32,
},
OutputName {
output: usize,
name: String,
},
OutputDone {
output: usize,
},
XdgOutputPosition {
xdg_output: usize,
x: i32,
y: i32,
},
XdgOutputSize {
xdg_output: usize,
width: i32,
height: i32,
},
SurfaceEnter {
surface: usize,
output: usize,
},
SurfaceLeave {
surface: usize,
output: usize,
},
XdgSurfaceConfigure {
xdg_surface: usize,
serial: u32,
},
ToplevelConfigure {
toplevel: usize,
width: i32,
height: i32,
states: Vec<u32>,
},
ToplevelClose {
toplevel: usize,
},
PopupConfigure {
popup: usize,
width: i32,
height: i32,
},
PopupDone {
popup: usize,
},
LayerConfigure {
layer_surface: usize,
serial: u32,
width: u32,
height: u32,
},
LayerClosed {
layer_surface: usize,
},
DecorationConfigure {
decoration: usize,
mode: u32,
},
PreferredScale {
object: usize,
scale: u32,
},
SeatCapabilities {
seat: usize,
capabilities: u32,
},
PointerEnter {
pointer: usize,
serial: u32,
surface: usize,
x: i32,
y: i32,
},
PointerLeave {
pointer: usize,
serial: u32,
surface: usize,
},
PointerMotion {
pointer: usize,
time: u32,
x: i32,
y: i32,
},
PointerButton {
pointer: usize,
serial: u32,
time: u32,
button: u32,
state: u32,
},
PointerAxis {
pointer: usize,
time: u32,
axis: u32,
value: i32,
},
PointerAxisSource {
pointer: usize,
source: u32,
},
PointerAxisDiscrete {
pointer: usize,
axis: u32,
discrete: i32,
},
PointerAxisValue120 {
pointer: usize,
axis: u32,
value120: i32,
},
PointerFrame {
pointer: usize,
},
RelativeMotion {
relative: usize,
utime_hi: u32,
utime_lo: u32,
dx_unaccel: i32,
dy_unaccel: i32,
},
KeyboardKeymap {
keyboard: usize,
format: u32,
fd: i32,
size: u32,
},
KeyboardEnter {
keyboard: usize,
serial: u32,
surface: usize,
},
KeyboardLeave {
keyboard: usize,
serial: u32,
surface: usize,
},
KeyboardKey {
keyboard: usize,
serial: u32,
time: u32,
key: u32,
state: u32,
},
KeyboardModifiers {
keyboard: usize,
serial: u32,
depressed: u32,
latched: u32,
locked: u32,
group: u32,
},
KeyboardRepeatInfo {
keyboard: usize,
rate: i32,
delay: i32,
},
DataOffer {
device: usize,
offer: usize,
},
OfferMime {
offer: usize,
mime: String,
},
Selection {
device: usize,
offer: usize,
},
DragEnter {
device: usize,
serial: u32,
surface: usize,
x: i32,
y: i32,
offer: usize,
},
DragLeave {
device: usize,
},
DragMotion {
device: usize,
time: u32,
x: i32,
y: i32,
},
DragDrop {
device: usize,
},
SourceSend {
source: usize,
mime: String,
fd: i32,
},
SourceCancelled {
source: usize,
},
}
#[derive(Debug)]
struct Sink {
lib: &'static Lib,
root: *mut Sink,
objects: Vec<(usize, ObjectKind)>,
events: Vec<RawEvent>,
}
impl Sink {
fn boxed(lib: &'static Lib) -> *mut Sink {
let raw = Box::into_raw(Box::new(Self {
lib,
root: ptr::null_mut(),
objects: Vec::new(),
events: Vec::new(),
}));
unsafe { (*raw).root = raw };
raw
}
fn kind_of(&self, proxy: usize) -> Option<ObjectKind> {
self.objects
.iter()
.find(|(candidate, _)| *candidate == proxy)
.map(|(_, kind)| *kind)
}
fn forget(&mut self, proxy: usize) {
self.objects.retain(|(candidate, _)| *candidate != proxy);
}
fn watch(&mut self, proxy: *mut WlProxy, kind: ObjectKind) {
if proxy.is_null() {
return;
}
self.objects.push((proxy as usize, kind));
unsafe {
(self.lib.proxy_add_dispatcher)(proxy, dispatch, self.root.cast(), ptr::null_mut());
}
}
}
unsafe extern "C" fn dispatch(
user_data: *const c_void,
target: *mut c_void,
opcode: u32,
_message: *const WlMessage,
args: *mut WlArgument,
) -> c_int {
let sink = unsafe { &mut *user_data.cast::<Sink>().cast_mut() };
let proxy = target as usize;
let Some(kind) = sink.kind_of(proxy) else {
return 0;
};
let args: &WlArgument = unsafe { &*args.cast_const() };
match kind {
ObjectKind::Ignored => {}
ObjectKind::Registry => {
if let Some(event) = unsafe { wl_registry::decode_event(opcode, args) } {
sink.events.push(match event {
wl_registry::Event::Global {
name,
interface,
version,
} => RawEvent::Global {
name,
interface: interface.to_string_lossy().into_owned(),
version,
},
wl_registry::Event::GlobalRemove { name } => RawEvent::GlobalRemove { name },
});
}
}
ObjectKind::WmBase => {
if let Some(xdg_wm_base::Event::Ping { serial }) =
unsafe { xdg_wm_base::decode_event(opcode, args) }
{
sink.events.push(RawEvent::Ping { serial });
}
}
ObjectKind::Output => {
if let Some(event) = unsafe { wl_output::decode_event(opcode, args) } {
let raw = match event {
wl_output::Event::Geometry { x, y, .. } => Some(RawEvent::OutputGeometry {
output: proxy,
x,
y,
}),
wl_output::Event::Mode {
flags,
width,
height,
refresh,
} => Some(RawEvent::OutputMode {
output: proxy,
flags,
width,
height,
refresh,
}),
wl_output::Event::Scale { factor } => Some(RawEvent::OutputScale {
output: proxy,
factor,
}),
wl_output::Event::Name { name } => Some(RawEvent::OutputName {
output: proxy,
name: name.to_string_lossy().into_owned(),
}),
wl_output::Event::Done => Some(RawEvent::OutputDone { output: proxy }),
wl_output::Event::Description { .. } => None,
};
sink.events.extend(raw);
}
}
ObjectKind::XdgOutput => {
if let Some(event) = unsafe { zxdg_output_v1::decode_event(opcode, args) } {
let raw = match event {
zxdg_output_v1::Event::LogicalPosition { x, y } => {
Some(RawEvent::XdgOutputPosition {
xdg_output: proxy,
x,
y,
})
}
zxdg_output_v1::Event::LogicalSize { width, height } => {
Some(RawEvent::XdgOutputSize {
xdg_output: proxy,
width,
height,
})
}
_ => None,
};
sink.events.extend(raw);
}
}
ObjectKind::Surface => {
if let Some(event) = unsafe { wl_surface::decode_event(opcode, args) } {
let raw = match event {
wl_surface::Event::Enter { output } => Some(RawEvent::SurfaceEnter {
surface: proxy,
output: output as usize,
}),
wl_surface::Event::Leave { output } => Some(RawEvent::SurfaceLeave {
surface: proxy,
output: output as usize,
}),
_ => None,
};
sink.events.extend(raw);
}
}
ObjectKind::XdgSurface => {
if let Some(xdg_surface::Event::Configure { serial }) =
unsafe { xdg_surface::decode_event(opcode, args) }
{
sink.events.push(RawEvent::XdgSurfaceConfigure {
xdg_surface: proxy,
serial,
});
}
}
ObjectKind::XdgToplevel => {
if let Some(event) = unsafe { xdg_toplevel::decode_event(opcode, args) } {
let raw = match event {
xdg_toplevel::Event::Configure {
width,
height,
states,
} => Some(RawEvent::ToplevelConfigure {
toplevel: proxy,
width,
height,
states: decode_state_array(states),
}),
xdg_toplevel::Event::Close => Some(RawEvent::ToplevelClose { toplevel: proxy }),
_ => None,
};
sink.events.extend(raw);
}
}
ObjectKind::XdgPopup => {
if let Some(event) = unsafe { xdg_popup::decode_event(opcode, args) } {
let raw = match event {
xdg_popup::Event::Configure { width, height, .. } => {
Some(RawEvent::PopupConfigure {
popup: proxy,
width,
height,
})
}
xdg_popup::Event::PopupDone => Some(RawEvent::PopupDone { popup: proxy }),
_ => None,
};
sink.events.extend(raw);
}
}
ObjectKind::LayerSurface => {
if let Some(event) = unsafe { zwlr_layer_surface_v1::decode_event(opcode, args) } {
sink.events.push(match event {
zwlr_layer_surface_v1::Event::Configure {
serial,
width,
height,
} => RawEvent::LayerConfigure {
layer_surface: proxy,
serial,
width,
height,
},
zwlr_layer_surface_v1::Event::Closed => RawEvent::LayerClosed {
layer_surface: proxy,
},
});
}
}
ObjectKind::Decoration => {
if let Some(zxdg_toplevel_decoration_v1::Event::Configure { mode }) =
unsafe { zxdg_toplevel_decoration_v1::decode_event(opcode, args) }
{
sink.events.push(RawEvent::DecorationConfigure {
decoration: proxy,
mode,
});
}
}
ObjectKind::FractionalScale => {
if let Some(wp_fractional_scale_v1::Event::PreferredScale { scale }) =
unsafe { wp_fractional_scale_v1::decode_event(opcode, args) }
{
sink.events.push(RawEvent::PreferredScale {
object: proxy,
scale,
});
}
}
ObjectKind::Seat => {
if let Some(wl_seat::Event::Capabilities { capabilities }) =
unsafe { wl_seat::decode_event(opcode, args) }
{
sink.events.push(RawEvent::SeatCapabilities {
seat: proxy,
capabilities,
});
}
}
ObjectKind::Pointer => {
if let Some(event) = unsafe { wl_pointer::decode_event(opcode, args) } {
sink.events.extend(decode_pointer(proxy, event));
}
}
ObjectKind::Keyboard => {
if let Some(event) = unsafe { wl_keyboard::decode_event(opcode, args) } {
sink.events.extend(decode_keyboard(proxy, event));
}
}
ObjectKind::DataDevice => {
if let Some(event) = unsafe { wl_data_device::decode_event(opcode, args) } {
if let wl_data_device::Event::DataOffer { id } = event {
sink.watch(id, ObjectKind::DataOffer);
}
sink.events.extend(decode_data_device(proxy, event));
}
}
ObjectKind::DataOffer => {
if let Some(wl_data_offer::Event::Offer { mime_type }) =
unsafe { wl_data_offer::decode_event(opcode, args) }
{
sink.events.push(RawEvent::OfferMime {
offer: proxy,
mime: mime_type.to_string_lossy().into_owned(),
});
}
}
ObjectKind::DataSource => {
if let Some(event) = unsafe { wl_data_source::decode_event(opcode, args) } {
let raw = match event {
wl_data_source::Event::Send { mime_type, fd } => Some(RawEvent::SourceSend {
source: proxy,
mime: mime_type.to_string_lossy().into_owned(),
fd,
}),
wl_data_source::Event::Cancelled => {
Some(RawEvent::SourceCancelled { source: proxy })
}
_ => None,
};
sink.events.extend(raw);
}
}
ObjectKind::RelativePointer => {
if let Some(zwp_relative_pointer_v1::Event::RelativeMotion {
utime_hi,
utime_lo,
dx_unaccel,
dy_unaccel,
..
}) = unsafe { zwp_relative_pointer_v1::decode_event(opcode, args) }
{
sink.events.push(RawEvent::RelativeMotion {
relative: proxy,
utime_hi,
utime_lo,
dx_unaccel,
dy_unaccel,
});
}
}
}
0
}
fn decode_pointer(proxy: usize, event: wl_pointer::Event) -> Option<RawEvent> {
Some(match event {
wl_pointer::Event::Enter {
serial,
surface,
surface_x,
surface_y,
} => RawEvent::PointerEnter {
pointer: proxy,
serial,
surface: surface as usize,
x: surface_x,
y: surface_y,
},
wl_pointer::Event::Leave { serial, surface } => RawEvent::PointerLeave {
pointer: proxy,
serial,
surface: surface as usize,
},
wl_pointer::Event::Motion {
time,
surface_x,
surface_y,
} => RawEvent::PointerMotion {
pointer: proxy,
time,
x: surface_x,
y: surface_y,
},
wl_pointer::Event::Button {
serial,
time,
button,
state,
} => RawEvent::PointerButton {
pointer: proxy,
serial,
time,
button,
state,
},
wl_pointer::Event::Axis { time, axis, value } => RawEvent::PointerAxis {
pointer: proxy,
time,
axis,
value,
},
wl_pointer::Event::AxisSource { axis_source } => RawEvent::PointerAxisSource {
pointer: proxy,
source: axis_source,
},
wl_pointer::Event::AxisDiscrete { axis, discrete } => RawEvent::PointerAxisDiscrete {
pointer: proxy,
axis,
discrete,
},
wl_pointer::Event::AxisValue120 { axis, value120 } => RawEvent::PointerAxisValue120 {
pointer: proxy,
axis,
value120,
},
wl_pointer::Event::Frame => RawEvent::PointerFrame { pointer: proxy },
_ => return None,
})
}
fn decode_keyboard(proxy: usize, event: wl_keyboard::Event<'_>) -> Option<RawEvent> {
Some(match event {
wl_keyboard::Event::Keymap { format, fd, size } => RawEvent::KeyboardKeymap {
keyboard: proxy,
format,
fd,
size,
},
wl_keyboard::Event::Enter {
serial, surface, ..
} => RawEvent::KeyboardEnter {
keyboard: proxy,
serial,
surface: surface as usize,
},
wl_keyboard::Event::Leave { serial, surface } => RawEvent::KeyboardLeave {
keyboard: proxy,
serial,
surface: surface as usize,
},
wl_keyboard::Event::Key {
serial,
time,
key,
state,
} => RawEvent::KeyboardKey {
keyboard: proxy,
serial,
time,
key,
state,
},
wl_keyboard::Event::Modifiers {
serial,
mods_depressed,
mods_latched,
mods_locked,
group,
} => RawEvent::KeyboardModifiers {
keyboard: proxy,
serial,
depressed: mods_depressed,
latched: mods_latched,
locked: mods_locked,
group,
},
wl_keyboard::Event::RepeatInfo { rate, delay } => RawEvent::KeyboardRepeatInfo {
keyboard: proxy,
rate,
delay,
},
})
}
fn decode_data_device(proxy: usize, event: wl_data_device::Event) -> Option<RawEvent> {
Some(match event {
wl_data_device::Event::DataOffer { id } => RawEvent::DataOffer {
device: proxy,
offer: id as usize,
},
wl_data_device::Event::Selection { id } => RawEvent::Selection {
device: proxy,
offer: id as usize,
},
wl_data_device::Event::Enter {
serial,
surface,
x,
y,
id,
} => RawEvent::DragEnter {
device: proxy,
serial,
surface: surface as usize,
x,
y,
offer: id as usize,
},
wl_data_device::Event::Leave => RawEvent::DragLeave { device: proxy },
wl_data_device::Event::Motion { time, x, y } => RawEvent::DragMotion {
device: proxy,
time,
x,
y,
},
wl_data_device::Event::Drop => RawEvent::DragDrop { device: proxy },
})
}
fn decode_state_array(bytes: &[u8]) -> Vec<u32> {
bytes
.as_chunks::<4>()
.0
.iter()
.map(|chunk| u32::from_ne_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]))
.collect()
}
struct Conn {
lib: &'static Lib,
display: NonNull<WlDisplay>,
fd: c_int,
registry: *mut WlProxy,
compositor: *mut WlProxy,
wm_base: *mut WlProxy,
xdg_output_manager: *mut WlProxy,
decoration_manager: *mut WlProxy,
fractional_scale_manager: *mut WlProxy,
viewporter: *mut WlProxy,
relative_pointer_manager: *mut WlProxy,
pointer_constraints: *mut WlProxy,
data_device_manager: *mut WlProxy,
layer_shell: *mut WlProxy,
sink: *mut Sink,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum Singleton {
Compositor,
WmBase,
Viewporter,
FractionalScaleManager,
XdgOutputManager,
DecorationManager,
RelativePointerManager,
PointerConstraints,
DataDeviceManager,
LayerShell,
}
impl Singleton {
fn slot(self, conn: &mut Conn) -> &mut *mut WlProxy {
match self {
Self::Compositor => &mut conn.compositor,
Self::WmBase => &mut conn.wm_base,
Self::Viewporter => &mut conn.viewporter,
Self::FractionalScaleManager => &mut conn.fractional_scale_manager,
Self::XdgOutputManager => &mut conn.xdg_output_manager,
Self::DecorationManager => &mut conn.decoration_manager,
Self::RelativePointerManager => &mut conn.relative_pointer_manager,
Self::PointerConstraints => &mut conn.pointer_constraints,
Self::DataDeviceManager => &mut conn.data_device_manager,
Self::LayerShell => &mut conn.layer_shell,
}
}
fn interface(self) -> &'static CStr {
match self {
Self::Compositor => wl_compositor::NAME,
Self::WmBase => xdg_wm_base::NAME,
Self::Viewporter => wp_viewporter::NAME,
Self::FractionalScaleManager => wp_fractional_scale_manager_v1::NAME,
Self::XdgOutputManager => zxdg_output_manager_v1::NAME,
Self::DecorationManager => zxdg_decoration_manager_v1::NAME,
Self::RelativePointerManager => zwp_relative_pointer_manager_v1::NAME,
Self::PointerConstraints => zwp_pointer_constraints_v1::NAME,
Self::DataDeviceManager => wl_data_device_manager::NAME,
Self::LayerShell => zwlr_layer_shell_v1::NAME,
}
}
}
impl Conn {
fn sink(&mut self) -> &mut Sink {
unsafe { &mut *self.sink }
}
fn watch(&mut self, proxy: *mut WlProxy, kind: ObjectKind) {
self.sink().watch(proxy, kind);
}
fn destroy(&mut self, proxy: *mut WlProxy) {
if proxy.is_null() {
return;
}
self.sink().forget(proxy as usize);
unsafe { (self.lib.proxy_destroy)(proxy) };
}
unsafe fn release(&mut self, proxy: *mut WlProxy, destructor: unsafe fn(*mut WlProxy)) {
if proxy.is_null() {
return;
}
self.sink().forget(proxy as usize);
unsafe { destructor(proxy) };
}
unsafe fn release_since(
&mut self,
proxy: *mut WlProxy,
since: u32,
destructor: unsafe fn(*mut WlProxy),
) {
if proxy.is_null() {
return;
}
if unsafe { ffi::proxy_version(proxy) } >= since {
unsafe { self.release(proxy, destructor) };
} else {
self.destroy(proxy);
}
}
fn display_proxy(&self) -> *mut WlProxy {
ffi::display_as_proxy(self.display.as_ptr())
}
fn flush(&self) {
unsafe { (self.lib.display_flush)(self.display.as_ptr()) };
}
fn roundtrip(&self) -> Result<(), ShellError> {
if unsafe { (self.lib.display_roundtrip)(self.display.as_ptr()) } < 0 {
return Err(self.disconnected());
}
Ok(())
}
fn drain(&self, timeout_ms: c_int, aux: &[c_int]) -> Result<(), ShellError> {
let display = self.display.as_ptr();
unsafe {
if (self.lib.display_dispatch_pending)(display) < 0 {
return Err(self.disconnected());
}
while (self.lib.display_prepare_read)(display) != 0 {
if (self.lib.display_dispatch_pending)(display) < 0 {
return Err(self.disconnected());
}
}
if (self.lib.display_flush)(display) < 0 && timeout_ms != 0 {
(self.lib.display_cancel_read)(display);
return Ok(());
}
if !ffi::poll_readable_with(self.fd, aux, timeout_ms) {
(self.lib.display_cancel_read)(display);
} else if (self.lib.display_read_events)(display) < 0 {
return Err(self.disconnected());
}
if (self.lib.display_dispatch_pending)(display) < 0 {
return Err(self.disconnected());
}
}
Ok(())
}
fn disconnected(&self) -> ShellError {
let code = unsafe { (self.lib.display_get_error)(self.display.as_ptr()) };
ShellError::Disconnected {
backend: ShellBackend::Wayland,
detail: format!("errno {code}"),
}
}
}
#[derive(Debug)]
struct Output {
proxy: usize,
xdg: *mut WlProxy,
global: u32,
id: MonitorId,
x: i32,
y: i32,
width: i32,
height: i32,
refresh_millihertz: u32,
scale: i32,
logical_x: i32,
logical_y: i32,
logical_width: i32,
logical_height: i32,
name: String,
settled: bool,
}
impl Output {
fn scale_factor(&self) -> f64 {
if self.logical_width > 0 && self.width > 0 {
f64::from(self.width) / f64::from(self.logical_width)
} else {
f64::from(self.scale.max(1))
}
}
#[expect(
clippy::cast_possible_truncation,
reason = "a desktop coordinate that overflows i32 after scaling would \
need a monitor layout billions of pixels wide; the clamp \
keeps it defined regardless"
)]
fn info(&self, is_primary: bool) -> MonitorInfo {
let width = self.width.max(0).unsigned_abs();
let height = self.height.max(0).unsigned_abs();
let scale = self.scale_factor();
let (x, y) = if self.logical_width > 0 {
let to_pixels = |value: i32| (f64::from(value) * scale).round();
(
to_pixels(self.logical_x).clamp(f64::from(i32::MIN), f64::from(i32::MAX)) as i32,
to_pixels(self.logical_y).clamp(f64::from(i32::MIN), f64::from(i32::MAX)) as i32,
)
} else {
(self.x, self.y)
};
MonitorInfo {
id: self.id,
name: self.name.clone(),
bounds: PhysicalRect::new(x, y, width, height),
work_area: PhysicalRect::new(x, y, width, height),
scale_factor: scale,
refresh_millihertz: self.refresh_millihertz,
is_primary,
}
}
}
struct Seat {
proxy: *mut WlProxy,
global: u32,
device: DeviceId,
capabilities: u32,
pointer: *mut WlProxy,
keyboard: *mut WlProxy,
relative_pointer: *mut WlProxy,
data: Option<data::Device>,
enter_serial: u32,
last_serial: u32,
press_serial: u32,
pointer_focus: Option<WindowId>,
pointer_position: PhysicalPoint,
frame: PointerFrame,
keyboard_focus: Option<WindowId>,
keymap: Option<xkb::Keymap>,
modifiers: Modifiers,
held: Vec<KeyCode>,
repeat: Repeat,
}
impl core::fmt::Debug for Seat {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("Seat")
.field("device", &self.device)
.field("capabilities", &self.capabilities)
.field("keymap", &self.keymap.is_some())
.field("data_device", &self.data.is_some())
.finish()
}
}
#[derive(Clone, Copy, Debug, Default)]
struct PointerFrame {
time: u32,
motion: Option<(f64, f64)>,
raw: Option<(f64, f64)>,
raw_micros: Option<u64>,
value120: [f64; 2],
discrete: [f64; 2],
continuous: [f64; 2],
axis_source: Option<u32>,
has_axis: bool,
}
impl PointerFrame {
fn is_empty(&self) -> bool {
self.motion.is_none() && self.raw.is_none() && !self.has_axis
}
fn scroll(&self, scale: f64) -> Option<ScrollDelta> {
if !self.has_axis {
return None;
}
let detents = |raw: [f64; 2], divisor: f64| ScrollDelta::Lines {
#[expect(
clippy::cast_possible_truncation,
reason = "ScrollDelta::Lines is f32 by seam definition; a detent \
count is a small integer"
)]
x: (-raw[1] / divisor) as f32,
#[expect(clippy::cast_possible_truncation, reason = "as above")]
y: (-raw[0] / divisor) as f32,
};
if self.value120 != [0.0, 0.0] {
return Some(detents(self.value120, 120.0));
}
if self.discrete != [0.0, 0.0] {
return Some(detents(self.discrete, 1.0));
}
if self.continuous != [0.0, 0.0] {
return Some(ScrollDelta::Pixels {
x: -self.continuous[1] * scale,
y: -self.continuous[0] * scale,
});
}
None
}
}
#[derive(Clone, Copy, Debug, Default)]
struct Repeat {
rate: i32,
delay: i32,
key: Option<RepeatKey>,
}
#[derive(Clone, Copy, Debug)]
struct RepeatKey {
window: WindowId,
scancode: u32,
key_code: Option<KeyCode>,
keysym: Keysym,
next_nanos: u64,
interval_nanos: u64,
}
impl Repeat {
fn start(&mut self, now_nanos: u64, key: RepeatKeyRequest) {
if self.rate <= 0 {
self.key = None;
return;
}
let interval_nanos = 1_000_000_000 / u64::try_from(self.rate).unwrap_or(1).max(1);
let delay_nanos = u64::try_from(self.delay.max(0)).unwrap_or(0) * 1_000_000;
self.key = Some(RepeatKey {
window: key.window,
scancode: key.scancode,
key_code: key.key_code,
keysym: key.keysym,
next_nanos: now_nanos.saturating_add(delay_nanos),
interval_nanos,
});
}
fn stop(&mut self, scancode: u32) {
if self.key.is_some_and(|key| key.scancode == scancode) {
self.key = None;
}
}
fn due_at(&self) -> Option<u64> {
self.key.map(|key| key.next_nanos)
}
}
#[derive(Clone, Copy, Debug)]
struct RepeatKeyRequest {
window: WindowId,
scancode: u32,
key_code: Option<KeyCode>,
keysym: Keysym,
}
#[derive(Clone, Debug, Default)]
struct PendingConfigure {
size: Option<LogicalSize>,
states: Vec<u32>,
}
#[derive(Debug)]
struct WlWindow {
surface: *mut WlProxy,
xdg_surface: *mut WlProxy,
toplevel: *mut WlProxy,
layer_surface: *mut WlProxy,
popup: *mut WlProxy,
parent: Option<WindowId>,
placement: Option<crate::PopupPlacement>,
panel_monitor: Option<MonitorId>,
decoration: *mut WlProxy,
fractional_scale: *mut WlProxy,
viewport: *mut WlProxy,
title: String,
requested_size: LogicalSize,
requested_mode: DisplayMode,
requested_constraints: SizeConstraints,
resizable: bool,
configuration: Option<WindowConfiguration>,
pending: PendingConfigure,
outputs: Vec<usize>,
scale: i32,
preferred_scale: Option<u32>,
pointer_mode: PointerMode,
constraints: Vec<(usize, *mut WlProxy)>,
cursor: Option<Option<CursorIcon>>,
accept_drops: bool,
focused: bool,
visible: bool,
close_pending: bool,
}
impl WlWindow {
fn logical_size(&self) -> LogicalSize {
self.pending.size.unwrap_or_else(|| {
self.configuration.map_or(self.requested_size, |config| {
LogicalSize::new(
f64::from(config.size.width) / config.scale_factor,
f64::from(config.size.height) / config.scale_factor,
)
})
})
}
fn scale_factor(&self) -> f64 {
match self.preferred_scale {
Some(scale) if scale > 0 => f64::from(scale) / FRACTIONAL_SCALE_DENOMINATOR,
_ => f64::from(self.scale.max(1)),
}
}
}
pub struct WaylandShell {
conn: Conn,
windows: Pool<WlWindow>,
outputs: Vec<Output>,
seats: Vec<Seat>,
monitors: Vec<MonitorInfo>,
next_monitor_id: u32,
next_device_id: u32,
next_request_id: u32,
transfers: Vec<Transfer>,
writes: fd::Writes,
held: Vec<HeldRead>,
singletons: Vec<(u32, Singleton)>,
queue: VecDeque<ShellEvent>,
time: TimeBase,
caps: ShellCaps,
wake: Option<std::sync::Arc<crate::linux::wake::WakePipe>>,
lost: Option<String>,
}
impl core::fmt::Debug for WaylandShell {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("WaylandShell")
.field("windows", &self.windows.len())
.field("monitors", &self.monitors.len())
.field("seats", &self.seats.len())
.field(
"transfers",
&(self.transfers.len() + self.writes.len() + self.held.len()),
)
.field("queued_events", &self.queue.len())
.field("connected", &self.lost.is_none())
.finish()
}
}
impl WaylandShell {
pub fn open() -> Result<Self, ShellError> {
let lib = ffi::load().map_err(|detail| ShellError::Connect {
backend: ShellBackend::Wayland,
detail: detail.to_string(),
})?;
let display = unsafe { (lib.display_connect)(ptr::null()) };
let Some(display) = NonNull::new(display) else {
return Err(ShellError::Connect {
backend: ShellBackend::Wayland,
detail: match std::env::var("WAYLAND_DISPLAY") {
Ok(name) => format!("no compositor at WAYLAND_DISPLAY={name}"),
Err(_) => "WAYLAND_DISPLAY is not set".to_string(),
},
});
};
let fd = unsafe { (lib.display_get_fd)(display.as_ptr()) };
let conn = Conn {
lib,
display,
fd,
registry: ptr::null_mut(),
compositor: ptr::null_mut(),
wm_base: ptr::null_mut(),
xdg_output_manager: ptr::null_mut(),
decoration_manager: ptr::null_mut(),
fractional_scale_manager: ptr::null_mut(),
viewporter: ptr::null_mut(),
relative_pointer_manager: ptr::null_mut(),
pointer_constraints: ptr::null_mut(),
data_device_manager: ptr::null_mut(),
layer_shell: ptr::null_mut(),
sink: Sink::boxed(lib),
};
let mut shell = Self {
conn,
windows: Pool::new(),
outputs: Vec::new(),
seats: Vec::new(),
monitors: Vec::new(),
next_monitor_id: 1,
next_device_id: 1,
next_request_id: 1,
transfers: Vec::new(),
writes: fd::Writes::new(),
held: Vec::new(),
singletons: Vec::new(),
queue: VecDeque::new(),
time: TimeBase::now(),
caps: ShellCaps::empty(),
wake: None,
lost: None,
};
shell.conn.registry =
unsafe { protocol::wayland::wl_display::get_registry(shell.conn.display_proxy()) };
if shell.conn.registry.is_null() {
return Err(ShellError::Connect {
backend: ShellBackend::Wayland,
detail: "wl_display.get_registry failed".to_string(),
});
}
let registry = shell.conn.registry;
shell.conn.watch(registry, ObjectKind::Registry);
shell.conn.roundtrip()?;
shell.process_raw();
shell.conn.roundtrip()?;
shell.process_raw();
if shell.conn.compositor.is_null() {
return Err(ShellError::Connect {
backend: ShellBackend::Wayland,
detail: "the compositor does not advertise wl_compositor".to_string(),
});
}
if shell.conn.wm_base.is_null() {
return Err(ShellError::Connect {
backend: ShellBackend::Wayland,
detail: "the compositor does not advertise xdg_wm_base, so it cannot show \
a window"
.to_string(),
});
}
shell.caps = shell.latch_caps();
shell.queue.clear();
Ok(shell)
}
fn latch_caps(&self) -> ShellCaps {
let mut caps = ShellCaps::MULTI_WINDOW | ShellCaps::EVENT_WAIT;
caps.set(ShellCaps::HW_UPSCALE, !self.conn.viewporter.is_null());
caps.set(
ShellCaps::FRACTIONAL_SCALE,
!self.conn.fractional_scale_manager.is_null() && !self.conn.viewporter.is_null(),
);
caps.set(
ShellCaps::SERVER_DECORATIONS,
!self.conn.decoration_manager.is_null(),
);
caps.set(
ShellCaps::RAW_POINTER_MOTION,
!self.conn.relative_pointer_manager.is_null(),
);
let constraints = !self.conn.pointer_constraints.is_null();
caps.set(ShellCaps::POINTER_LOCK, constraints);
caps.set(ShellCaps::POINTER_CONFINE, constraints);
let data_device = !self.conn.data_device_manager.is_null();
caps.set(ShellCaps::CLIPBOARD, data_device);
caps.set(ShellCaps::DRAG_DROP, data_device);
caps.set(ShellCaps::TEXT_IME, xkb::available());
let layer_shell = !self.conn.layer_shell.is_null();
caps.set(ShellCaps::PANEL, layer_shell);
caps.set(ShellCaps::PANEL_RESERVE, layer_shell);
caps.insert(ShellCaps::TRANSPARENT);
caps.insert(ShellCaps::POPUP);
caps.set(
ShellCaps::POPUP_REPOSITION,
!self.conn.wm_base.is_null()
&& unsafe { ffi::proxy_version(self.conn.wm_base) } >= popup::REPOSITION_SINCE,
);
caps
}
pub fn align_time_base(&mut self, elapsed: Duration) {
self.time.align(elapsed);
}
fn window(&self, window: WindowId) -> Result<&WlWindow, ShellError> {
self.windows
.get(window.cast())
.ok_or_else(|| ShellError::invalid_window(window))
}
fn window_mut(&mut self, window: WindowId) -> Result<&mut WlWindow, ShellError> {
self.windows
.get_mut(window.cast())
.ok_or_else(|| ShellError::invalid_window(window))
}
fn unsupported(what: &'static str) -> ShellError {
ShellError::Unsupported {
backend: ShellBackend::Wayland,
what,
}
}
fn window_by_proxy(&self, proxy: usize, pick: fn(&WlWindow) -> usize) -> Option<WindowId> {
self.windows
.iter()
.find(|(_, window)| pick(window) == proxy)
.map(|(handle, _)| handle.cast())
}
fn seat_index(&self, proxy: usize, pick: fn(&Seat) -> usize) -> Option<usize> {
self.seats.iter().position(|seat| pick(seat) == proxy)
}
#[expect(
clippy::too_many_lines,
reason = "one arm per protocol; splitting it would only move the same \
list somewhere a reader has to follow to"
)]
fn bind_global(&mut self, name: u32, interface: &str, version: u32) {
let registry = self.conn.registry;
macro_rules! bind {
($field:ident, $module:path, $version:expr, $slot:expr) => {{
use $module as target;
if self.conn.$field.is_null() {
self.conn.$field = unsafe {
wl_registry::bind(registry, name, &target::INTERFACE, version.min($version))
};
if !self.conn.$field.is_null() {
self.singletons.push((name, $slot));
}
}
}};
}
match interface {
"wl_compositor" => bind!(
compositor,
wl_compositor,
COMPOSITOR_VERSION,
Singleton::Compositor
),
"xdg_wm_base" => {
let fresh = self.conn.wm_base.is_null();
bind!(wm_base, xdg_wm_base, WM_BASE_VERSION, Singleton::WmBase);
if fresh {
let proxy = self.conn.wm_base;
self.conn.watch(proxy, ObjectKind::WmBase);
}
}
"wp_viewporter" => bind!(viewporter, wp_viewporter, 1, Singleton::Viewporter),
"wp_fractional_scale_manager_v1" => {
bind!(
fractional_scale_manager,
wp_fractional_scale_manager_v1,
1,
Singleton::FractionalScaleManager
);
}
"zxdg_output_manager_v1" => {
bind!(
xdg_output_manager,
zxdg_output_manager_v1,
XDG_OUTPUT_VERSION,
Singleton::XdgOutputManager
);
}
"zxdg_decoration_manager_v1" => {
bind!(
decoration_manager,
zxdg_decoration_manager_v1,
DECORATION_VERSION,
Singleton::DecorationManager
);
}
"zwp_relative_pointer_manager_v1" => {
bind!(
relative_pointer_manager,
zwp_relative_pointer_manager_v1,
1,
Singleton::RelativePointerManager
);
}
"zwp_pointer_constraints_v1" => {
bind!(
pointer_constraints,
zwp_pointer_constraints_v1,
1,
Singleton::PointerConstraints
);
}
"wl_data_device_manager" => {
bind!(
data_device_manager,
wl_data_device_manager,
DATA_DEVICE_VERSION,
Singleton::DataDeviceManager
);
}
"zwlr_layer_shell_v1" => {
bind!(
layer_shell,
zwlr_layer_shell_v1,
layer::LAYER_SHELL_VERSION,
Singleton::LayerShell
);
}
"wl_output" => {
let proxy = unsafe {
wl_registry::bind(
registry,
name,
&wl_output::INTERFACE,
version.min(OUTPUT_VERSION),
)
};
if proxy.is_null() {
return;
}
self.conn.watch(proxy, ObjectKind::Output);
let id = MonitorId(self.next_monitor_id);
self.next_monitor_id += 1;
self.outputs.push(Output {
proxy: proxy as usize,
xdg: ptr::null_mut(),
global: name,
id,
x: 0,
y: 0,
width: 0,
height: 0,
refresh_millihertz: 0,
scale: 1,
logical_x: 0,
logical_y: 0,
logical_width: 0,
logical_height: 0,
name: format!("wl-output-{}", id.0),
settled: false,
});
}
"wl_seat" => {
let proxy = unsafe {
wl_registry::bind(
registry,
name,
&wl_seat::INTERFACE,
version.min(SEAT_VERSION),
)
};
if proxy.is_null() {
return;
}
self.conn.watch(proxy, ObjectKind::Seat);
let device = DeviceId(self.next_device_id);
self.next_device_id += 1;
self.seats.push(Seat {
proxy,
global: name,
device,
capabilities: 0,
pointer: ptr::null_mut(),
keyboard: ptr::null_mut(),
relative_pointer: ptr::null_mut(),
data: None,
enter_serial: 0,
last_serial: 0,
press_serial: 0,
pointer_focus: None,
pointer_position: PhysicalPoint::ORIGIN,
frame: PointerFrame::default(),
keyboard_focus: None,
keymap: None,
modifiers: Modifiers::empty(),
held: Vec::new(),
repeat: Repeat::default(),
});
}
_ => {}
}
}
fn withdraw_singleton(&mut self, name: u32) {
let Some(index) = self.singletons.iter().position(|(bound, _)| *bound == name) else {
return;
};
let (_, slot) = self.singletons.remove(index);
let proxy = core::mem::replace(slot.slot(&mut self.conn), ptr::null_mut());
match slot {
Singleton::Compositor | Singleton::DataDeviceManager => self.conn.destroy(proxy),
Singleton::WmBase => {
if self.windows.is_empty() {
unsafe { self.conn.release(proxy, xdg_wm_base::destroy) };
} else {
self.conn.destroy(proxy);
}
}
Singleton::Viewporter => unsafe { self.conn.release(proxy, wp_viewporter::destroy) },
Singleton::FractionalScaleManager => unsafe {
self.conn
.release(proxy, wp_fractional_scale_manager_v1::destroy);
},
Singleton::XdgOutputManager => unsafe {
self.conn.release(proxy, zxdg_output_manager_v1::destroy);
},
Singleton::DecorationManager => unsafe {
self.conn
.release(proxy, zxdg_decoration_manager_v1::destroy);
},
Singleton::RelativePointerManager => unsafe {
self.conn
.release(proxy, zwp_relative_pointer_manager_v1::destroy);
},
Singleton::PointerConstraints => unsafe {
self.conn
.release(proxy, zwp_pointer_constraints_v1::destroy);
},
Singleton::LayerShell => unsafe {
self.conn
.release_since(proxy, 3, zwlr_layer_shell_v1::destroy);
},
}
self.conn.flush();
crcbl_core::log::warn!(
"the compositor withdrew {}; it will be bound again if it is re-advertised",
slot.interface().to_string_lossy()
);
}
fn ensure_data_devices(&mut self) {
if self.conn.data_device_manager.is_null() {
return;
}
let manager = self.conn.data_device_manager;
for index in 0..self.seats.len() {
if self.seats[index].data.is_some() {
continue;
}
let seat = self.seats[index].proxy;
let device = unsafe { wl_data_device_manager::get_data_device(manager, seat) };
if device.is_null() {
continue;
}
self.conn.watch(device, ObjectKind::DataDevice);
self.seats[index].data = Some(data::Device::new(device));
}
self.conn.flush();
}
fn output_mut(&mut self, proxy: usize) -> Option<&mut Output> {
self.outputs.iter_mut().find(|output| output.proxy == proxy)
}
fn ensure_xdg_outputs(&mut self) {
if self.conn.xdg_output_manager.is_null() {
return;
}
let manager = self.conn.xdg_output_manager;
let pending: Vec<usize> = self
.outputs
.iter()
.filter(|output| output.xdg.is_null())
.map(|output| output.proxy)
.collect();
for proxy in pending {
let xdg =
unsafe { zxdg_output_manager_v1::get_xdg_output(manager, proxy as *mut WlProxy) };
self.conn.watch(xdg, ObjectKind::XdgOutput);
if let Some(output) = self.output_mut(proxy) {
output.xdg = xdg;
}
}
}
fn republish_monitors(&mut self) {
let monitors: Vec<MonitorInfo> = self
.outputs
.iter()
.filter(|output| output.settled)
.enumerate()
.map(|(index, output)| output.info(index == 0))
.collect();
if monitors != self.monitors {
for monitor in crate::monitor::queue_removed(&self.monitors, &monitors, &mut self.queue)
{
let panels: Vec<WindowId> = self
.windows
.iter()
.filter(|(_, window)| window.panel_monitor == Some(monitor))
.map(|(handle, _)| handle.cast())
.collect();
for panel in panels {
self.close_requested(panel);
}
}
self.monitors = monitors;
self.queue.push_back(ShellEvent::MonitorsChanged);
}
}
#[expect(
clippy::too_many_lines,
reason = "a flat routing table from wire events to handlers; every arm \
is one or two lines and splitting it hides the routing"
)]
fn process_raw(&mut self) {
let events = core::mem::take(&mut self.conn.sink().events);
let mut monitors_dirty = false;
for event in events {
match event {
RawEvent::Global {
name,
interface,
version,
} => self.bind_global(name, &interface, version),
RawEvent::GlobalRemove { name } => {
if let Some(index) =
self.outputs.iter().position(|output| output.global == name)
{
let output = self.outputs.remove(index);
let proxy = output.proxy as *mut WlProxy;
unsafe {
self.conn.release(output.xdg, zxdg_output_v1::destroy);
self.conn.release_since(proxy, 3, wl_output::release);
}
monitors_dirty = true;
}
if let Some(index) = self.seats.iter().position(|seat| seat.global == name) {
self.remove_seat(index);
}
self.withdraw_singleton(name);
}
RawEvent::Ping { serial } => {
if !self.conn.wm_base.is_null() {
unsafe { xdg_wm_base::pong(self.conn.wm_base, serial) };
self.conn.flush();
}
}
RawEvent::OutputGeometry { output, x, y } => {
if let Some(output) = self.output_mut(output) {
output.x = x;
output.y = y;
}
}
RawEvent::OutputMode {
output,
flags,
width,
height,
refresh,
} => {
if flags & wl_output::mode::CURRENT != 0
&& let Some(output) = self.output_mut(output)
{
output.width = width;
output.height = height;
output.refresh_millihertz = refresh.max(0).unsigned_abs();
}
}
RawEvent::OutputScale { output, factor } => {
if let Some(output) = self.output_mut(output) {
output.scale = factor.max(1);
}
}
RawEvent::OutputName { output, name } => {
if let Some(output) = self.output_mut(output) {
output.name = name;
}
}
RawEvent::OutputDone { output } => {
if let Some(output) = self.output_mut(output) {
output.settled = true;
monitors_dirty = true;
}
}
RawEvent::XdgOutputPosition { xdg_output, x, y } => {
if let Some(output) = self.output_by_xdg(xdg_output) {
output.logical_x = x;
output.logical_y = y;
monitors_dirty = true;
}
}
RawEvent::XdgOutputSize {
xdg_output,
width,
height,
} => {
if let Some(output) = self.output_by_xdg(xdg_output) {
output.logical_width = width;
output.logical_height = height;
monitors_dirty = true;
}
}
RawEvent::SurfaceEnter { surface, output } => {
self.surface_output_changed(surface, output, true);
}
RawEvent::SurfaceLeave { surface, output } => {
self.surface_output_changed(surface, output, false);
}
RawEvent::ToplevelConfigure {
toplevel,
width,
height,
states,
} => {
let Some(id) = self.window_by_proxy(toplevel, |w| w.toplevel as usize) else {
continue;
};
let Ok(window) = self.window_mut(id) else {
continue;
};
window.pending.size = (width > 0 && height > 0)
.then(|| LogicalSize::new(f64::from(width), f64::from(height)));
window.pending.states = states;
}
RawEvent::XdgSurfaceConfigure {
xdg_surface,
serial,
} => {
if let Some(id) = self.window_by_proxy(xdg_surface, |w| w.xdg_surface as usize)
{
self.apply_configure(id, serial);
}
}
RawEvent::LayerConfigure {
layer_surface,
serial,
width,
height,
} => self.layer_configured(layer_surface, serial, width, height),
RawEvent::PopupConfigure {
popup,
width,
height,
} => {
if let Some(id) = self.window_by_proxy(popup, |w| w.popup as usize)
&& let Ok(window) = self.window_mut(id)
{
window.pending.size = (width > 0 && height > 0)
.then(|| LogicalSize::new(f64::from(width), f64::from(height)));
}
}
RawEvent::PopupDone { popup } => {
if let Some(id) = self.window_by_proxy(popup, |w| w.popup as usize) {
self.close_requested(id);
}
}
RawEvent::ToplevelClose { toplevel } => {
if let Some(id) = self.window_by_proxy(toplevel, |w| w.toplevel as usize) {
self.close_requested(id);
}
}
RawEvent::LayerClosed { layer_surface } => {
if let Some(id) =
self.window_by_proxy(layer_surface, |w| w.layer_surface as usize)
{
self.close_requested(id);
}
}
RawEvent::DecorationConfigure { decoration, mode } => {
self.decoration_configured(decoration, mode);
}
RawEvent::PreferredScale { object, scale } => {
self.preferred_scale_changed(object, scale);
}
RawEvent::SeatCapabilities { seat, capabilities } => {
self.seat_capabilities_changed(seat, capabilities);
}
event @ (RawEvent::DataOffer { .. }
| RawEvent::OfferMime { .. }
| RawEvent::Selection { .. }
| RawEvent::DragEnter { .. }
| RawEvent::DragLeave { .. }
| RawEvent::DragMotion { .. }
| RawEvent::DragDrop { .. }
| RawEvent::SourceSend { .. }
| RawEvent::SourceCancelled { .. }) => self.process_data(event),
other => self.process_input(other),
}
}
self.ensure_xdg_outputs();
self.ensure_data_devices();
if monitors_dirty {
self.republish_monitors();
}
self.flush_pointer_frames();
}
fn close_requested(&mut self, id: WindowId) {
let Ok(window) = self.window_mut(id) else {
return;
};
if window.close_pending {
return;
}
window.close_pending = true;
self.queue
.push_back(ShellEvent::CloseRequested { window: id });
}
fn output_by_xdg(&mut self, xdg: usize) -> Option<&mut Output> {
self.outputs
.iter_mut()
.find(|output| output.xdg as usize == xdg)
}
fn monitor_of(&self, entered: &[usize]) -> Option<MonitorId> {
let [only] = entered else {
return None;
};
self.outputs
.iter()
.find(|output| output.proxy == *only)
.map(|output| output.id)
}
fn surface_output_changed(&mut self, surface: usize, output: usize, entered: bool) {
let Some(id) = self.window_by_proxy(surface, |w| w.surface as usize) else {
return;
};
let scales: Vec<(usize, i32)> = self
.outputs
.iter()
.map(|output| (output.proxy, output.scale))
.collect();
let Ok(window) = self.window_mut(id) else {
return;
};
if entered {
if !window.outputs.contains(&output) {
window.outputs.push(output);
}
} else {
window.outputs.retain(|candidate| *candidate != output);
}
let entered = window.outputs.clone();
let monitor = self.monitor_of(&entered);
if let Ok(window) = self.window_mut(id)
&& let Some(config) = window.configuration.as_mut()
&& config.mode.is_borderless()
{
config.mode = DisplayMode::Borderless { monitor };
}
let Ok(window) = self.window_mut(id) else {
return;
};
let scale = window
.outputs
.iter()
.filter_map(|proxy| {
scales
.iter()
.find(|(candidate, _)| candidate == proxy)
.map(|(_, scale)| *scale)
})
.max()
.unwrap_or(1)
.max(1);
if scale == window.scale {
return;
}
window.scale = scale;
if window.preferred_scale.is_none() {
let surface_proxy = window.surface;
unsafe { wl_surface::set_buffer_scale(surface_proxy, scale) };
}
self.rescale(id);
}
fn preferred_scale_changed(&mut self, object: usize, scale: u32) {
let Some(id) = self.window_by_proxy(object, |w| w.fractional_scale as usize) else {
return;
};
let Ok(window) = self.window_mut(id) else {
return;
};
if window.preferred_scale == Some(scale) {
return;
}
window.preferred_scale = Some(scale);
self.rescale(id);
}
fn rescale(&mut self, id: WindowId) {
let Ok(window) = self.window_mut(id) else {
return;
};
let Some(config) = window.configuration else {
return;
};
let scale_factor = window.scale_factor();
if (config.scale_factor - scale_factor).abs() < f64::EPSILON {
return;
}
let logical = window.logical_size();
let size = logical.to_physical(scale_factor);
window.configuration = Some(WindowConfiguration {
size,
scale_factor,
mode: config.mode,
});
let (surface, viewport) = (window.surface, window.viewport);
unsafe {
set_viewport(viewport, logical);
wl_surface::commit(surface);
}
self.queue.push_back(ShellEvent::ScaleFactorChanged {
window: id,
scale_factor,
size,
});
self.conn.flush();
}
fn apply_configure(&mut self, id: WindowId, serial: u32) {
let on = {
let Ok(window) = self.window(id) else {
return;
};
self.monitor_of(window.outputs.as_slice())
};
let Ok(window) = self.window_mut(id) else {
return;
};
let fullscreen = window
.pending
.states
.contains(&xdg_toplevel::state::FULLSCREEN);
let mode = if fullscreen {
DisplayMode::Borderless { monitor: on }
} else {
DisplayMode::Windowed
};
let scale_factor = window.scale_factor();
let logical = window.logical_size();
let size = window
.requested_constraints
.apply(logical.to_physical(scale_factor), scale_factor);
let config = WindowConfiguration {
size,
scale_factor,
mode,
};
let changed = window.configuration != Some(config);
let scale_changed = window
.configuration
.is_some_and(|previous| previous.scale_factor != scale_factor);
window.configuration = Some(config);
let focused = window
.pending
.states
.contains(&xdg_toplevel::state::ACTIVATED);
let xdg = window.xdg_surface;
let layer = window.layer_surface;
let toplevel = window.toplevel;
let surface = window.surface;
let viewport = window.viewport;
unsafe {
set_viewport(viewport, logical);
if layer.is_null() {
xdg_surface::ack_configure(xdg, serial);
} else {
zwlr_layer_surface_v1::ack_configure(layer, serial);
}
wl_surface::commit(surface);
}
self.conn.flush();
if changed {
if scale_changed {
self.queue.push_back(ShellEvent::ScaleFactorChanged {
window: id,
scale_factor,
size,
});
}
self.queue.push_back(ShellEvent::Resized {
window: id,
size,
scale_factor,
});
}
if !toplevel.is_null() {
self.set_focus(id, focused);
}
}
fn apply_cursor(&mut self, index: usize, window: WindowId) {
let Ok(state) = self.window(window) else {
return;
};
if state.cursor != Some(None) {
return;
}
let seat = &self.seats[index];
let (pointer, serial) = (seat.pointer, seat.enter_serial);
if pointer.is_null() {
return;
}
unsafe { wl_pointer::set_cursor(pointer, serial, ptr::null_mut(), 0, 0) };
self.conn.flush();
}
fn reapply_constraints(&mut self) {
let windows: Vec<WindowId> = self
.windows
.iter()
.filter(|(_, window)| window.pointer_mode.is_captured())
.map(|(handle, _)| handle.cast())
.collect();
for window in windows {
let mode = self
.window(window)
.map_or(PointerMode::Free, |w| w.pointer_mode);
self.rebuild_constraint(window, mode);
}
}
fn rebuild_constraint(&mut self, window: WindowId, mode: PointerMode) {
let existing = match self.window_mut(window) {
Ok(state) => {
state.pointer_mode = mode;
core::mem::take(&mut state.constraints)
}
Err(_) => return,
};
for (_, proxy) in existing {
unsafe { self.conn.release(proxy, constraint_destructor()) };
}
if mode == PointerMode::Free || self.conn.pointer_constraints.is_null() {
self.conn.flush();
return;
}
let Ok(surface) = self.window(window).map(|state| state.surface) else {
return;
};
let manager = self.conn.pointer_constraints;
let pointers: Vec<*mut WlProxy> = self
.seats
.iter()
.map(|seat| seat.pointer)
.filter(|pointer| !pointer.is_null())
.collect();
let mut created = Vec::with_capacity(pointers.len());
for pointer in pointers {
let proxy = unsafe {
match mode {
PointerMode::Locked => zwp_pointer_constraints_v1::lock_pointer(
manager,
surface,
pointer,
ptr::null_mut(),
zwp_pointer_constraints_v1::lifetime::PERSISTENT,
),
_ => zwp_pointer_constraints_v1::confine_pointer(
manager,
surface,
pointer,
ptr::null_mut(),
zwp_pointer_constraints_v1::lifetime::PERSISTENT,
),
}
};
self.conn.watch(proxy, ObjectKind::Ignored);
created.push((pointer as usize, proxy));
}
if let Ok(state) = self.window_mut(window) {
state.constraints = created;
}
self.conn.flush();
}
fn drop_window_constraints_for_seat(&mut self, index: usize) {
let pointer = self.seats[index].pointer as usize;
let windows: Vec<WindowId> = self
.windows
.iter()
.filter(|(_, window)| {
window
.constraints
.iter()
.any(|(owner, _)| *owner == pointer)
})
.map(|(handle, _)| handle.cast())
.collect();
for window in windows {
let doomed: Vec<*mut WlProxy> = match self.window_mut(window) {
Ok(state) => {
let (mine, theirs) = state
.constraints
.iter()
.partition::<Vec<_>, _>(|(owner, _)| *owner == pointer);
state.constraints = theirs;
mine.into_iter().map(|(_, proxy)| proxy).collect()
}
Err(_) => continue,
};
for proxy in doomed {
unsafe { self.conn.release(proxy, constraint_destructor()) };
}
}
self.conn.flush();
}
fn destroy_objects(&mut self, id: WindowId, window: &WlWindow) {
for (_, proxy) in &window.constraints {
unsafe { self.conn.release(*proxy, constraint_destructor()) };
}
unsafe {
self.conn
.release(window.decoration, zxdg_toplevel_decoration_v1::destroy);
self.conn
.release(window.fractional_scale, wp_fractional_scale_v1::destroy);
self.conn.release(window.viewport, wp_viewport::destroy);
}
unsafe {
self.conn
.release(window.layer_surface, zwlr_layer_surface_v1::destroy);
self.conn.release(window.popup, xdg_popup::destroy);
self.conn.release(window.toplevel, xdg_toplevel::destroy);
self.conn.release(window.xdg_surface, xdg_surface::destroy);
self.conn.release(window.surface, wl_surface::destroy);
}
let doomed: Vec<usize> = self
.transfers
.iter()
.filter(|transfer| transfer.delivery.window() == id)
.filter_map(|transfer| transfer.delivery.drop_offer())
.collect();
self.transfers
.retain(|transfer| transfer.delivery.window() != id);
for offer in doomed {
self.destroy_offer(&data::Offer::new(offer as *mut WlProxy));
}
let drags: Vec<data::Offer> = self
.seats
.iter_mut()
.filter_map(|seat| {
let device = seat.data.as_mut()?;
let over_this_window = device
.drag
.as_ref()
.is_some_and(|drag| drag.window == Some(id));
over_this_window.then(|| device.drag.take()).flatten()
})
.map(|drag| drag.offer)
.collect();
for offer in &drags {
self.destroy_offer(offer);
}
for seat in &mut self.seats {
if seat.pointer_focus == Some(id) {
seat.pointer_focus = None;
seat.frame = PointerFrame::default();
}
if seat.keyboard_focus == Some(id) {
seat.keyboard_focus = None;
seat.held.clear();
}
if seat.repeat.key.is_some_and(|key| key.window == id) {
seat.repeat.key = None;
}
}
self.conn.flush();
}
}
const fn constraint_destructor() -> unsafe fn(*mut WlProxy) {
zwp_locked_pointer_v1::destroy
}
const fn axis_slot(axis: u32) -> Option<usize> {
match axis {
wl_pointer::axis::VERTICAL_SCROLL => Some(0),
wl_pointer::axis::HORIZONTAL_SCROLL => Some(1),
_ => None,
}
}
#[expect(
clippy::cast_possible_truncation,
reason = "clamped into i32's range before the cast, and a NaN casts to 0"
)]
fn round_logical(value: f64) -> i32 {
value.round().clamp(0.0, f64::from(i32::MAX)) as i32
}
unsafe fn set_viewport(viewport: *mut WlProxy, logical: LogicalSize) {
if viewport.is_null() {
return;
}
let round = |value: f64| round_logical(value).max(1);
unsafe { wp_viewport::set_destination(viewport, round(logical.width), round(logical.height)) };
}
impl Drop for WaylandShell {
fn drop(&mut self) {
while !self.windows.is_empty() {
let leaves: Vec<WindowId> = self
.windows
.iter()
.filter(|(handle, _)| {
let id: WindowId = handle.cast();
!self
.windows
.iter()
.any(|(_, window)| window.parent == Some(id))
})
.map(|(handle, _)| handle.cast())
.collect();
for id in leaves {
if let Some(window) = self.windows.remove(id.cast()) {
self.destroy_objects(id, &window);
}
}
}
for index in (0..self.seats.len()).rev() {
self.remove_seat(index);
}
let outputs: Vec<(*mut WlProxy, *mut WlProxy)> = self
.outputs
.iter()
.map(|output| (output.xdg, output.proxy as *mut WlProxy))
.collect();
for (xdg, output) in outputs {
unsafe {
self.conn.release(xdg, zxdg_output_v1::destroy);
self.conn.release_since(output, 3, wl_output::release);
}
}
let globals = [
self.conn.layer_shell,
self.conn.data_device_manager,
self.conn.pointer_constraints,
self.conn.relative_pointer_manager,
self.conn.fractional_scale_manager,
self.conn.viewporter,
self.conn.decoration_manager,
self.conn.xdg_output_manager,
self.conn.wm_base,
self.conn.compositor,
self.conn.registry,
];
for global in globals {
self.conn.destroy(global);
}
self.conn.flush();
unsafe { (self.conn.lib.display_disconnect)(self.conn.display.as_ptr()) };
drop(unsafe { Box::from_raw(self.conn.sink) });
}
}
impl Shell for WaylandShell {
fn backend(&self) -> ShellBackend {
ShellBackend::Wayland
}
fn caps(&self) -> ShellCaps {
self.caps
}
fn create_window(&mut self, desc: &WindowDesc<'_>) -> Result<WindowId, ShellError> {
crate::role::check(desc, self.caps(), self.backend(), |monitor| {
self.monitor(monitor).is_some()
})?;
let panel = match desc.role {
WindowRole::Toplevel => None,
WindowRole::Panel(panel) => Some(panel),
WindowRole::Popup(popup) => return self.create_popup(desc, &popup),
};
if let DisplayMode::Borderless {
monitor: Some(monitor),
} = desc.mode
&& self.monitor(monitor).is_none()
{
return Err(ShellError::NoSuchMonitor(monitor.0));
}
let title = CString::new(desc.title)
.map_err(|_| ShellError::InvalidDescriptor("title contains a NUL byte".to_string()))?;
let app_id = CString::new(desc.app_id)
.map_err(|_| ShellError::InvalidDescriptor("app_id contains a NUL byte".to_string()))?;
let namespace = match panel {
Some(panel) if !panel.namespace.is_empty() => {
CString::new(panel.namespace).map_err(|_| {
ShellError::InvalidDescriptor("namespace contains a NUL byte".to_string())
})?
}
_ => app_id.clone(),
};
if self.conn.compositor.is_null() {
return Err(ShellError::WindowCreation(
"the compositor withdrew wl_compositor".to_string(),
));
}
if panel.is_none() && self.conn.wm_base.is_null() {
return Err(ShellError::WindowCreation(
"the compositor withdrew xdg_wm_base".to_string(),
));
}
let surface = unsafe { wl_compositor::create_surface(self.conn.compositor) };
if surface.is_null() {
return Err(ShellError::WindowCreation(
"wl_compositor.create_surface failed".to_string(),
));
}
let roles = match panel {
Some(panel) => self
.create_layer_surface(surface, &panel, &namespace, desc.size)
.map(|layer| (ptr::null_mut(), ptr::null_mut(), layer)),
None => unsafe { self.create_toplevel(surface) }
.map(|(xdg, toplevel)| (xdg, toplevel, ptr::null_mut())),
};
let (xdg, toplevel, layer_surface) = match roles {
Ok(roles) => roles,
Err(error) => {
unsafe { self.conn.release(surface, wl_surface::destroy) };
return Err(error);
}
};
self.conn.watch(surface, ObjectKind::Surface);
self.conn.watch(xdg, ObjectKind::XdgSurface);
self.conn.watch(toplevel, ObjectKind::XdgToplevel);
self.conn.watch(layer_surface, ObjectKind::LayerSurface);
let (decoration, fractional_scale, viewport) = unsafe {
(
if toplevel.is_null() {
ptr::null_mut()
} else {
optional(self.conn.decoration_manager, |manager| {
zxdg_decoration_manager_v1::get_toplevel_decoration(manager, toplevel)
})
},
optional(self.conn.fractional_scale_manager, |manager| {
wp_fractional_scale_manager_v1::get_fractional_scale(manager, surface)
}),
optional(self.conn.viewporter, |manager| {
wp_viewporter::get_viewport(manager, surface)
}),
)
};
self.conn.watch(decoration, ObjectKind::Decoration);
self.conn
.watch(fractional_scale, ObjectKind::FractionalScale);
self.conn.watch(viewport, ObjectKind::Ignored);
let fullscreen_output = match desc.mode {
DisplayMode::Windowed => None,
DisplayMode::Borderless { monitor } => Some(self.output_proxy_for(monitor)),
};
unsafe {
if !toplevel.is_null() {
xdg_toplevel::set_title(toplevel, &title);
xdg_toplevel::set_app_id(toplevel, &app_id);
apply_constraints(toplevel, desc.constraints, desc.resizable, desc.size);
if let Some(output) = fullscreen_output {
xdg_toplevel::set_fullscreen(toplevel, output);
}
}
if !decoration.is_null() {
zxdg_toplevel_decoration_v1::set_mode(
decoration,
zxdg_toplevel_decoration_v1::mode::SERVER_SIDE,
);
}
wl_surface::commit(surface);
}
self.conn.flush();
let handle: WindowId = self
.windows
.insert(WlWindow {
surface,
xdg_surface: xdg,
toplevel,
layer_surface,
popup: ptr::null_mut(),
parent: None,
placement: None,
panel_monitor: panel.and_then(|panel| panel.monitor),
decoration,
fractional_scale,
viewport,
title: desc.title.to_string(),
requested_size: desc.size,
requested_mode: desc.mode,
requested_constraints: desc.constraints,
resizable: desc.resizable,
configuration: None,
pending: PendingConfigure::default(),
outputs: Vec::new(),
scale: 1,
preferred_scale: None,
pointer_mode: PointerMode::Free,
constraints: Vec::new(),
cursor: None,
accept_drops: desc.accept_drops,
focused: false,
visible: desc.visible,
close_pending: false,
})
.cast();
Ok(handle)
}
fn destroy_window(&mut self, window: WindowId) -> Result<(), ShellError> {
self.window(window)?;
let popups: Vec<WindowId> = self
.windows
.iter()
.filter(|(_, state)| state.parent == Some(window))
.map(|(handle, _)| handle.cast())
.collect();
for popup in popups {
self.destroy_window(popup)?;
}
let removed = self
.windows
.remove(window.cast())
.ok_or_else(|| ShellError::invalid_window(window))?;
self.destroy_objects(window, &removed);
self.queue.push_back(ShellEvent::WindowDestroyed { window });
Ok(())
}
fn window_state(&self, window: WindowId) -> Result<WindowState, ShellError> {
let state = self.window(window)?;
Ok(WindowState {
configuration: state.configuration,
requested_mode: state.requested_mode,
requested_constraints: state.requested_constraints,
focused: state.focused,
visible: state.visible,
pointer_mode: state.pointer_mode,
close_pending: state.close_pending,
})
}
fn set_title(&mut self, window: WindowId, title: &str) -> Result<(), ShellError> {
let encoded = CString::new(title)
.map_err(|_| ShellError::InvalidDescriptor("title contains a NUL byte".to_string()))?;
let state = self.window_mut(window)?;
state.title = title.to_string();
let toplevel = state.toplevel;
if toplevel.is_null() {
return Ok(());
}
unsafe { xdg_toplevel::set_title(toplevel, &encoded) };
self.conn.flush();
Ok(())
}
fn set_visible(&mut self, window: WindowId, visible: bool) -> Result<(), ShellError> {
self.window_mut(window)?.visible = visible;
Ok(())
}
fn set_mode(&mut self, window: WindowId, mode: DisplayMode) -> Result<(), ShellError> {
let output = match mode {
DisplayMode::Windowed => None,
DisplayMode::Borderless { monitor } => {
if let Some(id) = monitor
&& self.monitor(id).is_none()
{
return Err(ShellError::NoSuchMonitor(id.0));
}
Some(self.output_proxy_for(monitor))
}
};
let state = self.window_mut(window)?;
state.requested_mode = mode;
let toplevel = state.toplevel;
let surface = state.surface;
if toplevel.is_null() {
return Ok(());
}
unsafe {
match output {
Some(output) => xdg_toplevel::set_fullscreen(toplevel, output),
None => xdg_toplevel::unset_fullscreen(toplevel),
}
wl_surface::commit(surface);
}
self.conn.flush();
Ok(())
}
fn set_constraints(
&mut self,
window: WindowId,
constraints: SizeConstraints,
) -> Result<(), ShellError> {
let state = self.window_mut(window)?;
state.requested_constraints = constraints;
let toplevel = state.toplevel;
let surface = state.surface;
let requested = state.requested_size;
let resizable = state.resizable;
if toplevel.is_null() {
return Ok(());
}
unsafe {
apply_constraints(toplevel, constraints, resizable, requested);
wl_surface::commit(surface);
}
self.conn.flush();
Ok(())
}
fn reposition_popup(
&mut self,
popup: WindowId,
anchor: crate::LogicalRect,
size: LogicalSize,
) -> Result<(), ShellError> {
self.move_popup(popup, anchor, size)
}
fn monitors(&self) -> &[MonitorInfo] {
&self.monitors
}
fn pump(&mut self, sink: &mut dyn FnMut(ShellEvent)) {
self.keep_alive();
for _ in 0..self.queue.len() {
let Some(event) = self.queue.pop_front() else {
break;
};
sink(event);
}
}
fn keep_alive(&mut self) {
if self.lost.is_none()
&& let Err(error) = self.conn.drain(0, &[])
{
crcbl_core::log::error!("wayland connection lost: {error}");
self.lost = Some(error.to_string());
}
self.process_raw();
self.service_transfers();
self.resolve_held_reads();
self.drive_repeats();
}
fn wait_events(&mut self, timeout: Option<Duration>) {
if self.lost.is_some() {
return;
}
let mut deadline = match (timeout, self.next_repeat_in()) {
(Some(timeout), Some(repeat)) => Some(timeout.min(repeat)),
(Some(timeout), None) => Some(timeout),
(None, repeat) => repeat,
};
let mut aux = self.transfer_fds();
if !aux.is_empty() || !self.writes.is_empty() || !self.held.is_empty() {
deadline = Some(deadline.map_or(TRANSFER_POLL, |deadline| deadline.min(TRANSFER_POLL)));
}
let timeout_ms = deadline.map_or(-1, |deadline| {
c_int::try_from(deadline.as_millis()).unwrap_or(c_int::MAX)
});
if let Some(wake) = &self.wake {
aux.push(wake.fd());
}
if let Err(error) = self.conn.drain(timeout_ms, &aux) {
crcbl_core::log::error!("wayland connection lost: {error}");
self.lost = Some(error.to_string());
}
if let Some(wake) = &self.wake {
wake.clear();
}
}
fn waker(&mut self) -> Option<crate::ShellWaker> {
crate::waker::lazy_waker(&mut self.wake, "Wayland", crate::linux::wake::WakePipe::new)
}
fn surface_target(&self, window: WindowId) -> Result<SurfaceTarget, ShellError> {
let state = self.window(window)?;
let surface = NonNull::new(state.surface.cast::<c_void>())
.ok_or_else(|| ShellError::invalid_window(window))?;
Ok(SurfaceTarget::Wayland {
display: self.conn.display.cast::<c_void>(),
surface,
})
}
fn set_pointer_mode(&mut self, window: WindowId, mode: PointerMode) -> Result<(), ShellError> {
self.window(window)?;
if !self.caps.contains(mode.required_cap()) {
return Err(Self::unsupported(mode.as_str()));
}
self.rebuild_constraint(window, mode);
Ok(())
}
fn set_cursor(
&mut self,
window: WindowId,
cursor: Option<CursorIcon>,
) -> Result<(), ShellError> {
self.window_mut(window)?.cursor = Some(cursor);
let focused: Vec<usize> = (0..self.seats.len())
.filter(|index| self.seats[*index].pointer_focus == Some(window))
.collect();
for index in focused {
self.apply_cursor(index, window);
}
Ok(())
}
fn warp_pointer(
&mut self,
window: WindowId,
position: PhysicalPoint,
) -> Result<(), ShellError> {
let _ = position;
self.window(window)?;
Err(Self::unsupported("pointer warp"))
}
fn reply_close_request(
&mut self,
window: WindowId,
reply: CloseReply,
) -> Result<(), ShellError> {
let state = self.window_mut(window)?;
if !state.close_pending {
return Err(ShellError::NoPendingCloseRequest { window });
}
state.close_pending = false;
match reply {
CloseReply::Close => self.destroy_window(window),
CloseReply::Keep => Ok(()),
}
}
fn clipboard_offer(
&mut self,
window: WindowId,
offers: &[ClipboardOffer<'_>],
) -> Result<(), ShellError> {
self.window(window)?;
if !self.caps.contains(ShellCaps::CLIPBOARD) {
return Err(Self::unsupported("clipboard"));
}
if self.conn.data_device_manager.is_null() {
return Err(ShellError::Backend(
"the compositor withdrew wl_data_device_manager".to_string(),
));
}
let index = *self
.seats_for_window(window)
.first()
.ok_or_else(|| ShellError::Backend("no seat has a wl_data_device".to_string()))?;
let previous = self.seats[index]
.data
.as_mut()
.and_then(|device| device.source.take());
let serial = self.seats[index].last_serial;
let mut published = None;
if !offers.is_empty() {
if serial == 0 {
if let Some(source) = previous.as_ref() {
unsafe { self.conn.release(source.proxy, wl_data_source::destroy) };
}
if let Some(device) = self.seats[index].data.as_mut() {
device.source = None;
}
return Err(ShellError::NeedsUserInteraction {
backend: ShellBackend::Wayland,
what: "claiming the clipboard",
});
}
let manager = self.conn.data_device_manager;
let source = unsafe { wl_data_device_manager::create_data_source(manager) };
if source.is_null() {
if let Some(device) = self.seats[index].data.as_mut() {
device.source = previous;
}
return Err(ShellError::Backend(
"wl_data_device_manager.create_data_source failed".to_string(),
));
}
self.conn.watch(source, ObjectKind::DataSource);
let mut payload = Vec::with_capacity(offers.len());
for offer in offers {
let Ok(mime) = CString::new(offer.mime.as_str()) else {
continue;
};
unsafe { wl_data_source::offer(source, &mime) };
payload.push((offer.mime.as_str().to_string(), offer.bytes.to_vec()));
}
published = Some(data::Source {
proxy: source,
payload,
});
}
let device = self.seats[index]
.data
.as_ref()
.map_or(ptr::null_mut(), |device| device.proxy);
let source = published
.as_ref()
.map_or(ptr::null_mut(), |source| source.proxy);
unsafe { wl_data_device::set_selection(device, source, serial) };
self.conn.flush();
if let Some(previous) = previous.as_ref() {
unsafe { self.conn.release(previous.proxy, wl_data_source::destroy) };
}
if let Some(device) = self.seats[index].data.as_mut() {
device.source = published;
device.selection_seen = false;
}
Ok(())
}
fn clipboard_request(
&mut self,
window: WindowId,
mime: MimeType,
) -> Result<ClipboardRequestId, ShellError> {
self.window(window)?;
if !self.caps.contains(ShellCaps::CLIPBOARD) {
return Err(Self::unsupported("clipboard"));
}
let request = ClipboardRequestId(self.next_request_id);
self.next_request_id += 1;
match self.resolve_selection(window, mime) {
Resolution::Ready { offer, spelling } => {
let delivery = Delivery::Clipboard {
window,
request,
mime: ReceivedMime::new(&spelling),
};
if let Err(error) = self.start_receive(offer, &spelling, delivery) {
crcbl_core::log::warn!("clipboard read could not be started: {error}");
self.answer_read(
window,
request,
ReceivedMime::new(&spelling),
ClipboardContent::Unavailable,
);
}
}
Resolution::Empty => self.answer_read(
window,
request,
ReceivedMime::from(mime),
ClipboardContent::Empty,
),
Resolution::Unknown => self.held.push(HeldRead {
window,
request,
mime,
deadline_nanos: ffi::monotonic_nanos()
.saturating_add(u64::try_from(fd::TIMEOUT.as_nanos()).unwrap_or(u64::MAX)),
}),
}
Ok(request)
}
fn clipboard_readable(&self, window: WindowId) -> bool {
self.caps.contains(ShellCaps::CLIPBOARD)
&& self.window(window).is_ok()
&& self.seats_for_window(window).into_iter().any(|index| {
self.seats[index]
.data
.as_ref()
.is_some_and(|device| device.selection_seen)
})
}
fn align_event_clock(&mut self, elapsed: Duration) {
self.align_time_base(elapsed);
}
}
impl WaylandShell {
unsafe fn create_toplevel(
&mut self,
surface: *mut WlProxy,
) -> Result<(*mut WlProxy, *mut WlProxy), ShellError> {
unsafe {
let xdg = xdg_wm_base::get_xdg_surface(self.conn.wm_base, surface);
if xdg.is_null() {
return Err(ShellError::WindowCreation(
"xdg_wm_base.get_xdg_surface failed".to_string(),
));
}
let toplevel = xdg_surface::get_toplevel(xdg);
if toplevel.is_null() {
self.conn.release(xdg, xdg_surface::destroy);
return Err(ShellError::WindowCreation(
"xdg_surface.get_toplevel failed".to_string(),
));
}
Ok((xdg, toplevel))
}
}
fn output_proxy_for(&self, monitor: Option<MonitorId>) -> *mut WlProxy {
let Some(id) = monitor else {
return ptr::null_mut();
};
self.outputs
.iter()
.find(|output| output.id == id)
.map_or(ptr::null_mut(), |output| output.proxy as *mut WlProxy)
}
fn decoration_configured(&mut self, decoration: usize, mode: u32) {
let Some(window) = self.window_by_proxy(decoration, |state| state.decoration as usize)
else {
return;
};
if mode == zxdg_toplevel_decoration_v1::mode::SERVER_SIDE {
crcbl_core::log::debug!("{window:?}: the compositor draws this window's decorations");
} else {
crcbl_core::log::warn!(
"{window:?}: the compositor refused server-side decorations \
(xdg-decoration mode {mode}); this window has no title bar until \
the UI layer can draw one"
);
}
}
}
unsafe fn optional(
manager: *mut WlProxy,
create: impl FnOnce(*mut WlProxy) -> *mut WlProxy,
) -> *mut WlProxy {
if manager.is_null() {
return ptr::null_mut();
}
create(manager)
}
unsafe fn apply_constraints(
toplevel: *mut WlProxy,
constraints: SizeConstraints,
resizable: bool,
requested: LogicalSize,
) {
let to_pair = |size: Option<LogicalSize>| {
size.map_or((0, 0), |size| {
(round_logical(size.width), round_logical(size.height))
})
};
let (min, max) = if resizable {
(to_pair(constraints.min), to_pair(constraints.max))
} else {
let fixed = to_pair(Some(requested));
(fixed, fixed)
};
unsafe {
xdg_toplevel::set_min_size(toplevel, min.0, min.1);
xdg_toplevel::set_max_size(toplevel, max.0, max.1);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::PhysicalSize;
fn window_id() -> WindowId {
let mut pool: Pool<u8> = Pool::new();
pool.insert(0).cast()
}
#[test]
fn wayland_timestamps_rebase_onto_the_engine_epoch() {
let epoch = 95_000_000_000; let now = 101_250_000_000;
#[expect(
clippy::cast_possible_truncation,
reason = "the value fits by construction"
)]
let stamp = (now / 1_000_000) as u32;
assert_eq!(
TimeBase::rebase(epoch, now, stamp),
EventTime::from_millis(6_250),
"6.25s after the engine epoch, not 101s after boot"
);
}
#[test]
fn a_wrapped_timestamp_is_widened_towards_now_not_forwarded_raw() {
const WRAP: u64 = 1 << 32;
let now_millis = WRAP + 10;
let now = now_millis * 1_000_000;
let epoch = 0;
assert_eq!(
TimeBase::rebase(epoch, now, 5),
EventTime::from_millis(WRAP + 5),
"the event is 5ms after the wrap, not 5ms after boot"
);
let now_millis = WRAP + 3;
assert_eq!(
TimeBase::rebase(epoch, now_millis * 1_000_000, u32::MAX),
EventTime::from_millis(WRAP - 1),
);
}
#[test]
fn a_timestamp_ahead_of_now_before_the_first_wrap_does_not_underflow() {
let now = 60_000_000_000; assert_eq!(
TimeBase::rebase(0, now, u32::MAX),
EventTime::from_millis(u64::from(u32::MAX)),
"the only reconstruction that exists is the raw value"
);
assert_eq!(
TimeBase::rebase(0, now, 60_001),
EventTime::from_millis(60_001)
);
}
#[test]
fn timestamps_never_go_negative_when_an_event_predates_the_epoch() {
assert_eq!(
TimeBase::rebase(10_000_000_000, 10_000_000_000, 9_000),
EventTime::ZERO
);
}
#[test]
fn events_the_protocol_gives_no_time_for_are_stamped_now_not_zero() {
let base = TimeBase {
epoch_nanos: ffi::monotonic_nanos().saturating_sub(3_000_000_000),
};
let stamped = base.event_time_now();
assert!(
stamped >= EventTime::from_millis(2_900),
"an event with no compositor time is stamped now, got {stamped:?}"
);
assert_ne!(stamped, EventTime::ZERO);
}
#[test]
fn the_microsecond_clock_needs_no_wrap_handling_and_keeps_its_precision() {
let base = TimeBase {
epoch_nanos: 5_000_000_000,
};
assert_eq!(
base.event_time_micros(5_000_500),
EventTime::from_micros(500),
"half a millisecond after the epoch, not rounded to zero"
);
assert_eq!(base.event_time_micros(1), EventTime::ZERO);
}
#[test]
fn aligning_the_time_base_moves_the_epoch_backwards() {
let mut base = TimeBase::now();
let before = base.epoch_nanos;
base.align(Duration::from_secs(2));
assert!(
base.epoch_nanos <= before,
"an epoch two seconds ago is earlier than one taken now"
);
assert!(
before.saturating_sub(base.epoch_nanos) >= 1_900_000_000,
"the shift is the elapsed time, give or take the call overhead"
);
}
#[test]
fn repeats_start_after_the_delay_and_then_run_at_the_rate() {
let mut repeat = Repeat {
rate: 25,
delay: 600,
key: None,
};
let now = 1_000_000_000;
repeat.start(
now,
RepeatKeyRequest {
window: window_id(),
scancode: 30,
key_code: Some(KeyCode::KeyA),
keysym: Keysym::from_char('a'),
},
);
let key = repeat.key.expect("repeat armed");
assert_eq!(
key.next_nanos - now,
600_000_000,
"the first repeat waits the whole delay"
);
assert_eq!(key.interval_nanos, 40_000_000, "25 Hz is 40ms apart");
repeat.stop(31);
assert!(repeat.key.is_some());
repeat.stop(30);
assert!(repeat.key.is_none());
}
#[test]
fn a_zero_rate_disables_repeat_entirely() {
let mut repeat = Repeat {
rate: 0,
delay: 300,
key: None,
};
repeat.start(
0,
RepeatKeyRequest {
window: window_id(),
scancode: 30,
key_code: Some(KeyCode::KeyA),
keysym: Keysym::NONE,
},
);
assert!(repeat.key.is_none());
assert_eq!(repeat.due_at(), None);
}
#[test]
fn value120_accumulates_into_detents_with_the_engine_sign_convention() {
let mut frame = PointerFrame {
has_axis: true,
..PointerFrame::default()
};
frame.value120[0] = -120.0;
assert_eq!(
frame.scroll(1.0),
Some(ScrollDelta::Lines { x: 0.0, y: 1.0 })
);
let mut frame = PointerFrame {
has_axis: true,
..PointerFrame::default()
};
frame.value120[0] = -30.0;
frame.value120[0] += -30.0;
assert_eq!(
frame.scroll(1.0),
Some(ScrollDelta::Lines { x: 0.0, y: 0.5 }),
"two eighth-turns are a quarter of a detent, not two detents"
);
let mut frame = PointerFrame {
has_axis: true,
..PointerFrame::default()
};
frame.value120[1] = 120.0;
assert_eq!(
frame.scroll(1.0),
Some(ScrollDelta::Lines { x: -1.0, y: 0.0 })
);
}
#[test]
fn a_wheel_prefers_detents_and_a_touchpad_gets_pixels() {
let mut frame = PointerFrame {
has_axis: true,
axis_source: Some(wl_pointer::axis_source::WHEEL),
..PointerFrame::default()
};
frame.value120[0] = -120.0;
frame.continuous[0] = -15.0;
assert_eq!(
frame.scroll(1.0),
Some(ScrollDelta::Lines { x: 0.0, y: 1.0 })
);
let frame = PointerFrame {
has_axis: true,
axis_source: Some(wl_pointer::axis_source::FINGER),
continuous: [-13.0, 0.0],
..PointerFrame::default()
};
assert_eq!(
frame.scroll(2.0),
Some(ScrollDelta::Pixels { x: 0.0, y: 26.0 })
);
let frame = PointerFrame {
has_axis: true,
discrete: [1.0, 0.0],
..PointerFrame::default()
};
assert_eq!(
frame.scroll(1.0),
Some(ScrollDelta::Lines { x: 0.0, y: -1.0 })
);
}
#[test]
fn an_axis_frame_with_no_movement_produces_no_wheel_event() {
let frame = PointerFrame {
has_axis: true,
..PointerFrame::default()
};
assert_eq!(frame.scroll(1.0), None);
assert!(PointerFrame::default().is_empty());
assert_eq!(PointerFrame::default().scroll(1.0), None);
}
#[test]
fn a_frame_carrying_only_relative_motion_is_not_empty() {
let frame = PointerFrame {
raw: Some((3.0, -2.0)),
..PointerFrame::default()
};
assert!(!frame.is_empty());
}
#[test]
fn axis_slots_are_vertical_then_horizontal() {
assert_eq!(axis_slot(wl_pointer::axis::VERTICAL_SCROLL), Some(0));
assert_eq!(axis_slot(wl_pointer::axis::HORIZONTAL_SCROLL), Some(1));
assert_eq!(axis_slot(7), None, "an axis a later version might add");
}
#[test]
fn an_outputs_scale_comes_from_the_logical_size_not_from_wl_output_scale() {
let mut output = Output {
proxy: 1,
xdg: ptr::null_mut(),
global: 1,
id: MonitorId(1),
x: 0,
y: 0,
width: 1920,
height: 1080,
refresh_millihertz: 60_000,
scale: 2,
logical_x: 0,
logical_y: 0,
logical_width: 1280,
logical_height: 720,
name: "HEADLESS-1".to_string(),
settled: true,
};
assert!((output.scale_factor() - 1.5).abs() < 1e-9);
let info = output.info(true);
assert_eq!(
info.size(),
PhysicalSize::new(1920, 1080),
"the size stays the mode, which is what a swapchain is built from"
);
output.logical_width = 0;
assert!((output.scale_factor() - 2.0).abs() < f64::EPSILON);
}
#[test]
fn a_monitors_position_is_scaled_out_of_the_logical_desktop_space() {
let output = Output {
proxy: 2,
xdg: ptr::null_mut(),
global: 2,
id: MonitorId(2),
x: 1280,
y: 0,
width: 3840,
height: 2160,
refresh_millihertz: 60_000,
scale: 2,
logical_x: 1280,
logical_y: 0,
logical_width: 1920,
logical_height: 1080,
name: "DP-2".to_string(),
settled: true,
};
assert_eq!(output.info(false).bounds.x, 2560);
assert_eq!(output.info(false).bounds.y, 0);
assert_eq!(output.info(false).work_area, output.info(false).bounds);
}
#[test]
fn a_windows_scale_prefers_the_fractional_one() {
let mut window = test_window();
window.scale = 2;
assert!((window.scale_factor() - 2.0).abs() < f64::EPSILON);
window.preferred_scale = Some(180);
assert!((window.scale_factor() - 1.5).abs() < 1e-9);
window.preferred_scale = Some(0);
assert!((window.scale_factor() - 2.0).abs() < f64::EPSILON);
}
#[test]
fn the_toplevel_state_array_decodes_as_native_endian_u32s() {
let mut bytes = Vec::new();
bytes.extend_from_slice(&xdg_toplevel::state::ACTIVATED.to_ne_bytes());
bytes.extend_from_slice(&xdg_toplevel::state::FULLSCREEN.to_ne_bytes());
assert_eq!(
decode_state_array(&bytes),
vec![
xdg_toplevel::state::ACTIVATED,
xdg_toplevel::state::FULLSCREEN
]
);
assert!(decode_state_array(&[]).is_empty());
assert_eq!(decode_state_array(&[1, 0, 0, 0, 9]).len(), 1);
}
#[test]
fn the_protocol_descriptors_carry_the_names_the_registry_advertises() {
assert_eq!(wl_compositor::NAME, c"wl_compositor");
assert_eq!(xdg_wm_base::NAME, c"xdg_wm_base");
assert_eq!(wl_output::NAME, c"wl_output");
assert_eq!(wl_seat::NAME, c"wl_seat");
assert_eq!(zxdg_output_manager_v1::NAME, c"zxdg_output_manager_v1");
assert_eq!(
zxdg_decoration_manager_v1::NAME,
c"zxdg_decoration_manager_v1"
);
assert_eq!(
wp_fractional_scale_manager_v1::NAME,
c"wp_fractional_scale_manager_v1"
);
assert_eq!(wp_viewporter::NAME, c"wp_viewporter");
assert_eq!(
zwp_relative_pointer_manager_v1::NAME,
c"zwp_relative_pointer_manager_v1"
);
assert_eq!(
zwp_pointer_constraints_v1::NAME,
c"zwp_pointer_constraints_v1"
);
assert_eq!(zwlr_layer_shell_v1::NAME, c"zwlr_layer_shell_v1");
assert_eq!(xdg_toplevel::state::FULLSCREEN, 2);
assert_eq!(wl_output::mode::CURRENT, 1);
assert_eq!(wl_seat::capability::POINTER, 1);
assert_eq!(wl_seat::capability::KEYBOARD, 2);
assert_eq!(wl_keyboard::keymap_format::XKB_V1, 1);
assert_eq!(zxdg_toplevel_decoration_v1::mode::SERVER_SIDE, 2);
}
#[test]
fn opening_without_a_compositor_fails_cleanly_rather_than_aborting() {
if std::env::var_os("WAYLAND_DISPLAY").is_some() {
return;
}
let error = WaylandShell::open().expect_err("no compositor");
assert!(
matches!(
error,
ShellError::Connect {
backend: ShellBackend::Wayland,
..
}
),
"{error}"
);
}
fn test_window() -> WlWindow {
WlWindow {
surface: ptr::null_mut(),
xdg_surface: ptr::null_mut(),
toplevel: ptr::null_mut(),
layer_surface: ptr::null_mut(),
popup: ptr::null_mut(),
parent: None,
placement: None,
panel_monitor: None,
decoration: ptr::null_mut(),
fractional_scale: ptr::null_mut(),
viewport: ptr::null_mut(),
title: String::new(),
requested_size: LogicalSize::new(640.0, 480.0),
requested_mode: DisplayMode::Windowed,
requested_constraints: SizeConstraints::default(),
resizable: true,
configuration: None,
pending: PendingConfigure::default(),
outputs: Vec::new(),
scale: 1,
preferred_scale: None,
pointer_mode: PointerMode::Free,
constraints: Vec::new(),
cursor: None,
accept_drops: false,
focused: false,
visible: true,
close_pending: false,
}
}
}