From ca42b4ca1c7a9319b7fbc17cabab0cbb4120f0fb Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Mon, 14 Oct 2024 18:24:05 -0500 Subject: [PATCH 1/2] gtk: use CSS variables and color calcs introduced in 4.16 This takes advantage of CSS variables and color expressions to improve the `window-theme=ghostty` support. The only visibile difference from the previous implementation is that the header bar will darken if the Ghostty window is in the background, which is standard for GTK apps. This is conditional at runtime. If Ghostty detects that you're running against GTK 4.16 or newer it will use the CSS variables and color calcs. If you're running against older versions it will use CSS classes to achieve nearly the same effect. --- src/apprt/gtk/App.zig | 85 ++++++++++++++++++++++++++-------------- src/apprt/gtk/Window.zig | 6 ++- 2 files changed, 60 insertions(+), 31 deletions(-) diff --git a/src/apprt/gtk/App.zig b/src/apprt/gtk/App.zig index 155702296..16febf779 100644 --- a/src/apprt/gtk/App.zig +++ b/src/apprt/gtk/App.zig @@ -852,45 +852,72 @@ fn syncActionAccelerator( } fn loadRuntimeCss(config: *const Config, provider: *c.GtkCssProvider) !void { + var buf: [4096]u8 = undefined; + var fbs = std.io.fixedBufferStream(&buf); + const writer = fbs.writer(); + + const window_theme = config.@"window-theme"; const unfocused_fill: Config.Color = config.@"unfocused-split-fill" orelse config.background; const headerbar_background = config.background; const headerbar_foreground = config.foreground; - const fmt = + try writer.print( \\widget.unfocused-split {{ \\ opacity: {d:.2}; \\ background-color: rgb({d},{d},{d}); \\}} - \\window.window-theme-ghostty .top-bar, - \\window.window-theme-ghostty .bottom-bar, - \\window.window-theme-ghostty box > tabbar {{ - \\ background-color: rgb({d},{d},{d}); - \\ color: rgb({d},{d},{d}); - \\}} - ; - // The length required is always less than the length of the pre-formatted string: - // -> '{d:.2}' gets replaced with max 4 bytes (0.00) - // -> each {d} could be replaced with max 3 bytes - var css_buf: [fmt.len]u8 = undefined; + , .{ + 1.0 - config.@"unfocused-split-opacity", + unfocused_fill.r, + unfocused_fill.g, + unfocused_fill.b, + }); + + // this is specifically a runtime-only check + if (version.atLeast(4, 16, 0)) { + switch (window_theme) { + .ghostty => try writer.print( + \\:root {{ + \\ --headerbar-fg-color: rgb({d},{d},{d}); + \\ --headerbar-bg-color: rgb({d},{d},{d}); + \\ --headerbar-backdrop-color: oklab(from var(--headerbar-bg-color) calc(l * 0.9) a b / alpha); + \\}} + , + .{ + headerbar_foreground.r, + headerbar_foreground.g, + headerbar_foreground.b, + headerbar_background.r, + headerbar_background.g, + headerbar_background.b, + }, + ), + else => {}, + } + } else { + try writer.print( + \\window.window-theme-ghostty .top-bar, + \\window.window-theme-ghostty .bottom-bar, + \\window.window-theme-ghostty box > tabbar {{ + \\ background-color: rgb({d},{d},{d}); + \\ color: rgb({d},{d},{d}); + \\}} + , + .{ + headerbar_background.r, + headerbar_background.g, + headerbar_background.b, + headerbar_foreground.r, + headerbar_foreground.g, + headerbar_foreground.b, + }, + ); + } + + const css = fbs.getWritten(); - const css = try std.fmt.bufPrintZ( - &css_buf, - fmt, - .{ - 1.0 - config.@"unfocused-split-opacity", - unfocused_fill.r, - unfocused_fill.g, - unfocused_fill.b, - headerbar_background.r, - headerbar_background.g, - headerbar_background.b, - headerbar_foreground.r, - headerbar_foreground.g, - headerbar_foreground.b, - }, - ); // Clears any previously loaded CSS from this provider - c.gtk_css_provider_load_from_data(provider, css, @intCast(css.len)); + c.gtk_css_provider_load_from_data(provider, css.ptr, @intCast(css.len)); } /// Called by CoreApp to wake up the event loop. diff --git a/src/apprt/gtk/Window.zig b/src/apprt/gtk/Window.zig index 9f5bc0bad..14bb68b8e 100644 --- a/src/apprt/gtk/Window.zig +++ b/src/apprt/gtk/Window.zig @@ -23,6 +23,7 @@ const c = @import("c.zig").c; const adwaita = @import("adwaita.zig"); const gtk_key = @import("key.zig"); const Notebook = @import("notebook.zig").Notebook; +const version = @import("version.zig"); const log = std.log.scoped(.gtk); @@ -108,8 +109,9 @@ pub fn init(self: *Window, app: *App) !void { c.gtk_window_set_icon_name(gtk_window, "com.mitchellh.ghostty"); - // Apply class to color headerbar if window-theme is set to `ghostty`. - if (app.config.@"window-theme" == .ghostty) { + // Apply class to color headerbar if window-theme is set to `ghostty` and + // GTK version is before 4.16. + if (!version.atLeast(4, 16, 0) and app.config.@"window-theme" == .ghostty) { c.gtk_widget_add_css_class(@ptrCast(gtk_window), "window-theme-ghostty"); } From c614f2b485b273fdd80de9bad00b1ef56eac1ffc Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 16 Oct 2024 06:19:28 -0700 Subject: [PATCH 2/2] apprt/gtk: use stackfallback for runtime CSS, explicit errors --- src/apprt/gtk/App.zig | 76 ++++++++++++++++++++++++---------------- src/apprt/gtk/Window.zig | 3 +- 2 files changed, 48 insertions(+), 31 deletions(-) diff --git a/src/apprt/gtk/App.zig b/src/apprt/gtk/App.zig index 16febf779..67d08812e 100644 --- a/src/apprt/gtk/App.zig +++ b/src/apprt/gtk/App.zig @@ -12,6 +12,7 @@ const App = @This(); const std = @import("std"); const assert = std.debug.assert; +const Allocator = std.mem.Allocator; const builtin = @import("builtin"); const apprt = @import("../../apprt.zig"); const configpkg = @import("../../config.zig"); @@ -390,7 +391,12 @@ pub fn init(core_app: *CoreApp, opts: Options) !App { @ptrCast(css_provider), c.GTK_STYLE_PROVIDER_PRIORITY_APPLICATION + 3, ); - try loadRuntimeCss(&config, css_provider); + loadRuntimeCss(core_app.alloc, &config, css_provider) catch |err| switch (err) { + error.OutOfMemory => log.warn( + "out of memory loading runtime CSS, no runtime CSS applied", + .{}, + ), + }; return .{ .core_app = core_app, @@ -793,7 +799,15 @@ pub fn reloadConfig(self: *App) !?*const Config { fn syncConfigChanges(self: *App) !void { try self.updateConfigErrors(); try self.syncActionAccelerators(); - try loadRuntimeCss(&self.config, self.css_provider); + + // Load our runtime CSS. If this fails then our window is just stuck + // with the old CSS but we don't want to fail the entire sync operation. + loadRuntimeCss(self.core_app.alloc, &self.config, self.css_provider) catch |err| switch (err) { + error.OutOfMemory => log.warn( + "out of memory loading runtime CSS, no runtime CSS applied", + .{}, + ), + }; } /// This should be called whenever the configuration changes to update @@ -851,10 +865,15 @@ fn syncActionAccelerator( ); } -fn loadRuntimeCss(config: *const Config, provider: *c.GtkCssProvider) !void { - var buf: [4096]u8 = undefined; - var fbs = std.io.fixedBufferStream(&buf); - const writer = fbs.writer(); +fn loadRuntimeCss( + alloc: Allocator, + config: *const Config, + provider: *c.GtkCssProvider, +) Allocator.Error!void { + var stack_alloc = std.heap.stackFallback(4096, alloc); + var buf = std.ArrayList(u8).init(stack_alloc.get()); + defer buf.deinit(); + const writer = buf.writer(); const window_theme = config.@"window-theme"; const unfocused_fill: Config.Color = config.@"unfocused-split-fill" orelse config.background; @@ -873,7 +892,6 @@ fn loadRuntimeCss(config: *const Config, provider: *c.GtkCssProvider) !void { unfocused_fill.b, }); - // this is specifically a runtime-only check if (version.atLeast(4, 16, 0)) { switch (window_theme) { .ghostty => try writer.print( @@ -882,16 +900,14 @@ fn loadRuntimeCss(config: *const Config, provider: *c.GtkCssProvider) !void { \\ --headerbar-bg-color: rgb({d},{d},{d}); \\ --headerbar-backdrop-color: oklab(from var(--headerbar-bg-color) calc(l * 0.9) a b / alpha); \\}} - , - .{ - headerbar_foreground.r, - headerbar_foreground.g, - headerbar_foreground.b, - headerbar_background.r, - headerbar_background.g, - headerbar_background.b, - }, - ), + , .{ + headerbar_foreground.r, + headerbar_foreground.g, + headerbar_foreground.b, + headerbar_background.r, + headerbar_background.g, + headerbar_background.b, + }), else => {}, } } else { @@ -902,22 +918,22 @@ fn loadRuntimeCss(config: *const Config, provider: *c.GtkCssProvider) !void { \\ background-color: rgb({d},{d},{d}); \\ color: rgb({d},{d},{d}); \\}} - , - .{ - headerbar_background.r, - headerbar_background.g, - headerbar_background.b, - headerbar_foreground.r, - headerbar_foreground.g, - headerbar_foreground.b, - }, - ); + , .{ + headerbar_background.r, + headerbar_background.g, + headerbar_background.b, + headerbar_foreground.r, + headerbar_foreground.g, + headerbar_foreground.b, + }); } - const css = fbs.getWritten(); - // Clears any previously loaded CSS from this provider - c.gtk_css_provider_load_from_data(provider, css.ptr, @intCast(css.len)); + c.gtk_css_provider_load_from_data( + provider, + buf.items.ptr, + @intCast(buf.items.len), + ); } /// Called by CoreApp to wake up the event loop. diff --git a/src/apprt/gtk/Window.zig b/src/apprt/gtk/Window.zig index 14bb68b8e..65041d600 100644 --- a/src/apprt/gtk/Window.zig +++ b/src/apprt/gtk/Window.zig @@ -110,7 +110,8 @@ pub fn init(self: *Window, app: *App) !void { c.gtk_window_set_icon_name(gtk_window, "com.mitchellh.ghostty"); // Apply class to color headerbar if window-theme is set to `ghostty` and - // GTK version is before 4.16. + // GTK version is before 4.16. The conditional is because above 4.16 + // we use GTK CSS color variables. if (!version.atLeast(4, 16, 0) and app.config.@"window-theme" == .ghostty) { c.gtk_widget_add_css_class(@ptrCast(gtk_window), "window-theme-ghostty"); }