apprt/gtk-ng: runtime CSS and custom CSS

This commit is contained in:
Mitchell Hashimoto
2025-08-02 14:59:19 -07:00
parent 78f474b143
commit 053e3d307b
6 changed files with 249 additions and 3 deletions

View File

@@ -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 {

View File

@@ -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);
}

View File

@@ -0,0 +1,3 @@
.transparent {
background-color: transparent;
}

View File

@@ -0,0 +1,3 @@
.transparent {
background-color: transparent;
}

View File

@@ -0,0 +1,3 @@
.transparent {
background-color: transparent;
}

View File

@@ -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