desktop: enforce followMouse focus policy more strictly

With followMouse=yes in the config, a client activation request was
still allowed to focus a view that wasn't under the mouse cursor, but
it would then be immediately unfocused again. With some clients using
modal dialogs (e.g. Scribus), this could lead to an infinite cycle of
the client and labwc continually switching the focus back and forth.

To break the cycle, only allow focusing a view under the cursor when
when followMouse=yes.

v2: allow focusing a new/raised view appearing under the cursor
v3: allow focusing a sub-view/sibling of the requested view
v4: fix new xdg-shell views under mouse not being focused

Fixes: #2722
This commit is contained in:
John Lindgren 2025-06-12 10:55:56 -04:00
parent 5a50a02ba3
commit 34892c35be
6 changed files with 66 additions and 30 deletions

View file

@ -476,10 +476,12 @@ void xdg_shell_finish(struct server *server);
void desktop_focus_view(struct view *view, bool raise); void desktop_focus_view(struct view *view, bool raise);
/** /**
* desktop_focus_view_or_surface() - like desktop_focus_view() but can * desktop_focus_for_cursor_update() - like desktop_focus_view() but can
* also focus other (e.g. xwayland-unmanaged) surfaces * also focus other (e.g. xwayland-unmanaged) surfaces.
*
* Used only for cursor-driven focus updates.
*/ */
void desktop_focus_view_or_surface(struct seat *seat, struct view *view, void desktop_focus_for_cursor_update(struct seat *seat, struct view *view,
struct wlr_surface *surface, bool raise); struct wlr_surface *surface, bool raise);
void desktop_arrange_all_views(struct server *server); void desktop_arrange_all_views(struct server *server);

View file

@ -39,8 +39,27 @@ desktop_arrange_all_views(struct server *server)
} }
} }
void static void
desktop_focus_view(struct view *view, bool raise) set_or_offer_focus(struct view *view)
{
struct seat *seat = &view->server->seat;
switch (view_wants_focus(view)) {
case VIEW_WANTS_FOCUS_ALWAYS:
if (view->surface != seat->seat->keyboard_state.focused_surface) {
seat_focus_surface(seat, view->surface);
}
break;
case VIEW_WANTS_FOCUS_LIKELY:
case VIEW_WANTS_FOCUS_UNLIKELY:
view_offer_focus(view);
break;
case VIEW_WANTS_FOCUS_NEVER:
break;
}
}
static void
_desktop_focus_view(struct view *view, bool raise, bool for_cursor_update)
{ {
assert(view); assert(view);
/* /*
@ -77,34 +96,40 @@ desktop_focus_view(struct view *view, bool raise)
workspaces_switch_to(view->workspace, /*update_focus*/ false); workspaces_switch_to(view->workspace, /*update_focus*/ false);
} }
struct seat *seat = &view->server->seat;
switch (view_wants_focus(view)) {
case VIEW_WANTS_FOCUS_ALWAYS:
if (view->surface != seat->seat->keyboard_state.focused_surface) {
seat_focus_surface(seat, view->surface);
}
break;
case VIEW_WANTS_FOCUS_LIKELY:
case VIEW_WANTS_FOCUS_UNLIKELY:
view_offer_focus(view);
break;
case VIEW_WANTS_FOCUS_NEVER:
break;
}
if (raise) { if (raise) {
view_move_to_front(view); view_move_to_front(view);
} }
/*
* When followMouse=yes, only allow focusing a view that's under
* the cursor (after unminimizing and potentially raising it).
* Note that view_move_to_front() may also have raised sub- or
* sibling views. In that case, focus the one under the cursor.
*/
if (rc.focus_follow_mouse && !for_cursor_update) {
struct cursor_context ctx = get_cursor_context(view->server);
if (ctx.view && view_get_root(ctx.view) == view_get_root(view)) {
set_or_offer_focus(ctx.view);
}
} else {
set_or_offer_focus(view);
}
}
void
desktop_focus_view(struct view *view, bool raise)
{
_desktop_focus_view(view, raise, /* for_cursor_update */ false);
} }
/* TODO: focus layer-shell surfaces also? */ /* TODO: focus layer-shell surfaces also? */
void void
desktop_focus_view_or_surface(struct seat *seat, struct view *view, desktop_focus_for_cursor_update(struct seat *seat, struct view *view,
struct wlr_surface *surface, bool raise) struct wlr_surface *surface, bool raise)
{ {
assert(view || surface); assert(view || surface);
if (view) { if (view) {
desktop_focus_view(view, raise); _desktop_focus_view(view, raise, /* for_cursor_update */ true);
#if HAVE_XWAYLAND #if HAVE_XWAYLAND
} else { } else {
struct wlr_xwayland_surface *xsurface = struct wlr_xwayland_surface *xsurface =

View file

@ -629,7 +629,7 @@ cursor_process_motion(struct server *server, uint32_t time, double *sx, double *
* If followMouse=yes, update the keyboard focus when the * If followMouse=yes, update the keyboard focus when the
* cursor enters a surface * cursor enters a surface
*/ */
desktop_focus_view_or_surface(seat, desktop_focus_for_cursor_update(seat,
view_from_wlr_surface(new_focused_surface), view_from_wlr_surface(new_focused_surface),
new_focused_surface, rc.raise_on_focus); new_focused_surface, rc.raise_on_focus);
} }
@ -649,7 +649,7 @@ _cursor_update_focus(struct server *server)
* Always focus the surface below the cursor when * Always focus the surface below the cursor when
* followMouse=yes and followMouseRequiresMovement=no. * followMouse=yes and followMouseRequiresMovement=no.
*/ */
desktop_focus_view_or_surface(&server->seat, ctx.view, desktop_focus_for_cursor_update(&server->seat, ctx.view,
ctx.surface, rc.raise_on_focus); ctx.surface, rc.raise_on_focus);
} }
@ -1092,7 +1092,7 @@ cursor_process_button_press(struct seat *seat, uint32_t button, uint32_t time_ms
} }
#ifdef HAVE_XWAYLAND #ifdef HAVE_XWAYLAND
} else if (ctx.type == LAB_SSD_UNMANAGED) { } else if (ctx.type == LAB_SSD_UNMANAGED) {
desktop_focus_view_or_surface(seat, NULL, ctx.surface, desktop_focus_for_cursor_update(seat, NULL, ctx.surface,
/*raise*/ false); /*raise*/ false);
#endif #endif
} }

