diff --git a/src/apprt/gtk/Window.zig b/src/apprt/gtk/Window.zig index c07bb2bb49..da02a3fd47 100644 --- a/src/apprt/gtk/Window.zig +++ b/src/apprt/gtk/Window.zig @@ -157,9 +157,6 @@ pub fn init(self: *Window, app: *App) !void { if (app.config.@"gtk-titlebar") { const header = HeaderBar.init(self); - // If we are not decorated then we hide the titlebar. - header.setVisible(app.config.@"window-decoration"); - { const btn = c.gtk_menu_button_new(); c.gtk_widget_set_tooltip_text(btn, "Main Menu"); @@ -212,11 +209,6 @@ pub fn init(self: *Window, app: *App) !void { _ = c.g_signal_connect_data(gtk_window, "notify::decorated", c.G_CALLBACK(>kWindowNotifyDecorated), self, null, c.G_CONNECT_DEFAULT); - // If we are disabling decorations then disable them right away. - if (!app.config.@"window-decoration") { - c.gtk_window_set_decorated(gtk_window, 0); - } - // If Adwaita is enabled and is older than 1.4.0 we don't have the tab overview and so we // need to stick the headerbar into the content box. if (!adwaita.versionAtLeast(1, 4, 0) and adwaita.enabled(&self.app.config)) { @@ -390,14 +382,40 @@ pub fn init(self: *Window, app: *App) !void { /// TODO: Many of the initial style settings in `create` could possibly be made /// reactive by moving them here. pub fn syncAppearance(self: *Window, config: *const configpkg.Config) !void { - if (config.@"background-opacity" < 1) { - c.gtk_widget_remove_css_class(@ptrCast(self.window), "background"); - } else { - c.gtk_widget_add_css_class(@ptrCast(self.window), "background"); - } - // Perform protocol-specific config updates try self.protocol.onConfigUpdate(config); + + toggleCssClass( + @ptrCast(self.window), + "background", + config.@"background-opacity" >= 1, + ); + + // If we are disabling CSDs then disable them right away. + const csd_enabled = self.protocol.clientSideDecorationEnabled(); + c.gtk_window_set_decorated(self.window, @intFromBool(csd_enabled)); + + // If we are not decorated then we hide the titlebar. + if (self.header) |header| { + header.setVisible(config.@"gtk-titlebar" and csd_enabled); + } + + // Disable the title buttons (close, maximize, minimize, ...) + // *inside* the tab overview if CSDs are disabled. + // We do spare the search button, though. + // (...Why the heck is there a header bar inside the tab overview to begin with??) + if (self.tab_overview) |tab_overview| { + c.adw_tab_overview_set_show_start_title_buttons(@ptrCast(tab_overview), @intFromBool(csd_enabled)); + c.adw_tab_overview_set_show_end_title_buttons(@ptrCast(tab_overview), @intFromBool(csd_enabled)); + } +} + +fn toggleCssClass(widget: *c.GtkWidget, class: [:0]const u8, v: bool) void { + if (v) { + c.gtk_widget_add_css_class(widget, class); + } else { + c.gtk_widget_remove_css_class(widget, class); + } } /// Sets up the GTK actions for the window scope. Actions are how GTK handles @@ -526,17 +544,12 @@ pub fn toggleFullscreen(self: *Window) void { /// Toggle the window decorations for this window. pub fn toggleWindowDecorations(self: *Window) void { - const old_decorated = c.gtk_window_get_decorated(self.window) == 1; - const new_decorated = !old_decorated; - c.gtk_window_set_decorated(self.window, @intFromBool(new_decorated)); - - // If we have a titlebar, then we also show/hide it depending on the - // decorated state. GTK tends to consider the titlebar part of the frame - // and hides it with decorations, but libadwaita doesn't. This makes it - // explicit. - if (self.header) |headerbar| { - headerbar.setVisible(new_decorated); - } + self.app.config.@"window-decoration" = switch (self.app.config.@"window-decoration") { + .true => .server, + .server => .false, + .false => .true, + }; + self.syncAppearance(&self.app.config) catch {}; } /// Grabs focus on the currently selected tab. @@ -576,17 +589,14 @@ fn gtkWindowNotifyDecorated( _: *c.GParamSpec, _: ?*anyopaque, ) callconv(.C) void { - if (c.gtk_window_get_decorated(@ptrCast(object)) == 1) { - c.gtk_widget_remove_css_class(@ptrCast(object), "ssd"); - c.gtk_widget_remove_css_class(@ptrCast(object), "no-border-radius"); - } else { - // Fix any artifacting that may occur in window corners. The .ssd CSS - // class is defined in the GtkWindow documentation: - // https://docs.gtk.org/gtk4/class.Window.html#css-nodes. A definition - // for .ssd is provided by GTK and Adwaita. - c.gtk_widget_add_css_class(@ptrCast(object), "ssd"); - c.gtk_widget_add_css_class(@ptrCast(object), "no-border-radius"); - } + const is_decorated = c.gtk_window_get_decorated(@ptrCast(object)) == 1; + + // Fix any artifacting that may occur in window corners. The .ssd CSS + // class is defined in the GtkWindow documentation: + // https://docs.gtk.org/gtk4/class.Window.html#css-nodes. A definition + // for .ssd is provided by GTK and Adwaita. + toggleCssClass(@ptrCast(object), "ssd", !is_decorated); + toggleCssClass(@ptrCast(object), "no-border-radius", !is_decorated); } // Note: we MUST NOT use the GtkButton parameter because gtkActionNewTab diff --git a/src/apprt/gtk/protocol.zig b/src/apprt/gtk/protocol.zig index d9d2f2a527..d918562994 100644 --- a/src/apprt/gtk/protocol.zig +++ b/src/apprt/gtk/protocol.zig @@ -96,11 +96,13 @@ pub const Surface = struct { pub const DerivedConfig = struct { blur: Config.BackgroundBlur, adw_enabled: bool, + window_decoration: Config.WindowDecoration, pub fn init(config: *const Config) DerivedConfig { return .{ .blur = config.@"background-blur-radius", .adw_enabled = adwaita.enabled(config), + .window_decoration = config.@"window-decoration", }; } }; @@ -127,6 +129,14 @@ pub const Surface = struct { } } + pub fn clientSideDecorationEnabled(self: Surface) bool { + return switch (self.inner) { + // We can't do SSD anyway if the compositor doesn't support it + .wayland => |wl| self.derived_config.window_decoration == .true or wl.decoration == null, + .x11, .none => self.derived_config.window_decoration != .false, + }; + } + pub fn onConfigUpdate(self: *Surface, config: *const Config) !void { self.derived_config = DerivedConfig.init(config); diff --git a/src/apprt/gtk/protocol/wayland.zig b/src/apprt/gtk/protocol/wayland.zig index 985d7c5a80..aef1f00e71 100644 --- a/src/apprt/gtk/protocol/wayland.zig +++ b/src/apprt/gtk/protocol/wayland.zig @@ -12,7 +12,10 @@ const log = std.log.scoped(.gtk_wayland); /// Wayland state that contains application-wide Wayland objects (e.g. wl_display). pub const App = struct { display: *wl.Display, + blur_manager: ?*org.KdeKwinBlurManager = null, + // FIXME: replace with `zxdg_decoration_v1` once GTK merges https://gitlab.gnome.org/GNOME/gtk/-/merge_requests/6398 + decoration_manager: ?*org.KdeKwinServerDecorationManager = null, pub fn init(common: *protocol.App) !void { // Check if we're actually on Wayland @@ -45,6 +48,7 @@ pub const Surface = struct { /// A token that, when present, indicates that the window is blurred. blur_token: ?*org.KdeKwinBlur = null, + decoration: ?*org.KdeKwinServerDecoration = null, pub fn init(common: *protocol.Surface) void { const surface = c.gtk_native_get_surface(@ptrCast(common.gtk_window)) orelse return; @@ -56,21 +60,31 @@ pub const Surface = struct { ) == 0) return; - const self: Surface = .{ + var self: Surface = .{ .common = common, .app = &common.app.inner.wayland, .surface = @ptrCast(c.gdk_wayland_surface_get_wl_surface(surface) orelse return), }; + if (self.app.decoration_manager) |mgr| { + if (mgr.create(self.surface)) |deco| { + self.decoration = deco; + } else |err| { + log.warn("could not create decoration object={}", .{err}); + } + } + common.inner = .{ .wayland = self }; } pub fn deinit(self: Surface) void { if (self.blur_token) |blur| blur.release(); + if (self.decoration) |deco| deco.release(); } pub fn onConfigUpdate(self: *Surface) !void { try self.updateBlur(); + try self.updateDecoration(); } fn updateBlur(self: *Surface) !void { @@ -98,6 +112,18 @@ pub const Surface = struct { } } } + + fn updateDecoration(self: *Surface) !void { + if (self.decoration) |deco| { + const mode: org.KdeKwinServerDecoration.Mode = switch (self.common.derived_config.window_decoration) { + .true => .Client, + .server => .Server, + .false => .None, + }; + + deco.requestMode(@intCast(@intFromEnum(mode))); + } + } }; fn registryListener(registry: *wl.Registry, event: wl.Registry.Event, state: *App) void { @@ -108,6 +134,10 @@ fn registryListener(registry: *wl.Registry, event: wl.Registry.Event, state: *Ap state.blur_manager = iface; return; } + if (bindInterface(org.KdeKwinServerDecorationManager, registry, global, 1)) |iface| { + state.decoration_manager = iface; + return; + } }, .global_remove => {}, } diff --git a/src/build/SharedDeps.zig b/src/build/SharedDeps.zig index 077da96a62..eb80cb6c94 100644 --- a/src/build/SharedDeps.zig +++ b/src/build/SharedDeps.zig @@ -452,10 +452,14 @@ pub fn add( .target = target, .optimize = optimize, }); + + // FIXME: replace with `zxdg_decoration_v1` once GTK merges https://gitlab.gnome.org/GNOME/gtk/-/merge_requests/6398 scanner.addCustomProtocol(plasma_wayland_protocols.path("src/protocols/blur.xml")); + scanner.addCustomProtocol(plasma_wayland_protocols.path("src/protocols/server-decoration.xml")); scanner.generate("wl_compositor", 1); scanner.generate("org_kde_kwin_blur_manager", 1); + scanner.generate("org_kde_kwin_server_decoration_manager", 1); step.root_module.addImport("wayland", wayland); step.linkSystemLibrary2("wayland-client", dynamic_link_opts); diff --git a/src/config/Config.zig b/src/config/Config.zig index 7ec6a5dfee..d8cf5a61f6 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -1110,7 +1110,7 @@ keybind: Keybinds = .{}, /// /// macOS: To hide the titlebar without removing the native window borders /// or rounded corners, use `macos-titlebar-style = hidden` instead. -@"window-decoration": bool = true, +@"window-decoration": WindowDecoration = .true, /// The font that will be used for the application's window and tab titles. /// @@ -5780,6 +5780,27 @@ pub const BackgroundBlur = union(enum) { } }; +/// See window-decoration +pub const WindowDecoration = enum { + true, + server, + false, + + pub fn parseCLI(input: ?[]const u8) !WindowDecoration { + const input_ = input orelse { + // Emulate behavior for bools + return .true; + }; + + return if (cli.args.parseBool(input_)) |b| + if (b) .true else .false + else |_| if (std.mem.eql(u8, input_, "server")) + .server + else + error.InvalidValue; + } +}; + /// See theme pub const Theme = struct { light: []const u8,