From 2f3814ca5e6cfcbd504ff86b8120f7e6b7266f56 Mon Sep 17 00:00:00 2001 From: Tim Culverhouse Date: Sun, 26 Jul 2026 13:45:32 -0500 Subject: [PATCH] gtk: honor suspended window state GTK exposes the Wayland xdg_toplevel suspended state when the compositor knows a window is not visible. Ghostty previously only used widget map state, so it could continue rendering a mapped surface on an inactive workspace or behind other windows. Combine the mapped and suspended states for surface occlusion and update all displayed surfaces whenever the toplevel suspension state changes. Co-authored-by: Amp Amp-Thread-ID: https://ampcode.com/threads/T-019fa965-aa5f-7099-85b4-a9679d2c8bd3 --- src/apprt/gtk/class/surface.zig | 16 +++++++++++++--- src/apprt/gtk/class/window.zig | 22 ++++++++++++++++++++++ 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/src/apprt/gtk/class/surface.zig b/src/apprt/gtk/class/surface.zig index 3a950a6aa..c902f0fe3 100644 --- a/src/apprt/gtk/class/surface.zig +++ b/src/apprt/gtk/class/surface.zig @@ -3324,7 +3324,7 @@ pub const Surface = extern struct { self: *Self, ) callconv(.c) void { self.updateMapped(true); - self.updateOcclusion(true); + self.updateOcclusion(); } fn glareaUnmap( @@ -3332,7 +3332,7 @@ pub const Surface = extern struct { self: *Self, ) callconv(.c) void { self.updateMapped(false); - self.updateOcclusion(false); + self.updateOcclusion(); } fn updateMapped(self: *Self, mapped: bool) void { @@ -3341,13 +3341,23 @@ pub const Surface = extern struct { self.as(gobject.Object).notifyByPspec(properties.mapped.impl.param_spec); } - fn updateOcclusion(self: *Self, visible: bool) void { + /// Update the core surface visibility based on both GTK widget and + /// toplevel state. This is public so the window can call it when its + /// suspended state changes. + pub fn updateOcclusion(self: *Self) void { const surface = self.core() orelse return; + const visible = self.private().mapped and !self.windowSuspended(); surface.occlusionCallback(visible) catch |err| { log.warn("error in occlusion callback err={}", .{err}); }; } + fn windowSuspended(self: *Self) bool { + const native = self.as(gtk.Widget).getNative() orelse return false; + const window = gobject.ext.cast(gtk.Window, native) orelse return false; + return window.isSuspended() != 0; + } + fn glareaRender( _: *gtk.GLArea, _: *gdk.GLContext, diff --git a/src/apprt/gtk/class/window.zig b/src/apprt/gtk/class/window.zig index c34f2cb4f..f9e3b9841 100644 --- a/src/apprt/gtk/class/window.zig +++ b/src/apprt/gtk/class/window.zig @@ -1374,11 +1374,33 @@ pub const Window = extern struct { } } + // Notify every displayed surface when the compositor changes the + // Wayland xdg_toplevel suspended state. + _ = gobject.Object.signals.notify.connect( + self.as(gtk.Window), + *Self, + propSuspended, + self, + .{ .detail = "suspended" }, + ); + // When we are realized we always setup our appearance since this // calls some winproto functions. self.syncAppearance(); } + fn propSuspended( + _: *gtk.Window, + _: *gobject.ParamSpec, + self: *Self, + ) callconv(.c) void { + const tab = self.getSelectedTab() orelse return; + const tree = tab.getSurfaceTree() orelse return; + + var it = tree.iterator(); + while (it.next()) |entry| entry.view.updateOcclusion(); + } + fn btnNewTab(_: *adw.SplitButton, self: *Self) callconv(.c) void { self.performBindingAction(.new_tab); }