Introduction

Welcome to the Smithay Handbook.

This is a work in progress, and intended to serve as a general manual for building Wayland-related software in Rust, using the tools from the Smithay project.

The project revolves around 3 main components:

  • The wayland-rs repository contains low-level bindings to the Wayland protocols, under the form of several crates. The two most notable being wayland-client and wayland-server, which are the core bindings for client-side and server-side apps.
  • SCTK, or Smithay Client ToolKit, is a crate designed to handle a significant portion of the plumbing required for writing Wayland client apps. It comes on top of wayland-client, and this book will present it as well.
  • Smithay is the flagship crate of the project, and is designed as a framework for writing Wayland servers (also called compositors). It is built on top of the wayland-server crate, and also handles most of the interaction with the system (input devices, graphics, udev, sessions, ...).

The first part of this book is dedicated to client-side apps, while the second part focuses of server-side. If you are interested by the server-side stuff, it is recommended to first get familiar with the client-side, as it is easier to get into and a lot of its concepts map to server-side.

Wayland apps

This part of the book is dedicated to Wayland client apps, so writing programs that can run in a Wayland environment: display something to the screen, receive user input and process it. This include classic apps which display a window with their content, but can also include programs such as desktop components (widgets, bars, animated backgrounds, lockscreens, ...).

The first section will be dedicated to general principles of the wayland protocol, as well as the main objects of the wayland-client crate. The following sections will explore in more details the actual process of writing an app.

General principles

The Wayland protocol follows a client-server architecture: the Wayland compositor listens on an UNIX socket, to which client apps then connect in order to present their graphical interface. This UNIX connection transports information in both directions: the client uses it to tell the server what it wants to display, and the server uses it to tell the client about user actions (such as keyboard or pointer input). The Wayland connection is thus the heart of a client app, and is the source of events that drives its event loop.

The server tells to clients about the Wayland socket using the WAYLAND_DISPLAY environment variable. The listening socket is placed at $XDG_RUNTIME_DIR/$WAYLAND_DISPLAY. This will typically be a path like /run/user/1000/wayland-0.

When starting your app, it will need to find the Wayland socket and connect to it. The wayland-client crate does this for you with the Connection::connect_to_env() method. If no error occurs, this function will provide you with a Connection object.

This object is the very heart of your Wayland program. It represents your connection to the server, and this is from this object that everything will be further initialized. But to understand how this objects relates to the rest of the crate, we will first need to get a better understanding of the distinction between the protocol objects, and the Rust structs that your program will manipulate.

Objects

The Wayland protocol is an object-oriented protocol. This means that, as the client and server exchange messages, they build an abstract state made of several objects, which are represented by a numeric identifier. You can figure them as being scopes for messages: each message is associated with one of these protocol object. Each object has an "interface", which is a definition of which messages can be associated with it.

The messages are named "requests" when they are sent by the client to the server, and "events" when they are sent by the server to the client. An object interface is thus the list of which requests and which events can be associated with objects of this interface.

For example, an object representing a keyboard would receive the events from the server informing about which keys are pressed by the user. An object representing a window would send requests to update its content, and receive events informing about user interaction (such as resizing).

At the beginning, the protocol state contains a single object. Its identifier is 1, and its interface is wl_display. From this object the rest of the protocol state is setup by the client.

Objects and proxies

The protocol objects are created and destroyed by messages. A request sent by the client can have for effect the creation of a new object in the state, or the destruction of an object. Most of the time, creation and destruction of objects is done by the client, but there are exceptions. For this reason, protocol objects are not directly represented by Rust structs, but instead one layer of indirection is added. wayland-client provides you with Rust objects that we call "proxies".

Each proxy represents a protocol object, but several proxies can represent the same object. You can think of proxies as something akin Rc<_> or Arc<_>: they are an handle to the underlying protocol object. Furthermore, their lifetime is not tied to the protocol object's lifetime: they can outlive it. Once a protocol object has been destroyed, the proxies associated to it will become inert.

Each kind of protocol object is represented by a different Rust type, all of them implementing the Proxy trait. Sending requests to the server is done by invoking methods on that object.

User data

wayland-client makes it possible to associate some data with a protocol object. This data is set when the object is created, and can be accessed from any proxy representing this object via the Proxy::data() method. This user data mechanism comes with two important limitations:

  • You can only get an immutable & reference to the stored value. This means that if you need this value to be writable, you need to handle interior mutability by yourself.
  • The Proxy::data() is generic, and you need to know beforehand what is the type of the stored value. If the wrong type is provided as parameter, the function will return None.

