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.