From 9e2e99c55fb5f2c0709938eb590b62112f3d7446 Mon Sep 17 00:00:00 2001 From: Leah Amelia Chen Date: Wed, 18 Mar 2026 02:07:28 +0800 Subject: [PATCH 1/4] gtk/wayland: replace KDE blur with ext-background-effect-v1 The venerable KDE blur protocol has been replaced with the compositor- agnostic ext-background-effect-v1 protocol, to be implemented by Niri and others. The new protocol is much easier to use overall, though we do need to calculate the blur region manually like X11. --- build.zig.zon | 4 +- build.zig.zon.json | 5 ++ build.zig.zon.nix | 8 ++ build.zig.zon.txt | 1 + flatpak/zig-packages.json | 6 ++ src/apprt/gtk/class/window.zig | 21 +----- src/apprt/gtk/winproto/wayland.zig | 85 +++++++++++++++++----- src/apprt/gtk/winproto/wayland/Globals.zig | 26 ++++++- src/build/SharedDeps.zig | 6 +- 9 files changed, 116 insertions(+), 46 deletions(-) diff --git a/build.zig.zon b/build.zig.zon index 7a669a4a1..b0b66a052 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -91,8 +91,8 @@ .lazy = true, }, .wayland_protocols = .{ - .url = "https://deps.files.ghostty.org/wayland-protocols-258d8f88f2c8c25a830c6316f87d23ce1a0f12d9.tar.gz", - .hash = "N-V-__8AAKw-DAAaV8bOAAGqA0-oD7o-HNIlPFYKRXSPT03S", + .url = "https://gitlab.freedesktop.org/wayland/wayland-protocols/-/archive/1.47/wayland-protocols-1.47.tar.gz", + .hash = "N-V-__8AAFdWDwA0ktbNUi9pFBHCRN4weXIgIfCrVjfGxqgA", .lazy = true, }, .plasma_wayland_protocols = .{ diff --git a/build.zig.zon.json b/build.zig.zon.json index 4a88e2017..b4e9dc39c 100644 --- a/build.zig.zon.json +++ b/build.zig.zon.json @@ -139,6 +139,11 @@ "url": "https://deps.files.ghostty.org/wayland-protocols-258d8f88f2c8c25a830c6316f87d23ce1a0f12d9.tar.gz", "hash": "sha256-XO3K3egbdeYPI+XoO13SuOtO+5+Peb16NH0UiusFMPg=" }, + "N-V-__8AAFdWDwA0ktbNUi9pFBHCRN4weXIgIfCrVjfGxqgA": { + "name": "wayland_protocols", + "url": "https://gitlab.freedesktop.org/wayland/wayland-protocols/-/archive/1.47/wayland-protocols-1.47.tar.gz", + "hash": "sha256-3S3xSrX0EDgleq7cxLX7msDuAY8/D5SvkJcCjmDTMiM=" + }, "N-V-__8AAAzZywE3s51XfsLbP9eyEw57ae9swYB9aGB6fCMs": { "name": "wuffs", "url": "https://deps.files.ghostty.org/wuffs-122037b39d577ec2db3fd7b2130e7b69ef6cc1807d68607a7c232c958315d381b5cd.tar.gz", diff --git a/build.zig.zon.nix b/build.zig.zon.nix index 53e1b6c02..a3f18c692 100644 --- a/build.zig.zon.nix +++ b/build.zig.zon.nix @@ -306,6 +306,14 @@ in hash = "sha256-XO3K3egbdeYPI+XoO13SuOtO+5+Peb16NH0UiusFMPg="; }; } + { + name = "N-V-__8AAFdWDwA0ktbNUi9pFBHCRN4weXIgIfCrVjfGxqgA"; + path = fetchZigArtifact { + name = "wayland_protocols"; + url = "https://gitlab.freedesktop.org/wayland/wayland-protocols/-/archive/1.47/wayland-protocols-1.47.tar.gz"; + hash = "sha256-3S3xSrX0EDgleq7cxLX7msDuAY8/D5SvkJcCjmDTMiM="; + }; + } { name = "N-V-__8AAAzZywE3s51XfsLbP9eyEw57ae9swYB9aGB6fCMs"; path = fetchZigArtifact { diff --git a/build.zig.zon.txt b/build.zig.zon.txt index 4ac9e6592..75585dcb8 100644 --- a/build.zig.zon.txt +++ b/build.zig.zon.txt @@ -34,3 +34,4 @@ https://deps.files.ghostty.org/zig_wayland-1b5c038ec10da20ed3a15b0b2a6db1c21383e https://deps.files.ghostty.org/zlib-1220fed0c74e1019b3ee29edae2051788b080cd96e90d56836eea857b0b966742efb.tar.gz https://github.com/ivanstepanovftw/zigimg/archive/d7b7ab0ba0899643831ef042bd73289510b39906.tar.gz https://github.com/ocornut/imgui/archive/refs/tags/v1.92.5-docking.tar.gz +https://gitlab.freedesktop.org/wayland/wayland-protocols/-/archive/1.47/wayland-protocols-1.47.tar.gz diff --git a/flatpak/zig-packages.json b/flatpak/zig-packages.json index e58ecd448..b971163b4 100644 --- a/flatpak/zig-packages.json +++ b/flatpak/zig-packages.json @@ -167,6 +167,12 @@ "dest": "vendor/p/N-V-__8AAKw-DAAaV8bOAAGqA0-oD7o-HNIlPFYKRXSPT03S", "sha256": "5cedcadde81b75e60f23e5e83b5dd2b8eb4efb9f8f79bd7a347d148aeb0530f8" }, + { + "type": "archive", + "url": "https://gitlab.freedesktop.org/wayland/wayland-protocols/-/archive/1.47/wayland-protocols-1.47.tar.gz", + "dest": "vendor/p/N-V-__8AAFdWDwA0ktbNUi9pFBHCRN4weXIgIfCrVjfGxqgA", + "sha256": "dd2df14ab5f41038257aaedcc4b5fb9ac0ee018f3f0f94af9097028e60d33223" + }, { "type": "archive", "url": "https://deps.files.ghostty.org/wuffs-122037b39d577ec2db3fd7b2130e7b69ef6cc1807d68607a7c232c958315d381b5cd.tar.gz", diff --git a/src/apprt/gtk/class/window.zig b/src/apprt/gtk/class/window.zig index c01cad618..8cef7b765 100644 --- a/src/apprt/gtk/class/window.zig +++ b/src/apprt/gtk/class/window.zig @@ -1071,21 +1071,6 @@ pub const Window = extern struct { self.syncAppearance(); } - fn propGdkSurfaceHeight( - _: *gdk.Surface, - _: *gobject.ParamSpec, - self: *Self, - ) callconv(.c) void { - // X11 needs to fix blurring on resize, but winproto implementations - // could do anything. - self.private().winproto.resizeEvent() catch |err| { - log.warn( - "winproto resize event failed error={}", - .{err}, - ); - }; - } - fn propIsActive( _: *gtk.Window, _: *gobject.ParamSpec, @@ -1111,7 +1096,7 @@ pub const Window = extern struct { }; } - fn propGdkSurfaceWidth( + fn propGdkSurfaceDims( _: *gdk.Surface, _: *gobject.ParamSpec, self: *Self, @@ -1282,14 +1267,14 @@ pub const Window = extern struct { _ = gobject.Object.signals.notify.connect( gdk_surface, *Self, - propGdkSurfaceWidth, + propGdkSurfaceDims, self, .{ .detail = "width" }, ); _ = gobject.Object.signals.notify.connect( gdk_surface, *Self, - propGdkSurfaceHeight, + propGdkSurfaceDims, self, .{ .detail = "height" }, ); diff --git a/src/apprt/gtk/winproto/wayland.zig b/src/apprt/gtk/winproto/wayland.zig index b90e9d115..b7207e545 100644 --- a/src/apprt/gtk/winproto/wayland.zig +++ b/src/apprt/gtk/winproto/wayland.zig @@ -10,6 +10,7 @@ const layer_shell = @import("gtk4-layer-shell"); const wayland = @import("wayland"); const wl = wayland.client.wl; +const ext = wayland.client.ext; const kde = wayland.client.kde; const org = wayland.client.org; const xdg = wayland.client.xdg; @@ -96,8 +97,8 @@ pub const Window = struct { /// The context from the app where we can load our Wayland interfaces. globals: *Globals, - /// A token that, when present, indicates that the window is blurred. - blur_token: ?*org.KdeKwinBlur = null, + /// Object that controls background effects like background blur. + bg_effect: ?*ext.BackgroundEffectSurfaceV1 = null, /// Object that controls the decoration mode (client/server/auto) /// of the window. @@ -148,6 +149,20 @@ pub const Window = struct { break :deco deco; }; + const bg_effect: ?*ext.BackgroundEffectSurfaceV1 = bg: { + const mgr = app.globals.get(.ext_background_effect) orelse + break :bg null; + + const bg_effect: *ext.BackgroundEffectSurfaceV1 = mgr.getBackgroundEffect( + wl_surface, + ) catch |err| { + log.warn("could not create background effect object={}", .{err}); + break :bg null; + }; + + break :bg bg_effect; + }; + if (apprt_window.isQuickTerminal()) { _ = gdk.Surface.signals.enter_monitor.connect( gdk_surface, @@ -163,17 +178,22 @@ pub const Window = struct { .surface = wl_surface, .globals = app.globals, .decoration = deco, + .bg_effect = bg_effect, }; } pub fn deinit(self: Window, alloc: Allocator) void { _ = alloc; - if (self.blur_token) |blur| blur.release(); + if (self.bg_effect) |bg| bg.destroy(); if (self.decoration) |deco| deco.release(); if (self.slide) |slide| slide.release(); } - pub fn resizeEvent(_: *Window) !void {} + pub fn resizeEvent(self: *Window) !void { + self.syncBlur() catch |err| { + log.err("failed to sync blur={}", .{err}); + }; + } pub fn syncAppearance(self: *Window) !void { self.syncBlur() catch |err| { @@ -224,28 +244,53 @@ pub const Window = struct { /// Update the blur state of the window. fn syncBlur(self: *Window) !void { - const manager = self.globals.get(.kde_blur_manager) orelse return; + const compositor = self.globals.get(.compositor) orelse return; + const bg = self.bg_effect orelse return; + if (!self.globals.state.bg_effect_capabilities.blur) return; + const config = if (self.apprt_window.getConfig()) |v| v.get() else return; const blur = config.@"background-blur"; - if (self.blur_token) |tok| { - // Only release token when transitioning from blurred -> not blurred - if (!blur.enabled()) { - manager.unset(self.surface); - tok.release(); - self.blur_token = null; - } - } else { - // Only acquire token when transitioning from not blurred -> blurred - if (blur.enabled()) { - const tok = try manager.create(self.surface); - tok.commit(); - self.blur_token = tok; - } - } + const region = region: { + if (!blur.enabled()) break :region null; + + // NOTE(pluiedev): CSDs are a f--king mistake. + // Please, GNOME, stop this nonsense of making a window ~30% bigger + // internally than how they really are just for your shadows and + // rounded corners and all that fluff. Please. I beg of you. + + const native = self.apprt_window.as(gtk.Native); + const surface = native.getSurface() orelse break :region null; + const region = try compositor.createRegion(); + + var x: f64 = 0; + var y: f64 = 0; + native.getSurfaceTransform(&x, &y); + // Slightly inset the blur region + x += 1; + y += 1; + + var width: f64 = @floatFromInt(surface.getWidth()); + var height: f64 = @floatFromInt(surface.getHeight()); + width -= x * 2; + height -= y * 2; + if (width <= 0 or height <= 0) break :region null; + + // FIXME: Add rounded corners + region.add( + @intFromFloat(x), + @intFromFloat(y), + @intFromFloat(width), + @intFromFloat(height), + ); + break :region region; + }; + errdefer if (region) |r| r.destroy(); + + bg.setBlurRegion(region); } fn syncDecoration(self: *Window) !void { diff --git a/src/apprt/gtk/winproto/wayland/Globals.zig b/src/apprt/gtk/winproto/wayland/Globals.zig index 369d6a473..83052cbeb 100644 --- a/src/apprt/gtk/winproto/wayland/Globals.zig +++ b/src/apprt/gtk/winproto/wayland/Globals.zig @@ -5,6 +5,7 @@ const Allocator = std.mem.Allocator; const wayland = @import("wayland"); const wl = wayland.client.wl; +const ext = wayland.client.ext; const kde = wayland.client.kde; const org = wayland.client.org; const xdg = wayland.client.xdg; @@ -26,7 +27,8 @@ const Binding = struct { }; pub const Tag = enum { - kde_blur_manager, + compositor, + ext_background_effect, kde_decoration_manager, kde_slide_manager, kde_output_order, @@ -34,7 +36,8 @@ pub const Tag = enum { fn Type(comptime self: Tag) type { return switch (self) { - .kde_blur_manager => org.KdeKwinBlurManager, + .compositor => wl.Compositor, + .ext_background_effect => ext.BackgroundEffectManagerV1, .kde_decoration_manager => org.KdeKwinServerDecorationManager, .kde_slide_manager => org.KdeKwinSlideManager, .kde_output_order => kde.OutputOrderV1, @@ -56,6 +59,8 @@ pub const State = struct { default_deco_mode: ?org.KdeKwinServerDecorationManager.Mode = null, + bg_effect_capabilities: ext.BackgroundEffectManagerV1.Capability = .{}, + /// Reset cached state derived from kde_output_order_v1. fn resetOutputOrder(self: *State, alloc: Allocator) void { if (self.primary_output_name) |name| alloc.free(name); @@ -102,6 +107,11 @@ fn onGlobalAttached(self: *Globals, comptime tag: Tag) void { // keeps listener setup and object lifetime in one // place and also supports globals that appear later. switch (tag) { + .ext_background_effect => { + const v = self.get(tag) orelse return; + v.setListener(*Globals, bgEffectListener, self); + self.needs_roundtrip = true; + }, .kde_decoration_manager => { const v = self.get(tag) orelse return; v.setListener(*Globals, decoManagerListener, self); @@ -179,6 +189,18 @@ fn registryListener( } } +fn bgEffectListener( + _: *ext.BackgroundEffectManagerV1, + event: ext.BackgroundEffectManagerV1.Event, + self: *Globals, +) void { + switch (event) { + .capabilities => |cap| { + self.state.bg_effect_capabilities = cap.flags; + }, + } +} + fn decoManagerListener( _: *org.KdeKwinServerDecorationManager, event: org.KdeKwinServerDecorationManager.Event, diff --git a/src/build/SharedDeps.zig b/src/build/SharedDeps.zig index 4b1ea936d..9dc4162ae 100644 --- a/src/build/SharedDeps.zig +++ b/src/build/SharedDeps.zig @@ -626,9 +626,6 @@ fn addGtkNg( .wayland_protocols = wayland_protocols_dep.path(""), }); - scanner.addCustomProtocol( - plasma_wayland_protocols_dep.path("src/protocols/blur.xml"), - ); // FIXME: replace with `zxdg_decoration_v1` once GTK merges https://gitlab.gnome.org/GNOME/gtk/-/merge_requests/6398 scanner.addCustomProtocol( plasma_wayland_protocols_dep.path("src/protocols/server-decoration.xml"), @@ -640,13 +637,14 @@ fn addGtkNg( plasma_wayland_protocols_dep.path("src/protocols/kde-output-order-v1.xml"), ); scanner.addSystemProtocol("staging/xdg-activation/xdg-activation-v1.xml"); + scanner.addSystemProtocol("staging/ext-background-effect/ext-background-effect-v1.xml"); scanner.generate("wl_compositor", 1); - scanner.generate("org_kde_kwin_blur_manager", 1); scanner.generate("org_kde_kwin_server_decoration_manager", 1); scanner.generate("org_kde_kwin_slide_manager", 1); scanner.generate("kde_output_order_v1", 1); scanner.generate("xdg_activation_v1", 1); + scanner.generate("ext_background_effect_manager_v1", 1); step.root_module.addImport("wayland", b.createModule(.{ .root_source_file = scanner.result, From 5abf21c1e229266749fcc711b2b7a07e366a4542 Mon Sep 17 00:00:00 2001 From: Leah Amelia Chen Date: Wed, 18 Mar 2026 02:27:53 +0800 Subject: [PATCH 2/4] gtk/wayland: complete blur region calculation It took me a while and with lots of trial and error to arrive here, but the end result looks pretty good. --- src/apprt/gtk/winproto/BlurRegion.zig | 185 ++++++++++++++++++++++++++ src/apprt/gtk/winproto/wayland.zig | 63 +++++---- 2 files changed, 215 insertions(+), 33 deletions(-) create mode 100644 src/apprt/gtk/winproto/BlurRegion.zig diff --git a/src/apprt/gtk/winproto/BlurRegion.zig b/src/apprt/gtk/winproto/BlurRegion.zig new file mode 100644 index 000000000..e7d8ff7a5 --- /dev/null +++ b/src/apprt/gtk/winproto/BlurRegion.zig @@ -0,0 +1,185 @@ +const BlurRegion = @This(); +const std = @import("std"); +const Allocator = std.mem.Allocator; + +const gobject = @import("gobject"); +const gdk = @import("gdk"); +const gtk = @import("gtk"); + +const Window = @import("../winproto.zig").Window; +const ApprtWindow = @import("../class/window.zig").Window; + +slices: std.ArrayList(Slice), + +/// A rectangular slice of the blur region. +// Marked `extern` since we want to be able to use this in X11 directly. +pub const Slice = extern struct { + x: Pos, + y: Pos, + width: Pos, + height: Pos, +}; + +// X11 compatibility. Ideally this should just be an `i32` like Wayland, +// but XLib sucks +const Pos = c_long; + +pub const empty: BlurRegion = .{ + .slices = .empty, +}; + +pub fn deinit(self: *BlurRegion, alloc: Allocator) void { + self.slices.deinit(alloc); + self.slices = .empty; +} + +// Calculate the blur regions for a window. +// +// Since we have rounded corners by default, we need to carve out the +// pixels on each corner to avoid the "korners bug". +// (cf. https://github.com/cutefishos/fishui/blob/41d4ba194063a3c7fff4675619b57e6ac0504f06/src/platforms/linux/blurhelper/windowblur.cpp#L134) +pub fn calcForWindow( + alloc: Allocator, + window: *ApprtWindow, + csd: bool, + to_device_coordinates: bool, +) Allocator.Error!BlurRegion { + const native = window.as(gtk.Native); + const surface = native.getSurface() orelse return .empty; + + var slices: std.ArrayList(Slice) = .empty; + errdefer slices.deinit(alloc); + + // Calculate the primary blur region + // (the one that covers most of the screen). + // It's easier to do this inside a vector since we have to scale + // everything by the scale factor anyways. + + // NOTE(pluiedev): CSDs are a f--king mistake. + // Please, GNOME, stop this nonsense of making a window ~30% bigger + // internally than how they really are just for your shadows and + // rounded corners and all that fluff. Please. I beg of you. + const x: Pos, const y: Pos = off: { + var x: f64 = 0; + var y: f64 = 0; + native.getSurfaceTransform(&x, &y); + // Slightly inset the corners + x += 1; + y += 1; + break :off .{ @intFromFloat(x), @intFromFloat(y) }; + }; + + var width = @as(Pos, surface.getWidth()); + var height = @as(Pos, surface.getHeight()); + + // Trim off the offsets. Be careful not to get negative. + width -= x * 2; + height -= y * 2; + if (width <= 0 or height <= 0) return .empty; + + // Empirically determined. + const are_corners_rounded = rounded: { + // This cast should always succeed as all of our windows + // should be toplevel. If this fails, something very strange + // is going on. + const toplevel = gobject.ext.cast( + gdk.Toplevel, + surface, + ) orelse break :rounded false; + + const state = toplevel.getState(); + if (state.fullscreen or state.maximized or state.tiled) + break :rounded false; + + break :rounded csd; + }; + + const new_slices = try approxRoundedRect( + alloc, + x, + y, + width, + height, + // See https://gnome.pages.gitlab.gnome.org/libadwaita/doc/main/css-variables.html#window-radius + if (are_corners_rounded) 15 else 0, + ); + + if (to_device_coordinates) { + // Transform surface coordinates to device coordinates. + const sf = surface.getScaleFactor(); + for (new_slices.items) |*s| { + s.x *= sf; + s.y *= sf; + s.width *= sf; + s.height *= sf; + } + } + + return .{ .slices = new_slices }; +} + +/// Whether two sets of blur regions are equal. +pub fn eql(self: BlurRegion, other: BlurRegion) bool { + if (self.slices.items.len != other.slices.items.len) return false; + for (self.slices.items, other.slices.items) |this, that| { + if (!std.meta.eql(this, that)) return false; + } + return true; +} + +/// Approximate a rounded rectangle with many smaller rectangles. +fn approxRoundedRect( + alloc: Allocator, + x: Pos, + y: Pos, + width: Pos, + height: Pos, + radius: Pos, +) Allocator.Error!std.ArrayList(Slice) { + const r_f: f32 = @floatFromInt(radius); + + var slices: std.ArrayList(Slice) = .empty; + errdefer slices.deinit(alloc); + + // Add the central rectangle + try slices.append(alloc, .{ + .x = x, + .y = y + radius, + .width = width, + .height = height - 2 * radius, + }); + + // Add the corner rows. This is honestly quite cursed. + var row: Pos = 0; + while (row < radius) : (row += 1) { + // y distance from this row to the center corner circle + const dy = @as(f32, @floatFromInt(radius - row)) - 0.5; + + // x distance - as given by the definition of a circle + const dx = @sqrt(r_f * r_f - dy * dy); + + // How much each row should be offset, rounded to an integer + const row_x: Pos = @intFromFloat(r_f - @round(dx + 0.5)); + + // Remove the offset from both ends + const row_w = width - 2 * row_x; + + // Top slice + try slices.append(alloc, .{ + .x = x + row_x, + .y = y + row, + .width = row_w, + .height = 1, + }); + + // Bottom slice + try slices.append(alloc, .{ + .x = x + row_x, + .y = y + height - 1 - row, + .width = row_w, + .height = 1, + }); + } + + return slices; +} diff --git a/src/apprt/gtk/winproto/wayland.zig b/src/apprt/gtk/winproto/wayland.zig index b7207e545..ac4a3ea50 100644 --- a/src/apprt/gtk/winproto/wayland.zig +++ b/src/apprt/gtk/winproto/wayland.zig @@ -19,6 +19,7 @@ const Config = @import("../../../config.zig").Config; const Globals = @import("wayland/Globals.zig"); const input = @import("../../../input.zig"); const ApprtWindow = @import("../class/window.zig").Window; +const BlurRegion = @import("BlurRegion.zig"); const log = std.log.scoped(.winproto_wayland); @@ -112,6 +113,8 @@ pub const Window = struct { /// requesting attention from the user. activation_token: ?*xdg.ActivationTokenV1 = null, + blur_region: BlurRegion = .empty, + pub fn init( alloc: Allocator, app: *App, @@ -254,43 +257,37 @@ pub const Window = struct { return; const blur = config.@"background-blur"; - const region = region: { - if (!blur.enabled()) break :region null; + if (!blur.enabled()) { + self.blur_region.deinit(self.globals.alloc); + bg.setBlurRegion(null); + return; + } - // NOTE(pluiedev): CSDs are a f--king mistake. - // Please, GNOME, stop this nonsense of making a window ~30% bigger - // internally than how they really are just for your shadows and - // rounded corners and all that fluff. Please. I beg of you. + var region: BlurRegion = try .calcForWindow( + self.globals.alloc, + self.apprt_window, + self.clientSideDecorationEnabled(), + false, + ); + errdefer region.deinit(self.globals.alloc); - const native = self.apprt_window.as(gtk.Native); - const surface = native.getSurface() orelse break :region null; - const region = try compositor.createRegion(); + if (region.eql(self.blur_region)) { + // Region didn't change. Don't do anything. + region.deinit(self.globals.alloc); + return; + } - var x: f64 = 0; - var y: f64 = 0; - native.getSurfaceTransform(&x, &y); - // Slightly inset the blur region - x += 1; - y += 1; + const wl_region = try compositor.createRegion(); + errdefer if (wl_region) |r| r.destroy(); + for (region.slices.items) |s| wl_region.add( + @intCast(s.x), + @intCast(s.y), + @intCast(s.width), + @intCast(s.height), + ); - var width: f64 = @floatFromInt(surface.getWidth()); - var height: f64 = @floatFromInt(surface.getHeight()); - width -= x * 2; - height -= y * 2; - if (width <= 0 or height <= 0) break :region null; - - // FIXME: Add rounded corners - region.add( - @intFromFloat(x), - @intFromFloat(y), - @intFromFloat(width), - @intFromFloat(height), - ); - break :region region; - }; - errdefer if (region) |r| r.destroy(); - - bg.setBlurRegion(region); + bg.setBlurRegion(wl_region); + self.blur_region = region; } fn syncDecoration(self: *Window) !void { From 80ab5d92eab280296bab23067d257b87d1244fab Mon Sep 17 00:00:00 2001 From: Leah Amelia Chen Date: Wed, 18 Mar 2026 02:27:53 +0800 Subject: [PATCH 3/4] gtk/x11: use BlurRegion --- src/apprt/gtk/winproto/x11.zig | 84 +++++++++++----------------------- 1 file changed, 26 insertions(+), 58 deletions(-) diff --git a/src/apprt/gtk/winproto/x11.zig b/src/apprt/gtk/winproto/x11.zig index 675434fd1..92ecf2700 100644 --- a/src/apprt/gtk/winproto/x11.zig +++ b/src/apprt/gtk/winproto/x11.zig @@ -19,6 +19,7 @@ pub const c = @cImport({ const input = @import("../../../input.zig"); const Config = @import("../../../config.zig").Config; const ApprtWindow = @import("../class/window.zig").Window; +const BlurRegion = @import("BlurRegion.zig"); const log = std.log.scoped(.gtk_x11); @@ -169,13 +170,13 @@ pub const Window = struct { app: *App, apprt_window: *ApprtWindow, x11_surface: *gdk_x11.X11Surface, + alloc: Allocator, - blur_region: Region = .{}, + blur_region: BlurRegion = .empty, // Cache last applied values to avoid redundant X11 property updates. // Redundant property updates seem to cause some visual glitches // with some window managers: https://github.com/ghostty-org/ghostty/pull/8075 - last_applied_blur_region: ?Region = null, last_applied_decoration_hints: ?MotifWMHints = null, pub fn init( @@ -183,8 +184,6 @@ pub const Window = struct { app: *App, apprt_window: *ApprtWindow, ) !Window { - _ = alloc; - const surface = apprt_window.as(gtk.Native).getSurface() orelse return error.NotX11Surface; @@ -195,6 +194,7 @@ pub const Window = struct { return .{ .app = app, + .alloc = alloc, .apprt_window = apprt_window, .x11_surface = x11_surface, }; @@ -213,26 +213,6 @@ pub const Window = struct { pub fn syncAppearance(self: *Window) !void { // The user could have toggled between CSDs and SSDs, // therefore we need to recalculate the blur region offset. - self.blur_region = blur: { - // NOTE(pluiedev): CSDs are a f--king mistake. - // Please, GNOME, stop this nonsense of making a window ~30% bigger - // internally than how they really are just for your shadows and - // rounded corners and all that fluff. Please. I beg of you. - var x: f64 = 0; - var y: f64 = 0; - - self.apprt_window.as(gtk.Native).getSurfaceTransform(&x, &y); - - // Transform surface coordinates to device coordinates. - const scale: f64 = @floatFromInt(self.apprt_window.as(gtk.Widget).getScaleFactor()); - x *= scale; - y *= scale; - - break :blur .{ - .x = @intFromFloat(x), - .y = @intFromFloat(y), - }; - }; self.syncBlur() catch |err| { log.err("failed to synchronize blur={}", .{err}); }; @@ -249,36 +229,29 @@ pub const Window = struct { } fn syncBlur(self: *Window) !void { - // FIXME: This doesn't currently factor in rounded corners on Adwaita, - // which means that the blur region will grow slightly outside of the - // window borders. Unfortunately, actually calculating the rounded - // region can be quite complex without having access to existing APIs - // (cf. https://github.com/cutefishos/fishui/blob/41d4ba194063a3c7fff4675619b57e6ac0504f06/src/platforms/linux/blurhelper/windowblur.cpp#L134) - // and I think it's not really noticeable enough to justify the effort. - // (Wayland also has this visual artifact anyway...) - - const gtk_widget = self.apprt_window.as(gtk.Widget); const config = if (self.apprt_window.getConfig()) |v| v.get() else return; // When blur is disabled, remove the property if it was previously set const blur = config.@"background-blur"; if (!blur.enabled()) { - if (self.last_applied_blur_region != null) { - try self.deleteProperty(self.app.atoms.kde_blur); - self.last_applied_blur_region = null; - } - + self.blur_region.deinit(self.alloc); + self.blur_region = .empty; + try self.deleteProperty(self.app.atoms.kde_blur); return; } - // Transform surface coordinates to device coordinates. - const scale = gtk_widget.getScaleFactor(); - self.blur_region.width = gtk_widget.getWidth() * scale; - self.blur_region.height = gtk_widget.getHeight() * scale; + var region: BlurRegion = try .calcForWindow( + self.alloc, + self.apprt_window, + self.clientSideDecorationEnabled(), + true, + ); + errdefer region.deinit(self.alloc); // Only update X11 properties when the blur region actually changes - if (self.last_applied_blur_region) |last| { - if (std.meta.eql(self.blur_region, last)) return; + if (region.eql(self.blur_region)) { + region.deinit(self.alloc); + return; } log.debug("set blur={}, window xid={}, region={}", .{ @@ -288,14 +261,14 @@ pub const Window = struct { }); try self.changeProperty( - Region, + BlurRegion.Slice, self.app.atoms.kde_blur, c.XA_CARDINAL, ._32, .{ .mode = .replace }, - &self.blur_region, + self.blur_region.slices.items, ); - self.last_applied_blur_region = self.blur_region; + self.blur_region = region; } fn syncDecorations(self: *Window) !void { @@ -335,7 +308,7 @@ pub const Window = struct { self.app.atoms.motif_wm_hints, ._32, .{ .mode = .replace }, - &hints, + &.{hints}, ); self.last_applied_decoration_hints = hints; } @@ -410,9 +383,11 @@ pub const Window = struct { options: struct { mode: PropertyChangeMode, }, - value: *T, + values: []const T, ) X11Error!void { - const data: format.bufferType() = @ptrCast(value); + const data: format.bufferType() = @ptrCast(@constCast(values)); + // The number of "words" that each element `T` occupies. + const words_per_elem = @divExact(@sizeOf(T), @sizeOf(format.elemType())); const status = c.XChangeProperty( @ptrCast(@alignCast(self.app.display)), @@ -422,7 +397,7 @@ pub const Window = struct { @intFromEnum(format), @intFromEnum(options.mode), data, - @divExact(@sizeOf(T), @sizeOf(format.elemType())), + @intCast(words_per_elem * values.len), ); // For some godforsaken reason Xlib alternates between @@ -498,13 +473,6 @@ const PropertyFormat = enum(c_int) { } }; -const Region = extern struct { - x: c_long = 0, - y: c_long = 0, - width: c_long = 0, - height: c_long = 0, -}; - // See Xm/MwmUtil.h, packaged with the Motif Window Manager const MotifWMHints = extern struct { flags: packed struct(c_ulong) { From 27fd1c7788a3ebdf347b54c5bc9c1c329f175397 Mon Sep 17 00:00:00 2001 From: Leah Amelia Chen Date: Wed, 18 Mar 2026 03:19:22 +0800 Subject: [PATCH 4/4] gtk/winproto: fix memleak & other tweaks --- src/apprt/gtk/class/window.zig | 2 +- src/apprt/gtk/winproto.zig | 4 +- src/apprt/gtk/winproto/BlurRegion.zig | 8 ++-- src/apprt/gtk/winproto/noop.zig | 3 +- src/apprt/gtk/winproto/wayland.zig | 6 +-- src/apprt/gtk/winproto/x11.zig | 66 ++++++++++++++------------- 6 files changed, 47 insertions(+), 42 deletions(-) diff --git a/src/apprt/gtk/class/window.zig b/src/apprt/gtk/class/window.zig index 8cef7b765..bf2a2fe7c 100644 --- a/src/apprt/gtk/class/window.zig +++ b/src/apprt/gtk/class/window.zig @@ -1235,7 +1235,7 @@ pub const Window = extern struct { fn finalize(self: *Self) callconv(.c) void { const priv = self.private(); priv.tab_bindings.unref(); - priv.winproto.deinit(Application.default().allocator()); + priv.winproto.deinit(); gobject.Object.virtual_methods.finalize.call( Class.parent, diff --git a/src/apprt/gtk/winproto.zig b/src/apprt/gtk/winproto.zig index 6731bfb31..d409d6c26 100644 --- a/src/apprt/gtk/winproto.zig +++ b/src/apprt/gtk/winproto.zig @@ -117,9 +117,9 @@ pub const Window = union(Protocol) { }; } - pub fn deinit(self: *Window, alloc: Allocator) void { + pub fn deinit(self: *Window) void { switch (self.*) { - inline else => |*v| v.deinit(alloc), + inline else => |*v| v.deinit(), } } diff --git a/src/apprt/gtk/winproto/BlurRegion.zig b/src/apprt/gtk/winproto/BlurRegion.zig index e7d8ff7a5..f3041dae0 100644 --- a/src/apprt/gtk/winproto/BlurRegion.zig +++ b/src/apprt/gtk/winproto/BlurRegion.zig @@ -63,9 +63,11 @@ pub fn calcForWindow( var x: f64 = 0; var y: f64 = 0; native.getSurfaceTransform(&x, &y); - // Slightly inset the corners - x += 1; - y += 1; + // Slightly inset the corners if we're using CSDs + if (csd) { + x += 1; + y += 1; + } break :off .{ @intFromFloat(x), @intFromFloat(y) }; }; diff --git a/src/apprt/gtk/winproto/noop.zig b/src/apprt/gtk/winproto/noop.zig index 0d8cc18ce..950ee0f37 100644 --- a/src/apprt/gtk/winproto/noop.zig +++ b/src/apprt/gtk/winproto/noop.zig @@ -46,9 +46,8 @@ pub const Window = struct { return .{}; } - pub fn deinit(self: Window, alloc: Allocator) void { + pub fn deinit(self: *Window) void { _ = self; - _ = alloc; } pub fn updateConfigEvent( diff --git a/src/apprt/gtk/winproto/wayland.zig b/src/apprt/gtk/winproto/wayland.zig index ac4a3ea50..12c7fb8a2 100644 --- a/src/apprt/gtk/winproto/wayland.zig +++ b/src/apprt/gtk/winproto/wayland.zig @@ -185,8 +185,8 @@ pub const Window = struct { }; } - pub fn deinit(self: Window, alloc: Allocator) void { - _ = alloc; + pub fn deinit(self: *Window) void { + self.blur_region.deinit(self.globals.alloc); if (self.bg_effect) |bg| bg.destroy(); if (self.decoration) |deco| deco.release(); if (self.slide) |slide| slide.release(); @@ -203,7 +203,7 @@ pub const Window = struct { log.err("failed to sync blur={}", .{err}); }; self.syncDecoration() catch |err| { - log.err("failed to sync blur={}", .{err}); + log.err("failed to sync decoration={}", .{err}); }; if (self.apprt_window.isQuickTerminal()) { diff --git a/src/apprt/gtk/winproto/x11.zig b/src/apprt/gtk/winproto/x11.zig index 92ecf2700..8109959da 100644 --- a/src/apprt/gtk/winproto/x11.zig +++ b/src/apprt/gtk/winproto/x11.zig @@ -200,24 +200,25 @@ pub const Window = struct { }; } - pub fn deinit(self: Window, alloc: Allocator) void { - _ = self; - _ = alloc; + pub fn deinit(self: *Window) void { + self.blur_region.deinit(self.alloc); } pub fn resizeEvent(self: *Window) !void { // The blur region must update with window resizes - try self.syncBlur(); + self.syncBlur() catch |err| { + log.err("failed to sync blur={}", .{err}); + }; } pub fn syncAppearance(self: *Window) !void { // The user could have toggled between CSDs and SSDs, // therefore we need to recalculate the blur region offset. self.syncBlur() catch |err| { - log.err("failed to synchronize blur={}", .{err}); + log.err("failed to sync blur={}", .{err}); }; self.syncDecorations() catch |err| { - log.err("failed to synchronize decorations={}", .{err}); + log.err("failed to sync decorations={}", .{err}); }; } @@ -233,19 +234,16 @@ pub const Window = struct { // When blur is disabled, remove the property if it was previously set const blur = config.@"background-blur"; - if (!blur.enabled()) { - self.blur_region.deinit(self.alloc); - self.blur_region = .empty; - try self.deleteProperty(self.app.atoms.kde_blur); - return; - } - var region: BlurRegion = try .calcForWindow( - self.alloc, - self.apprt_window, - self.clientSideDecorationEnabled(), - true, - ); + var region: BlurRegion = if (blur.enabled()) + try .calcForWindow( + self.alloc, + self.apprt_window, + self.clientSideDecorationEnabled(), + true, + ) + else + .empty; errdefer region.deinit(self.alloc); // Only update X11 properties when the blur region actually changes @@ -254,20 +252,26 @@ pub const Window = struct { return; } - log.debug("set blur={}, window xid={}, region={}", .{ - blur, - self.x11_surface.getXid(), - self.blur_region, - }); + if (region.slices.items.len > 0) { + log.debug("set blur={}, window xid={}, region={}", .{ + blur, + self.x11_surface.getXid(), + region, + }); - try self.changeProperty( - BlurRegion.Slice, - self.app.atoms.kde_blur, - c.XA_CARDINAL, - ._32, - .{ .mode = .replace }, - self.blur_region.slices.items, - ); + try self.changeProperty( + BlurRegion.Slice, + self.app.atoms.kde_blur, + c.XA_CARDINAL, + ._32, + .{ .mode = .replace }, + region.slices.items, + ); + } else { + try self.deleteProperty(self.app.atoms.kde_blur); + } + + self.blur_region.deinit(self.alloc); self.blur_region = region; }