Lets now discuss the mechanism by which wayland-client handles the stream of events from the server and allows you to process it: event queues and dispatching.

Event queues and dispatching

If the Connection is the heart of your Wayland app, the EventQueue will be its backbone. As described in the previous section, messages are sent in both directions, and so far we only discussed how to send requests (using the methods of proxies), but not how to receive events. This is done via a dispatching mechanism powered by the event queues.

State and dispatching

Generally, an app built using wayland-client will be structured as a central type, which we refer to as the state of the app. This type needs to provide several methods that are used as callbacks by wayland-client to deliver events from the server.

Providing these methods is done by implementing the Dispatch trait on your state. This is a parametric trait with two type parameters: the first one is a Wayland interface, and the second is the type of the user data you want to associate with this kind of object.

For example, to handle events sent to a WlSurface object, with user data MyUserData, you would provide the following implementation:


#![allow(unused)]
fn main() {
impl Dispatch<WlSurface, MyUserData> for Mystate {
    fn event(
        state: &mut Self,
        proxy: &WlSurface,
        event: <WlSurface as Proxy>::Event,
        data: &MyUserData,
        conn: &Connection
        qhandle: &QueueHandle<MyState>
    ) {
        /*
         * Here is your logic for handling the event
         */
    }
}
}

Let's explain the arguments of this method one by one:

  1. As you can see, the method does not take &mut Self as argument, but instead state: &mut Self. This is related to a mechanism of wayland-client that allows composition by delegating Dispatch implementations to fields of your state struct. This is detailed below.
  2. The second argument is a proxy representing the Wayland object with which this event is associated
  3. The first argument is an enum representing the content of the event. You are thus expected to match it in order to process it.
  4. The fourth argument is a reference to the user data associated with this object.
  5. The fifth argument is a reference to the underlying Connection. Most of the time you will not need to use it, but it some circumstances it can be needed.
  6. The last argument is a handle to the [EventLoop] currently handling this object. You will need it if you want to invoke requests that create new objects.

Note that the event enums are marked as #[non_exhaustive], as new events can be introduced in later revisions of the Wayland protocol. The Wayland protocol has a versioning system for objects (which this is details later in this book), as a result your app will not receive these new events unless you explicitly opt in by requiring a higher version from the server. Nevertheless, wayland-client still needs to statically support them. These enums being non-exhaustive thus allows the crate to handle new versions of the protocol without it being a breaking change.

As a result, when you implement the Dispatch trait you need to keep an eye on which version of the object you're going to work with, and construct your match accordingly. Having a catch-all _ => unreachable!() arm is here an easy way to ensure you are not ignoring events that you can actually receive.

Another important point no note is that the Dispatch trait is parameterized by the user data type. This means that you can provide two different implementations for the same Wayland interface, and the event queue will invoke one or the other depending on which type of user data was provided when the object was created.

Event queues

In some cases however, having a single state may be limiting. Some apps have their logic clearly separated in independent parts, and may want to have these parts run concurrently on multiple threads. To handle this, wayland-client has a concept of EventQueue. Most apps will be fine with a single event queue, but the principle is the same with one or more.

Event queues are created from the Connection with the Connection::new_event_queue() method. Both this method and the EventQueue struct have a type parameter, which is the type of your state struct. This means that an event queue can only be used with a single type as state. This allows wayland-client to statically ensure that your app provides all the needed Dispatch implementations.

Once the event queue is created, you can retrieve its handle with the EventQueue::handle() method. This handle is required by all methods that create a new Wayland object, and allows you to specify which event queue (and thus which state) should handle the events for this object.

For a simple app where the Wayland connection is the only source of events to process, the main structure of the program would then look like this:


#![allow(unused)]
fn main() {
use wayland_client::Connection;

let connection = Connection::connect_to_env().unwrap();
let mut event_queue = connection.new_event_queue();

/*
 * Here the initialization code of your app...
 */

// And the main loop:
//
// This assumes that the `state` struct contains an `exit` boolean field, that is
// set to true when the app decided it should exit.
while !state.exit {
    event_queue.blocking_dispatch(&mut state).expect("Wayland connection lost!");
}
}

