Merge pull request #1506 from jcollie/gtk-resources

GTK: Add compiled-in GTK resources and use them for icons.
This commit is contained in:
Mitchell Hashimoto
2024-03-28 14:48:40 -07:00
committed by GitHub
13 changed files with 180 additions and 105 deletions

View File

@ -1165,6 +1165,43 @@ fn addDeps(
.gtk => {
step.linkSystemLibrary2("gtk4", dynamic_link_opts);
if (config.libadwaita) step.linkSystemLibrary2("adwaita-1", dynamic_link_opts);
{
const gresource = @import("src/apprt/gtk/gresource.zig");
const wf = b.addWriteFiles();
const gresource_xml = wf.add(
"gresource.xml",
if (config.libadwaita)
gresource.gresource_xml_libadwaita
else
gresource.gresource_xml_gtk,
);
const generate_resources_c = b.addSystemCommand(&.{
"glib-compile-resources",
"--c-name",
"ghostty",
"--generate-source",
"--target",
});
const ghostty_resources_c = generate_resources_c.addOutputFileArg("ghostty_resources.c");
generate_resources_c.addFileArg(gresource_xml);
generate_resources_c.extra_file_dependencies = if (config.libadwaita) &gresource.dependencies_libadwaita else &gresource.dependencies_gtk;
step.addCSourceFile(.{ .file = ghostty_resources_c, .flags = &.{} });
const generate_resources_h = b.addSystemCommand(&.{
"glib-compile-resources",
"--c-name",
"ghostty",
"--generate-header",
"--target",
});
const ghostty_resources_h = generate_resources_h.addOutputFileArg("ghostty_resources.h");
generate_resources_h.addFileArg(gresource_xml);
generate_resources_h.extra_file_dependencies = if (config.libadwaita) &gresource.dependencies_libadwaita else &gresource.dependencies_gtk;
step.addIncludePath(ghostty_resources_h.dirname());
}
},
}
}

View File

