From aafd2de5b71af84ed1c6ba7dccaeab4e5c044161 Mon Sep 17 00:00:00 2001 From: Andrew Hamilton Date: Wed, 26 Aug 2026 10:11:18 +1000 Subject: [PATCH] contain client fullscreen inside kick; decouple from the host window a client's fullscreen request used to be mirrored onto the host window via libdecor, but labwc 0.9.8 expires its 100 ms configure deadline under load and then applies "move but keep old size" permanently -- the enlarged image anchored at the old origin with black margins. the mirror is gone: a client's fullscreen now fills kick's output only, leaving the host window alone. fullscreening the host remains an explicit act (prefix+F11 or the compositor), and the host-to-clients propagation is unchanged. --- README.md | 19 +++++++++++-------- kick.c | 15 ++++++++------- tests/test_smoke.py | 22 +++++++++++++--------- 3 files changed, 32 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index 60296d6..be1bb10 100644 --- a/README.md +++ b/README.md @@ -268,16 +268,19 @@ browser's `F11`). ## Fullscreen -Fullscreen mirrors between the host window and the clients: +## Fullscreen + +Fullscreening the kick window and fullscreening a client are independent: - Fullscreen the kick window (your host compositor's fullscreen keybinding, - an app's own `F11`, or `Alt+F11`) and every window of every tab -- mains - and stacked tab-group members alike -- is told it is fullscreen, so - clients drop their decorations and fill the screen. -- A client that requests fullscreen itself (e.g. a video player) also - fullscreens the kick host window. -- Leaving fullscreen returns every window to maximized (X11 tab-group - members keep their own geometry). + or `Alt+F11`) and every window of every tab -- mains and stacked + tab-group members alike -- is told it is fullscreen, so clients drop + their decorations and fill the screen. +- A client that requests fullscreen itself (e.g. a video player) fills the + kick window's content only: the host window itself stays exactly where + and how it is. Use `Alt+F11` as well if you want the whole monitor. +- Leaving host fullscreen returns every window to maximized (X11 + tab-group members keep their own geometry). ## Standalone mode diff --git a/kick.c b/kick.c index f689af5..ae9dce2 100644 --- a/kick.c +++ b/kick.c @@ -2642,12 +2642,13 @@ static void handle_request_fullscreen(struct wl_listener *listener, void *data) if (!toplevel->base->initialized) return; /* wlroots 0.19 emits request_fullscreen with NULL data; the direction lives in toplevel->requested.fullscreen. Honor the client's request - and mirror it onto the host window: a fullscreen client fills the - host window, and an un-fullscreen client puts it back. */ + within kick only: the window fills our output while the host window + itself is left alone. Fullscreening the host window is a separate, + explicit act (prefix+F11 or the compositor), which propagates back + down through clients_set_fullscreen(). */ bool fs = toplevel->requested.fullscreen; wlr_xdg_toplevel_set_fullscreen(toplevel, fs); kick_log(KICK_LOG_INFO, "client fullscreen=%d\n", fs); - host_queue_fullscreen(fs); } /* Propagate the host window's fullscreen state onto every client -- each @@ -3644,11 +3645,10 @@ static void handle_xs_request_fullscreen(struct wl_listener *listener, struct kick_app *app = wl_container_of(listener, app, request_fullscreen); struct wlr_xwayland_surface *xs = app->xsurface; if (!xs) return; - /* Mirror onto the host window like the xdg path. */ + /* Same containment as the xdg path: fullscreen stays inside kick. */ bool fs = xs->fullscreen; wlr_xwayland_surface_set_fullscreen(xs, fs); kick_log(KICK_LOG_INFO, "client fullscreen=%d\n", fs); - host_queue_fullscreen(fs); } static void handle_xs_request_configure(struct wl_listener *listener, @@ -5766,6 +5766,7 @@ static void host_teardown_window(void) { static void host_frame_configure(struct libdecor_frame *frame, struct libdecor_configuration *config, void *data) +{ (void)data; int width = 0, height = 0; bool size_ok = @@ -6361,8 +6362,8 @@ static bool process_key_event(uint32_t time, uint32_t key, uint32_t state, /* Function keys are not grabbed (prefix+F11 above is the one exception): they pass through to the focused client, which may have its own bindings (e.g. a browser's F11 fullscreen request, which - handle_request_fullscreen mirrors onto the host window). Frame dumps - are available via SIGUSR1 instead. */ + handle_request_fullscreen contains inside kick). Frame dumps are + available via SIGUSR1 instead. */ /* Alt+Up / Alt+Down cycle tabs. Grabbed so the focused client never sees them. The host sends a single physical press, so repeats are synthesized by kbd_repeat_start(); subsequent host presses of an diff --git a/tests/test_smoke.py b/tests/test_smoke.py index fcfab11..d01992c 100644 --- a/tests/test_smoke.py +++ b/tests/test_smoke.py @@ -1484,18 +1484,22 @@ def test_function_keys_pass_through(session): assert session.proc.poll() is None -def test_client_fullscreen_round_trip(session): - """A client's fullscreen request is honored and mirrored onto the host - window; the host configure then propagates fullscreen to every tab. - wltest toggles xdg_toplevel fullscreen on the 'F' key, so tap it and - watch both directions of the mirror.""" +def test_client_fullscreen_stays_inside_kick(session): + """A client's fullscreen request is honored within kick but never + mirrored onto the host window: wltest toggles xdg_toplevel fullscreen + on the 'F' key, so tap it and check only the client-side state moves.""" session.wait_any_active() session.tap(F) - session.wait_for("host fullscreen=1") - session.wait_for("clients fullscreen=1") + session.wait_for("client fullscreen=1") + time.sleep(0.6) # give a wrong mirror time to show up in the log + assert "host fullscreen=" not in session.read_log(), \ + "client fullscreen leaked onto the host window" + assert "clients fullscreen=" not in session.read_log(), \ + "client fullscreen propagated to other tabs" session.tap(F) - session.wait_for("host fullscreen=0") - session.wait_for("clients fullscreen=0") + session.wait_for("client fullscreen=0") + time.sleep(0.4) + assert "host fullscreen=" not in session.read_log() assert session.proc.poll() is None