mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-21 11:16:08 +03:00
gtk: add support for using GTK Builder UI files and Blueprints (#5714)
Adds buildtime and comptime checks to make sure that Blueprints/UI files are availble and correctly formed. Will also compile Blueprints to UI files so that they are available to GTK code.
This commit is contained in:
@ -32,6 +32,7 @@
|
||||
gtk4,
|
||||
gobject-introspection,
|
||||
libadwaita,
|
||||
blueprint-compiler,
|
||||
adwaita-icon-theme,
|
||||
hicolor-icon-theme,
|
||||
harfbuzz,
|
||||
@ -159,6 +160,7 @@ in
|
||||
libXrandr
|
||||
|
||||
# Only needed for GTK builds
|
||||
blueprint-compiler
|
||||
libadwaita
|
||||
gtk4
|
||||
glib
|
||||
|
@ -15,6 +15,7 @@
|
||||
gtk4,
|
||||
gobject-introspection,
|
||||
libadwaita,
|
||||
blueprint-compiler,
|
||||
wrapGAppsHook4,
|
||||
gsettings-desktop-schemas,
|
||||
git,
|
||||
@ -82,6 +83,7 @@ in
|
||||
zig_hook
|
||||
gobject-introspection
|
||||
wrapGAppsHook4
|
||||
blueprint-compiler
|
||||
]
|
||||
++ lib.optionals enableWayland [
|
||||
wayland-scanner
|
||||
|
61
src/apprt/gtk/Builder.zig
Normal file
61
src/apprt/gtk/Builder.zig
Normal file
@ -0,0 +1,61 @@
|
||||
/// Wrapper around GTK's builder APIs that perform some comptime checks.
|
||||
const Builder = @This();
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
const gtk = @import("gtk");
|
||||
const gobject = @import("gobject");
|
||||
|
||||
resource_name: [:0]const u8,
|
||||
|
||||
pub fn init(comptime name: []const u8, comptime kind: enum { blp, ui }) Builder {
|
||||
comptime {
|
||||
switch (kind) {
|
||||
.blp => {
|
||||
// Use @embedFile to make sure that the file exists at compile
|
||||
// time. Zig _should_ discard the data so that it doesn't end
|
||||
// up in the final executable. At runtime we will load the data
|
||||
// from a GResource.
|
||||
_ = @embedFile("ui/" ++ name ++ ".blp");
|
||||
|
||||
// Check to make sure that our file is listed as a
|
||||
// `blueprint_file` in `gresource.zig`. If it isn't Ghostty
|
||||
// could crash at runtime when we try and load a nonexistent
|
||||
// GResource.
|
||||
const gresource = @import("gresource.zig");
|
||||
for (gresource.blueprint_files) |blueprint_file| {
|
||||
if (std.mem.eql(u8, blueprint_file, name)) break;
|
||||
} else @compileError("missing blueprint file '" ++ name ++ "' in gresource.zig");
|
||||
},
|
||||
.ui => {
|
||||
// Use @embedFile to make sure that the file exists at compile
|
||||
// time. Zig _should_ discard the data so that it doesn't end
|
||||
// up in the final executable. At runtime we will load the data
|
||||
// from a GResource.
|
||||
_ = @embedFile("ui/" ++ name ++ ".ui");
|
||||
|
||||
// Check to make sure that our file is listed as a `ui_file` in
|
||||
// `gresource.zig`. If it isn't Ghostty could crash at runtime
|
||||
// when we try and load a nonexistent GResource.
|
||||
const gresource = @import("gresource.zig");
|
||||
for (gresource.ui_files) |ui_file| {
|
||||
if (std.mem.eql(u8, ui_file, name)) break;
|
||||
} else @compileError("missing ui file '" ++ name ++ "' in gresource.zig");
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return .{
|
||||
.resource_name = "/com/mitchellh/ghostty/ui/" ++ name ++ ".ui",
|
||||
};
|
||||
}
|
||||
|
||||
pub fn setWidgetClassTemplate(self: *const Builder, class: *gtk.WidgetClass) void {
|
||||
class.setTemplateFromResource(self.resource_name);
|
||||
}
|
||||
|
||||
pub fn getObject(self: *const Builder, name: [:0]const u8) ?gobject.Object {
|
||||
const builder = gtk.Builder.newFromResource(self.resource_name);
|
||||
defer builder.unref();
|
||||
return builder.getObject(name);
|
||||
}
|
39
src/apprt/gtk/builder_check.zig
Normal file
39
src/apprt/gtk/builder_check.zig
Normal file
@ -0,0 +1,39 @@
|
||||
const std = @import("std");
|
||||
const build_options = @import("build_options");
|
||||
|
||||
const gtk = @import("gtk");
|
||||
const adw = if (build_options.adwaita) @import("adw") else void;
|
||||
|
||||
pub fn main() !void {
|
||||
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
|
||||
const alloc = gpa.allocator();
|
||||
|
||||
const filename = filename: {
|
||||
var it = try std.process.argsWithAllocator(alloc);
|
||||
defer it.deinit();
|
||||
|
||||
_ = it.next() orelse return error.NoFilename;
|
||||
break :filename try alloc.dupeZ(u8, it.next() orelse return error.NoFilename);
|
||||
};
|
||||
defer alloc.free(filename);
|
||||
|
||||
const data = try std.fs.cwd().readFileAlloc(alloc, filename, std.math.maxInt(u16));
|
||||
defer alloc.free(data);
|
||||
|
||||
if ((comptime !build_options.adwaita) and std.mem.indexOf(u8, data, "lib=\"Adw\"") != null) {
|
||||
std.debug.print("{s}: skipping builder check because Adwaita is not enabled!\n", .{filename});
|
||||
return;
|
||||
}
|
||||
|
||||
if (gtk.initCheck() == 0) {
|
||||
std.debug.print("{s}: skipping builder check because we can't connect to display!\n", .{filename});
|
||||
return;
|
||||
}
|
||||
|
||||
if (comptime build_options.adwaita) {
|
||||
adw.init();
|
||||
}
|
||||
|
||||
const builder = gtk.Builder.newFromString(data.ptr, @intCast(data.len));
|
||||
defer builder.unref();
|
||||
}
|
@ -53,29 +53,33 @@ const icons = [_]struct {
|
||||
},
|
||||
};
|
||||
|
||||
pub const gresource_xml = comptimeGenerateGResourceXML();
|
||||
pub const ui_files = [_][]const u8{};
|
||||
pub const blueprint_files = [_][]const u8{};
|
||||
|
||||
fn comptimeGenerateGResourceXML() []const u8 {
|
||||
comptime {
|
||||
@setEvalBranchQuota(13000);
|
||||
var counter = std.io.countingWriter(std.io.null_writer);
|
||||
try writeGResourceXML(&counter.writer());
|
||||
pub fn main() !void {
|
||||
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
|
||||
const alloc = gpa.allocator();
|
||||
|
||||
var buf: [counter.bytes_written]u8 = undefined;
|
||||
var stream = std.io.fixedBufferStream(&buf);
|
||||
try writeGResourceXML(stream.writer());
|
||||
const final = buf;
|
||||
return final[0..stream.getWritten().len];
|
||||
var extra_ui_files = std.ArrayList([]const u8).init(alloc);
|
||||
defer {
|
||||
for (extra_ui_files.items) |item| alloc.free(item);
|
||||
extra_ui_files.deinit();
|
||||
}
|
||||
|
||||
var it = try std.process.argsWithAllocator(alloc);
|
||||
defer it.deinit();
|
||||
|
||||
while (it.next()) |filename| {
|
||||
if (std.mem.eql(u8, std.fs.path.extension(filename), ".ui")) {
|
||||
try extra_ui_files.append(try alloc.dupe(u8, filename));
|
||||
}
|
||||
}
|
||||
|
||||
fn writeGResourceXML(writer: anytype) !void {
|
||||
const writer = std.io.getStdOut().writer();
|
||||
|
||||
try writer.writeAll(
|
||||
\\<?xml version="1.0" encoding="UTF-8"?>
|
||||
\\<gresources>
|
||||
\\
|
||||
);
|
||||
try writer.writeAll(
|
||||
\\ <gresource prefix="/com/mitchellh/ghostty">
|
||||
\\
|
||||
);
|
||||
@ -87,9 +91,6 @@ fn writeGResourceXML(writer: anytype) !void {
|
||||
}
|
||||
try writer.writeAll(
|
||||
\\ </gresource>
|
||||
\\
|
||||
);
|
||||
try writer.writeAll(
|
||||
\\ <gresource prefix="/com/mitchellh/ghostty/icons">
|
||||
\\
|
||||
);
|
||||
@ -99,6 +100,23 @@ fn writeGResourceXML(writer: anytype) !void {
|
||||
.{ icon.alias, icon.source },
|
||||
);
|
||||
}
|
||||
try writer.writeAll(
|
||||
\\ </gresource>
|
||||
\\ <gresource prefix="/com/mitchellh/ghostty/ui">
|
||||
\\
|
||||
);
|
||||
for (ui_files) |ui_file| {
|
||||
try writer.print(
|
||||
" <file compressed=\"true\" preprocess=\"xml-stripblanks\" alias=\"{0s}.ui\">src/apprt/gtk/ui/{0s}.ui</file>\n",
|
||||
.{ui_file},
|
||||
);
|
||||
}
|
||||
for (extra_ui_files.items) |ui_file| {
|
||||
try writer.print(
|
||||
" <file compressed=\"true\" preprocess=\"xml-stripblanks\" alias=\"{s}\">{s}</file>\n",
|
||||
.{ std.fs.path.basename(ui_file), ui_file },
|
||||
);
|
||||
}
|
||||
try writer.writeAll(
|
||||
\\ </gresource>
|
||||
\\</gresources>
|
||||
@ -107,12 +125,24 @@ fn writeGResourceXML(writer: anytype) !void {
|
||||
}
|
||||
|
||||
pub const dependencies = 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});
|
||||
const total = css_files.len + icons.len + ui_files.len + blueprint_files.len;
|
||||
var deps: [total][]const u8 = undefined;
|
||||
var index: usize = 0;
|
||||
for (css_files) |css_file| {
|
||||
deps[index] = std.fmt.comptimePrint("src/apprt/gtk/{s}", .{css_file});
|
||||
index += 1;
|
||||
}
|
||||
for (icons, css_files.len..) |icon, i| {
|
||||
deps[i] = std.fmt.comptimePrint("images/icons/icon_{s}.png", .{icon.source});
|
||||
for (icons) |icon| {
|
||||
deps[index] = std.fmt.comptimePrint("images/icons/icon_{s}.png", .{icon.source});
|
||||
index += 1;
|
||||
}
|
||||
for (ui_files) |ui_file| {
|
||||
deps[index] = std.fmt.comptimePrint("src/apprt/gtk/ui/{s}.ui", .{ui_file});
|
||||
index += 1;
|
||||
}
|
||||
for (blueprint_files) |blueprint_file| {
|
||||
deps[index] = std.fmt.comptimePrint("src/apprt/gtk/ui/{s}.blp", .{blueprint_file});
|
||||
index += 1;
|
||||
}
|
||||
break :deps deps;
|
||||
};
|
||||
|
@ -490,8 +490,48 @@ pub fn add(
|
||||
{
|
||||
const gresource = @import("../apprt/gtk/gresource.zig");
|
||||
|
||||
const wf = b.addWriteFiles();
|
||||
const gresource_xml = wf.add("gresource.xml", gresource.gresource_xml);
|
||||
const gresource_xml = gresource_xml: {
|
||||
const generate_gresource_xml = b.addExecutable(.{
|
||||
.name = "generate_gresource_xml",
|
||||
.root_source_file = b.path("src/apprt/gtk/gresource.zig"),
|
||||
.target = b.host,
|
||||
});
|
||||
|
||||
const generate = b.addRunArtifact(generate_gresource_xml);
|
||||
|
||||
for (gresource.blueprint_files) |blueprint_file| {
|
||||
const blueprint_compiler = b.addSystemCommand(&.{
|
||||
"blueprint-compiler",
|
||||
"compile",
|
||||
"--output",
|
||||
});
|
||||
const ui_file = blueprint_compiler.addOutputFileArg(b.fmt("{s}.ui", .{blueprint_file}));
|
||||
blueprint_compiler.addFileArg(b.path(b.fmt("src/apprt/gtk/ui/{s}.blp", .{blueprint_file})));
|
||||
generate.addFileArg(ui_file);
|
||||
}
|
||||
|
||||
break :gresource_xml generate.captureStdOut();
|
||||
};
|
||||
|
||||
{
|
||||
const gtk_builder_check = b.addExecutable(.{
|
||||
.name = "gtk_builder_check",
|
||||
.root_source_file = b.path("src/apprt/gtk/builder_check.zig"),
|
||||
.target = b.host,
|
||||
});
|
||||
gtk_builder_check.root_module.addOptions("build_options", self.options);
|
||||
gtk_builder_check.linkSystemLibrary2("gtk4", dynamic_link_opts);
|
||||
if (self.config.adwaita) gtk_builder_check.linkSystemLibrary2("libadwaita-1", dynamic_link_opts);
|
||||
gtk_builder_check.linkLibC();
|
||||
|
||||
for (gresource.dependencies) |pathname| {
|
||||
const extension = std.fs.path.extension(pathname);
|
||||
if (!std.mem.eql(u8, extension, ".ui")) continue;
|
||||
const check = b.addRunArtifact(gtk_builder_check);
|
||||
check.addFileArg(b.path(pathname));
|
||||
step.step.dependOn(&check.step);
|
||||
}
|
||||
}
|
||||
|
||||
const generate_resources_c = b.addSystemCommand(&.{
|
||||
"glib-compile-resources",
|
||||
|
Reference in New Issue
Block a user