@ -151,8 +151,16 @@ pub fn init(core_app: *CoreApp, opts: Options) !App {
break :app @ptrCast(adw_app);
};
errdefer c.g_object_unref(app);
const gapp = @as(*c.GApplication, @ptrCast(app));
// force the resource path to a known value so that it doesn't depend on
// the app id and load in compiled resources
c.g_application_set_resource_base_path(gapp, "/com/mitchellh/ghostty");
c.g_resources_register(c.ghostty_get_resource());
// The `activate` signal is used when Ghostty is first launched and when a
// secondary Ghostty is launched and requests a new window.
_ = c.g_signal_connect_data(
app,
"activate",
@ -169,7 +177,6 @@ pub fn init(core_app: *CoreApp, opts: Options) !App {
if (c.g_main_context_acquire(ctx) == 0) return error.GtkContextAcquireFailed;
errdefer c.g_main_context_release(ctx);
const gapp = @as(*c.GApplication, @ptrCast(app));
var err_: ?*c.GError = null;
if (c.g_application_register(
gapp,

View File

@ -53,6 +53,7 @@ fn init(self: *ConfigErrors, app: *App) !void {
c.gtk_window_set_title(gtk_window, "Configuration Errors");
c.gtk_window_set_default_size(gtk_window, 600, 275);
c.gtk_window_set_resizable(gtk_window, 0);
c.gtk_window_set_icon_name(gtk_window, "com.mitchellh.ghostty");
_ = c.g_signal_connect_data(window, "destroy", c.G_CALLBACK(&gtkDestroy), self, null, c.G_CONNECT_DEFAULT);
// Set some state

View File

@ -943,32 +943,14 @@ pub fn showDesktopNotification(
0 => "Ghostty",
else => title,
};
const notif = c.g_notification_new(t.ptr);
defer c.g_object_unref(notif);
c.g_notification_set_body(notif, body.ptr);
// Find our icon in the current icon theme. Not pretty, but the builtin GIO
// method "g_themed_icon_new" doesn't search XDG_DATA_DIRS, so any install
// not in /usr/share will be unable to find an icon
const display = c.gdk_display_get_default();
const theme = c.gtk_icon_theme_get_for_display(display);
const icon = c.gtk_icon_theme_lookup_icon(
theme,
"com.mitchellh.ghostty",
null,
48,
1, // Window scale
c.GTK_TEXT_DIR_LTR,
0,
);
const icon = c.g_themed_icon_new("com.mitchellh.ghostty");
defer c.g_object_unref(icon);
// Get the filepath of the icon we found
const file = c.gtk_icon_paintable_get_file(icon);
defer c.g_object_unref(file);
// Create a GIO icon
const gicon = c.g_file_icon_new(file);
defer c.g_object_unref(gicon);
c.g_notification_set_icon(notif, gicon);
c.g_notification_set_icon(notif, icon);
const g_app: *c.GApplication = @ptrCast(self.app.app);

View File

@ -18,7 +18,6 @@ const CoreSurface = @import("../../Surface.zig");
const App = @import("App.zig");
const Surface = @import("Surface.zig");
const Tab = @import("Tab.zig");
const icon = @import("icon.zig");
const c = @import("c.zig");
const log = std.log.scoped(.gtk);
@ -31,10 +30,6 @@ window: *c.GtkWindow,
/// The notebook (tab grouping) for this window.
notebook: *c.GtkNotebook,
/// The resources directory for the icon (if any). We need to retain a
/// pointer to this because GTK can use it at any time.
icon: icon.Icon,
pub fn create(alloc: Allocator, app: *App) !*Window {
// Allocate a fixed pointer for our window. We try to minimize
// allocations but windows and other GUI requirements are so minimal
@ -53,7 +48,6 @@ pub fn init(self: *Window, app: *App) !void {
// Set up our own state
self.* = .{
.app = app,
.icon = undefined,
.window = undefined,
.notebook = undefined,
};
@ -70,10 +64,7 @@ pub fn init(self: *Window, app: *App) !void {
// to disable this so that terminal programs can capture F10 (such as htop)
c.gtk_window_set_handle_menubar_accel(gtk_window, 0);
// If we don't have the icon then we'll try to add our resources dir
// to the search path and see if we can find it there.
self.icon = try icon.appIcon(self.app, window);
c.gtk_window_set_icon_name(gtk_window, self.icon.name);
c.gtk_window_set_icon_name(gtk_window, "com.mitchellh.ghostty");
// Apply background opacity if we have it
if (app.config.@"background-opacity" < 1) {
@ -189,9 +180,7 @@ fn initActions(self: *Window) void {
}
}
pub fn deinit(self: *Window) void {
self.icon.deinit(self.app);
}
pub fn deinit(_: *Window) void {}
/// Add a new tab to this window.
pub fn newTab(self: *Window, parent: ?*CoreSurface) !void {

View File

@ -7,6 +7,9 @@ const c = @cImport({
@cInclude("gdk/x11/gdkx.h");
// Xkb for X11 state handling
@cInclude("X11/XKBlib.h");
// generated header files
@cInclude("ghostty_resources.h");
});
pub usingnamespace c;

124
src/apprt/gtk/gresource.zig Normal file
View File

@ -0,0 +1,124 @@
const std = @import("std");
const css_files = [_][]const u8{
"style.css",
"style-dark.css",
"style-hc.css",
"style-hc-dark.css",
};
const icons = [_]struct {
alias: []const u8,
source: []const u8,
}{
.{
.alias = "16x16",
.source = "16x16",
},
.{
.alias = "16x16@2",
.source = "16x16@2x@2x",
},
.{
.alias = "32x32",
.source = "32x32",
},
.{
.alias = "32x32@2",
.source = "32x32@2x@2x",
},
.{
.alias = "128x128",
.source = "128x128",
},
.{
.alias = "128x128@2",
.source = "128x128@2x@2x",
},
.{
.alias = "256x256",
.source = "256x256",
},
.{
.alias = "256x256@2",
.source = "256x256@2x@2x",
},
.{
.alias = "512x512",
.source = "512x512",
},
};
pub const gresource_xml_gtk = comptimeGenerateGResourceXML(false);
pub const gresource_xml_libadwaita = comptimeGenerateGResourceXML(true);
fn comptimeGenerateGResourceXML(comptime libadwaita: bool) []const u8 {
comptime {
@setEvalBranchQuota(13000);
var counter = std.io.countingWriter(std.io.null_writer);
try writeGResourceXML(libadwaita, &counter.writer());
var buf: [counter.bytes_written]u8 = undefined;
var stream = std.io.fixedBufferStream(&buf);
try writeGResourceXML(libadwaita, stream.writer());
return stream.getWritten();
}
}
fn writeGResourceXML(libadwaita: bool, writer: anytype) !void {
try writer.writeAll(
\\<?xml version="1.0" encoding="UTF-8"?>
\\<gresources>
\\
);
if (libadwaita) {
try writer.writeAll(
\\ <gresource prefix="/com/mitchellh/ghostty">
\\
);
for (css_files) |css_file| {
try writer.print(
" <file compressed=\"true\" alias=\"{s}\">src/apprt/gtk/{s}</file>\n",
.{ css_file, css_file },
);
}
try writer.writeAll(
\\ </gresource>
\\
);
}
try writer.writeAll(
\\ <gresource prefix="/com/mitchellh/ghostty/icons">
\\
);
for (icons) |icon| {
try writer.print(
" <file preprocess=\"to-pixdata\" alias=\"{s}/apps/com.mitchellh.ghostty.png\">images/icons/icon_{s}.png</file>\n",
.{ icon.alias, icon.source },
);
}
try writer.writeAll(
\\ </gresource>
\\</gresources>
\\
);
}
pub const dependencies_gtk = deps: {
var deps: [icons.len][]const u8 = undefined;
for (icons, 0..) |icon, i| {
deps[i] = std.fmt.comptimePrint("images/icons/icon_{s}.png", .{icon.source});
}
break :deps deps;
};
pub const dependencies_libadwaita = deps: {
var deps: [css_files.len + icons.len][]const u8 = undefined;
for (css_files, 0..) |css_file, i| {
deps[i] = std.fmt.comptimePrint("src/apprt/gtk/{s}", .{css_file});
}
for (icons, css_files.len..) |icon, i| {
deps[i] = std.fmt.comptimePrint("images/icons/icon_{s}.png", .{icon.source});
}
break :deps deps;
};

View File

@ -1,63 +0,0 @@
const std = @import("std");
const App = @import("App.zig");
const c = @import("c.zig");
const global_state = &@import("../../main.zig").state;
const log = std.log.scoped(.gtk_icon);
/// An icon. The icon may be associated with some allocated state so when
/// the icon is no longer in use it should be deinitialized.
pub const Icon = struct {
name: [:0]const u8,
state: ?[:0]const u8 = null,
pub fn deinit(self: *const Icon, app: *App) void {
if (self.state) |v| app.core_app.alloc.free(v);
}
};
/// Returns the application icon that can be used anywhere. This attempts to
/// find the icon in the theme and if it can't be found, it is loaded from
/// the resources dir. If the resources dir can't be found, we'll log a warning
/// and let GTK choose a fallback.
pub fn appIcon(app: *App, widget: *c.GtkWidget) !Icon {
const icon_name = "com.mitchellh.ghostty";
var result: Icon = .{ .name = icon_name };
// If we don't have the icon then we'll try to add our resources dir
// to the search path and see if we can find it there.
const icon_theme = c.gtk_icon_theme_get_for_display(c.gtk_widget_get_display(widget));
if (c.gtk_icon_theme_has_icon(icon_theme, icon_name) == 0) icon: {
const resources_dir = global_state.resources_dir orelse {
log.info("gtk app missing Ghostty icon and no resources dir detected", .{});
log.info("gtk app will not have Ghostty icon", .{});
break :icon;
};
// The resources dir usually is `/usr/share/ghostty` but GTK icons
// go into `/usr/share/icons`.
const base = std.fs.path.dirname(resources_dir) orelse {
log.warn(
"unexpected error getting dirname of resources dir dir={s}",
.{resources_dir},
);
break :icon;
};
// Note that this method for adding the icon search path is
// a fallback mechanism. The recommended mechanism is the
// Freedesktop Icon Theme Specification. We distribute a ".desktop"
// file in zig-out/share that should be installed to the proper
// place.
const dir = try std.fmt.allocPrintZ(app.core_app.alloc, "{s}/icons", .{base});
errdefer app.core_app.alloc.free(dir);
result.state = dir;
c.gtk_icon_theme_add_search_path(icon_theme, dir.ptr);
if (c.gtk_icon_theme_has_icon(icon_theme, icon_name) == 0) {
log.warn("Ghostty icon for gtk app not found", .{});
}
}
return result;
}

View File

@ -7,7 +7,6 @@ const Surface = @import("Surface.zig");
const TerminalWindow = @import("Window.zig");
const ImguiWidget = @import("ImguiWidget.zig");
const c = @import("c.zig");
const icon = @import("icon.zig");
const CoreInspector = @import("../../inspector/main.zig").Inspector;
const log = std.log.scoped(.inspector);
@ -125,14 +124,12 @@ pub const Inspector = struct {
const Window = struct {
inspector: *Inspector,
window: *c.GtkWindow,
icon: icon.Icon,
imgui_widget: ImguiWidget,
pub fn init(self: *Window, inspector: *Inspector) !void {
// Initialize to undefined
self.* = .{
.inspector = inspector,
.icon = undefined,
.window = undefined,
.imgui_widget = undefined,
};
@ -144,8 +141,7 @@ const Window = struct {
self.window = gtk_window;
c.gtk_window_set_title(gtk_window, "Ghostty: Terminal Inspector");
c.gtk_window_set_default_size(gtk_window, 1000, 600);
self.icon = try icon.appIcon(self.inspector.surface.app, window);
c.gtk_window_set_icon_name(gtk_window, self.icon.name);
c.gtk_window_set_icon_name(gtk_window, "com.mitchellh.ghostty");
// Initialize our imgui widget
try self.imgui_widget.init();
@ -163,7 +159,6 @@ const Window = struct {
}
pub fn deinit(self: *Window) void {
self.icon.deinit(self.inspector.surface.app);
self.inspector.locationDidClose();
}

View File

View File

View File

0
src/apprt/gtk/style.css Normal file
View File