mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-21 19:26:09 +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,
|
gtk4,
|
||||||
gobject-introspection,
|
gobject-introspection,
|
||||||
libadwaita,
|
libadwaita,
|
||||||
|
blueprint-compiler,
|
||||||
adwaita-icon-theme,
|
adwaita-icon-theme,
|
||||||
hicolor-icon-theme,
|
hicolor-icon-theme,
|
||||||
harfbuzz,
|
harfbuzz,
|
||||||
@ -159,6 +160,7 @@ in
|
|||||||
libXrandr
|
libXrandr
|
||||||
|
|
||||||
# Only needed for GTK builds
|
# Only needed for GTK builds
|
||||||
|
blueprint-compiler
|
||||||
libadwaita
|
libadwaita
|
||||||
gtk4
|
gtk4
|
||||||
glib
|
glib
|
||||||
|
@ -15,6 +15,7 @@
|
|||||||
gtk4,
|
gtk4,
|
||||||
gobject-introspection,
|
gobject-introspection,
|
||||||
libadwaita,
|
libadwaita,
|
||||||
|
blueprint-compiler,
|
||||||
wrapGAppsHook4,
|
wrapGAppsHook4,
|
||||||
gsettings-desktop-schemas,
|
gsettings-desktop-schemas,
|
||||||
git,
|
git,
|
||||||
@ -82,6 +83,7 @@ in
|
|||||||
zig_hook
|
zig_hook
|
||||||
gobject-introspection
|
gobject-introspection
|
||||||
wrapGAppsHook4
|
wrapGAppsHook4
|
||||||
|
blueprint-compiler
|
||||||
]
|
]
|
||||||
++ lib.optionals enableWayland [
|
++ lib.optionals enableWayland [
|
||||||
wayland-scanner
|
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 {
|
pub fn main() !void {
|
||||||
comptime {
|
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
|
||||||
@setEvalBranchQuota(13000);
|
const alloc = gpa.allocator();
|
||||||
var counter = std.io.countingWriter(std.io.null_writer);
|
|
||||||
try writeGResourceXML(&counter.writer());
|
|
||||||
|
|
||||||
var buf: [counter.bytes_written]u8 = undefined;
|
var extra_ui_files = std.ArrayList([]const u8).init(alloc);
|
||||||
var stream = std.io.fixedBufferStream(&buf);
|
defer {
|
||||||
try writeGResourceXML(stream.writer());
|
for (extra_ui_files.items) |item| alloc.free(item);
|
||||||
const final = buf;
|
extra_ui_files.deinit();
|
||||||
return final[0..stream.getWritten().len];
|
}
|
||||||
|
|
||||||
|
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(
|
try writer.writeAll(
|
||||||
\\<?xml version="1.0" encoding="UTF-8"?>
|
\\<?xml version="1.0" encoding="UTF-8"?>
|
||||||
\\<gresources>
|
\\<gresources>
|
||||||
\\
|
|
||||||
);
|
|
||||||
try writer.writeAll(
|
|
||||||
\\ <gresource prefix="/com/mitchellh/ghostty">
|
\\ <gresource prefix="/com/mitchellh/ghostty">
|
||||||
\\
|
\\
|
||||||
);
|
);
|
||||||
@ -87,9 +91,6 @@ fn writeGResourceXML(writer: anytype) !void {
|
|||||||
}
|
}
|
||||||
try writer.writeAll(
|
try writer.writeAll(
|
||||||
\\ </gresource>
|
\\ </gresource>
|
||||||
\\
|
|
||||||
);
|
|
||||||
try writer.writeAll(
|
|
||||||
\\ <gresource prefix="/com/mitchellh/ghostty/icons">
|
\\ <gresource prefix="/com/mitchellh/ghostty/icons">
|
||||||
\\
|
\\
|
||||||
);
|
);
|
||||||
@ -99,6 +100,23 @@ fn writeGResourceXML(writer: anytype) !void {
|
|||||||
.{ icon.alias, icon.source },
|
.{ 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(
|
try writer.writeAll(
|
||||||
\\ </gresource>
|
\\ </gresource>
|
||||||
\\</gresources>
|
\\</gresources>
|
||||||
@ -107,12 +125,24 @@ fn writeGResourceXML(writer: anytype) !void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub const dependencies = deps: {
|
pub const dependencies = deps: {
|
||||||
var deps: [css_files.len + icons.len][]const u8 = undefined;
|
const total = css_files.len + icons.len + ui_files.len + blueprint_files.len;
|
||||||
for (css_files, 0..) |css_file, i| {
|
var deps: [total][]const u8 = undefined;
|
||||||
deps[i] = std.fmt.comptimePrint("src/apprt/gtk/{s}", .{css_file});
|
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| {
|
for (icons) |icon| {
|
||||||
deps[i] = std.fmt.comptimePrint("images/icons/icon_{s}.png", .{icon.source});
|
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;
|
break :deps deps;
|
||||||
};
|
};
|
||||||
|
@ -490,8 +490,48 @@ pub fn add(
|
|||||||
{
|
{
|
||||||
const gresource = @import("../apprt/gtk/gresource.zig");
|
const gresource = @import("../apprt/gtk/gresource.zig");
|
||||||
|
|
||||||
const wf = b.addWriteFiles();
|
const gresource_xml = gresource_xml: {
|
||||||
const gresource_xml = wf.add("gresource.xml", gresource.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(&.{
|
const generate_resources_c = b.addSystemCommand(&.{
|
||||||
"glib-compile-resources",
|
"glib-compile-resources",
|
||||||
|
Reference in New Issue
Block a user