The EventQueue::blocking_dispatch() method will put your app to sleep and wait until new events from the server are available, dispatch them to the appropriate Dispatch methods of the state, and then return.

If it returns an error, then the Wayland connection has already been closed and can no longer be used. This would typically happen in two main situations: either your app triggered a protocol error and the connection was killed by the server, or the server has shut down.

While this simple structure is sufficient for this introduction, more advanced programs generally don't want to just sleep waiting for events. For example a game or a video player needs to continue to update its contents even if no event occurs. EventQueue provides other methods to dispatch events in a non-blocking way, see its API documentation for more details.

Dispatch delegation

wayland-client also provides means to provide a generic implementation of Dispatch that downstream crates or modules may use in a composition-like fashion. SCTK heavily uses this mechanism to provide generic and modular helpers, so while you'll probably not need to implement such helpers at first, it is important to have a general idea of how they work to use them.

The general structure of this mechanism is as follows:

  1. You provide a sub-state type that contains the data necessary to handle the events of the subset of interfaces your helper should manage.
  2. You provide generic implementations of the Dispatch trait on this sub-state type, by explicitly making its third type parameter as generic (rather than letting it default to Self)
  3. The downstream module will then have your sub-state as a field of its state struct, and use the delegate_dispatch! macro to delegate its Dispatch implementation to your helper.

See the API documentation of Dispatch and delegate_dispatch! for more details and examples.

With all this context given, we are now ready to initialize our first app!

Initializing an app

As described previously, once initialized, wayland-client provides you with a Connection object, representing your wayland connection.

From this connection, we can then recover the WlDisplay proxy, which represents the initial wl_display object, the starting point of your Wayland interactions.

We also need to create and EventQueue, which will be needed to process the events from all our objects.

The skeleton of a Wayland app may thus look like this:

use wayland_client::Connection;

fn main() {
    let connection = Connection::connect_to_env()
                        .expect("Failed to find a Wayland socket.");

    // initialize your State struct
    let my_state = State::new();

    let mut event_queue = connection.new_event_queue();
    let display = connection.display();

    /*
    * Proceed to initialize the rest of your app
    */

    // And the main loop:
    //
    // This assumes that the `state` struct contains an `exit` boolean field,
    // that is set to true when the app decided it should exit.
    while !state.exit {
        event_queue
            .blocking_dispatch(&mut state)
            .expect("Wayland connection lost!");
    }
}

With that in place, we can now proceed to the last core concept of the protocol: the globals and the registry.

The registry and globals

The Wayland protocol is designed in a modular fashion: all capabilities proposed by the server to clients are each represented by modules, that are called "globals" in the Wayland jargon.

A global is a blueprint advertised by the Wayland server to its clients, that the client can instantiate into a Wayland protocol object, which will expose the appropriate requests and events for interacting with the capability they represent. This process of advertising and instantiating is done with another special protocol object: the registry.

The registry

The registry is a protocol object with interface wl_registry. It is created from an attached wl_display via its get_registry() method. Upon creation, the server will send it a stream of events telling the client about which globals are available.

A global advertisement is composed of 3 values:

  • the global name, an u32 identifier which represents the global within the globals list (it is not the same thing as the protocol id of the objects created by instantiating this global)
  • the global interface, a string containing the name of the interface of the protocol objects created from this global
  • the global version, an u32 greater or equal to 1, which is used for protocol versioning

The Wayland protocol can evolve, and the interfaces are versioned. The number the server sends is the highest version it supports. The server must support all lower versions as well.

Upon receiving this list, the client can then instantiate the globals it wishes to use into protocol objects using the bind method of the registry.

The two kinds of globals

The various globals that a Wayland server can advertise can be roughly classified in two kinds, depending on whether they can be advertised multiple times by the server.

Singleton globals represent a capability of the compositor. This is something the Wayland server makes possible for clients to do. They generally exist from the start, never change and are advertised only once.

Multi globals however represent some device the server has access to. For example a monitor, or an input device. These globals can thus exist with multiplicity. For example, the server will advertise one wl_output global for each monitor that is plugged in the computer, each with a different name. Furthermore, these globals can appear or disappear during the lifetime of the app, as such devices are plugged in or out.

The registry Global event signals a new global, while its GlobalRemove event signals that a given global has been removed. When a such a global is removed, all the protocol objects derived from it will generally become inert, and the client is then expected to do cleanup by destroying them using the appropriate requests.

Global helpers in wayland-client

