diff --git a/src/apprt/gtk-ng/build/gresource.zig b/src/apprt/gtk-ng/build/gresource.zig index 0c73f925b..2c835a172 100644 --- a/src/apprt/gtk-ng/build/gresource.zig +++ b/src/apprt/gtk-ng/build/gresource.zig @@ -49,9 +49,9 @@ pub const blueprints: []const Blueprint = &.{ /// CSS files in css_path pub const css = [_][]const u8{ "style.css", - // "style-dark.css", - // "style-hc.css", - // "style-hc-dark.css", + "style-dark.css", + "style-hc.css", + "style-hc-dark.css", }; pub const Blueprint = struct { diff --git a/src/apprt/gtk-ng/class/application.zig b/src/apprt/gtk-ng/class/application.zig index 4eee1ff55..a8a9015b3 100644 --- a/src/apprt/gtk-ng/class/application.zig +++ b/src/apprt/gtk-ng/class/application.zig @@ -132,6 +132,12 @@ pub const Application = extern struct { /// glib source for our signal handler. signal_source: ?c_uint = null, + /// CSS Provider for any styles based on Ghostty configuration values. + css_provider: *gtk.CssProvider, + + /// Providers for loading custom stylesheets defined by user + custom_css_providers: std.ArrayListUnmanaged(*gtk.CssProvider) = .empty, + pub var offset: c_int = 0; }; @@ -267,6 +273,16 @@ pub const Application = extern struct { const config_obj: *Config = try .new(alloc, &config); errdefer config_obj.unref(); + // Internally, GTK ensures that only one instance of this provider + // exists in the provider list for the display. + const css_provider = gtk.CssProvider.new(); + gtk.StyleContext.addProviderForDisplay( + display, + css_provider.as(gtk.StyleProvider), + gtk.STYLE_PROVIDER_PRIORITY_APPLICATION + 3, + ); + errdefer css_provider.unref(); + // Initialize the app. const self = gobject.ext.newInstance(Self, .{ .application_id = app_id.ptr, @@ -287,8 +303,22 @@ pub const Application = extern struct { .core_app = core_app, .config = config_obj, .winproto = wp, + .css_provider = css_provider, + .custom_css_providers = .empty, }; + // Signals + _ = gobject.Object.signals.notify.connect( + self, + *Self, + propConfig, + self, + .{ .detail = "config" }, + ); + + // Trigger initial config changes + self.as(gobject.Object).notifyByPspec(properties.config.impl.param_spec); + return self; } @@ -303,6 +333,22 @@ pub const Application = extern struct { priv.config.unref(); priv.winproto.deinit(alloc); if (priv.transient_cgroup_base) |base| alloc.free(base); + if (gdk.Display.getDefault()) |display| { + gtk.StyleContext.removeProviderForDisplay( + display, + priv.css_provider.as(gtk.StyleProvider), + ); + + for (priv.custom_css_providers.items) |provider| { + gtk.StyleContext.removeProviderForDisplay( + display, + provider.as(gtk.StyleProvider), + ); + } + } + priv.css_provider.unref(); + for (priv.custom_css_providers.items) |provider| provider.unref(); + priv.custom_css_providers.deinit(alloc); } /// The global allocator that all other classes should use by @@ -659,6 +705,155 @@ pub const Application = extern struct { } } + fn loadRuntimeCss( + self: *Self, + ) Allocator.Error!void { + const alloc = self.allocator(); + + var buf: std.ArrayListUnmanaged(u8) = .empty; + defer buf.deinit(alloc); + + const writer = buf.writer(alloc); + + const config = self.private().config.get(); + const window_theme = config.@"window-theme"; + const unfocused_fill: CoreConfig.Color = config.@"unfocused-split-fill" orelse config.background; + const headerbar_background = config.@"window-titlebar-background" orelse config.background; + const headerbar_foreground = config.@"window-titlebar-foreground" orelse config.foreground; + + try writer.print( + \\widget.unfocused-split {{ + \\ opacity: {d:.2}; + \\ background-color: rgb({d},{d},{d}); + \\}} + , .{ + 1.0 - config.@"unfocused-split-opacity", + unfocused_fill.r, + unfocused_fill.g, + unfocused_fill.b, + }); + + if (config.@"split-divider-color") |color| { + try writer.print( + \\.terminal-window .notebook separator {{ + \\ color: rgb({[r]d},{[g]d},{[b]d}); + \\ background: rgb({[r]d},{[g]d},{[b]d}); + \\}} + , .{ + .r = color.r, + .g = color.g, + .b = color.b, + }); + } + + if (config.@"window-title-font-family") |font_family| { + try writer.print( + \\.window headerbar {{ + \\ font-family: "{[font_family]s}"; + \\}} + , .{ .font_family = font_family }); + } + + switch (window_theme) { + .ghostty => try writer.print( + \\:root {{ + \\ --ghostty-fg: rgb({d},{d},{d}); + \\ --ghostty-bg: rgb({d},{d},{d}); + \\ --headerbar-fg-color: var(--ghostty-fg); + \\ --headerbar-bg-color: var(--ghostty-bg); + \\ --headerbar-backdrop-color: oklab(from var(--headerbar-bg-color) calc(l * 0.9) a b / alpha); + \\ --overview-fg-color: var(--ghostty-fg); + \\ --overview-bg-color: var(--ghostty-bg); + \\ --popover-fg-color: var(--ghostty-fg); + \\ --popover-bg-color: var(--ghostty-bg); + \\ --window-fg-color: var(--ghostty-fg); + \\ --window-bg-color: var(--ghostty-bg); + \\}} + \\windowhandle {{ + \\ background-color: var(--headerbar-bg-color); + \\ color: var(--headerbar-fg-color); + \\}} + \\windowhandle:backdrop {{ + \\ background-color: var(--headerbar-backdrop-color); + \\}} + , .{ + headerbar_foreground.r, + headerbar_foreground.g, + headerbar_foreground.b, + headerbar_background.r, + headerbar_background.g, + headerbar_background.b, + }), + else => {}, + } + + const data = try alloc.dupeZ(u8, buf.items); + defer alloc.free(data); + + // Clears any previously loaded CSS from this provider + loadCssProviderFromData( + self.private().css_provider, + data, + ); + } + + fn loadCustomCss(self: *Self) !void { + const priv = self.private(); + const alloc = self.allocator(); + const display = gdk.Display.getDefault() orelse { + log.warn("unable to get display", .{}); + return; + }; + + // unload the previously loaded style providers + for (priv.custom_css_providers.items) |provider| { + gtk.StyleContext.removeProviderForDisplay( + display, + provider.as(gtk.StyleProvider), + ); + provider.unref(); + } + priv.custom_css_providers.clearRetainingCapacity(); + + const config = priv.config.getMut(); + for (config.@"gtk-custom-css".value.items) |p| { + const path, const optional = switch (p) { + .optional => |path| .{ path, true }, + .required => |path| .{ path, false }, + }; + const file = std.fs.openFileAbsolute(path, .{}) catch |err| { + if (err != error.FileNotFound or !optional) { + log.warn( + "error opening gtk-custom-css file {s}: {}", + .{ path, err }, + ); + } + continue; + }; + defer file.close(); + + log.info("loading gtk-custom-css path={s}", .{path}); + const contents = try file.reader().readAllAlloc( + alloc, + 5 * 1024 * 1024, // 5MB, + ); + defer alloc.free(contents); + + const data = try alloc.dupeZ(u8, contents); + defer alloc.free(data); + + const provider = gtk.CssProvider.new(); + errdefer provider.unref(); + try priv.custom_css_providers.append(alloc, provider); + loadCssProviderFromData(provider, data); + gtk.StyleContext.addProviderForDisplay( + display, + provider.as(gtk.StyleProvider), + gtk.STYLE_PROVIDER_PRIORITY_USER, + ); + } + } + //--------------------------------------------------------------- // Properties @@ -684,6 +879,28 @@ pub const Application = extern struct { self.showConfigErrorsDialog(); } + fn propConfig( + _: *Application, + _: *gobject.ParamSpec, + self: *Self, + ) callconv(.c) void { + // Load our runtime and custom CSS. If this fails then our window is + // just stuck with the old CSS but we don't want to fail the entire + // config change operation. + self.loadRuntimeCss() catch |err| switch (err) { + error.OutOfMemory => log.warn( + "out of memory loading runtime CSS, no runtime CSS applied", + .{}, + ), + }; + self.loadCustomCss() catch |err| { + log.warn( + "failed to load custom CSS, no custom CSS applied, err={}", + .{err}, + ); + }; + } + //--------------------------------------------------------------- // Libghostty Callbacks @@ -1902,3 +2119,8 @@ fn findActiveWindow(data: ?*const anyopaque, _: ?*const anyopaque) callconv(.c) // Abusing integers to be enums and booleans is a terrible idea, C. return if (window.isActive() != 0) 0 else -1; } + +fn loadCssProviderFromData(provider: *gtk.CssProvider, data: [:0]const u8) void { + assert(gtk_version.runtimeAtLeast(4, 12, 0)); + provider.loadFromString(data); +} diff --git a/src/apprt/gtk-ng/css/style-dark.css b/src/apprt/gtk-ng/css/style-dark.css new file mode 100644 index 000000000..a9aa2dcc0 --- /dev/null +++ b/src/apprt/gtk-ng/css/style-dark.css @@ -0,0 +1,3 @@ +.transparent { + background-color: transparent; +} diff --git a/src/apprt/gtk-ng/css/style-hc-dark.css b/src/apprt/gtk-ng/css/style-hc-dark.css new file mode 100644 index 000000000..a9aa2dcc0 --- /dev/null +++ b/src/apprt/gtk-ng/css/style-hc-dark.css @@ -0,0 +1,3 @@ +.transparent { + background-color: transparent; +} diff --git a/src/apprt/gtk-ng/css/style-hc.css b/src/apprt/gtk-ng/css/style-hc.css new file mode 100644 index 000000000..a9aa2dcc0 --- /dev/null +++ b/src/apprt/gtk-ng/css/style-hc.css @@ -0,0 +1,3 @@ +.transparent { + background-color: transparent; +} diff --git a/valgrind.supp b/valgrind.supp index 040328648..3535ecc45 100644 --- a/valgrind.supp +++ b/valgrind.supp @@ -13,6 +13,21 @@ # You must gracefully exit Ghostty (do not SIGINT) by closing all windows # and quitting. Otherwise, we leave a number of GTK resources around. +{ + GTK CSS Provider Leak + Memcheck:Leak + match-leak-kinds: definite + fun:calloc + fun:g_malloc0 + fun:gtk_css_value_alloc + fun:_gtk_css_reference_value_new + fun:parse_ruleset + fun:gtk_css_provider_load_internal + fun:gtk_css_provider_load_from_bytes + fun:gtk_css_provider_load_from_string + ... +} + { GDK SVG Loading Leaks Memcheck:Leak