From 814bacd6974c61fe1e127851c36628bc4eeb4c98 Mon Sep 17 00:00:00 2001 From: ebenali <54529923+ebenali@users.noreply.github.com> Date: Sun, 21 Jun 2026 17:17:56 +0000 Subject: [PATCH] fix: don't toggle fullscreen/maximize on client request; advertise pointer caps MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two bugs, the first explaining the "wbconf/GTK windows explode and lose their titlebar when dragged" report. 1) Client fullscreen/maximize requests were TOGGLED instead of applied. xdg_toplevel_request_fullscreen did `set_fullscreen(!current)` and request_maximize did `set_maximized(!full)`. wlroots raises these events for BOTH set and unset, with the wanted value in requested.{fullscreen,maximized}. GTK proactively sends unset_fullscreen while managing its window state, so a GTK window being dragged emitted `request_fullscreen` with requested=false on a non-fullscreen window — which the toggle turned INTO fullscreen. The window blew up to fill the screen and GTK hid its titlebar (fullscreen). Reproduced deterministically (see below). Honour requested.fullscreen / requested.maximized instead of toggling, and make set_toplevel_fullscreen a no-op when the state is unchanged + skip redundant maximize requests, so a redundant unset can't corrupt geometry by restoring an unset restore-rect. The keybinding actions (ToggleMaximize / Fullscreen) still toggle, as intended. 2) Virtual pointers never advertised seat pointer capability. The virtual-pointer protocol attaches its device via handle_new_pointer directly, bypassing new_input_notify — the only place that called wlr_seat_set_capabilities. So with only a virtual pointer (e.g. the headless backend the tests use), the seat advertised no pointer and NO client ever received pointer events (compositor-handled input like SSD buttons still worked, which masked it). Factor the capability update into a helper and call it from the virtual-pointer and virtual-keyboard paths too. This also makes client input scriptable from the command line via the virtual-pointer protocol, which is how bug #1 was finally reproduced. Verified: built a toolkit-free xdg client and a zwlr_virtual_pointer drag tool; before the fix, dragging a GTK header bar logged `fullscreen-on 1280x720`; after, the window stays put. Full suite 27 green (g++/ASan, incl. the maximize/ fullscreen window_state integration test); clang + release clean. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- waybox/seat.cpp | 21 ++++++++++++++++----- waybox/xdg_shell.cpp | 30 ++++++++++++++++++++++-------- 2 files changed, 38 insertions(+), 13 deletions(-) diff --git a/waybox/seat.cpp b/waybox/seat.cpp index 3178eec..6513980 100644 --- a/waybox/seat.cpp +++ b/waybox/seat.cpp @@ -665,6 +665,19 @@ static void handle_new_pointer(struct wb_server *server, struct wlr_input_device wlr_cursor_attach_input_device(server->cursor->cursor, device); } +/* Advertise the seat's input capabilities to clients. waybox always owns a + * cursor, so POINTER is always advertised; KEYBOARD is advertised once at least + * one keyboard exists. This must run whenever an input device is added through + * ANY path — including the virtual-pointer/virtual-keyboard protocols, which + * bypass new_input_notify — otherwise clients never receive input from a + * virtual device (e.g. under the headless backend used by the tests). */ +static void update_seat_capabilities(struct wb_server *server) { + uint32_t caps = WL_SEAT_CAPABILITY_POINTER; + if (!wl_list_empty(&server->seat->keyboards)) + caps |= WL_SEAT_CAPABILITY_KEYBOARD; + wlr_seat_set_capabilities(server->seat->seat, caps); +} + static void new_input_notify(struct wb_server *server, void *data) { struct wlr_input_device *device = static_cast(data); switch (device->type) { @@ -681,11 +694,7 @@ static void new_input_notify(struct wb_server *server, void *data) { break; } - uint32_t caps = WL_SEAT_CAPABILITY_POINTER; - if (!wl_list_empty(&server->seat->keyboards)) { - caps |= WL_SEAT_CAPABILITY_KEYBOARD; - } - wlr_seat_set_capabilities(server->seat->seat, caps); + update_seat_capabilities(server); } void seat_focus_surface(struct wb_seat *seat, struct wlr_surface *surface) { @@ -731,6 +740,7 @@ struct wb_seat *wb_seat_create(struct wb_server *server) { auto *virtual_keyboard = static_cast(data); handle_new_keyboard(server, &virtual_keyboard->keyboard.base); + update_seat_capabilities(server); }); server->virtual_pointer_manager = wlr_virtual_pointer_manager_v1_create(server->wl_display); @@ -740,6 +750,7 @@ struct wb_seat *wb_seat_create(struct wb_server *server) { auto *event = static_cast(data); handle_new_pointer(server, &event->new_pointer->pointer.base); + update_seat_capabilities(server); }); seat->seat = wlr_seat_create(server->wl_display, "seat0"); diff --git a/waybox/xdg_shell.cpp b/waybox/xdg_shell.cpp index 7de2278..7b68b43 100644 --- a/waybox/xdg_shell.cpp +++ b/waybox/xdg_shell.cpp @@ -751,6 +751,12 @@ void set_toplevel_maximized(struct wb_toplevel *toplevel, bool horz, bool vert) void set_toplevel_fullscreen(struct wb_toplevel *toplevel, bool fullscreen) { if (!toplevel->xdg_toplevel->base->initialized) return; + /* No-op if the state is already what was asked for. Clients (notably GTK) + * send redundant unset_fullscreen requests while managing their state; + * acting on them would corrupt geometry (restoring an unset restore rect) + * or needlessly re-fullscreen. */ + if (fullscreen == toplevel->xdg_toplevel->current.fullscreen) + return; if (fullscreen) { toplevel->restore_fullscreen = toplevel->geometry; @@ -788,22 +794,30 @@ void set_toplevel_fullscreen(struct wb_toplevel *toplevel, bool fullscreen) { static void xdg_toplevel_request_fullscreen( struct wb_toplevel *toplevel, void *data) { - /* Toggle fullscreen on the client's request (e.g. a video player). A - * request before the first commit would assert in wlroots, so it is - * ignored until the surface is initialized; the client can re-request. */ + /* Apply the state the client actually asked for. wlroots raises this event + * for both set_fullscreen and unset_fullscreen, with the desired value in + * requested.fullscreen — toggling instead would turn a client's redundant + * "unset fullscreen" (which GTK sends while managing its window state) into + * an unexpected fullscreen, blowing the window up to full screen. A request + * before the first commit would assert in wlroots, so ignore it until the + * surface is initialized; the client can re-request. */ if (!toplevel->xdg_toplevel->base->initialized) return; set_toplevel_fullscreen(toplevel, - !toplevel->xdg_toplevel->current.fullscreen); + toplevel->xdg_toplevel->requested.fullscreen); } static void xdg_toplevel_request_maximize(struct wb_toplevel *toplevel, void *data) { - /* The client asked to (un)maximize, e.g. via its CSD maximize button. - * Toggle full maximize. Ignored until initialized (see above). */ + /* Apply the maximize state the client asked for (requested.maximized), for + * the same reason as fullscreen above: a redundant unset_maximized must not + * be toggled into a maximize. Ignored until initialized. */ if (!toplevel->xdg_toplevel->base->initialized) return; - const bool full = toplevel->max_horz && toplevel->max_vert; - set_toplevel_maximized(toplevel, !full, !full); + bool maximize = toplevel->xdg_toplevel->requested.maximized; + bool currently_full = toplevel->max_horz && toplevel->max_vert; + if (maximize == currently_full) + return; /* redundant request (e.g. GTK's unset while not maximized) */ + set_toplevel_maximized(toplevel, maximize, maximize); } static void xdg_toplevel_request_minimize(struct wb_toplevel *toplevel, void *data) {