Tracking the list of globals, their versions, and instantiating them requires some work. wayland-client provides GlobalList helper that can automate part of this, by providing you with an initial list of globals at the startup of your app. This allows you proceed to the initialization of your app and state in a linear fashion, rather than doing so in the callbacks of your Dispatch implementation for registry. This does not replace that implementation though, and you still need to provide it to handle dynamic global creation or destruction.

Using this abstraction, we can put together a small app that connects to the wayland server, receives the list of globals, and prints them to the console.

The main entry point for using this global helpers is the registry_queue_init function. This function takes a reference to your [Connection] as argument, an will internally:

  1. Create an [EventQueue]
  2. Create a wl_registry object, and do an initial blocking roundtrip with the server to retrieve the list of globals
  3. Return a GlobalList containing this list of globals, as well as the created [EventQueue] that you can then use in your app.

The created registry is registered to that event queue, and a proxy to it can be accessed via the [GlobalList::registry()] method of the returned GlobalList.

use wayland_client::{
    Connection, Dispatch, QueueHandle,
    globals::{registry_queue_init, Global, GlobalListContents},
    protocol::wl_registry,
};

// We need a State struct even if we don't use it
struct State;


// You need to provide a Dispatch<WlRegistry, GlobalListContents> impl for your app
impl Dispatch<wl_registry::WlRegistry, GlobalListContents> for State {
    fn event(
        state: &mut State,
        proxy: &wl_registry::WlRegistry,
        event: wl_registry::Event,
        // The `GlobalListContents` is a container with an up-to-date list of
        // the currently existing globals
        data: &GlobalListContents,
        conn: &Connection,
        qhandle: &QueueHandle<State>,
    ) {
        /*
         * This simple program does not handle dynamic global events,
         * so we don't do anything here.
         */
    }
}

fn main() {
    let connection = Connection::connect_to_env().unwrap();
    let (globals, queue) = registry_queue_init::<State>(&connection).unwrap();

    // Print the contents of the list
    // We cannot iterate the list directly because of thread-safety constraints,
    // so we clone it and iterate on the returned Vec
    for global in globals.contents().clone_list() {
        println!(
            "Global #{} with interface \"{}\" and version {}",
            global.name,
            global.interface,
            global.version
        );
    }
}

This first section gave you a general overview of how the Wayland protocol and the wayland-client crate work. In the next section, we'll start to work with SCTK (Smithay's Client ToolKit), a crate designed to provide several abstractions that simplify all the plumbing needed to create a Wayland app.

Getting started with SCTK

In the previous section we explored the general structure of the Wayland protocol, as well as how a Wayland app is initialized using the display and the registry to get the list of globals.

We will now introduce SCTK (short for Smithay Client Toolkit), a toolkit crate that provides various abstractions to simplify large parts of the plumbing required to make a Wayland app. This section will cover the steps to setup an app using SCTK, create a window and draw to it.

The Environment

SCTK provides a system that handles the listing and instantiation of globals needed by your app: the Environment. This system is highly modular, but SCTK also provides a preset for it, that will automatically instantiate all the globals needed for a regular app. This preset comes in the form of two macros: default_environment! and new_default_environment!.

The first one is used to declare the environment struct for your app. We will use the desktop preset, and we need to also provide a name, this example will use MyApp, but you can use anything. The second macro needs to be called for initializing the environment, and we need to give it the desktop preset as well.

The new_default_environment! takes care of connecting to the Wayland socket, creating an event queue, and initializing the environment on it. On success it returns a tuple of 3 values:

  • The environment, of type Environment<MyApp>
  • The Display
  • The EventQueue that the environment is associated with

The environment will then provide several methods mapping the functionality of the various underlying globals. SCTK generally provides an higher-level interface to the underlying functionality than what the Wayland protocol directly encodes via its globals.

The previous example of listing all globals can be reframed as such using the environment system:

use smithay_client_toolkit::{default_environment, new_default_environment};

default_environment!(MyApp, desktop);

fn main() {
    let (environment, display, event_queue) = new_default_environment!(MyApp, desktop)
        .expect("Failed to initialize the Wayland environment.");

    // environment.manager is the underlying GlobalManager
    println!("Available globals:");
    for (name, interface, version) in environment.manager.list() {
        println!("{}: {} (version {})", name, interface, version);
    }
}

Creating a Window

In order to display content on Wayland, it is necessary to first understand a central concept of the Wayland protocol: the surfaces.

