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.