From 7716f9885665ee2501aa7a26446013aca761e040 Mon Sep 17 00:00:00 2001 From: Leah Amelia Chen Date: Wed, 15 Jan 2025 23:20:59 +0100 Subject: [PATCH] gtk(wayland): respect compositor SSD preferences Compositors can actually tell us whether they want to use CSD or SSD! --- src/apprt/gtk/App.zig | 18 +++++----- src/apprt/gtk/Window.zig | 4 +-- src/apprt/gtk/winproto/wayland.zig | 56 ++++++++++++++++++++---------- src/apprt/gtk/winproto/x11.zig | 2 +- src/config/Config.zig | 52 +++++++++++++-------------- 5 files changed, 74 insertions(+), 58 deletions(-) diff --git a/src/apprt/gtk/App.zig b/src/apprt/gtk/App.zig index 96275684e5..63ba0a6929 100644 --- a/src/apprt/gtk/App.zig +++ b/src/apprt/gtk/App.zig @@ -1881,16 +1881,14 @@ fn initContextMenu(self: *App) void { c.g_menu_append(section, "Terminal Inspector", "win.toggle_inspector"); } - if (!self.config.@"window-decoration".isCSD()) { - const section = c.g_menu_new(); - defer c.g_object_unref(section); - const submenu = c.g_menu_new(); - defer c.g_object_unref(submenu); - - initMenuContent(@ptrCast(submenu)); - c.g_menu_append_submenu(section, "Menu", @ptrCast(@alignCast(submenu))); - c.g_menu_append_section(menu, null, @ptrCast(@alignCast(section))); - } + const section = c.g_menu_new(); + defer c.g_object_unref(section); + const submenu = c.g_menu_new(); + defer c.g_object_unref(submenu); + + initMenuContent(@ptrCast(submenu)); + c.g_menu_append_submenu(section, "Menu", @ptrCast(@alignCast(submenu))); + c.g_menu_append_section(menu, null, @ptrCast(@alignCast(section))); self.context_menu = menu; } diff --git a/src/apprt/gtk/Window.zig b/src/apprt/gtk/Window.zig index 10af251010..03fcd05db4 100644 --- a/src/apprt/gtk/Window.zig +++ b/src/apprt/gtk/Window.zig @@ -584,8 +584,8 @@ pub fn toggleFullscreen(self: *Window) void { /// Toggle the window decorations for this window. pub fn toggleWindowDecorations(self: *Window) void { self.app.config.@"window-decoration" = switch (self.app.config.@"window-decoration") { - .client, .server => .none, - .none => .server, + .auto, .client, .server => .none, + .none => .client, }; self.updateConfig(&self.app.config) catch {}; } diff --git a/src/apprt/gtk/winproto/wayland.zig b/src/apprt/gtk/winproto/wayland.zig index efe0d89cd8..7a28fc92cb 100644 --- a/src/apprt/gtk/winproto/wayland.zig +++ b/src/apprt/gtk/winproto/wayland.zig @@ -22,6 +22,8 @@ pub const App = struct { // FIXME: replace with `zxdg_decoration_v1` once GTK merges // https://gitlab.gnome.org/GNOME/gtk/-/merge_requests/6398 kde_decoration_manager: ?*org.KdeKwinServerDecorationManager = null, + + default_deco_mode: ?org.KdeKwinServerDecorationManager.Mode = null, }; pub fn init( @@ -57,6 +59,12 @@ pub const App = struct { registry.setListener(*Context, registryListener, context); if (display.roundtrip() != .SUCCESS) return error.RoundtripFailed; + if (context.kde_decoration_manager != null) { + // FIXME: Roundtrip again because we have to wait for the decoration + // manager to respond with the preferred default mode. Ew. + if (display.roundtrip() != .SUCCESS) return error.RoundtripFailed; + } + return .{ .display = display, .context = context, @@ -82,25 +90,22 @@ pub const App = struct { ) void { switch (event) { // https://wayland.app/protocols/wayland#wl_registry:event:global - .global => |global| global: { + .global => |global| { log.debug("wl_registry.global: interface={s}", .{global.interface}); if (registryBind( org.KdeKwinBlurManager, registry, global, - 1, )) |blur_manager| { context.kde_blur_manager = blur_manager; - break :global; } else if (registryBind( org.KdeKwinServerDecorationManager, registry, global, - 1, )) |deco_manager| { context.kde_decoration_manager = deco_manager; - break :global; + deco_manager.setListener(*Context, decoManagerListener, context); } }, @@ -119,7 +124,6 @@ pub const App = struct { comptime T: type, registry: *wl.Registry, global: anytype, - version: u32, ) ?*T { if (std.mem.orderZ( u8, @@ -127,7 +131,7 @@ pub const App = struct { T.interface.name, ) != .eq) return null; - return registry.bind(global.name, T, version) catch |err| { + return registry.bind(global.name, T, T.generated_version) catch |err| { log.warn("error binding interface {s} error={}", .{ global.interface, err, @@ -135,6 +139,18 @@ pub const App = struct { return null; }; } + + fn decoManagerListener( + _: *org.KdeKwinServerDecorationManager, + event: org.KdeKwinServerDecorationManager.Event, + context: *Context, + ) void { + switch (event) { + .default_mode => |mode| { + context.default_deco_mode = @enumFromInt(mode.mode); + }, + } + } }; /// Per-window (wl_surface) state for the Wayland protocol. @@ -235,13 +251,14 @@ pub const Window = struct { } pub fn clientSideDecorationEnabled(self: Window) bool { - // Note: we should change this to being the actual mode - // state emitted by the decoration manager. + // Compositor doesn't support the SSD protocol + if (self.decoration == null) return true; - // We are CSD if we don't support the SSD Wayland protocol - // or if we do but we're in CSD mode. - return self.decoration == null or - self.config.window_decoration.isCSD(); + return switch (self.getDecorationMode()) { + .Client => true, + .Server, .None => false, + else => unreachable, + }; } /// Update the blur state of the window. @@ -269,14 +286,17 @@ pub const Window = struct { fn syncDecoration(self: *Window) !void { const deco = self.decoration orelse return; - const mode: org.KdeKwinServerDecoration.Mode = switch (self.config.window_decoration) { + // The protocol requests uint instead of enum so we have + // to convert it. + deco.requestMode(@intCast(@intFromEnum(self.getDecorationMode()))); + } + + fn getDecorationMode(self: Window) org.KdeKwinServerDecorationManager.Mode { + return switch (self.config.window_decoration) { + .auto => self.app_context.default_deco_mode orelse .Client, .client => .Client, .server => .Server, .none => .None, }; - - // The protocol requests uint instead of enum so we have - // to convert it. - deco.requestMode(@intCast(@intFromEnum(mode))); } }; diff --git a/src/apprt/gtk/winproto/x11.zig b/src/apprt/gtk/winproto/x11.zig index fe3b9218d4..4f607d1efb 100644 --- a/src/apprt/gtk/winproto/x11.zig +++ b/src/apprt/gtk/winproto/x11.zig @@ -168,7 +168,7 @@ pub const Window = struct { .blur = config.@"background-blur-radius".enabled(), .has_decoration = switch (config.@"window-decoration") { .none => false, - .client, .server => true, + .auto, .client, .server => true, }, }; } diff --git a/src/config/Config.zig b/src/config/Config.zig index 4aba8ce322..baac2cde7b 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -1138,27 +1138,33 @@ keybind: Keybinds = .{}, /// borders, etc. will not be shown. On macOS, this will also disable /// tabs (enforced by the system). /// -/// * `client` - Prefer client-side decorations. This is the default. +/// * `auto` - Automatically decide to use either client-side or server-side +/// decorations based on the detected preferences of the current OS and +/// desktop environment. This option usually makes Ghostty look the most +/// "native" for your desktop. +/// +/// * `client` - Prefer client-side decorations. /// /// * `server` - Prefer server-side decorations. This is only relevant /// on Linux with GTK. This currently only works on Linux with Wayland /// and the `org_kde_kwin_server_decoration` protocol available (e.g. -/// KDE Plasma, but almost any non-Gnome desktop supports this protocol). +/// KDE Plasma, but almost any non-GNOME desktop supports this protocol). /// /// If `server` is set but the environment doesn't support server-side /// decorations, client-side decorations will be used instead. /// -/// The default value is `client`. +/// The default value is `auto`. /// -/// This setting also accepts boolean true and false values. If set to `true`, -/// this is equivalent to `client`. If set to `false`, this is equivalent to -/// `none`. This is a convenience for users who live primarily on systems -/// that don't differentiate between client and server-side decorations -/// (e.g. macOS and Windows). +/// For the sake of backwards compatibility and convenience, this setting also +/// accepts boolean true and false values. If set to `true`, this is equivalent +/// to `auto`. If set to `false`, this is equivalent to `none`. +/// This is convenient for users who live primarily on systems that don't +/// differentiate between client and server-side decorations (e.g. macOS and +/// Windows). /// /// The "toggle_window_decorations" keybind action can be used to create /// a keybinding to toggle this setting at runtime. This will always toggle -/// back to "server" if the current value is "none" (this is an issue +/// back to "auto" if the current value is "none" (this is an issue /// that will be fixed in the future). /// /// Changing this configuration in your configuration and reloading will @@ -1166,7 +1172,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": WindowDecoration = .client, +@"window-decoration": WindowDecoration = .auto, /// The font that will be used for the application's window and tab titles. /// @@ -5917,44 +5923,32 @@ pub const BackgroundBlur = union(enum) { /// See window-decoration pub const WindowDecoration = enum { + auto, client, server, none, pub fn parseCLI(input_: ?[]const u8) !WindowDecoration { - const input = input_ orelse return .client; + const input = input_ orelse return .auto; return if (cli.args.parseBool(input)) |b| - if (b) .client else .none + if (b) .auto else .none else |_| if (std.meta.stringToEnum(WindowDecoration, input)) |v| v else error.InvalidValue; } - /// Returns true if the window decoration setting results in - /// CSD (client-side decorations). Note that this only returns the - /// user requested behavior. Depending on available APIs (e.g. - /// Wayland protocols), the actual behavior may differ and the apprt - /// should rely on actual windowing APIs to determine the actual - /// status. - pub fn isCSD(self: WindowDecoration) bool { - return switch (self) { - .client => true, - .server, .none => false, - }; - } - test "parse WindowDecoration" { const testing = std.testing; { const v = try WindowDecoration.parseCLI(null); - try testing.expectEqual(WindowDecoration.client, v); + try testing.expectEqual(WindowDecoration.auto, v); } { const v = try WindowDecoration.parseCLI("true"); - try testing.expectEqual(WindowDecoration.client, v); + try testing.expectEqual(WindowDecoration.auto, v); } { const v = try WindowDecoration.parseCLI("false"); @@ -5968,6 +5962,10 @@ pub const WindowDecoration = enum { const v = try WindowDecoration.parseCLI("client"); try testing.expectEqual(WindowDecoration.client, v); } + { + const v = try WindowDecoration.parseCLI("auto"); + try testing.expectEqual(WindowDecoration.auto, v); + } { const v = try WindowDecoration.parseCLI("none"); try testing.expectEqual(WindowDecoration.none, v);