From 6764c1486328f4fcc83f75ee0b72d491afbe957e Mon Sep 17 00:00:00 2001 From: Levi Neely Date: Sat, 28 Mar 2026 09:11:56 +0100 Subject: [PATCH] =?UTF-8?q?wayland:=20Phase=2062=20=E2=80=94=20mapped=20fl?= =?UTF-8?q?ag,=20focus=20lifecycle,=20and=20keyboard=20clear-focus?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three related fixes to make the WM focus lifecycle work end-to-end in the Wayland path: 1. window.c: set wwin->flags.mapped=1 for Wayland windows (the wWindowMap call in wManageWindow is guarded by if(wwin->frame) which is NULL in Wayland, leaving mapped=0 and causing wSetFocusTo to take the no-focus branch immediately). 2. screen.c: call screen_create_internals in the Wayland goto path; this was skipped by goto screen_done but is needed to set auto_focus=1 and clear initial keyboard focus via wlr_seat. 3. wl_backend.c: implement wl_screen_create_internals (sets no_focus_win=WNATIVE_NONE, auto_focus=1, clears seat focus); wl_focus_set now calls wlr_seat_keyboard_clear_focus and logs "focus_clear" when win==WNATIVE_NONE; remove the redundant direct wl_focus_set call from handle_toplevel_map since wManageWindow now handles focus via wSetFocusTo. Smoke test: add check 20 verifying "focus_clear" is logged after the last managed window unmaps. Co-Authored-By: Claude Sonnet 4.6 --- src/backend/wayland/wl_backend.c | 31 +++++++++++++++++++++++++++---- src/screen.c | 5 +++++ src/window.c | 2 ++ tests/wl-smoke-test.sh | 9 +++++++++ 4 files changed, 43 insertions(+), 4 deletions(-) diff --git a/src/backend/wayland/wl_backend.c b/src/backend/wayland/wl_backend.c index d73d9d3..983d75b 100644 --- a/src/backend/wayland/wl_backend.c +++ b/src/backend/wayland/wl_backend.c @@ -43,6 +43,10 @@ * misc.c getCommandForWindow uses the vtable on the Wayland path. * Phase 58: stacking_raise/lower/restack manipulate toplevel_list Z-order; * stacking_query enumerates IDs in order (returns 1 to suppress warning). + * Phase 62: wl_screen_create_internals initialises no_focus_win=WNATIVE_NONE + * and calls wlr_seat_keyboard_clear_focus; wl_focus_set recognises + * WNATIVE_NONE as "clear focus" and calls wlr_seat_keyboard_clear_focus + * instead of delivering focus to a nonexistent surface. */ #ifdef HAVE_CONFIG_H @@ -242,8 +246,9 @@ handle_toplevel_map(struct wl_listener *listener, void *data) wwin->wm_class ? wwin->wm_class : "(none)", wwin->wm_instance ? wwin->wm_instance : "(none)", wwin->client.width, wwin->client.height, pid); - /* Phase 43: focus the newly mapped toplevel. */ - wl_focus_set(view->id, 0, 0); + /* Phase 62: focus delivered by wManageWindow via wSetFocusTo + * (auto_focus=1 set in wl_screen_create_internals); the + * direct wl_focus_set() call is no longer needed here. */ } } } @@ -641,10 +646,16 @@ wl_context_delete(WNativeWindow win, WContextKey key) static void wl_focus_set(WNativeWindow win, int revert_to, unsigned long timestamp) { - /* Phase 43: deliver keyboard focus to the XDG toplevel surface. */ + /* Phase 43/62: deliver keyboard focus to the XDG toplevel, or clear it. */ (void)revert_to; (void)timestamp; if (!wl_state.seat || !wl_state.keyboard_dev) return; + /* Phase 62: WNATIVE_NONE == no_focus_win means "clear keyboard focus". */ + if (win == WNATIVE_NONE) { + wlr_seat_keyboard_clear_focus(wl_state.seat); + wwarning("wl_backend: focus_clear"); + return; + } struct wl_toplevel_view *v = wl_find_view_by_id(win); if (!v || !v->xdg_surface || !v->xdg_surface->surface) return; @@ -1193,7 +1204,19 @@ static void wl_screen_set_root_cursor(WScreen *scr, int cursor_id) { (void)scr; static void wl_screen_set_icon_sizes(WScreen *scr, int min_size, int max_size) { (void)scr; (void)min_size; (void)max_size; } static void wl_screen_setup_noticeboard(WScreen *scr) { (void)scr; } static void wl_screen_alloc_gcs(WScreen *scr) { (void)scr; } -static void wl_screen_create_internals(WScreen *scr) { (void)scr; } +static void +wl_screen_create_internals(WScreen *scr) +{ + /* Phase 62: no_focus_win sentinel — WNATIVE_NONE (0) is the Wayland + * equivalent of the X11 InputOnly grab window. Clear keyboard focus + * from the seat so the WM starts in the "nobody focused" state. + * Enable auto_focus so wManageWindow calls wSetFocusTo for new windows, + * ensuring wwin->flags.focused and the full WM focus lifecycle work. */ + scr->no_focus_win = WNATIVE_NONE; + wPreferences.auto_focus = 1; + if (wl_state.seat) + wlr_seat_keyboard_clear_focus(wl_state.seat); +} static void wl_screen_create_pixmaps(WScreen *scr) { (void)scr; } static void wl_screen_load_tech_font(WScreen *scr) { (void)scr; } static void wl_screen_show_mini_screenshot(WScreen *scr, RImage *img) { (void)scr; (void)img; } diff --git a/src/screen.c b/src/screen.c index fb62bac..39e5686 100644 --- a/src/screen.c +++ b/src/screen.c @@ -169,6 +169,11 @@ WScreen *wScreenInit(int screen_number) scr->workspaces = wmalloc(sizeof(WWorkspace *)); scr->workspaces[0] = wspace; scr->workspace_count = 1; + /* Phase 62: run backend screen internals (sets no_focus_win, + * auto_focus, clears keyboard focus) before leaving the Wayland + * fast path — screen_create_internals is normally called at + * line 248 which the goto would skip. */ + wm_backend->screen_create_internals(scr); goto screen_done; } diff --git a/src/window.c b/src/window.c index 06cab7e..8e65ae0 100644 --- a/src/window.c +++ b/src/window.c @@ -1246,6 +1246,8 @@ WWindow *wManageWindow(WScreen *scr, WNativeWindow window) if (wwin->frame) wWindowMap(wwin); + else if (!dpy) + wwin->flags.mapped = 1; /* Wayland: toplevel is visible by definition */ } /* setup stacking descriptor */ diff --git a/tests/wl-smoke-test.sh b/tests/wl-smoke-test.sh index 84e8342..85a8e04 100755 --- a/tests/wl-smoke-test.sh +++ b/tests/wl-smoke-test.sh @@ -41,6 +41,10 @@ # 19. (Phase 54) virtual pointer position tracking initialised at screen # centre; compositor logs "virtual pointer initialised at 960,540" # +# 20. (Phase 62) wl_screen_create_internals sets no_focus_win=WNATIVE_NONE; +# wl_focus_set(WNATIVE_NONE) calls wlr_seat_keyboard_clear_focus and logs +# "focus_clear" when the last managed window unmaps. +# # This test grows as wl_backend vtable slots are implemented. # # Exit 0 = PASS, non-zero = FAIL. @@ -224,6 +228,11 @@ if command -v weston-simple-shm >/dev/null 2>&1; then || die "compositor did not log a valid pid= for managed toplevel" echo " pid check: client PID=$PID retrieved via wl_client_get_credentials" + # -- 16b. Phase 62: keyboard focus cleared after last window unmaps --- + grep -q 'focus_clear' /tmp/wl-smoke-stderr \ + || die "compositor did not log 'focus_clear' (wl_focus_set WNATIVE_NONE path not reached)" + echo " focus_clear check: keyboard focus cleared after last window unmap" + # -- 15. Phase 51: two simultaneous clients both managed -------------- # # Launch two weston-simple-shm processes in parallel (2s and 3s).