Surfaces

One of the core globals of the Wayland protocol is the wl_compositor. It allows the creation of surfaces (objects with the interface wl_surface), which are the main building block for displaying content to the screen.

You can think of a surface as a canvas. In order to display content to the screen, it is needed to draw the content on the surface (see next page), and to assign a role to it.

Roles are how Wayland apps tell the server what a surface is meant to be. The most central role is the role of a "shell surface": this surface is meant to be a window the user can interact with. However other roles are possible, such as assigning a surface to replace the pointer icon.

SCTK's environment provides a quick method for creating new surfaces: Environment::create_surface().

SCTK's Window

The Wayland protocol defines another global, xdg_shell which is used to create the appropriate objects for assigning the "shell surface" role to a surface. For this current quickstart however, we don't need to dig into its complexity, instead we can use the Window adapter provided by SCTK, which handles most of the plumbing for us.

We can create a Window by using the Environment::create_window() method. It requires us to provide a WlSurface which will be the base of this window, initial dimensions for the window, and a callback to process events generated by the user manipulation the Window. The Window provided by SCTK will take care of declaring the surface as being a shell surface, and of drawing some simple decorations for the window if the environment does not provide them. The drawing of these decorations is also a modular element, and we will use there the implementation provided by SCTK, that we pass as a type parameter.

use smithay_client_toolkit::{
    default_environment, new_default_environment,
    window::ConceptFrame
};

default_environment!(MyApp, desktop);

fn main() {
    let (environment, display, event_queue) = new_default_environment!(MyApp, desktop)
        .expect("Failed to initialize the Wayland environment.");

    let surface = environment.create_surface().detach();

    let window = environment
        .create_window::<ConceptFrame, _>(
            surface,    // the surface this window is based on
            None,       // theme manager
            (800, 600), // the initial dimensions
            |event, dispatch_data| { /* A closure to handle the window-related events */ },
        )
        .expect("Unable to setup the window.");
}

Window events

The Event generated by the Window are of three kind:

