From f63242f7fb755b44df17ddef518089ca713f2778 Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Wed, 12 Feb 2025 14:14:37 -0600 Subject: [PATCH] gtk: add support for using GTK Builder UI files and Blueprints 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. --- nix/devShell.nix | 2 + nix/package.nix | 2 + src/apprt/gtk/Builder.zig | 61 ++++++++++++++++++++++++++ src/apprt/gtk/builder_check.zig | 39 +++++++++++++++++ src/apprt/gtk/gresource.zig | 78 +++++++++++++++++++++++---------- src/build/SharedDeps.zig | 44 ++++++++++++++++++- 6 files changed, 200 insertions(+), 26 deletions(-) create mode 100644 src/apprt/gtk/Builder.zig create mode 100644 src/apprt/gtk/builder_check.zig diff --git a/nix/devShell.nix b/nix/devShell.nix index 3014b34b7..b3dccef25 100644 --- a/nix/devShell.nix +++ b/nix/devShell.nix @@ -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 diff --git a/nix/package.nix b/nix/package.nix index 892d5e956..29d5aea29 100644 --- a/nix/package.nix +++ b/nix/package.nix @@ -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 diff --git a/src/apprt/gtk/Builder.zig b/src/apprt/gtk/Builder.zig new file mode 100644 index 000000000..bd2e6beaf --- /dev/null +++ b/src/apprt/gtk/Builder.zig @@ -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); +} diff --git a/src/apprt/gtk/builder_check.zig b/src/apprt/gtk/builder_check.zig new file mode 100644 index 000000000..153dfb0eb --- /dev/null +++ b/src/apprt/gtk/builder_check.zig @@ -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(); +} diff --git a/src/apprt/gtk/gresource.zig b/src/apprt/gtk/gresource.zig index 327680993..050605b00 100644 --- a/src/apprt/gtk/gresource.zig +++ b/src/apprt/gtk/gresource.zig @@ -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(); } -} -fn writeGResourceXML(writer: anytype) !void { + 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)); + } + } + + const writer = std.io.getStdOut().writer(); + try writer.writeAll( \\ \\ - \\ - ); - try writer.writeAll( \\ \\ ); @@ -87,9 +91,6 @@ fn writeGResourceXML(writer: anytype) !void { } try writer.writeAll( \\ - \\ - ); - try writer.writeAll( \\ \\ ); @@ -99,6 +100,23 @@ fn writeGResourceXML(writer: anytype) !void { .{ icon.alias, icon.source }, ); } + try writer.writeAll( + \\ + \\ + \\ + ); + for (ui_files) |ui_file| { + try writer.print( + " src/apprt/gtk/ui/{0s}.ui\n", + .{ui_file}, + ); + } + for (extra_ui_files.items) |ui_file| { + try writer.print( + " {s}\n", + .{ std.fs.path.basename(ui_file), ui_file }, + ); + } try writer.writeAll( \\ \\ @@ -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; }; diff --git a/src/build/SharedDeps.zig b/src/build/SharedDeps.zig index 34666cf8a..f00b79f01 100644 --- a/src/build/SharedDeps.zig +++ b/src/build/SharedDeps.zig @@ -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",