view: avoid repeated focus changes unminimizing parent/child views

When unminimizing a group of N parent/child views, we currently end up
triggering N focus changes (as well as O(N^2) surface restackings) due
to calling desktop_focus_view() for each view in turn.

Since desktop_focus_view() already raises all sibling views together
via view_move_to_front(), let's make view_minimize() call it only once
at the very end, once all views are visible.

Test cases:
- Audacious with floating plugin views (XWayland)
- xfce4-terminal with About dialog (xdg-shell)

v2: also avoid repeated focus changes when minimizing
This commit is contained in:
John Lindgren 2026-01-16 00:59:39 -05:00 committed by Johan Malm
parent 7d7ece21d9
commit cd291fe051
2 changed files with 49 additions and 28 deletions

View file

@ -11,6 +11,11 @@ view_impl_map(struct view *view)
{ {
view_update_visibility(view); view_update_visibility(view);
/* Leave minimized, if minimized before map */
if (!view->minimized) {
desktop_focus_view(view, /* raise */ true);
}
if (!view->been_mapped) { if (!view->been_mapped) {
window_rules_apply(view, LAB_WINDOW_RULE_EVENT_ON_FIRST_MAP); window_rules_apply(view, LAB_WINDOW_RULE_EVENT_ON_FIRST_MAP);
} }
@ -41,6 +46,19 @@ view_impl_unmap(struct view *view)
{ {
view_update_visibility(view); view_update_visibility(view);
/*
* When exiting an xwayland application with multiple views
* mapped, a race condition can occur: after the topmost view
* is unmapped, the next view under it is offered focus, but is
* also unmapped before accepting focus (so server->active_view
* remains NULL). To avoid being left with no active view at
* all, check for that case also.
*/
struct server *server = view->server;
if (view == server->active_view || !server->active_view) {
desktop_focus_topmost_view(server);
}
/* /*
* Destroy the foreign toplevel handle so the unmapped view * Destroy the foreign toplevel handle so the unmapped view
* doesn't show up in panels and the like. * doesn't show up in panels and the like.

View file

@ -746,7 +746,7 @@ view_adjust_size(struct view *view, int *w, int *h)
} }
static void static void
_minimize(struct view *view, bool minimized) _minimize(struct view *view, bool minimized, bool *need_refocus)
{ {
assert(view); assert(view);
if (view->minimized == minimized) { if (view->minimized == minimized) {
@ -759,8 +759,15 @@ _minimize(struct view *view, bool minimized)
view->minimized = minimized; view->minimized = minimized;
wl_signal_emit_mutable(&view->events.minimized, NULL); wl_signal_emit_mutable(&view->events.minimized, NULL);
view_update_visibility(view); view_update_visibility(view);
/*
* Need to focus a different view when:
* - minimizing the active view
* - unminimizing any mapped view
*/
*need_refocus |= (minimized ?
(view == view->server->active_view) : view->mapped);
} }
static void static void
@ -773,7 +780,7 @@ view_append_children(struct view *view, struct wl_array *children)
} }
static void static void
minimize_sub_views(struct view *view, bool minimized) minimize_sub_views(struct view *view, bool minimized, bool *need_refocus)
{ {
struct view **child; struct view **child;
struct wl_array children; struct wl_array children;
@ -781,8 +788,8 @@ minimize_sub_views(struct view *view, bool minimized)
wl_array_init(&children); wl_array_init(&children);
view_append_children(view, &children); view_append_children(view, &children);
wl_array_for_each(child, &children) { wl_array_for_each(child, &children) {
_minimize(*child, minimized); _minimize(*child, minimized, need_refocus);
minimize_sub_views(*child, minimized); minimize_sub_views(*child, minimized, need_refocus);
} }
wl_array_release(&children); wl_array_release(&children);
} }
@ -797,8 +804,10 @@ void
view_minimize(struct view *view, bool minimized) view_minimize(struct view *view, bool minimized)
{ {
assert(view); assert(view);
struct server *server = view->server;
bool need_refocus = false;
if (view->server->input_mode == LAB_INPUT_STATE_CYCLE) { if (server->input_mode == LAB_INPUT_STATE_CYCLE) {
wlr_log(WLR_ERROR, "not minimizing window while window switching"); wlr_log(WLR_ERROR, "not minimizing window while window switching");
return; return;
} }
@ -809,8 +818,20 @@ view_minimize(struct view *view, bool minimized)
* 'open file' dialog), so it saves trying to unmap them twice * 'open file' dialog), so it saves trying to unmap them twice
*/ */
struct view *root = view_get_root(view); struct view *root = view_get_root(view);
_minimize(root, minimized); _minimize(root, minimized, &need_refocus);
minimize_sub_views(root, minimized); minimize_sub_views(root, minimized, &need_refocus);
/*
* Update focus only at the end to avoid repeated focus changes.
* desktop_focus_view() will raise all sibling views together.
*/
if (need_refocus) {
if (minimized) {
desktop_focus_topmost_view(server);
} else {
desktop_focus_view(view, /* raise */ true);
}
}
} }
bool bool
@ -2383,30 +2404,12 @@ view_update_visibility(struct view *view)
} }
wlr_scene_node_set_enabled(&view->scene_tree->node, visible); wlr_scene_node_set_enabled(&view->scene_tree->node, visible);
struct server *server = view->server;
if (visible) {
desktop_focus_view(view, /*raise*/ true);
} else {
/*
* When exiting an xwayland application with multiple
* views mapped, a race condition can occur: after the
* topmost view is unmapped, the next view under it is
* offered focus, but is also unmapped before accepting
* focus (so server->active_view remains NULL). To avoid
* being left with no active view at all, check for that
* case also.
*/
if (view == server->active_view || !server->active_view) {
desktop_focus_topmost_view(server);
}
}
/* /*
* Show top layer when a fullscreen view is hidden. * Show top layer when a fullscreen view is hidden.
* Hide it if a fullscreen view is shown (or uncovered). * Hide it if a fullscreen view is shown (or uncovered).
*/ */
desktop_update_top_layer_visibility(server); desktop_update_top_layer_visibility(view->server);
/* /*
* We may need to disable adaptive sync if view was fullscreen. * We may need to disable adaptive sync if view was fullscreen.
@ -2421,7 +2424,7 @@ view_update_visibility(struct view *view)
/* Update usable area to account for XWayland "struts" (panels) */ /* Update usable area to account for XWayland "struts" (panels) */
if (view_has_strut_partial(view)) { if (view_has_strut_partial(view)) {
output_update_all_usable_areas(server, false); output_update_all_usable_areas(view->server, false);
} }
} }