Documentation Index
Fetch the complete documentation index at: https://mintlify.com/smithay/wayland-rs/llms.txt
Use this file to discover all available pages before exploring further.
Every capability a Wayland compositor exposes—surfaces, seats, outputs, shared memory, XDG shell—is advertised as a global through wl_registry. Before your client can use any protocol object it must first discover the matching global and bind it to create a local proxy. wayland-client offers two levels of abstraction for this task: the raw WlRegistry + Dispatch pattern for full control, and the higher-level GlobalList / registry_queue_init helper for the common case.
The wl_registry pattern
At the wire level, globals work like this:
- The client sends
wl_display.get_registry, creating a WlRegistry object.
- The compositor responds with a stream of
wl_registry.global events, one per available global.
- The client calls
wl_registry.bind to instantiate a particular global as a local proxy.
- Later, if a global is removed (for example a monitor is unplugged), the compositor sends
wl_registry.global_remove.
Raw registry pattern
Implementing Dispatch<WlRegistry, _> directly gives you complete control:
use wayland_client::{
protocol::wl_registry, Connection, Dispatch, QueueHandle,
};
struct AppState {
compositor_name: Option<u32>,
}
// Dispatch<WlRegistry, ()> is implemented for AppState.
// `&self` here is AppState (the implementor / user-data type);
// `state` gives mutable access to AppState.
impl Dispatch<wl_registry::WlRegistry, ()> for AppState {
fn event(
&self,
state: &mut AppState,
registry: &wl_registry::WlRegistry,
event: wl_registry::Event,
_conn: &Connection,
qh: &QueueHandle<AppState>,
) {
match event {
wl_registry::Event::Global { name, interface, version } => {
println!("Global: [{name}] {interface} v{version}");
if interface == "wl_compositor" {
// Bind the compositor at version 5 (or lower if server advertises less)
let compositor = registry.bind::<
wayland_client::protocol::wl_compositor::WlCompositor, _, _
>(name, 5, qh, ());
// compositor is a WlCompositor proxy
let _ = compositor;
}
}
wl_registry::Event::GlobalRemove { name } => {
println!("Global removed: {name}");
// clean up any objects bound from this global
}
_ => {}
}
}
}
fn main() {
let conn = Connection::connect_to_env().unwrap();
let mut event_queue = conn.new_event_queue::<AppState>();
let qh = event_queue.handle();
conn.display().get_registry(&qh, ());
let mut state = AppState { compositor_name: None };
event_queue.roundtrip(&mut state).unwrap();
}
The version argument to wl_registry.bind is the version you want, capped by what the server advertises. Passing a version higher than the server supports is a protocol error. The GlobalList::bind() helper (below) handles this automatically.
GlobalList and registry_queue_init
For the common startup pattern—enumerate all globals, bind the ones you need, then enter the main loop—wayland-client provides registry_queue_init in the globals module:
use wayland_client::{
Connection,
globals::registry_queue_init,
};
registry_queue_init takes a Connection reference and returns a (GlobalList, EventQueue<State>) pair. Internally it:
- Creates a new
EventQueue.
- Sends
wl_display.get_registry.
- Performs a
Connection::roundtrip() to collect the initial wl_registry.global flood into a GlobalListContents buffer.
- Returns the
GlobalList (which owns the registry and the contents) plus the fresh event queue.
GlobalListContents: Dispatch<WlRegistry, State> requirement
To use registry_queue_init, GlobalListContents must implement Dispatch<wl_registry::WlRegistry, State> for your State type. GlobalListContents acts as the user-data for the registry object and its event() method will be called for dynamic registry events (globals added or removed after the initial roundtrip):
use wayland_client::{
Connection, Dispatch, QueueHandle,
globals::GlobalListContents,
protocol::wl_registry,
};
struct State;
// `GlobalListContents` is the user-data type for the WlRegistry created by
// `registry_queue_init`. `State` is your application state type.
impl Dispatch<wl_registry::WlRegistry, State> for GlobalListContents {
fn event(
&self,
_state: &mut State,
_proxy: &wl_registry::WlRegistry,
event: wl_registry::Event,
_conn: &Connection,
_qhandle: &QueueHandle<State>,
) {
// React to dynamic global changes here.
// The GlobalListContents is already updated before this is called.
match event {
wl_registry::Event::Global { name, interface, version } => {
println!("New global appeared: [{name}] {interface} v{version}");
}
wl_registry::Event::GlobalRemove { name } => {
println!("Global removed: {name}");
}
_ => {}
}
}
}
Complete GlobalList example
use wayland_client::{
Connection, Dispatch, QueueHandle,
globals::{registry_queue_init, GlobalListContents},
protocol::{wl_compositor, wl_registry, wl_shm},
};
struct State;
// Required: dynamic registry event handler
impl Dispatch<wl_registry::WlRegistry, State> for GlobalListContents {
fn event(
&self,
_state: &mut State,
_proxy: &wl_registry::WlRegistry,
_event: wl_registry::Event,
_conn: &Connection,
_qhandle: &QueueHandle<State>,
) {
// Handle dynamic global changes if needed
}
}
// Required: handle events for objects we bind
impl Dispatch<wl_compositor::WlCompositor, ()> for State {
fn event(
&self,
_state: &mut State,
_proxy: &wl_compositor::WlCompositor,
_event: wl_compositor::Event,
_conn: &Connection,
_qhandle: &QueueHandle<State>,
) {
// wl_compositor has no events
}
}
impl Dispatch<wl_shm::WlShm, ()> for State {
fn event(
&self,
_state: &mut State,
_proxy: &wl_shm::WlShm,
event: wl_shm::Event,
_conn: &Connection,
_qhandle: &QueueHandle<State>,
) {
// wl_shm.format events list supported pixel formats
if let wl_shm::Event::Format { format } = event {
println!("Supported format: {format:?}");
}
}
}
fn main() {
let conn = Connection::connect_to_env().unwrap();
// Enumerate globals and get a ready-to-use event queue
let (globals, mut event_queue) =
registry_queue_init::<State>(&conn).unwrap();
let qh = event_queue.handle();
// Inspect the full list without binding
globals.contents().with_list(|list| {
for g in list {
println!("[{}] {} v{}", g.name, g.interface, g.version);
}
});
// Bind wl_compositor, requesting versions 4–6
let compositor: wl_compositor::WlCompositor =
globals.bind(&qh, 4..=6, ()).unwrap();
// Bind wl_shm at version 1
let _shm: wl_shm::WlShm =
globals.bind(&qh, 1..=1, ()).unwrap();
let mut state = State;
// Dispatch to receive wl_shm.format events, etc.
event_queue.roundtrip(&mut state).unwrap();
let _ = compositor;
}
GlobalList::bind()
pub fn bind<I, State, U>(
&self,
qh: &QueueHandle<State>,
version: RangeInclusive<u32>,
udata: U,
) -> Result<I, BindError>
The version range lets you express both a minimum and a maximum:
- If the server’s advertised version is below the range start, you get
BindError::UnsupportedVersion. Your code can handle this gracefully (log a warning, disable a feature).
- If the server’s advertised version is above the range end,
bind silently caps the bound version to the range end.
- Requesting a maximum higher than the version compiled into the generated code is a panic (programmer error).
match globals.bind::<wl_compositor::WlCompositor, _, _>(&qh, 4..=6, ()) {
Ok(compositor) => { /* use compositor */ }
Err(BindError::NotPresent(name)) => {
eprintln!("Global '{name}' not advertised by compositor");
}
Err(BindError::UnsupportedVersion { interface, requested, available }) => {
eprintln!(
"{interface}: need v{requested}, compositor only has v{available}"
);
}
}
BindError variants
| Variant | Meaning |
|---|
BindError::NotPresent(&'static str) | No global with the requested interface name was found in the list. |
BindError::UnsupportedVersion { interface, requested, available } | The global exists but its advertised version is below the minimum you requested. |
GlobalListContents
GlobalListContents is a Mutex<Vec<Global>> wrapper exposed through two methods:
// Zero-copy: pass a closure that receives &[Global]
globals.contents().with_list(|list| {
for g in list {
println!("{}: {}", g.name, g.interface);
}
});
// Owned copy: allocates a Vec<Global>
let snapshot: Vec<Global> = globals.contents().clone_list();
Each Global has three fields:
name: u32 — the server-assigned numeric handle used in bind and global_remove.
interface: String — the protocol interface name, e.g. "wl_compositor".
version: u32 — the maximum version the compositor supports.
Waiting for the initial global list
registry_queue_init performs a roundtrip internally, so by the time it returns the GlobalListContents already holds all currently advertised globals. You do not need to call roundtrip again just to enumerate globals:
let (globals, event_queue) = registry_queue_init::<State>(&conn).unwrap();
// Safe to call immediately — the list is already populated
globals.contents().with_list(|list| {
assert!(!list.is_empty(), "compositor advertised no globals");
});
Handling global removal
Multi-instance globals—wl_output (monitors), wl_seat (input devices)—can be added and removed at any time. GlobalListContents automatically removes the entry when it receives wl_registry.global_remove, but you still need to clean up any proxies you bound from that global in your Dispatch<WlRegistry, GlobalListContents> handler:
impl Dispatch<wl_registry::WlRegistry, State> for GlobalListContents {
fn event(
&self,
state: &mut State,
_proxy: &wl_registry::WlRegistry,
event: wl_registry::Event,
_conn: &Connection,
_qhandle: &QueueHandle<State>,
) {
if let wl_registry::Event::GlobalRemove { name } = event {
// Remove any bound object whose name matches
state.outputs.retain(|o| o.registry_name != name);
}
}
}
Do not use GlobalList::bind() for multi-instance globals like wl_output and wl_seat. bind() looks up globals by interface name and returns the first match, which is not reliable when multiple instances coexist. Handle those globals in your Dispatch<WlRegistry, _> implementation using the raw registry.bind() call, keyed on the numeric name.
Destroying the registry
GlobalList::destroy() attempts to destroy the underlying WlRegistry object using the wl_fixes protocol extension (available on compositors that support it). This frees server-side resources and stops dynamic registry events from being delivered:
// globals is consumed; no further registry events will arrive
globals.destroy();
If the compositor does not support wl_fixes, the call is a no-op—the registry object remains alive for the lifetime of the connection.