Windowing — Linux Wayland
Overview
WIP — xdg_popup for menus, text-input v3 IME, and KDE blur are wired but several edge cases (popup grabs, fractional scale, nested popups) still TODO. The Wayland backend is LinuxWindow::Wayland(WaylandWindow). Selection and the shared LinuxWindow enum are documented on the X11 page — this page covers what is Wayland-specific.
The struct is WaylandWindow. Notable differences from X11Window:
- All globals (
wl_compositor,wl_shm,wl_seat,xdg_wm_base,wl_subcompositor,org_kde_kwin_blur_manager,zwp_text_input_manager_v3) are owned as raw*mutpointers, bound from the registry on startup. - Wayland uses surface-local logical coordinates — there is no XY screen address. The window cannot ask for absolute position; the compositor decides where it ends up.
- A separate
wl_event_queueis allocated per window so dispatching one window's events does not block on another's. - Configure/ack handshake replaces ConfigureNotify: the compositor sends
xdg_surface::configure(serial)and the client must replyxdg_surface_ack_configure(serial)after applying the size. WaylandPopupis a separate struct fromWaylandWindowbecause xdg_popup is a different surface role from xdg_toplevel.
Loading libwayland-client + libwayland-egl
Wayland::new loads libwayland-client.so.0, libwayland-egl.so.1, and optionally libwayland-cursor.so.0 via the same Library / load_first_available machinery as X11.
Wayland's variadic wl_proxy_marshal_constructor / wl_proxy_marshal are stored as raw *const c_void and cast at the call site — Rust function pointers cannot represent C variadic signatures.
The protocol interfaces (wl_compositor_interface, xdg_wm_base_interface, etc.) are stored as wl_interface structs (not pointers) so they can be passed to wl_registry_bind by reference.
XKB (libxkbcommon.so.0) is loaded the same way and re-used from super::x11::dlopen::Xkb — the keyboard translation layer is identical between X11 and Wayland.
Render modes
enum RenderMode {
Gpu(gl::GlContext, GlFunctions),
Cpu(Option<CpuFallbackState>), // wl_shm pool + buffer + mmap fd
}
struct CpuFallbackState {
pool: *mut wl_shm_pool,
buffer: *mut wl_buffer,
data: *mut u8,
width: i32, height: i32, stride: i32,
fd: i32, // shm fd kept open until drop
damage_rects: Vec<LogicalRect>,
}
GPU mode uses EGL with wl_egl_window as the native window:
wl_egl_window_create(surface, w, h)produces a native handle.eglGetDisplay,eglInitialize,eglBindAPI(EGL_OPENGL_API),eglChooseConfig,eglCreateContext(the same EGL flow as X11 — the Wayland GL module mirrors the X11 one).eglCreateWindowSurface(display, config, wl_egl_window, null).- Frame:
eglSwapBuffers.
CPU fallback uses POSIX shared memory:
memfd_create("azul-shm", MFD_CLOEXEC)(withshm_open/O_TMPFILEfallbacks).ftruncate(fd, stride * height),mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0).wl_shm_create_pool(shm, fd, size)→wl_shm_pool_create_buffer(pool, 0, w, h, stride, WL_SHM_FORMAT_ARGB8888).- Frame:
cpurender → AzulPixmap → memcpy into mmap → wl_surface_attach → wl_surface_damage_per_rect → wl_surface_commit.
The damage_rects are kept on CpuFallbackState so the next commit can call wl_surface_damage for each region instead of a full surface — Wayland compositors use this to skip recompositing unchanged tiles.
GPU mode also tracks gpu_damage_rects on the window for the same reason.
Event loop
Wayland is compositor-driven: client never reads input directly. The compositor sends events on the wire, and wl_display_dispatch_queue runs the registered listener callbacks (wl_pointer_listener, wl_keyboard_listener, xdg_surface_listener, etc.) which mutate WaylandWindow.current_window_state.
WaylandWindow::poll_event drains the queue:
wl_display_dispatch_queue_pending— non-blocking dispatch.- After dispatch, call the unified
process_window_events()so the state diff betweenprevious_window_stateandcurrent_window_statefires the correct callbacks.
The idle wait in wait_for_x11_connection_activity (despite the name) is shared with Wayland — it selects on wl_display_get_fd for the active backend.
The frame_callback_pending flag gates rendering: the window only commits a new frame after the compositor's wl_callback::done signals the previous frame is on screen. This prevents queueing more buffers than the compositor can consume.
XDG surface lifecycle
wl_compositor.create_surface() → wl_surface
xdg_wm_base.get_xdg_surface(surface) → xdg_surface
xdg_surface.get_toplevel() → xdg_toplevel
─ configure(serial) ─ from compositor
─ size_change_event ─ from compositor on resize
xdg_surface.ack_configure(serial) ← from us
wl_surface.commit() ← from us
─ surface mapped ─ window appears
configure events arrive after the compositor has decided initial geometry (window manager rules, output scale, restored size). Not acking promptly causes the compositor to drop the connection.
XKB keyboard translation
translate_keysym_to_virtual_keycode is a flat match from XKB keysym constants (XKB_KEY_Escape = 0xff1b, XKB_KEY_space = 0x0020, etc.) to VirtualKeyCode. The same keysym table is used by X11 — XKB is the shared abstraction. WaylandKeyboardState holds the xkb_context, xkb_keymap, xkb_state and feeds raw key codes through xkb_state_key_get_one_sym before dispatching.
Modifiers track via xkb_state_update_mask on every wl_keyboard::modifiers event; the resulting state populates KeyboardState.shift_down / ctrl_down / alt_down / super_down on the diffed current_window_state.
Pointer events
PointerState tracks the most recent serial, needed for any subsequent compositor request that requires it (xdg_toplevel_move, xdg_popup_grab, cursor surface changes).
Cursor changes: load wl_cursor_theme via wl_cursor_theme_load, look up by name ("left_ptr", "text", "hand2", etc.), call wl_pointer_set_cursor(serial, cursor_surface, hotspot_x, hotspot_y) with the dedicated cursor_surface so the theme bitmap is uploaded into a wl_buffer and shown.
Scroll uses wl_pointer::axis (continuous wheel) plus axis_discrete (notched wheel) plus axis_source for distinguishing finger / wheel / continuous; record_scroll_from_hit_test collapses this to a unified (delta_x, delta_y, source) tuple.
IME — text-input v3 with GTK fallback
The struct holds three IME-related slots:
text_input_manager: Option<*mut zwp_text_input_manager_v3>— bound from the registry ifzwp_text_input_manager_v3is advertised.text_input: Option<*mut zwp_text_input_v3>— created viatext_input_manager_get_text_input(seat)once.gtk_im_context: Option<*mut GtkIMContext>— fallback when the compositor does not implement text-input v3.
text_input_active and text_input_enabled are gates so that enable() is only called once per focus, and disable() is sent on focus-out. text_input_pending (events::TextInputPendingState) buffers preedit / commit string / cursor rectangle pairs that arrive across multiple events; the v3 protocol delivers them as preedit_string + commit_string + delete_surrounding_text then a final done(serial) — all four must apply atomically.
The pending state ends up at the standard process_text_input(text) path on LayoutWindow, identical to the X11 / macOS / Win32 input path.
Tooltips via wl_subsurface
TooltipWindow creates a child surface:
let tooltip_surface = wl_compositor_create_surface(compositor);
let tooltip_subsurface =
wl_subcompositor_get_subsurface(subcompositor, tooltip_surface, parent_surface);
wl_subsurface_set_position(tooltip_subsurface, x, y);
wl_subsurface_set_desync(tooltip_subsurface);
Subsurfaces are composited with the parent — they share the parent's mapped state and stacking. Show/hide flips wl_surface_attach(buffer) vs wl_surface_attach(NULL) followed by wl_surface_commit on the parent.
The tooltip body is currently a placeholder rasteriser — solid rectangles per glyph cell. Real text rendering will route through the same cpurender path used for window content; the constants TOOLTIP_CHAR_WIDTH_PX = 7 etc. will go away once that lands.
Menus via xdg_popup (planned)
Menu popups currently take the same path as X11 — create_menu_popup_options returns WindowCreateOptions for a generic toplevel. The TODO documents the missing piece: when pending_create.window_state.flags.window_type == WindowType::Menu, xdg_surface::get_popup should be called with an xdg_positioner anchored to MenuLayoutData::trigger_rect (stashed in the layout-callback RefAny for exactly this purpose).
MenuLayoutData carries the trigger rect in parent-surface logical coordinates so the popup positioner can anchor relative to the trigger node — Wayland clients have no way to address absolute screen coordinates, so positioning must be done through the compositor with this rect.
WaylandPopup is the eventual home of the real popup window. It already includes xdg_popup, xdg_positioner, and parent_surface fields, plus a listener_context boxed for manual cleanup.
KDE blur protocol
org.kde.kwin.blur_manager and org.kde.kwin.blur are bound from the registry on KDE Plasma sessions. When the user enables WindowBackgroundMaterial::Blur, blur_manager_create(surface) returns a per-surface blur object that the compositor uses to blur whatever is behind the window. blur::set_region(NULL) blurs the entire surface; calling commit on the parent surface activates the effect.
This protocol is KDE-only; GNOME (mutter) and other Wayland compositors ignore the request.
Multi-monitor — wl_output
Wayland delivers outputs through the registry as wl_output_interface globals. Each wl_output fires geometry, mode, scale, name, done events that WaylandWindow::known_outputs accumulates into MonitorState records.
current_outputs tracks which outputs the surface is currently on (via wl_surface::enter / leave). When the active output changes its scale, dynamic_selector_context.dpi and the renderer DPI are updated and the layout regenerates.
MonitorState::get_monitor_id builds a stable MonitorId from make + model + name + position, falling back to just name when make/model are unset (some compositors don't populate them).
D-Bus screensaver inhibit
Same pattern as X11: screensaver_inhibit_cookie: Option<u32> plus a shared *mut DBusConnection from linux/dbus. The org.freedesktop.ScreenSaver.Inhibit call is sent when WindowFlags::keep_screen_awake flips on; UnInhibit on flip-off or window close.
Clipboard
The Wayland clipboard module currently delegates to the x11-clipboard crate, which requires XWayland. On a pure Wayland session (no XWayland), Clipboard::new() fails and clipboard ops become no-ops. A native wl_data_device / zwp_primary_selection_v1 implementation is the long-term plan — the boilerplate listener wiring would mirror the IME setup.
Defines
The Wayland defines module is the literal Rust translation of the Wayland protocol XML. Each wl_*_listener struct contains extern "C" fn pointers that match the protocol's event signatures. Adding a new global means:
- Define the C-ABI struct in
defines.rs. - Add the
wl_interfaceconstant. - Add the
*_add_listenerand any*_destroytodlopen.rs. - Bind from the registry handler in
mod.rs. - Implement the
extern "C" fnlistener callbacks (these run on the dispatch thread — they receive a*mut c_voiddata pointer that should be cast to*mut WaylandWindow).
Multi-window
Same linux/registry.rs as X11. Wayland uses wl_display as u64 as the registry key since Wayland clients have no equivalent of X11 Window IDs.
Known issues / TODOs
xdg_popupfor menus is the most important pending piece — the current toplevel-based popups don't grab input correctly and don't dismiss on outside click.wp_fractional_scale_v1for non-integer DPI scaling is not bound. Currently scale is rounded to the nearest integer per output.- Native
wl_data_deviceclipboard would remove the XWayland dependency. - Drag-and-drop (
wl_data_device::start_drag) is not implemented.
Coming Up Next
- Linux X11 — Linux X11 shell - Xlib, GLX, XInput2
- Linux DBus — Linux DBus integration for a11y, dialogs, notifications
- Common — Shared shell infrastructure across platforms
- Windowing Overview — Per-window aggregate, headless variant, and the platform shell layer