diff --git a/kick.c b/kick.c index 343ee7a..c513209 100644 --- a/kick.c +++ b/kick.c @@ -34,7 +34,8 @@ #include #include -#include "xdg-decoration-client.h" +#include + #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 diff --git a/meson.build b/meson.build index 84c6aca..6fa35e3 100644 --- a/meson.build +++ b/meson.build @@ -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',