draw window decorations via libdecor so GNOME windows move and resize

The host window previously asked for server-side decoration; Mutter
forces client-side, so the window had no frame at all and could not be
dragged or resized.  Hand the window to libdecor: it negotiates a
server-side bar when the host offers one and otherwise draws its own
titlebar with move/resize/close buttons (GNOME).
This commit is contained in:
Andrew Hamilton 2026-08-20 20:44:43 +10:00
parent 75735e38f4
commit 548d6da88e
2 changed files with 120 additions and 86 deletions

194
kick.c
View file

@ -34,7 +34,8 @@
#include <wayland-egl.h>
#include <wayland-server.h>
#include "xdg-decoration-client.h"
#include <libdecor.h>
#include "xdg-shell-client.h"
#include "wlr-data-control-client.h"
@ -303,10 +304,12 @@ struct kick_host {
struct wl_compositor *compositor;
struct wl_surface *surface;
struct xdg_wm_base *wm_base;
struct xdg_surface *xdg_surface;
struct xdg_toplevel *toplevel;
struct zxdg_decoration_manager_v1 *decoration;
struct zxdg_toplevel_decoration_v1 *decoration_obj;
/* The nested window is a libdecor frame around our wl_surface: libdecor
owns the xdg_surface/xdg_toplevel plus the window decorations,
negotiating server-side decoration when the host offers it and drawing
its own titlebar (with move/resize/close) otherwise (GNOME). */
struct libdecor *libdecor;
struct libdecor_frame *frame;
struct wl_seat *seat;
struct wl_keyboard *keyboard;
struct wl_pointer *pointer;
@ -523,6 +526,7 @@ static void kick_request_redraw(void) {
wlr_output_schedule_frame(server.output);
}
static struct kick_app *app_for_toplevel(struct wlr_xdg_toplevel *toplevel);
static void host_teardown_window(void);
static int app_index(struct kick_app *app);
static struct kick_app *pick_successor(struct kick_app *app,
int prefer_right);
@ -2149,8 +2153,7 @@ static void handle_toplevel_destroy(struct wl_listener *listener, void *data) {
set_active_app(succ);
} else {
/* last tab closed: close the host window (nested only) and exit */
if (host.toplevel) xdg_toplevel_destroy(host.toplevel);
if (host.surface) wl_surface_destroy(host.surface);
host_teardown_window();
shutdown_cleanup();
exit(0);
}
@ -2211,8 +2214,7 @@ static void close_active_tab(void) {
/* Last live tab: tear the session down now. */
kick_log(KICK_LOG_INFO, "kick: closing tab %d (pid %d)\n",
died, (int)shown);
if (host.toplevel) xdg_toplevel_destroy(host.toplevel);
if (host.surface) wl_surface_destroy(host.surface);
host_teardown_window();
shutdown_cleanup();
exit(0);
}
@ -2246,11 +2248,11 @@ static void handle_request_fullscreen(struct wl_listener *listener, void *data)
bool fs = toplevel->requested.fullscreen;
wlr_xdg_toplevel_set_fullscreen(toplevel, fs);
kick_log(KICK_LOG_INFO, "client fullscreen=%d\n", fs);
if (host.toplevel) {
if (host.frame) {
if (fs)
xdg_toplevel_set_fullscreen(host.toplevel, NULL);
libdecor_frame_set_fullscreen(host.frame, NULL);
else
xdg_toplevel_unset_fullscreen(host.toplevel);
libdecor_frame_unset_fullscreen(host.frame);
}
}
@ -4060,9 +4062,6 @@ static void host_registry_global(void *data, struct wl_registry *registry,
} else if (strcmp(interface, xdg_wm_base_interface.name) == 0) {
host.wm_base = wl_registry_bind(registry, name,
&xdg_wm_base_interface, version < 1 ? version : 1);
} else if (strcmp(interface, zxdg_decoration_manager_v1_interface.name) == 0) {
host.decoration = wl_registry_bind(registry, name,
&zxdg_decoration_manager_v1_interface, 1);
} else if (strcmp(interface, wl_seat_interface.name) == 0) {
/* Bind the seat and rely on the capabilities event (which the
compositor always sends after a bind) to (re)create the keyboard
@ -4140,17 +4139,6 @@ static const struct xdg_wm_base_listener host_xdg_wm_listener = {
.ping = host_xdg_wm_ping,
};
static void host_xdg_surface_configure(void *data, struct xdg_surface *s,
uint32_t serial)
{
(void)data;
xdg_surface_ack_configure(s, serial);
}
static const struct xdg_surface_listener host_xdg_surface_listener = {
.configure = host_xdg_surface_configure,
};
static void host_resize(int width, int height) {
if (width < 50) width = 50;
if (height < 50) height = 50;
@ -4167,52 +4155,89 @@ static void host_resize(int width, int height) {
kick_request_redraw();
}
static void host_xdg_toplevel_configure(void *data, struct xdg_toplevel *t,
int32_t width, int32_t height,
struct wl_array *states)
{
(void)data; (void)t;
host.fullscreen = false;
uint32_t *state;
wl_array_for_each(state, states)
if (*state == XDG_TOPLEVEL_STATE_FULLSCREEN)
host.fullscreen = true;
/* Mirror the host window's state onto the clients: fullscreen begets
fullscreen, anything else begets maximized. */
if (host.fullscreen != server.clients_fullscreen) {
kick_log(KICK_LOG_INFO, "host fullscreen=%d\n", host.fullscreen);
clients_set_fullscreen(host.fullscreen);
/* Tear down the nested window: drop the libdecor frame (and its decoration
subsurfaces) and the content surface. Only called from the exit paths, so
no ack/commit dance is needed. */
static void host_teardown_window(void) {
if (host.frame) {
libdecor_frame_unref(host.frame);
host.frame = NULL;
}
if (host.surface) {
wl_surface_destroy(host.surface);
host.surface = NULL;
}
}
static void host_frame_configure(struct libdecor_frame *frame,
struct libdecor_configuration *config,
void *data)
{
(void)data;
int width = 0, height = 0;
if (!libdecor_configuration_get_content_size(config, frame, &width, &height)) {
/* The initial configure commonly offers no size (the compositor
lets a CSD client pick one). The old xdg_toplevel configure
handler also treated 0x0 as "keep the current size": commit the
current layout anyway so libdecor gets a real sized configure
once we present a buffer. */
width = host.width;
height = host.height;
}
enum libdecor_window_state state = 0;
bool have_state = libdecor_configuration_get_window_state(config, &state);
if (have_state) {
bool fs = (state & LIBDECOR_WINDOW_STATE_FULLSCREEN) != 0;
if (fs != host.fullscreen) {
kick_log(KICK_LOG_INFO, "host fullscreen=%d\n", fs);
clients_set_fullscreen(fs);
}
host.fullscreen = fs;
}
/* libdecor reports the *content* size; the host window wraps that content
with its decorations. The content is exactly our wlr output. */
if (width > 0 && height > 0)
host_resize(width, height);
host.configured = true;
struct libdecor_state *st = libdecor_state_new(width, height);
libdecor_frame_commit(frame, st, config);
libdecor_state_free(st);
if (host.surface)
wl_surface_commit(host.surface);
}
static void host_xdg_toplevel_close(void *data, struct xdg_toplevel *t) {
(void)data; (void)t;
static void host_frame_close(struct libdecor_frame *frame, void *data) {
(void)frame; (void)data;
kick_log(KICK_LOG_INFO, "kick: window closed\n");
host_teardown_window();
shutdown_cleanup();
exit(0);
}
static const struct xdg_toplevel_listener host_xdg_toplevel_listener = {
.configure = host_xdg_toplevel_configure,
.close = host_xdg_toplevel_close,
};
static void host_decoration_configure(void *data,
struct zxdg_toplevel_decoration_v1 *dec,
uint32_t mode)
{
(void)data; (void)dec;
kick_log(KICK_LOG_INFO, "kick: host decoration mode=%u (%s)\n",
mode,
mode == ZXDG_TOPLEVEL_DECORATION_V1_MODE_SERVER_SIDE
? "server-side" : "client-side");
static void host_frame_commit(struct libdecor_frame *frame, void *data) {
/* Decoration via (synchronous) subsurfaces: libdecor asks us to commit the
content surface so the decoration appears atomically with the content. */
(void)frame; (void)data;
if (host.surface)
wl_surface_commit(host.surface);
}
static const struct zxdg_toplevel_decoration_v1_listener host_decoration_listener = {
.configure = host_decoration_configure,
static struct libdecor_frame_interface host_frame_iface = {
.configure = host_frame_configure,
.close = host_frame_close,
.commit = host_frame_commit,
};
static void host_libdecor_error(struct libdecor *ctx, enum libdecor_error error,
const char *message)
{
(void)ctx;
fprintf(stderr, "kick: libdecor error %d: %s\n", (int)error,
message ? message : "(null)");
}
static struct libdecor_interface host_libdecor_iface = {
.error = host_libdecor_error,
};
/* ---------- host keyboard ---------- */
@ -5432,25 +5457,31 @@ static bool host_init(void) {
}
host.surface = wl_compositor_create_surface(host.compositor);
host.xdg_surface = xdg_wm_base_get_xdg_surface(host.wm_base, host.surface);
host.toplevel = xdg_surface_get_toplevel(host.xdg_surface);
xdg_toplevel_set_title(host.toplevel, "kick");
if (host.decoration) {
/* Ask the host compositor (labwc) for a server-side title bar, so
the nested window always has a host-drawn decoration above our
own tab content (including full-bleed clients like the test). */
host.decoration_obj = zxdg_decoration_manager_v1_get_toplevel_decoration(
host.decoration, host.toplevel);
zxdg_toplevel_decoration_v1_add_listener(host.decoration_obj,
&host_decoration_listener, NULL);
zxdg_toplevel_decoration_v1_set_mode(host.decoration_obj,
ZXDG_TOPLEVEL_DECORATION_V1_MODE_SERVER_SIDE);
host.libdecor = libdecor_new(host.display, &host_libdecor_iface);
if (!host.libdecor) {
fprintf(stderr, "kick: libdecor failed to initialise\n");
return false;
}
xdg_surface_add_listener(host.xdg_surface, &host_xdg_surface_listener, NULL);
xdg_toplevel_add_listener(host.toplevel, &host_xdg_toplevel_listener, NULL);
host.frame = libdecor_decorate(host.libdecor, host.surface,
&host_frame_iface, NULL);
if (!host.frame) {
fprintf(stderr, "kick: libdecor_decorate failed\n");
return false;
}
libdecor_frame_set_title(host.frame, "kick");
libdecor_frame_set_app_id(host.frame, "kick");
/* Ask libdecor for a frame around our content. It negotiates xdg
decorations with the host: a server-side title bar when the host offers
one (labwc, sway), its own client-side title bar (drawing in a
subsurface above our content) otherwise -- GNOME/Mutter forces CSD, so
the window becomes draggable and resizable through that bar. */
libdecor_frame_map(host.frame);
/* libdecor pongs pings on its own wm_base binding; ours is separate, so
keep ponging the ones addressed to it or the host declares us hung. */
xdg_wm_base_add_listener(host.wm_base, &host_xdg_wm_listener, NULL);
wl_surface_commit(host.surface);
wl_display_roundtrip(host.display);
libdecor_dispatch(host.libdecor, 0);
/* Host keyboard/pointer are created by the seat capabilities handler
during the registry round-trip above; nothing more to fetch here. */
@ -5950,8 +5981,7 @@ static void sweep_dead_startup_groups(void) {
set_active_app(succ);
else {
/* the last tab died before ever connecting: tear down */
if (host.toplevel) xdg_toplevel_destroy(host.toplevel);
if (host.surface) wl_surface_destroy(host.surface);
host_teardown_window();
shutdown_cleanup();
exit(0);
}
@ -6731,25 +6761,30 @@ int main(int argc, char *argv[]) {
}
}
/* Nested event loop: service both the server and the host displays */
/* Nested event loop: service the server, the host display, and libdecor
(whose decoration plugin runs its own event handling). */
int server_fd = wl_event_loop_get_fd(server.loop);
int host_fd = wl_display_get_fd(host.display);
int decor_fd = host.libdecor ? libdecor_get_fd(host.libdecor) : -1;
for (;;) {
wl_display_dispatch_pending(host.display);
libdecor_dispatch(host.libdecor, 0);
/* Never block in a host read: use the non-blocking prepare/read
pattern so the server loop (headless frame timer) is always
serviced while waiting on the host socket. */
if (wl_display_prepare_read(host.display) == 0) {
wl_display_flush(host.display);
struct pollfd fds[2] = {
struct pollfd fds[3] = {
{ .fd = server_fd, .events = POLLIN },
{ .fd = host_fd, .events = POLLIN },
{ .fd = decor_fd, .events = POLLIN },
};
int nfds = decor_fd >= 0 ? 3 : 2;
int ret;
do {
ret = poll(fds, 2, -1);
ret = poll(fds, nfds, -1);
} while (ret < 0 && errno == EINTR);
if (ret > 0 && (fds[1].revents & (POLLIN | POLLERR)))
@ -6765,6 +6800,7 @@ int main(int argc, char *argv[]) {
wl_display_flush_clients(server.wl_display);
sweep_dead_startup();
wl_display_dispatch_pending(host.display);
libdecor_dispatch(host.libdecor, 0);
/* Render only when something changed (kick's own redraws or scene
damage) and no page flip is in flight; an idle screen never

View file

@ -32,6 +32,8 @@ deps = [
dependency('librsvg-2.0'),
# Direct pixman use in kick.c (pointer-constraint region extents).
dependency('pixman-1'),
# Server-side/client-side window decorations via libdecor (nested mode).
dependency('libdecor-0'),
]
cc = meson.get_compiler('c')
@ -57,18 +59,14 @@ wayland_scanner_dep = dependency('wayland-scanner', native: true)
wayland_scanner = find_program(
wayland_scanner_dep.get_variable('wayland_scanner'))
# (input xml, output base name). The output base names keep the committed
# suffixes: xdg-decoration is the unstable-v1 protocol but is still named
# plain "xdg-decoration" because the sources #include those exact names.
# (input xml, output base name). The output base names keep historical
# suffixes: xdg-shell is generated plain because the sources #include that
# exact name.
protocols = {
'xdg-shell': [
wl_protocols_dir / 'stable/xdg-shell/xdg-shell.xml',
'xdg-shell',
],
'xdg-decoration': [
wl_protocols_dir / 'unstable/xdg-decoration/xdg-decoration-unstable-v1.xml',
'xdg-decoration',
],
'wlr-data-control-unstable-v1': [
'wlr-protocols/wlr-data-control-unstable-v1.xml',
'wlr-data-control',