From 084a20c8655d8fd28318b7a829957672b646593d Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 1 Aug 2025 14:55:53 -0700 Subject: [PATCH] apprt/gtk-ng: hook up all the syncAppearance calls for winproto --- src/apprt/gtk-ng/class/window.zig | 58 ++++++++++++++++++++++++--- src/apprt/gtk-ng/css/style.css | 8 ++++ src/apprt/gtk-ng/ui/1.5/window.blp | 10 ++++- src/apprt/gtk-ng/winproto/wayland.zig | 24 ++++++++--- src/apprt/gtk-ng/winproto/x11.zig | 19 +++++++-- 5 files changed, 102 insertions(+), 17 deletions(-) diff --git a/src/apprt/gtk-ng/class/window.zig b/src/apprt/gtk-ng/class/window.zig index 2a3fab8cf..ac68bddc3 100644 --- a/src/apprt/gtk-ng/class/window.zig +++ b/src/apprt/gtk-ng/class/window.zig @@ -503,7 +503,17 @@ pub const Window = extern struct { /// happens that might affect how the window appears (config change, /// fullscreen, etc.). fn syncAppearance(self: *Self) void { - // TODO: CSD/SSD + const priv = self.private(); + const csd_enabled = priv.winproto.clientSideDecorationEnabled(); + self.as(gtk.Window).setDecorated(@intFromBool(csd_enabled)); + + // 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. + self.toggleCssClass("csd", csd_enabled); + self.toggleCssClass("ssd", !csd_enabled); + self.toggleCssClass("no-border-radius", !csd_enabled); // Trigger all our dynamic properties that depend on the config. inline for (&.{ @@ -520,15 +530,28 @@ pub const Window = extern struct { } // Remainder uses the config - const priv = self.private(); const config = if (priv.config) |v| v.get() else return; + // Apply class to color headerbar if window-theme is set to `ghostty` and + // GTK version is before 4.16. The conditional is because above 4.16 + // we use GTK CSS color variables. + self.toggleCssClass( + "window-theme-ghostty", + !gtk_version.atLeast(4, 16, 0) and + config.@"window-theme" == .ghostty, + ); + // Move the tab bar to the proper location. priv.toolbar.remove(priv.tab_bar.as(gtk.Widget)); switch (config.@"gtk-tabs-location") { .top => priv.toolbar.addTopBar(priv.tab_bar.as(gtk.Widget)), .bottom => priv.toolbar.addBottomBar(priv.tab_bar.as(gtk.Widget)), } + + // Do our window-protocol specific appearance sync. + priv.winproto.syncAppearance() catch |err| { + log.warn("failed to sync winproto appearance error={}", .{err}); + }; } fn toggleCssClass(self: *Self, class: [:0]const u8, value: bool) void { @@ -614,8 +637,14 @@ pub const Window = extern struct { } fn getHeaderbarVisible(self: *Self) bool { - // TODO: CSD/SSD - // TODO: QuickTerminal + const priv = self.private(); + + // Never display the header bar when CSDs are disabled. + const csd_enabled = priv.winproto.clientSideDecorationEnabled(); + if (!csd_enabled) return false; + + // Never display the header bar as a quick terminal. + if (priv.quick_terminal) return false; // If we're fullscreen we never show the header bar. if (self.as(gtk.Window).isFullscreen() != 0) return false; @@ -749,6 +778,24 @@ pub const Window = extern struct { self.toggleCssClass("background", self.getBackgroundOpaque()); } + fn propScaleFactor( + _: *adw.ApplicationWindow, + _: *gobject.ParamSpec, + self: *Self, + ) callconv(.c) void { + // On some platforms (namely X11) we need to refresh our appearance when + // the scale factor changes. In theory this could be more fine-grained as + // a full refresh could be expensive, but a) this *should* be rare, and + // b) quite noticeable visual bugs would occur if this is not present. + self.private().winproto.syncAppearance() catch |err| { + log.warn( + "failed to sync appearance after scale factor has been updated={}", + .{err}, + ); + return; + }; + } + //--------------------------------------------------------------- // Virtual methods @@ -1451,11 +1498,12 @@ pub const Window = extern struct { class.bindTemplateCallback("tab_create_window", &tabViewCreateWindow); class.bindTemplateCallback("notify_n_pages", &tabViewNPages); class.bindTemplateCallback("notify_selected_page", &tabViewSelectedPage); + class.bindTemplateCallback("notify_background_opaque", &propBackgroundOpaque); class.bindTemplateCallback("notify_config", &propConfig); class.bindTemplateCallback("notify_fullscreened", &propFullscreened); class.bindTemplateCallback("notify_maximized", &propMaximized); class.bindTemplateCallback("notify_menu_active", &propMenuActive); - class.bindTemplateCallback("notify_background_opaque", &propBackgroundOpaque); + class.bindTemplateCallback("notify_scale_factor", &propScaleFactor); // Virtual methods gobject.Object.virtual_methods.dispose.implement(class, &dispose); diff --git a/src/apprt/gtk-ng/css/style.css b/src/apprt/gtk-ng/css/style.css index 4aa03c598..1e3e09d9f 100644 --- a/src/apprt/gtk-ng/css/style.css +++ b/src/apprt/gtk-ng/css/style.css @@ -4,6 +4,14 @@ * https://gnome.pages.gitlab.gnome.org/libadwaita/doc/1.3/styles-and-appearance.html#custom-styles */ +window.ssd.no-border-radius { + /* Without clearing the border radius, at least on Mutter with + * gtk-titlebar=true and gtk-adwaita=false, there is some window artifacting + * that this will mitigate. + */ + border-radius: 0 0; +} + /* * GhosttySurface URL overlay */ diff --git a/src/apprt/gtk-ng/ui/1.5/window.blp b/src/apprt/gtk-ng/ui/1.5/window.blp index 02b035c5a..658107a8f 100644 --- a/src/apprt/gtk-ng/ui/1.5/window.blp +++ b/src/apprt/gtk-ng/ui/1.5/window.blp @@ -8,10 +8,11 @@ template $GhosttyWindow: Adw.ApplicationWindow { close-request => $close_request(); realize => $realize(); + notify::background-opaque => $notify_background_opaque(); notify::config => $notify_config(); notify::fullscreened => $notify_fullscreened(); notify::maximized => $notify_maximized(); - notify::background-opaque => $notify_background_opaque(); + notify::scale-factor => $notify_scale_factor(); default-width: 800; default-height: 600; // GTK4 grabs F10 input by default to focus the menubar icon. We want @@ -21,8 +22,13 @@ template $GhosttyWindow: Adw.ApplicationWindow { content: Adw.TabOverview tab_overview { create-tab => $overview_create_tab(); notify::open => $overview_notify_open(); - enable-new-tab: true; view: tab_view; + enable-new-tab: true; + // Disable the title buttons (close, maximize, minimize, ...) + // *inside* the tab overview if CSDs are disabled. + // We do spare the search button, though. + show-start-title-buttons: bind template.decorated; + show-end-title-buttons: bind template.decorated; Adw.ToolbarView toolbar { top-bar-style: bind template.toolbar-style; diff --git a/src/apprt/gtk-ng/winproto/wayland.zig b/src/apprt/gtk-ng/winproto/wayland.zig index c0e243b6f..918be3636 100644 --- a/src/apprt/gtk-ng/winproto/wayland.zig +++ b/src/apprt/gtk-ng/winproto/wayland.zig @@ -364,7 +364,11 @@ 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 blur = self.apprt_window.config.background_blur; + 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 @@ -392,7 +396,12 @@ pub const Window = struct { } fn getDecorationMode(self: Window) org.KdeKwinServerDecorationManager.Mode { - return switch (self.apprt_window.config.window_decoration) { + const config = if (self.apprt_window.getConfig()) |v| + v.get() + else + return .Client; + + return switch (config.@"window-decoration") { .auto => self.app_context.default_deco_mode orelse .Client, .client => .Client, .server => .Server, @@ -401,12 +410,15 @@ pub const Window = struct { } fn syncQuickTerminal(self: *Window) !void { - const window = self.apprt_window.window.as(gtk.Window); - const config = &self.apprt_window.config; + const window = self.apprt_window.as(gtk.Window); + const config = if (self.apprt_window.getConfig()) |v| + v.get() + else + return; layer_shell.setKeyboardMode( window, - switch (config.quick_terminal_keyboard_interactivity) { + switch (config.@"quick-terminal-keyboard-interactivity") { .none => .none, .@"on-demand" => on_demand: { if (layer_shell.getProtocolVersion() < 4) { @@ -419,7 +431,7 @@ pub const Window = struct { }, ); - const anchored_edge: ?layer_shell.ShellEdge = switch (config.quick_terminal_position) { + const anchored_edge: ?layer_shell.ShellEdge = switch (config.@"quick-terminal-position") { .left => .left, .right => .right, .top => .top, diff --git a/src/apprt/gtk-ng/winproto/x11.zig b/src/apprt/gtk-ng/winproto/x11.zig index c77b9978d..db226f78a 100644 --- a/src/apprt/gtk-ng/winproto/x11.zig +++ b/src/apprt/gtk-ng/winproto/x11.zig @@ -239,7 +239,12 @@ pub const Window = struct { } pub fn clientSideDecorationEnabled(self: Window) bool { - return switch (self.config.window_decoration) { + const config = if (self.apprt_window.getConfig()) |v| + v.get() + else + return true; + + return switch (config.@"window-decoration") { .auto, .client => true, .server, .none => false, }; @@ -255,13 +260,14 @@ pub const Window = struct { // (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; // Transform surface coordinates to device coordinates. - const scale = self.apprt_window.as(gtk.Widget).getScaleFactor(); + const scale = gtk_widget.getScaleFactor(); self.blur_region.width = gtk_widget.getWidth() * scale; self.blur_region.height = gtk_widget.getHeight() * scale; - const blur = self.config.background_blur; + const blur = config.@"background-blur"; log.debug("set blur={}, window xid={}, region={}", .{ blur, self.x11_surface.getXid(), @@ -283,6 +289,11 @@ pub const Window = struct { } fn syncDecorations(self: *Window) !void { + const config = if (self.apprt_window.getConfig()) |v| + v.get() + else + return; + var hints: MotifWMHints = .{}; self.getWindowProperty( @@ -303,7 +314,7 @@ pub const Window = struct { }; hints.flags.decorations = true; - hints.decorations.all = switch (self.config.window_decoration) { + hints.decorations.all = switch (config.@"window-decoration") { .server => true, .auto, .client, .none => false, };