wayland: Phase 62 — mapped flag, focus lifecycle, and keyboard clear-focus

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 <noreply@anthropic.com>
This commit is contained in:
Levi Neely 2026-03-28 09:11:56 +01:00
parent 04f8a46ae5
commit 6764c14863
4 changed files with 43 additions and 4 deletions

View File

@ -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; }

View File

@ -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;
}

View File

@ -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 */

View File

@ -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).