From 37997f8dbe1221f19343e6b9fa907ce2b944f1a2 Mon Sep 17 00:00:00 2001 From: Daniel Kinzler Date: Tue, 26 May 2026 18:26:17 +0200 Subject: [PATCH 1/3] Use a timeout callback to wait for changes in window active state to settle. Depending on the backend a window might temporarily become inactive. Fixes an issue where quick-terminal would disappear when opening the surface context menu. --- src/apprt/gtk/class/window.zig | 62 +++++++++++++++++++++++----------- 1 file changed, 42 insertions(+), 20 deletions(-) diff --git a/src/apprt/gtk/class/window.zig b/src/apprt/gtk/class/window.zig index 0c8dfaa7c..7294f2aa3 100644 --- a/src/apprt/gtk/class/window.zig +++ b/src/apprt/gtk/class/window.zig @@ -220,6 +220,9 @@ pub const Window = extern struct { /// behaves slightly differently under certain scenarios. quick_terminal: bool = false, + /// Timeout source to react to this window becoming (in)active. + timeout: ?c_uint = null, + /// The window decoration override. If this is not set then we'll /// inherit whatever the config has. This allows overriding the /// config on a per-window basis. @@ -855,6 +858,35 @@ pub const Window = extern struct { } } + fn onTimeout(ud: ?*anyopaque) callconv(.c) c_int { + const self: *Self = @ptrCast(@alignCast(ud orelse return 0)); + const priv = self.private(); + priv.timeout = null; + + // Hide quick-terminal if set to autohide + if (self.isQuickTerminal()) { + if (self.getConfig()) |cfg| { + if (cfg.get().@"quick-terminal-autohide" and + self.as(gtk.Window).isActive() == 0 and + self.as(gtk.Widget).isVisible() == 1) + { + self.toggleVisibility(); + } + } + } + + // Don't change urgency if we're not the active window. + if (self.as(gtk.Window).isActive() == 0) return 0; + + self.winproto().setUrgent(false) catch |err| { + log.warn( + "winproto failed to reset urgency={}", + .{err}, + ); + }; + return 0; + } + //--------------------------------------------------------------- // Properties @@ -1076,27 +1108,17 @@ pub const Window = extern struct { _: *gobject.ParamSpec, self: *Self, ) callconv(.c) void { - // Hide quick-terminal if set to autohide - if (self.isQuickTerminal()) { - if (self.getConfig()) |cfg| { - if (cfg.get().@"quick-terminal-autohide" and - self.as(gtk.Window).isActive() == 0 and - self.as(gtk.Widget).isVisible() == 1) - { - self.toggleVisibility(); - } - } - } + const priv = self.private(); - // Don't change urgency if we're not the active window. - if (self.as(gtk.Window).isActive() == 0) return; - - self.winproto().setUrgent(false) catch |err| { - log.warn( - "winproto failed to reset urgency={}", - .{err}, - ); - }; + // Use a timeout callback to wait for focus state to settle, + // because depending on the windowing backend the window might + // become inactive and immediately active again. This happens + // e.g. on Wayland when opening a context menu. + if (priv.timeout == null) priv.timeout = glib.timeoutAdd( + 100, + onTimeout, + self, + ); } fn propGdkSurfaceDims( From 1753d57bfdf0ac694ac624e7d63ec9fecd220bc6 Mon Sep 17 00:00:00 2001 From: Daniel Kinzler Date: Thu, 28 May 2026 15:08:12 +0200 Subject: [PATCH 2/3] remove timeout source when window is disposed --- src/apprt/gtk/class/window.zig | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/apprt/gtk/class/window.zig b/src/apprt/gtk/class/window.zig index 7294f2aa3..eede346f3 100644 --- a/src/apprt/gtk/class/window.zig +++ b/src/apprt/gtk/class/window.zig @@ -1237,6 +1237,13 @@ pub const Window = extern struct { fn dispose(self: *Self) callconv(.c) void { const priv = self.private(); + if (priv.timeout) |v| { + if (glib.Source.remove(v) == 0) { + log.warn("unable to remove timeout source", .{}); + } + priv.timeout = null; + } + priv.command_palette.set(null); if (priv.config) |v| { From ff963f3119bffbd9366a5b7d98fbcaba06fc9f05 Mon Sep 17 00:00:00 2001 From: Daniel Kinzler Date: Fri, 29 May 2026 17:40:25 +0200 Subject: [PATCH 3/3] Renamed timeout source and callback function. Added comment explaining timeout delay. --- src/apprt/gtk/class/window.zig | 44 ++++++++++++++++++++++++---------- 1 file changed, 32 insertions(+), 12 deletions(-) diff --git a/src/apprt/gtk/class/window.zig b/src/apprt/gtk/class/window.zig index eede346f3..2a1a5435a 100644 --- a/src/apprt/gtk/class/window.zig +++ b/src/apprt/gtk/class/window.zig @@ -221,7 +221,7 @@ pub const Window = extern struct { quick_terminal: bool = false, /// Timeout source to react to this window becoming (in)active. - timeout: ?c_uint = null, + handle_active_state_source: ?c_uint = null, /// The window decoration override. If this is not set then we'll /// inherit whatever the config has. This allows overriding the @@ -858,10 +858,13 @@ pub const Window = extern struct { } } - fn onTimeout(ud: ?*anyopaque) callconv(.c) c_int { + /// Callback to handle this window becoming active or inactive. + /// Triggered by propIsActive with a timeout to debounce temporary + /// changes in active state. + fn handleActiveState(ud: ?*anyopaque) callconv(.c) c_int { const self: *Self = @ptrCast(@alignCast(ud orelse return 0)); const priv = self.private(); - priv.timeout = null; + priv.handle_active_state_source = null; // Hide quick-terminal if set to autohide if (self.isQuickTerminal()) { @@ -1113,12 +1116,29 @@ pub const Window = extern struct { // Use a timeout callback to wait for focus state to settle, // because depending on the windowing backend the window might // become inactive and immediately active again. This happens - // e.g. on Wayland when opening a context menu. - if (priv.timeout == null) priv.timeout = glib.timeoutAdd( - 100, - onTimeout, - self, - ); + // e.g. on Wayland when opening a context menu or a submenu + // inside a context menu. + if (priv.handle_active_state_source == null) { + priv.handle_active_state_source = glib.timeoutAddFull( + // Use priority of an idle callback instead of the higher + // default timeout priority. This allows us to use a shorter + // timeout duration. + glib.PRIORITY_DEFAULT_IDLE, + // 50ms was chosen to be conservative. From testing we know + // that, depending on the backend and system performance, a + // shorter timeout or just an idle callback can be enough for + // the focus to settle. On the other hand a delay of e.g. 10ms + // does not work reliably on some slow systems. The downside + // of a high value is that some operations in handleActiveState, + // e.g. hiding the quick-terminal, will be visibly delayed. + // However, 50ms should barely be noticeable. We can change + // this in the future if necessary. + 50, + handleActiveState, + self, + null, + ); + } } fn propGdkSurfaceDims( @@ -1237,11 +1257,11 @@ pub const Window = extern struct { fn dispose(self: *Self) callconv(.c) void { const priv = self.private(); - if (priv.timeout) |v| { + if (priv.handle_active_state_source) |v| { if (glib.Source.remove(v) == 0) { - log.warn("unable to remove timeout source", .{}); + log.warn("unable to remove handle active state source", .{}); } - priv.timeout = null; + priv.handle_active_state_source = null; } priv.command_palette.set(null);