From 06251b3e47caf0f115f1fa8c755bc9b27e2ccb5b Mon Sep 17 00:00:00 2001 From: Ben Leppke Date: Tue, 27 Oct 2026 12:30:00 -0500 Subject: [PATCH 1/2] refactor(active-hint): extract border stacking helper Move the border z-order bookkeeping out of restack() into stack_border_above_window(). No behavior change. --- src/window/window.ts | 127 ++++++++++++++++++++++--------------------- 1 file changed, 65 insertions(+), 62 deletions(-) diff --git a/src/window/window.ts b/src/window/window.ts index 70bf587..f5d60d5 100644 --- a/src/window/window.ts +++ b/src/window/window.ts @@ -680,81 +680,84 @@ export class ShellWindow { if (!parent) return GLib.SOURCE_REMOVE; this.update_border_layout(); + this.stack_border_above_window(actor, parent, border); - if (border.get_parent() !== parent) { - if (border.get_parent()) { - border.get_parent()!.remove_child(border); - } - parent.add_child(border); - } + return GLib.SOURCE_REMOVE; + }; + + const old_id = this._restack_id; + this._restack_id = null; + if (old_id !== null) lib.later_remove(old_id); + if (immediate) { + action(); + } else { + id = lib.later_add(Meta.LaterType.BEFORE_REDRAW, action); + this._restack_id = id; + } + } - parent.set_child_above_sibling(border, actor); + /** Places the border above its window, below always-on-top windows and managed popups. */ + stack_border_above_window(actor: any, parent: any, border: any) { + if (border.get_parent() !== parent) { + if (border.get_parent()) { + border.get_parent()!.remove_child(border); + } + parent.add_child(border); + } + parent.set_child_above_sibling(border, actor); - for (const above_actor of (global as any).get_window_actors()) { - const meta = above_actor.get_meta_window(); - if (!meta || !meta.is_above()) continue; - if (above_actor !== actor && above_actor.get_parent() === parent) { - parent.set_child_below_sibling(border, above_actor); - break; - } + for (const above_actor of (global as any).get_window_actors()) { + const meta = above_actor.get_meta_window(); + if (!meta || !meta.is_above()) continue; + if (above_actor !== actor && above_actor.get_parent() === parent) { + parent.set_child_below_sibling(border, above_actor); + break; } + } - const siblings: Array = parent.get_children(); - const candidates: Set = new Set(); - - for (const window of this.ext.windows.values()) { - const trans_parent = window.meta.get_transient_for(); - if (!trans_parent) continue; - const parent_actor = trans_parent.get_compositor_private() as any; - if (parent_actor !== actor) continue; - const window_actor = window.meta.get_compositor_private() as any; - if (window_actor && window_actor.get_parent() === parent) { - candidates.add(window_actor); - } + const siblings: Array = parent.get_children(); + const candidates: Set = new Set(); + + for (const window of this.ext.windows.values()) { + const trans_parent = window.meta.get_transient_for(); + if (!trans_parent) continue; + const parent_actor = trans_parent.get_compositor_private() as any; + if (parent_actor !== actor) continue; + const window_actor = window.meta.get_compositor_private() as any; + if (window_actor && window_actor.get_parent() === parent) { + candidates.add(window_actor); } + } - for (const popup_actor of (global as any).get_window_actors()) { - const wtype = popup_actor.get_meta_window()?.get_window_type(); - if ( - wtype === Meta.WindowType.DROPDOWN_MENU || - wtype === Meta.WindowType.POPUP_MENU || - wtype === Meta.WindowType.COMBO || - wtype === Meta.WindowType.TOOLTIP - ) { - if (popup_actor.get_parent() === parent) { - candidates.add(popup_actor); - } + for (const popup_actor of (global as any).get_window_actors()) { + const wtype = popup_actor.get_meta_window()?.get_window_type(); + if ( + wtype === Meta.WindowType.DROPDOWN_MENU || + wtype === Meta.WindowType.POPUP_MENU || + wtype === Meta.WindowType.COMBO || + wtype === Meta.WindowType.TOOLTIP + ) { + if (popup_actor.get_parent() === parent) { + candidates.add(popup_actor); } } + } - if (candidates.size > 0) { - // Find the candidate with the lowest index among siblings (closest to bottom). - let lowest: any = null; - let lowest_idx = siblings.length; - for (const candidate of candidates) { - const idx = siblings.indexOf(candidate); - if (idx !== -1 && idx < lowest_idx) { - lowest_idx = idx; - lowest = candidate; - } - } - if (lowest !== null) { - parent.set_child_below_sibling(border, lowest); + if (candidates.size > 0) { + // Find the candidate with the lowest index among siblings (closest to bottom). + let lowest: any = null; + let lowest_idx = siblings.length; + for (const candidate of candidates) { + const idx = siblings.indexOf(candidate); + if (idx !== -1 && idx < lowest_idx) { + lowest_idx = idx; + lowest = candidate; } } - - return GLib.SOURCE_REMOVE; - }; - - const old_id = this._restack_id; - this._restack_id = null; - if (old_id !== null) lib.later_remove(old_id); - if (immediate) { - action(); - } else { - id = lib.later_add(Meta.LaterType.BEFORE_REDRAW, action); - this._restack_id = id; + if (lowest !== null) { + parent.set_child_below_sibling(border, lowest); + } } } From 37e8be76e77b106bf755501d1e6610a18e00a0e0 Mon Sep 17 00:00:00 2001 From: Ben Leppke Date: Tue, 27 Oct 2026 12:35:00 -0500 Subject: [PATCH 2/2] fix(active-hint): restack focused hint below non-grabbing popups Some Wayland popups, such as Firefox menus, do not take a seat grab. Mutter therefore does not focus and raise them above the foreign hint actor, leaving the hint drawn on top of the popup. The existing window_created restack can run before the popup actor is in the window stack. Re-stack the focused hint from display::restacked, after Mutter has finished ordering. GTK menus take a seat grab and are raised above the hint, so they are unaffected. Focused window only; overlay-all is unchanged. --- src/extension.ts | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/extension.ts b/src/extension.ts index 667a6b8..3ed4460 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1504,6 +1504,21 @@ export class Ext extends Ecs.System { } } + private reassert_focused_hint_stack() { + if (!this.settings.active_hint()) return; + if (this._bordered_entity === null) return; + + const win = this.windows.get(this._bordered_entity); + if (!win || !win.border || !win.border.visible) return; + if (win.meta.is_fullscreen() || win.meta.minimized) return; + + const actor = win.meta.get_compositor_private() as any; + const parent = actor?.get_parent(); + if (!actor || !parent) return; + + win.stack_border_above_window(actor, parent, win.border); + } + hide_all_borders(instant: boolean = false) { for (const window of this.windows.values()) { window.hide_border(); @@ -2824,6 +2839,11 @@ export class Ext extends Ecs.System { this.register({ tag: 3, window }); }); + // Non-grabbing popups are not raised above the hint; re-stack after ordering. + this.connect(display as any, 'restacked', () => { + this.reassert_focused_hint_stack(); + }); + // GNOME 40 removed the first argument of the callback this.connect(display as any, 'grab-op-begin', (_display, win, op) => { this.on_grab_start(win, op);