The first is Event::Close. It is sent to you when the user has requested that the window be closed (for example by clicking the relevant button on the decorations). Depending on the desktop environment, the window may or may not have been hidden by the Wayland server, so you cannot rely on it, you need to hide your window, either by dropping the Window (and maybe exiting your program if that's what is intended), or by submitting empty content to the surface (see next page).

The second kind is Event::Refresh. This event is used by the window to tell you it needs to redraw the decorations. This is not done automatically because it is a costly operation. You will generally want to manually trigger it by calling Window::refresh just before redrawing the contents of your window.

The last kind is Event::Configure. This event tells you that something about the state of the window has changed. If the new_size field is Some(), this means that the Wayland server is suggesting a new size to your window, most likely the result of a resizing action from the user. The state fields provides a list of state about your window (is it maximized, or in the process of being resized ?), that you may need to properly draw your contents (see State for the details). This event implies that the decorations need to be redrawn.

Note that when your window is in the process of being resized by the user, the Wayland server will send a lot of Configure events with new sizes. Attempting to redraw your content in response to every one of them will render your app extremely laggy. Your app actually only needs to process the last one of the batch to behave correctly, as such it is good practice to buffer their content into some global state (using the DispatchData for example) and process this accumulated data once the dispatching process of the event queue is finished.

Furthermore, unless your app is fullscreen, the size suggested by the Wayland server is a non-binding suggestion, you are free to ignore it, or to adjust it (if your app should only resize itself in increments for example).

The structure of an app processing its window events into a global state would look like that:

use smithay_client_toolkit::{
    default_environment, new_default_environment,
    window::{ConceptFrame, Event as WindowEvent},
};

default_environment!(MyApp, desktop);

struct MyState {
    new_size: Option<(u32, u32)>,
    close_requested: bool,
    refresh_requested: bool,
    /* The rest of the state of your app */
}

fn main() {
    let (environment, _display, mut event_queue) =
        new_default_environment!(MyApp, desktop)
        .expect("Failed to initialize the Wayland environment.");

    let surface = environment.create_surface().detach();

    let mut window = environment.create_window::<ConceptFrame, _>(
        surface,    // the surface this window is based on
        None,       // theme manager
        (800, 600), // the initial dimensions
        |event, mut dispatch_data| {
            // We acess the global state through `DispatchData`
            let state = dispatch_data.get::<MyState>().unwrap();
            match event {
                // Store the request to close the window or refresh the frame to process
                // it later in the main loop
                WindowEvent::Close => state.close_requested = true,
                WindowEvent::Refresh => state.refresh_requested = true,
                // If the configure event contains a new size, overwrite the currently
                // stored new_size with it
                WindowEvent::Configure { new_size, .. } => if new_size.is_some() {
                    state.new_size = new_size
                }
            };
        }
    ).expect("Unable to setup the window.");

    // Initialize the global state, for use in the main loop
    let mut global_state = MyState {
        new_size: None,
        close_requested: false,
        refresh_requested: false,
        /* The rest of the state of your app */
    };

    // The mail event loop of the program
    loop {
        // Provide the global state to dispatch(), so that the Window
        // callback can access it
        event_queue.dispatch(
            &mut global_state,
            |_,_,_| panic!("An event was received not assigned to any callback!")
        ).expect("Wayland connection lost!");

        if global_state.close_requested {
            // The user requested to close the app, exit the loop
            break;
        }
        // If we changed size, we need to tell it to the Window, so that it draws
        // decorations with the correct size. And we thus need to tell it *before*
        // calling the refresh() method.
        if let Some((w, h)) = global_state.new_size {
            window.resize(w, h);
        }
        if global_state.refresh_requested || global_state.new_size.is_some() {
            // refresh the decorations if needed & reset the refresh flag
            window.refresh();
            global_state.refresh_requested = false;
        }

        /*
         * We can now redraw our contents if needed: is we have a new size,
         * or if the run of our app requires us to redraw. See next page about
         * how to draw to a surface.
         */
    }
}

Let's continue to the next page to see how we can actually draw content to this Window.

Drawing to a Window

Drawing to a surface is done by attaching buffers to it, though the WlSurface::attach() method. This method takes as argument an optional WlBuffer, and coordinates of this buffer relative to the current content. This last bit allows to control in which direction the rectangle of the surface should grow or shrink, but is pretty anecdotal. Most of the time you will just set these to (0, 0).

The Option<&WlBuffer> is the main part of drawing. A buffer defines the totality of the contents of a surface, including its size. It is mostly a reference to an array of pixels that the Wayland server will use to fill the surface contents. Updating the contents of a surface amounts to attaching a new buffer to it, replacing the previous one. Attaching None to a surface erases its content, causing the surface to be hidden by the server.

The act of drawing is thus creating a WlBuffer referring to an array of pixels with the appropriate content. There are two main approaches for producing such a buffer: drawing to shared memory, or via OpenGL/Vulkan. We will now focus on the first method, leaving OpenGL and Vulkan for later.

Shared Memory

The principle of shared memory for drawing content is that the client first creates a memory-backed file (for example using memfd_create), and shares its file descriptor with the server. The client can then draw its content by writing to the file, and create buffers pointing to the appropriate part of the file, similarly to a rust slice pointing to part of a Vec. The client can thus write the content of several different buffers to the same file, avoiding the need to open many file descriptors (file descriptor leaks are a real thing!).

When a buffer has been attached to a surface and submitted to the compositor, its associated pixel array should no longer be changed until it has finished reading it, or graphical glitches may occur. As a result, clients are encouraged to do double-buffering: maintaining two shared memory files, and drawing to one while the other is in use by the server.

As one can expect, the capability to create buffers backend by shared memory is represented by a global: wl_shm. However as previously, SCTK provides an abstraction to make handling such shared memory easier: the DoubleMemPool. As its name can let you guess, it also manages double-buffering.

We can create a DoubleMemPool from the environment using the Environment::create_double_pool() method:


#![allow(unused)]
fn main() {
let mut double_pool = environment.create_double_pool(|_| {})
    .expect("Failed to create a memory-backed file.");
}

The DoubleMemPool keeps automatically track of which of its underlying memory pools is still being used by the server, allowing us to use the other. If at some point we try to draw and both are still in use, DoubleMemPool will not let us access any pool. If that happens, the callback we gave to Environment::create_double_pool() will be invoked as soon as one pool is free again and we can draw. We will not be doing such reactive drawing in this example, so we just pass an empty callback.

Drawing on the memory pool

When we want to draw, we can start by invoking DoubleMemPool::pool(), which gives us access to a free MemPool or None if both are currently in use. Once we have it, we can write to it like any file, it implements the std::io::Write and std::io::Seek traits.

For this example, let's just fill the window with red. We will be writing the pixel contents in ARGB8888 format. This means that each pixel will be represented by 4 bytes: Alpha, Red, Green and Blue values. Our fully opaque red is thus #FFD00000, or &[0xFF, 0xD0, 0, 0].

To draw this content, we shall first ensure that the MemPool is large enough to store those contents, then seek back to the beginning of the file, and write enough of these pixels to fill the whole surface.


#![allow(unused)]
fn main() {
use std::io::{Write, Seek, SeekFrom, BufWriter};
// Only try to draw if there is a free pool
if let Some(pool) = double_pool.pool() {
    // number of pixels in the surface
    let pxcount = width * height;
    // number of bytes corresponding to these pixels
    let bytecount = 4*pxcount;

    // Resize the pool accordingly. It is important to use this method
    // rather than just let the memory file grow by writing to it, because
    // it tells the server about the new size of the file. Note that a
    // memory pool can never shrink, so if the size we request is smaller than
    // its current size, this call will do nothing.
    pool.resize(bytecount).unwrap();

    // Now, seek to the beggining of the memory file, to overwrite its contents
    pool.seek(SeekFrom::Start(0)).unwrap();

    // Finally do the actual drawing. We use a BufWriter to increase performance
    {
        let mut writer = BufWriter::new(&mut *pool);
        let pixel: u32 = 0xFF_D0_00_00;
        for _ in 0..pxcount {
            writer.write_all(&pixel.to_ne_bytes()).unwrap();
        }
        writer.flush().unwrap();
    }
}
}

Creating and attaching the buffer

Once the MemPool contains the correct pixels, we can create a buffer from it using the MemPool::buffer() method. This method requires several arguments to correctly define the buffer relative to the memory pool:

  • offset: how many bytes from the start of the pool does the buffer start
  • width: the width of the buffer in pixels
  • height: the height of the buffer in pixels
  • stride: the number of bytes between the start of each line
  • format: the pixel format used for interpreting the bytes (in our case Format::Argb8888)

The pixel stream from the memory pool is thus interpreted by the server as one line after another, from left to right and from top to bottom. In our case, the offset is 0, as we wrote our content at the start of the pool. The width and height are the dimensions of the content we've drawn. Here stride is just width * 4, as there are 4 bytes per pixel.

We can thus create our buffer using:


#![allow(unused)]
fn main() {
use smithay_client_toolkit::shm::Format;

let buffer = pool.buffer(0, width, height, 4 * height, Format::Argb8888);
}

And finally, the last remaining thing to do is to attach this buffer to the surface, declare the damage, and commit the surface. Declaring the damage tells the server which part of the content actually changed since the last submitted buffer. This allows it to optimize its drawing process to only update parts of the screen that need updating. For this example, we will simply declare the whole buffer as damaged.


#![allow(unused)]
fn main() {
surface.attach(Some(&buffer), 0, 0);
surface.damage_buffer(0, 0, width, height);
surface.commit();
}

The commit() call is necessary because the state of the surface is double-buffered. We are changing two properties of the surface here: its attached buffer, and the parts that are damaged. Once we've sent these requests the server will keep these new properties on hold, and apply them atomically when we send the commit request.

With this, you are now able to display content on a Wayland window, congratulations! To wrap all this together, the next page will be an exercise to code a simple image viewer app: it'll display an image and stretch its content when resized.

Exercise: an image viewer

This exercise aims at allowing you to put together all the things we have seen in this introduction to SCTK. The goal is to write an app that loads an image from disk and displays it as the only content of its window. It should react to resizing by stretching the image so that it fills the size given to the window.

Using the image crate

Manipulating images is not the main goal of this exercise, so you can just use the image crate to do it for you. In particular:

General advice

To organize your program, you'll probably find it useful to split the actual drawing code into its own function, taking as argument the &mut MemPool to use, the &WlSurface to which commit the buffer, the requested dimensions, and the image.

The xdg_shell global, used by Window, requires you to wait until you have received at least one Configure event before drawing your content. Committing a buffer before you've received it will be considered as a protocol error by the server, which will cause it to kill your connection.

Be careful about the pixel format, image provides you with the pixels in RGBA format, but the Wayland server will expect them as ARGB.

Solution

A solution to this exercise with detailed comments can be found as the image_viewer.rs example of SCTK.

Wayland Compositors

Work in Progress.