View file

@ -11,7 +11,6 @@
void void
view_impl_map(struct view *view) view_impl_map(struct view *view)
{ {
desktop_focus_view(view, /*raise*/ true);
view_update_title(view); view_update_title(view);
view_update_app_id(view); view_update_app_id(view);
if (!view->been_mapped) { if (!view->been_mapped) {

View file

@ -217,8 +217,18 @@ handle_commit(struct wl_listener *listener, void *data)
} }
if (update_required) { if (update_required) {
bool was_empty = wlr_box_empty(current);
view_impl_apply_geometry(view, size.width, size.height); view_impl_apply_geometry(view, size.width, size.height);
/*
* Try to focus the view once it has a valid surface
* size. Before this point, the under-mouse checks in
* desktop_focus_view() fail with focus-follows-mouse.
*/
if (was_empty && !wlr_box_empty(&size)) {
desktop_focus_view(view, /*raise*/ true);
}
/* /*
* Some views (e.g., terminals that scale as multiples of rows * Some views (e.g., terminals that scale as multiples of rows
* and columns, or windows that impose a fixed aspect ratio), * and columns, or windows that impose a fixed aspect ratio),

View file

@ -826,14 +826,14 @@ xwayland_view_map(struct view *view)
/* /*
* If the view was focused (on the xwayland server side) before * If the view was focused (on the xwayland server side) before
* being mapped, update the seat focus now. Note that this only * being mapped, just update the seat focus. Otherwise, try to
* really matters in the case of Globally Active input windows. * focus the view now.
* In all other cases, it's redundant since view_impl_map()
* results in the view being focused anyway.
*/ */
if (xwayland_view->focused_before_map) { if (xwayland_view->focused_before_map) {
xwayland_view->focused_before_map = false; xwayland_view->focused_before_map = false;
seat_focus_surface(&view->server->seat, view->surface); seat_focus_surface(&view->server->seat, view->surface);
} else {
desktop_focus_view(view, /*raise*/ true);
} }
view_impl_map(view); view_impl_map(view);