xdg-shell
xdg-shell is a Wayland protocol extension that is meant to be used instead of core Wayland's wl_shell
. To cite Jasper St. Pierre, one of Wayland developers, "xdg-shell is a direct replacement for wl_shell
. wl_shell_surface
had a number of frustrating limitations, and due to its inclusion in the Wayland 1.0 core, it is harder to change and make better."
There are a few things to understand here.
First of all, xdg-shell is a protocol extension. It is not a part of the Wayand core protocol. Therefore, the wayland-client library that we've been using doesn't provide direct support for it. However, since Wayland was designed from the start to be extensible, it's possible (and fairly easy) to automatically generate all the additional code that will allow our program to use xdg-shell the same way we use core Wayland.
Second, xdg-shell is (yet) an unstable extension. Newer versions are not guaranteed to be compatible with older versions or vice versa. You'll have to use the exact version your compositor supports, which is at the moment likely xdg-shell-unstable-v6 (and that's what I'm going to use in this book).
Wayland has a built-in mechanism for the client and the server to negotiate which versions of which interfaces they support (version
argument in the wl_registry.global
event and wl_registry.bind
request), but it only works for backwards-compatible changes. Unstable protocol extensions instead incorporate their version into the interface naming in order to make it completely impossible to use a different version. For example, the interface that's meant to be called xdg_shell
is actually called zxdg_shell_v6
. I'm going to still refer to it as xdg_sell
in this book.
To see what version of xdg-shell your compositor supports (if any), list all globals advertised by the registry, either by running weston-info
or using your own program like the one we wrote in the last section.
You can find the XML protocol definition of xdg-shell in the freedesktop's wayland-protocols repo. Specifically, here's xdg-shell-unstable-v6.xml
. You can either download that file manually or use your system's local copy, if it's present (mine is at /usr/share/wayland-protocols/unstable/xdg-shell/xdg-shell-unstable-v6.xml
). Next, use the wayland-scanner
tool to generate the .h
and .c
files:
$ wayland-scanner client-header xdg-shell-unstable-v6.xml xdg-shell.h
$ wayland-scanner code xdg-shell-unstable-v6.xml xdg-shell.c
Include the generated xdg-shell.h
header alongside wayland-client.h
in main.c
:
#include <wayland-client.h>
#include "xdg-shell.h"
and compile it all together with
$ gcc main.c xdg-shell.c -l wayland-client -o runme
It should compile without errors and display the same black square as before (because we haven't changed anything yet).
This is starting to get repetitive, so let's write a makefile. Put this into Makefile
:
runme: main.c xdg-shell.h xdg-shell.c
gcc main.c xdg-shell.c -l wayland-client -o runme
xdg-shell-unstable-v6.xml:
curl -O https://cgit.freedesktop.org/wayland/wayland-protocols/plain/unstable/xdg-shell/xdg-shell-unstable-v6.xml
xdg-shell.h: xdg-shell-unstable-v6.xml
wayland-scanner client-header xdg-shell-unstable-v6.xml xdg-shell.h
xdg-shell.c: xdg-shell-unstable-v6.xml
wayland-scanner code xdg-shell-unstable-v6.xml xdg-shell.c
.PHONY: clean
clean:
rm runme xdg-shell.c xdg-shell.h xdg-shell-unstable-v6.xml
(adapt it to use your system's copy of xdg-shell-unstable-v6.xml
if you want to.) Now, you can compile and run it with just
$ make
$ ./runme
Let's start actually using xdg-shell. First, let's bind the global xdg_shell
interface instead of wl_shell
:
struct wl_compositor *compositor;
struct wl_shm *shm;
struct zxdg_shell_v6 *xdg_shell;
void registry_global_handler
(
void *data,
struct wl_registry *registry,
uint32_t name,
const char *interface,
uint32_t version
) {
if (strcmp(interface, "wl_compositor") == 0) {
compositor = wl_registry_bind(registry, name,
&wl_compositor_interface, 3);
} else if (strcmp(interface, "wl_shm") == 0) {
shm = wl_registry_bind(registry, name,
&wl_shm_interface, 1);
} else if (strcmp(interface, "zxdg_shell_v6") == 0) {
xdg_shell = wl_registry_bind(registry, name,
&zxdg_shell_v6_interface, 1);
}
}
Next, let's replace wl_shell
calls with the corresponding xdg-shell ones:
struct wl_surface *surface = wl_compositor_create_surface(compositor);
struct zxdg_surface_v6 *xdg_surface =
zxdg_shell_v6_get_xdg_surface(xdg_shell, surface);
struct zxdg_toplevel_v6 *xdg_toplevel =
zxdg_surface_v6_get_toplevel(xdg_surface);
(Notice how only the interfaces have z*_v6
in their names, whereas their methods don't.)
Unlike with wl_shell_surface
, where a surface is given the role of a shell surface and then additionally set to be top-level, in xdg-shell xdg_surface
itself is not a role, but xdg_toplevel
is. If you think about it in terms of inheritance, we have wl_surface
, then xdg_surface
, then xdg_toplevel
.
If you compile and run the program now, you're going to get an error:
$ make
$ ./runme
wl_surface@3: error 3: buffer committed to unconfigured xdg_surface
That happens because xdg-shell requires an xdg_surface
to be configured before you can commit a buffer to the corresponding wl_surface
. We need to handle xdg_toplevel.configure
event, which is basically the compositor telling us what size should we draw our surface (it's just a hint, we're free to ignore it) and whether the surface is currently maximized, fullscreen, active and so on (so that we can decide how to draw window decorations). We also need to handle the xdg_shell.configure
event, which is a clever way to make everything atomic and race-free.
void xdg_toplevel_configure_handler
(
void *data,
struct zxdg_toplevel_v6 *xdg_toplevel,
int32_t width,
int32_t height,
struct wl_array *states
) {
printf("configure: %dx%d\n", width, height);
}
void xdg_toplevel_close_handler
(
void *data,
struct zxdg_toplevel_v6 *xdg_toplevel
) {
printf("close\n");
}
const struct zxdg_toplevel_v6_listener xdg_toplevel_listener = {
.configure = xdg_toplevel_configure_handler,
.close = xdg_toplevel_close_handler
};
void xdg_surface_configure_handler
(
void *data,
struct zxdg_surface_v6 *xdg_surface,
uint32_t serial
) {
zxdg_surface_v6_ack_configure(xdg_surface, serial);
}
const struct zxdg_surface_v6_listener xdg_surface_listener = {
.configure = xdg_surface_configure_handler
};
For this simple example, we ignore all the hints given to us in xdg_toplevel.configure
, but correctly acknowledge getting xdg_surface.configure
with xdg_surface.ack_configure
. We also wrote a stub xdg_toplevel.close
handler, because wayland-client requires you to provide handlers for all events issued by an interface when you add a listener. We'll implement proper closing and shutting down later.
Next, in main()
:
struct wl_surface *surface = wl_compositor_create_surface(compositor);
struct zxdg_surface_v6 *xdg_surface =
zxdg_shell_v6_get_xdg_surface(xdg_shell, surface);
zxdg_surface_v6_add_listener(xdg_surface, &xdg_surface_listener, NULL);
struct zxdg_toplevel_v6 *xdg_toplevel =
zxdg_surface_v6_get_toplevel(xdg_surface);
zxdg_toplevel_v6_add_listener(xdg_toplevel, &xdg_toplevel_listener, NULL);
// signal that the surface is ready to be configured
wl_surface_commit(surface);
// ...
// create a pool
// create a buffer
// ...
// wait for the surface to be configured
wl_display_roundtrip(display);
wl_surface_attach(surface, buffer, 0, 0);
wl_surface_commit(surface);
while (1) {
wl_display_dispatch(display);
}
In addition to setting up the listeners, we have also made two important changes. First, we call wl_surface.commit
right away, prior to attaching any buffer to it. This causes the compositor to issue the appropriate configure
events. We wait for them to be received and handled with an additional wl_display_roundtrip()
before we attach the buffer.
This should be enough to successfully display the black square, but while we're at it, let's also implement responding to the compositor's pings, so that it doesn't deem our program unresponsive:
void xdg_shell_ping_handler
(
void *data,
struct zxdg_shell_v6 *xdg_shell,
uint32_t serial
) {
zxdg_shell_v6_pong(xdg_shell, serial);
printf("ping-pong\n");
}
const struct zxdg_shell_v6_listener xdg_shell_listener = {
.ping = xdg_shell_ping_handler
};
and
// wait for the "initial" set of globals to appear
wl_display_roundtrip(display);
zxdg_shell_v6_add_listener(xdg_shell, &xdg_shell_listener, NULL);
in main()
.
Compile and run it:
$ make
$ ./runme
configure: 0x0
configure: 0x0
ping-pong
^C
We got our black square back, with xdg-shell this time! We can see that the compositor doesn't provide us with any useful size hints (but it will if you try to snap the window maximized, for example). It also pings our program sometimes (in the case of GNOME Shell, it's when a window gets selected in the Overview).