diff --git a/build.zig.zon b/build.zig.zon index 497cef406e..cef0696951 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 5b557a4938..b24c8f2d72 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 1cdecbb85b..50880f7266 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 468208621b..66b91c8f1d 100644 --- a/build.zig.zon.txt +++ b/build.zig.zon.txt @@ -34,3 +34,4 @@ https://deps.files.ghostty.org/zlib-1220fed0c74e1019b3ee29edae2051788b080cd96e90 https://github.com/ivanstepanovftw/zigimg/archive/d7b7ab0ba0899643831ef042bd73289510b39906.tar.gz https://github.com/ocornut/imgui/archive/refs/tags/v1.92.5-docking.tar.gz https://github.com/vancluever/z2d/archive/refs/tags/v0.10.0.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 d5c96064d2..102a02fe33 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 4a16580ef3..8a8c09cda2 100644 --- a/src/apprt/gtk/class/window.zig +++ b/src/apprt/gtk/class/window.zig @@ -281,7 +281,11 @@ pub const Window = extern struct { // We initialize our windowing protocol to none because we can't // actually initialize this until we get realized. - priv.winproto = .none; + priv.winproto = .{ + .alloc = Application.default().allocator(), + .apprt_window = self, + .inner = .none, + }; // Add our dev CSS class if we're in debug mode. if (comptime build_config.is_debug) { @@ -1005,14 +1009,13 @@ pub const Window = extern struct { self.syncAppearance(); } - fn propGdkSurfaceHeight( + fn propGdkSurfaceDims( _: *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| { + // Update the background blur + self.private().winproto.updateBlur() catch |err| { log.warn( "winproto resize event failed error={}", .{err}, @@ -1045,21 +1048,6 @@ pub const Window = extern struct { }; } - fn propGdkSurfaceWidth( - _: *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 propFullscreened( _: *adw.ApplicationWindow, _: *gobject.ParamSpec, @@ -1216,14 +1204,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.zig b/src/apprt/gtk/winproto.zig index 3c1da2b218..1894f8f8b9 100644 --- a/src/apprt/gtk/winproto.zig +++ b/src/apprt/gtk/winproto.zig @@ -2,6 +2,7 @@ const std = @import("std"); const build_options = @import("build_options"); const Allocator = std.mem.Allocator; +const gtk = @import("gtk"); const gdk = @import("gdk"); const Config = @import("../../config.zig").Config; @@ -12,6 +13,7 @@ const ApprtWindow = @import("class/window.zig").Window; pub const noop = @import("winproto/noop.zig"); pub const x11 = @import("winproto/x11.zig"); pub const wayland = @import("winproto/wayland.zig"); +const blur = @import("winproto/blur.zig"); pub const Protocol = enum { none, @@ -86,69 +88,118 @@ pub const App = union(Protocol) { /// really "Surface"-specific state. But Ghostty uses the term "Surface" /// heavily to mean something completely different, so we use "Window" here /// to better match what it generally maps to in the Ghostty codebase. -pub const Window = union(Protocol) { - none: noop.Window, - wayland: if (build_options.wayland) wayland.Window else noop.Window, - x11: if (build_options.x11) x11.Window else noop.Window, +pub const Window = struct { + alloc: Allocator, + + apprt_window: *ApprtWindow, + + // Remember the current blur region 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 + blur_region: blur.Region = .empty, + + inner: Inner, + + const Inner = union(Protocol) { + none: noop.Window, + wayland: if (build_options.wayland) wayland.Window else noop.Window, + x11: if (build_options.x11) x11.Window else noop.Window, + }; pub fn init( alloc: Allocator, app: *App, apprt_window: *ApprtWindow, ) !Window { - return switch (app.*) { - inline else => |*v, tag| { - inline for (@typeInfo(Window).@"union".fields) |field| { - if (comptime std.mem.eql( - u8, - field.name, - @tagName(tag), - )) return @unionInit( - Window, - field.name, - try field.type.init( - alloc, - v, - apprt_window, - ), - ); - } - }, + const inner = inner: { + switch (app.*) { + inline else => |*v, tag| { + inline for (@typeInfo(Inner).@"union".fields) |field| { + if (comptime std.mem.eql( + u8, + field.name, + @tagName(tag), + )) break :inner @unionInit( + Inner, + field.name, + try field.type.init( + alloc, + v, + apprt_window, + ), + ); + } + }, + } + }; + + return .{ + .alloc = alloc, + .apprt_window = apprt_window, + .inner = inner, }; } pub fn deinit(self: *Window, alloc: Allocator) void { - switch (self.*) { + self.blur_region.deinit(alloc); + + switch (self.inner) { inline else => |*v| v.deinit(alloc), } } - pub fn resizeEvent(self: *Window) !void { - switch (self.*) { - inline else => |*v| try v.resizeEvent(), + pub fn updateBlur(self: *Window) !void { + // Update the blur region in response to the resize. + 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 = config.@"background-blur"; + + var new_region: blur.Region = if (blur_config.enabled()) + try .calcForWindow(self) + else + .empty; + errdefer new_region.deinit(self.alloc); + + if (!new_region.eql(self.blur_region)) { + self.blur_region.deinit(self.alloc); + self.blur_region = new_region; + + switch (self.inner) { + inline else => |*v| try v.setBlur(self.blur_region), + } + } else { + new_region.deinit(self.alloc); } } + pub fn blurRegionInDeviceCoords(self: Window) bool { + return switch (self.inner) { + inline else => |v| v.blurRegionInDeviceCoords(), + }; + } + pub fn syncAppearance(self: *Window) !void { - switch (self.*) { + try self.updateBlur(); + switch (self.inner) { inline else => |*v| try v.syncAppearance(), } } pub fn clientSideDecorationEnabled(self: Window) bool { - return switch (self) { + return switch (self.inner) { inline else => |v| v.clientSideDecorationEnabled(), }; } pub fn addSubprocessEnv(self: *Window, env: *std.process.EnvMap) !void { - switch (self.*) { + switch (self.inner) { inline else => |*v| try v.addSubprocessEnv(env), } } pub fn setUrgent(self: *Window, urgent: bool) !void { - switch (self.*) { + switch (self.inner) { inline else => |*v| try v.setUrgent(urgent), } } diff --git a/src/apprt/gtk/winproto/blur.zig b/src/apprt/gtk/winproto/blur.zig new file mode 100644 index 0000000000..117989c92b --- /dev/null +++ b/src/apprt/gtk/winproto/blur.zig @@ -0,0 +1,171 @@ +//! Protocol-independent utilities used to implement background blur. +const std = @import("std"); +const Allocator = std.mem.Allocator; + +const gtk = @import("gtk"); +const gdk = @import("gdk"); +const gobject = @import("gobject"); +const Window = @import("../winproto.zig").Window; + +pub const Region = struct { + 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: Region = .{ + .slices = .empty, + }; + + pub fn deinit(self: *Region, alloc: std.mem.Allocator) void { + self.slices.deinit(alloc); + } + + // Calculate the blur region 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". + pub fn calcForWindow(window: *Window) Allocator.Error!Region { + const native = window.apprt_window.as(gtk.Native); + const surface = native.getSurface() orelse return .empty; + + // 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 window.clientSideDecorationEnabled(); + }; + + const slices = try approxRoundedRect( + window.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, + ); + + // We have to convert surface/logical coordinates to + // device/physical coordinates on X11. More reasons to hate it. + if (window.blurRegionInDeviceCoords()) { + const sf = surface.getScaleFactor(); + for (slices.items) |*s| { + s.x *= sf; + s.y *= sf; + s.width *= sf; + s.height *= sf; + } + } + + return .{ .slices = slices }; + } + + /// Whether two sets of blur regions are equal. + pub fn eql(self: Region, other: Region) 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/noop.zig b/src/apprt/gtk/winproto/noop.zig index ed69736f81..0de8b93ea5 100644 --- a/src/apprt/gtk/winproto/noop.zig +++ b/src/apprt/gtk/winproto/noop.zig @@ -6,6 +6,7 @@ const gdk = @import("gdk"); const Config = @import("../../../config.zig").Config; const input = @import("../../../input.zig"); const ApprtWindow = @import("../class/window.zig").Window; +const blur = @import("blur.zig"); const log = std.log.scoped(.winproto_noop); @@ -57,7 +58,11 @@ pub const Window = struct { _: *const ApprtWindow.DerivedConfig, ) !void {} - pub fn resizeEvent(_: *Window) !void {} + pub fn setBlur(_: *Window, _: blur.Region) !void {} + + pub fn blurRegionInDeviceCoords(_: Window) bool { + return false; + } pub fn syncAppearance(_: *Window) !void {} diff --git a/src/apprt/gtk/winproto/wayland.zig b/src/apprt/gtk/winproto/wayland.zig index ec02fbee53..e2fabd59d8 100644 --- a/src/apprt/gtk/winproto/wayland.zig +++ b/src/apprt/gtk/winproto/wayland.zig @@ -12,10 +12,12 @@ const wayland = @import("wayland"); const Config = @import("../../../config.zig").Config; const input = @import("../../../input.zig"); const ApprtWindow = @import("../class/window.zig").Window; +const blur = @import("blur.zig"); const wl = wayland.client.wl; const org = wayland.client.org; const xdg = wayland.client.xdg; +const ext = wayland.client.ext; const log = std.log.scoped(.winproto_wayland); @@ -25,8 +27,13 @@ pub const App = struct { context: *Context, const Context = struct { + compositor: ?*wl.Compositor = null, + kde_blur_manager: ?*org.KdeKwinBlurManager = null, + ext_bg_effect_manager: ?*ext.BackgroundEffectManagerV1 = null, + ext_bg_capabilities: ext.BackgroundEffectManagerV1.Capability = .{}, + // FIXME: replace with `zxdg_decoration_v1` once GTK merges // https://gitlab.gnome.org/GNOME/gtk/-/merge_requests/6398 kde_decoration_manager: ?*org.KdeKwinServerDecorationManager = null, @@ -83,9 +90,19 @@ pub const App = struct { registry.setListener(*Context, registryListener, context); if (display.roundtrip() != .SUCCESS) return error.RoundtripFailed; - // Do another round-trip to get the default decoration mode + // Sometimes we need to do another roundtrip + // to get all information we need. + var needs_roundtrip = false; if (context.kde_decoration_manager) |deco_manager| { deco_manager.setListener(*Context, decoManagerListener, context); + needs_roundtrip = true; + } + if (context.ext_bg_effect_manager) |mgr| { + mgr.setListener(*Context, effectManagerListener, context); + needs_roundtrip = true; + } + + if (needs_roundtrip) { if (display.roundtrip() != .SUCCESS) return error.RoundtripFailed; } @@ -221,6 +238,18 @@ pub const App = struct { }, } } + + fn effectManagerListener( + _: *ext.BackgroundEffectManagerV1, + event: ext.BackgroundEffectManagerV1.Event, + context: *Context, + ) void { + switch (event) { + .capabilities => |cap| { + context.ext_bg_capabilities = cap.flags; + }, + } + } }; /// Per-window (wl_surface) state for the Wayland protocol. @@ -236,6 +265,9 @@ pub const Window = struct { /// A token that, when present, indicates that the window is blurred. blur_token: ?*org.KdeKwinBlur = null, + /// Object that controls background effects like blur. + bg_effect: ?*ext.BackgroundEffectSurfaceV1 = null, + /// Object that controls the decoration mode (client/server/auto) /// of the window. decoration: ?*org.KdeKwinServerDecoration = null, @@ -285,6 +317,16 @@ pub const Window = struct { break :deco deco; }; + const bg_effect: ?*ext.BackgroundEffectSurfaceV1 = bg: { + const mgr = app.context.ext_bg_effect_manager orelse + break :bg null; + + break :bg mgr.getBackgroundEffect(wl_surface) catch |err| { + log.warn("could not create background effect object={}", .{err}); + break :bg null; + }; + }; + if (apprt_window.isQuickTerminal()) { _ = gdk.Surface.signals.enter_monitor.connect( gdk_surface, @@ -300,12 +342,13 @@ pub const Window = struct { .surface = wl_surface, .app_context = app.context, .decoration = deco, + .bg_effect = bg_effect, }; } pub fn deinit(self: Window, alloc: Allocator) void { _ = alloc; - if (self.blur_token) |blur| blur.release(); + if (self.blur_token) |token| token.release(); if (self.decoration) |deco| deco.release(); if (self.slide) |slide| slide.release(); } @@ -313,9 +356,6 @@ pub const Window = struct { pub fn resizeEvent(_: *Window) !void {} pub fn syncAppearance(self: *Window) !void { - self.syncBlur() catch |err| { - log.err("failed to sync blur={}", .{err}); - }; self.syncDecoration() catch |err| { log.err("failed to sync blur={}", .{err}); }; @@ -360,31 +400,56 @@ pub const Window = struct { } /// Update the blur state of the window. - fn syncBlur(self: *Window) !void { - const manager = self.app_context.kde_blur_manager orelse return; - const config = if (self.apprt_window.getConfig()) |v| - v.get() - else - return; - const blur = config.@"background-blur"; + pub fn setBlur(self: *Window, region: blur.Region) !void { + if (region.slices.items.len > 0) { + const compositor = self.app_context.compositor orelse return; + + const wl_region = try compositor.createRegion(); + errdefer wl_region.destroy(); + + for (region.slices.items) |slice| { + // X11 coming over to bite Wayland. + wl_region.add( + @intCast(slice.x), + @intCast(slice.y), + @intCast(slice.width), + @intCast(slice.height), + ); + } - 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; + // Does the compositor support the `ext-background-effect-v1` + // protocol? If so, try that first, though it might not actually + // support the blur setting. + if (self.bg_effect) |fx| fx: { + if (!self.app_context.ext_bg_capabilities.blur) break :fx; + fx.setBlurRegion(wl_region); + } else if (self.app_context.kde_blur_manager) |mgr| { + // Do we already have a blur token? If not, create one. + const token = self.blur_token orelse try mgr.create(self.surface); + defer self.blur_token = token; + token.setRegion(wl_region); + token.commit(); } } 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; + if (self.bg_effect) |fx| fx: { + if (!self.app_context.ext_bg_capabilities.blur) break :fx; + fx.setBlurRegion(null); + } else if (self.app_context.kde_blur_manager) |mgr| fx: { + // Destroy the blur token if present. + const token = self.blur_token orelse break :fx; + mgr.unset(self.surface); + token.release(); + self.blur_token = null; } } } + /// On certain winprotos, the blur region is specified in device + /// coordinates and have to be scaled by the GDK scale factor. + pub fn blurRegionInDeviceCoords(_: Window) bool { + return false; + } + fn syncDecoration(self: *Window) !void { const deco = self.decoration orelse return; diff --git a/src/apprt/gtk/winproto/x11.zig b/src/apprt/gtk/winproto/x11.zig index 1e73c61390..975589fb5d 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 blur = @import("blur.zig"); const log = std.log.scoped(.gtk_x11); @@ -170,13 +171,6 @@ pub const Window = struct { app: *App, apprt_window: *ApprtWindow, x11_surface: *gdk_x11.X11Surface, - - blur_region: Region = .{}, - - // 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( @@ -206,37 +200,7 @@ pub const Window = struct { _ = alloc; } - pub fn resizeEvent(self: *Window) !void { - // The blur region must update with window resizes - try self.syncBlur(); - } - 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}); - }; self.syncDecorations() catch |err| { log.err("failed to synchronize decorations={}", .{err}); }; @@ -249,54 +213,25 @@ 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; - } - - return; + pub fn setBlur(self: *Window, region: blur.Region) !void { + if (region.slices.items.len > 0) { + try self.changeProperty( + blur.Region.Slice, + self.app.atoms.kde_blur, + c.XA_CARDINAL, + ._32, + .{ .mode = .replace }, + region.slices.items, + ); + } else { + try self.deleteProperty(self.app.atoms.kde_blur); } + } - // 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; - - // 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; - } - - log.debug("set blur={}, window xid={}, region={}", .{ - blur, - self.x11_surface.getXid(), - self.blur_region, - }); - - try self.changeProperty( - Region, - self.app.atoms.kde_blur, - c.XA_CARDINAL, - ._32, - .{ .mode = .replace }, - &self.blur_region, - ); - self.last_applied_blur_region = self.blur_region; + /// On certain winprotos, the blur region is specified in device + /// coordinates and have to be scaled by the GDK scale factor. + pub fn blurRegionInDeviceCoords(_: Window) bool { + return true; } fn syncDecorations(self: *Window) !void { @@ -336,7 +271,7 @@ pub const Window = struct { self.app.atoms.motif_wm_hints, ._32, .{ .mode = .replace }, - &hints, + &.{hints}, ); self.last_applied_decoration_hints = hints; } @@ -411,9 +346,11 @@ pub const Window = struct { options: struct { mode: PropertyChangeMode, }, - value: *T, + value: []const T, ) X11Error!void { - const data: format.bufferType() = @ptrCast(value); + const data: format.bufferType() = @ptrCast(@constCast(value)); + // 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)), @@ -423,7 +360,7 @@ pub const Window = struct { @intFromEnum(format), @intFromEnum(options.mode), data, - @divExact(@sizeOf(T), @sizeOf(format.elemType())), + @intCast(words_per_elem * value.len), ); // For some godforsaken reason Xlib alternates between @@ -499,13 +436,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) { diff --git a/src/build/SharedDeps.zig b/src/build/SharedDeps.zig index 0ca43e78d6..b3ad6dfe60 100644 --- a/src/build/SharedDeps.zig +++ b/src/build/SharedDeps.zig @@ -635,12 +635,14 @@ fn addGtkNg( plasma_wayland_protocols_dep.path("src/protocols/slide.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("xdg_activation_v1", 1); + scanner.generate("ext_background_effect_manager_v1", 1); step.root_module.addImport("wayland", b.createModule(.{ .root_source_file = scanner.result,