apprt/gtk-ng: runtime CSS and custom CSS (#8133)
A simple port, nothing creative here. Found one definite leak in GTK, added a suppression. No leaks in Ghostty code.pull/8147/head
commit
afdaab9cc6
|
|
@ -49,9 +49,9 @@ pub const blueprints: []const Blueprint = &.{
|
||||||
/// CSS files in css_path
|
/// CSS files in css_path
|
||||||
pub const css = [_][]const u8{
|
pub const css = [_][]const u8{
|
||||||
"style.css",
|
"style.css",
|
||||||
// "style-dark.css",
|
"style-dark.css",
|
||||||
// "style-hc.css",
|
"style-hc.css",
|
||||||
// "style-hc-dark.css",
|
"style-hc-dark.css",
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const Blueprint = struct {
|
pub const Blueprint = struct {
|
||||||
|
|
|
||||||
|
|
@ -132,6 +132,12 @@ pub const Application = extern struct {
|
||||||
/// glib source for our signal handler.
|
/// glib source for our signal handler.
|
||||||
signal_source: ?c_uint = null,
|
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;
|
pub var offset: c_int = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -267,6 +273,16 @@ pub const Application = extern struct {
|
||||||
const config_obj: *Config = try .new(alloc, &config);
|
const config_obj: *Config = try .new(alloc, &config);
|
||||||
errdefer config_obj.unref();
|
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.
|
// Initialize the app.
|
||||||
const self = gobject.ext.newInstance(Self, .{
|
const self = gobject.ext.newInstance(Self, .{
|
||||||
.application_id = app_id.ptr,
|
.application_id = app_id.ptr,
|
||||||
|
|
@ -287,8 +303,22 @@ pub const Application = extern struct {
|
||||||
.core_app = core_app,
|
.core_app = core_app,
|
||||||
.config = config_obj,
|
.config = config_obj,
|
||||||
.winproto = wp,
|
.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;
|
return self;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -303,6 +333,22 @@ pub const Application = extern struct {
|
||||||
priv.config.unref();
|
priv.config.unref();
|
||||||
priv.winproto.deinit(alloc);
|
priv.winproto.deinit(alloc);
|
||||||
if (priv.transient_cgroup_base) |base| alloc.free(base);
|
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
|
/// 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
|
// Properties
|
||||||
|
|
||||||
|
|
@ -684,6 +879,28 @@ pub const Application = extern struct {
|
||||||
self.showConfigErrorsDialog();
|
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
|
// 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.
|
// Abusing integers to be enums and booleans is a terrible idea, C.
|
||||||
return if (window.isActive() != 0) 0 else -1;
|
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);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
.transparent {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
.transparent {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
.transparent {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
@ -13,6 +13,21 @@
|
||||||
# You must gracefully exit Ghostty (do not SIGINT) by closing all windows
|
# You must gracefully exit Ghostty (do not SIGINT) by closing all windows
|
||||||
# and quitting. Otherwise, we leave a number of GTK resources around.
|
# 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
|
GDK SVG Loading Leaks
|
||||||
Memcheck:Leak
|
Memcheck:Leak
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue