diff --git a/src/apprt/gtk-ng.zig b/src/apprt/gtk-ng.zig index dea15a3ed..19d450a54 100644 --- a/src/apprt/gtk-ng.zig +++ b/src/apprt/gtk-ng.zig @@ -1,5 +1,9 @@ const internal_os = @import("../os/main.zig"); +// The required comptime API for any apprt. pub const App = @import("gtk-ng/App.zig"); pub const Surface = @import("gtk-ng/Surface.zig"); pub const resourcesDir = internal_os.resourcesDir; + +// The exported API, custom for the apprt. +pub const GhosttyApplication = @import("gtk-ng/class/application.zig").GhosttyApplication; diff --git a/src/apprt/gtk-ng/App.zig b/src/apprt/gtk-ng/App.zig index 8c12a3b54..428ecc457 100644 --- a/src/apprt/gtk-ng/App.zig +++ b/src/apprt/gtk-ng/App.zig @@ -1,28 +1,236 @@ +/// This is the main entrypoint to the apprt for Ghostty. Ghostty will +/// initialize this in main to start the application.. const App = @This(); const std = @import("std"); const Allocator = std.mem.Allocator; - +const adw = @import("adw"); +const gio = @import("gio"); const apprt = @import("../../apprt.zig"); +const configpkg = @import("../../config.zig"); +const internal_os = @import("../../os/main.zig"); +const xev = @import("../../global.zig").xev; +const Config = configpkg.Config; const CoreApp = @import("../../App.zig"); +const GhosttyApplication = @import("class/application.zig").GhosttyApplication; +const Surface = @import("Surface.zig"); +const gtk_version = @import("gtk_version.zig"); +const adw_version = @import("adw_version.zig"); + +const log = std.log.scoped(.gtk); + +/// The GObject GhosttyApplication instance +app: *GhosttyApplication, + pub fn init( self: *App, core_app: *CoreApp, + + // Required by the apprt interface but we don't use it. opts: struct {}, ) !void { - _ = self; - _ = core_app; _ = opts; + const alloc = core_app.alloc; + + // Log our GTK versions + gtk_version.logVersion(); + adw_version.logVersion(); + + // Set gettext global domain to be our app so that our unqualified + // translations map to our translations. + try internal_os.i18n.initGlobalDomain(); + + // Load our configuration. + const config: *Config = try alloc.create(Config); + errdefer alloc.destroy(config); + config.* = try Config.load(core_app.alloc); + errdefer config.deinit(); + + // If we had configuration errors, then log them. + if (!config._diagnostics.empty()) { + var buf = std.ArrayList(u8).init(alloc); + defer buf.deinit(); + for (config._diagnostics.items()) |diag| { + try diag.write(buf.writer()); + log.warn("configuration error: {s}", .{buf.items}); + buf.clearRetainingCapacity(); + } + + // If we have any CLI errors, exit. + if (config._diagnostics.containsLocation(.cli)) { + log.warn("CLI errors detected, exiting", .{}); + std.posix.exit(1); + } + } + + // Setup our event loop backend + if (config.@"async-backend" != .auto) { + const result: bool = switch (config.@"async-backend") { + .auto => unreachable, + .epoll => if (comptime xev.dynamic) xev.prefer(.epoll) else false, + .io_uring => if (comptime xev.dynamic) xev.prefer(.io_uring) else false, + }; + + if (result) { + log.info( + "libxev manual backend={s}", + .{@tagName(xev.backend)}, + ); + } else { + log.warn( + "libxev manual backend failed, using default={s}", + .{@tagName(xev.backend)}, + ); + } + } + + // Setup GTK + setGtkEnv(config) catch |err| switch (err) { + error.NoSpaceLeft => { + // If we fail to set GTK environment variables then we still + // try to start the application... + log.warn( + "error setting GTK environment variables err={}", + .{err}, + ); + }, + }; + adw.init(); + + // Initialize our application class + const app: *GhosttyApplication = .new(core_app, config); + errdefer app.unref(); + + self.* = .{ + .app = app, + }; return; } +/// This sets various GTK-related environment variables as necessary +/// given the runtime environment or configuration. +fn setGtkEnv(config: *const Config) error{NoSpaceLeft}!void { + var gdk_debug: struct { + /// output OpenGL debug information + opengl: bool = false, + /// disable GLES, Ghostty can't use GLES + @"gl-disable-gles": bool = false, + // GTK's new renderer can cause blurry font when using fractional scaling. + @"gl-no-fractional": bool = false, + /// Disabling Vulkan can improve startup times by hundreds of + /// milliseconds on some systems. We don't use Vulkan so we can just + /// disable it. + @"vulkan-disable": bool = false, + } = .{ + .opengl = config.@"gtk-opengl-debug", + }; + + var gdk_disable: struct { + @"gles-api": bool = false, + /// current gtk implementation for color management is not good enough. + /// see: https://bugs.kde.org/show_bug.cgi?id=495647 + /// gtk issue: https://gitlab.gnome.org/GNOME/gtk/-/issues/6864 + @"color-mgmt": bool = true, + /// Disabling Vulkan can improve startup times by hundreds of + /// milliseconds on some systems. We don't use Vulkan so we can just + /// disable it. + vulkan: bool = false, + } = .{}; + + environment: { + if (gtk_version.runtimeAtLeast(4, 18, 0)) { + gdk_disable.@"color-mgmt" = false; + } + + if (gtk_version.runtimeAtLeast(4, 16, 0)) { + // From gtk 4.16, GDK_DEBUG is split into GDK_DEBUG and GDK_DISABLE. + // For the remainder of "why" see the 4.14 comment below. + gdk_disable.@"gles-api" = true; + gdk_disable.vulkan = true; + break :environment; + } + if (gtk_version.runtimeAtLeast(4, 14, 0)) { + // We need to export GDK_DEBUG to run on Wayland after GTK 4.14. + // Older versions of GTK do not support these values so it is safe + // to always set this. Forwards versions are uncertain so we'll have + // to reassess... + // + // Upstream issue: https://gitlab.gnome.org/GNOME/gtk/-/issues/6589 + gdk_debug.@"gl-disable-gles" = true; + gdk_debug.@"vulkan-disable" = true; + + if (gtk_version.runtimeUntil(4, 17, 5)) { + // Removed at GTK v4.17.5 + gdk_debug.@"gl-no-fractional" = true; + } + break :environment; + } + + // Versions prior to 4.14 are a bit of an unknown for Ghostty. It + // is an environment that isn't tested well and we don't have a + // good understanding of what we may need to do. + gdk_debug.@"vulkan-disable" = true; + } + + { + var buf: [1024]u8 = undefined; + var fmt = std.io.fixedBufferStream(&buf); + const writer = fmt.writer(); + var first: bool = true; + inline for (@typeInfo(@TypeOf(gdk_debug)).@"struct".fields) |field| { + if (@field(gdk_debug, field.name)) { + if (!first) try writer.writeAll(","); + try writer.writeAll(field.name); + first = false; + } + } + try writer.writeByte(0); + const value = fmt.getWritten(); + log.warn("setting GDK_DEBUG={s}", .{value[0 .. value.len - 1]}); + _ = internal_os.setenv("GDK_DEBUG", value[0 .. value.len - 1 :0]); + } + + { + var buf: [1024]u8 = undefined; + var fmt = std.io.fixedBufferStream(&buf); + const writer = fmt.writer(); + var first: bool = true; + inline for (@typeInfo(@TypeOf(gdk_disable)).@"struct".fields) |field| { + if (@field(gdk_disable, field.name)) { + if (!first) try writer.writeAll(","); + try writer.writeAll(field.name); + first = false; + } + } + try writer.writeByte(0); + const value = fmt.getWritten(); + log.warn("setting GDK_DISABLE={s}", .{value[0 .. value.len - 1]}); + _ = internal_os.setenv("GDK_DISABLE", value[0 .. value.len - 1 :0]); + } +} + pub fn run(self: *App) !void { - _ = self; + try self.app.run(self); } pub fn terminate(self: *App) void { + // We force deinitialize the app. We don't unref because other things + // tend to have a reference at this point, so this just forces the + // disposal now. + self.app.deinit(); +} + +pub fn performAction( + self: *App, + target: apprt.Target, + comptime action: apprt.Action.Key, + value: apprt.Action.Value(action), +) !bool { _ = self; + _ = target; + _ = value; + return false; } pub fn performIpc( @@ -36,3 +244,15 @@ pub fn performIpc( _ = value; return false; } + +/// Close the given surface. +pub fn redrawSurface(self: *App, surface: *Surface) void { + _ = self; + _ = surface; +} + +/// Redraw the inspector for the given surface. +pub fn redrawInspector(self: *App, surface: *Surface) void { + _ = self; + _ = surface; +} diff --git a/src/apprt/gtk-ng/Surface.zig b/src/apprt/gtk-ng/Surface.zig index df7b04f5f..094334210 100644 --- a/src/apprt/gtk-ng/Surface.zig +++ b/src/apprt/gtk-ng/Surface.zig @@ -1,5 +1,57 @@ const Surface = @This(); +const apprt = @import("../../apprt.zig"); +const CoreSurface = @import("../../Surface.zig"); + +core_surface: CoreSurface, + pub fn deinit(self: *Surface) void { _ = self; } + +pub fn close(self: *Surface, process_active: bool) void { + _ = self; + _ = process_active; +} + +pub fn shouldClose(self: *Surface) bool { + _ = self; + return false; +} + +pub fn getTitle(self: *Surface) ?[:0]const u8 { + _ = self; + return null; +} + +pub fn getContentScale(self: *const Surface) !apprt.ContentScale { + _ = self; + return .{ .x = 1, .y = 1 }; +} + +pub fn getCursorPos(self: *const Surface) !apprt.CursorPos { + _ = self; + return .{ .x = 0, .y = 0 }; +} + +pub fn clipboardRequest( + self: *Surface, + clipboard_type: apprt.Clipboard, + state: apprt.ClipboardRequest, +) !void { + _ = self; + _ = clipboard_type; + _ = state; +} + +pub fn setClipboardString( + self: *Surface, + val: [:0]const u8, + clipboard_type: apprt.Clipboard, + confirm: bool, +) !void { + _ = self; + _ = val; + _ = clipboard_type; + _ = confirm; +} diff --git a/src/apprt/gtk-ng/adw_version.zig b/src/apprt/gtk-ng/adw_version.zig new file mode 100644 index 000000000..7ce88f585 --- /dev/null +++ b/src/apprt/gtk-ng/adw_version.zig @@ -0,0 +1,122 @@ +const std = @import("std"); + +// Until the gobject bindings are built at the same time we are building +// Ghostty, we need to import `adwaita.h` directly to ensure that the version +// macros match the version of `libadwaita` that we are building/linking +// against. +const c = @cImport({ + @cInclude("adwaita.h"); +}); + +const adw = @import("adw"); + +const log = std.log.scoped(.gtk); + +pub const comptime_version: std.SemanticVersion = .{ + .major = c.ADW_MAJOR_VERSION, + .minor = c.ADW_MINOR_VERSION, + .patch = c.ADW_MICRO_VERSION, +}; + +pub fn getRuntimeVersion() std.SemanticVersion { + return .{ + .major = adw.getMajorVersion(), + .minor = adw.getMinorVersion(), + .patch = adw.getMicroVersion(), + }; +} + +pub fn logVersion() void { + log.info("libadwaita version build={} runtime={}", .{ + comptime_version, + getRuntimeVersion(), + }); +} + +/// Verifies that the running libadwaita version is at least the given +/// version. This will return false if Ghostty is configured to not build with +/// libadwaita. +/// +/// This can be run in both a comptime and runtime context. If it is run in a +/// comptime context, it will only check the version in the headers. If it is +/// run in a runtime context, it will check the actual version of the library we +/// are linked against. So generally you probably want to do both checks! +/// +/// This is inlined so that the comptime checks will disable the runtime checks +/// if the comptime checks fail. +pub inline fn atLeast( + comptime major: u16, + comptime minor: u16, + comptime micro: u16, +) bool { + // If our header has lower versions than the given version, we can return + // false immediately. This prevents us from compiling against unknown + // symbols and makes runtime checks very slightly faster. + if (comptime comptime_version.order(.{ + .major = major, + .minor = minor, + .patch = micro, + }) == .lt) return false; + + // If we're in comptime then we can't check the runtime version. + if (@inComptime()) return true; + + return runtimeAtLeast(major, minor, micro); +} + +/// Verifies that the libadwaita version at runtime is at least the given version. +/// +/// This function should be used in cases where the only the runtime behavior +/// is affected by the version check. For checks which would affect code +/// generation, use `atLeast`. +pub inline fn runtimeAtLeast( + comptime major: u16, + comptime minor: u16, + comptime micro: u16, +) bool { + // We use the functions instead of the constants such as c.GTK_MINOR_VERSION + // because the function gets the actual runtime version. + const runtime_version = getRuntimeVersion(); + return runtime_version.order(.{ + .major = major, + .minor = minor, + .patch = micro, + }) != .lt; +} + +test "versionAtLeast" { + const testing = std.testing; + + const funs = &.{ atLeast, runtimeAtLeast }; + inline for (funs) |fun| { + try testing.expect(fun(c.ADW_MAJOR_VERSION, c.ADW_MINOR_VERSION, c.ADW_MICRO_VERSION)); + try testing.expect(!fun(c.ADW_MAJOR_VERSION, c.ADW_MINOR_VERSION, c.ADW_MICRO_VERSION + 1)); + try testing.expect(!fun(c.ADW_MAJOR_VERSION, c.ADW_MINOR_VERSION + 1, c.ADW_MICRO_VERSION)); + try testing.expect(!fun(c.ADW_MAJOR_VERSION + 1, c.ADW_MINOR_VERSION, c.ADW_MICRO_VERSION)); + try testing.expect(fun(c.ADW_MAJOR_VERSION - 1, c.ADW_MINOR_VERSION, c.ADW_MICRO_VERSION)); + try testing.expect(fun(c.ADW_MAJOR_VERSION - 1, c.ADW_MINOR_VERSION + 1, c.ADW_MICRO_VERSION)); + try testing.expect(fun(c.ADW_MAJOR_VERSION - 1, c.ADW_MINOR_VERSION, c.ADW_MICRO_VERSION + 1)); + try testing.expect(fun(c.ADW_MAJOR_VERSION, c.ADW_MINOR_VERSION - 1, c.ADW_MICRO_VERSION + 1)); + } +} + +// Whether AdwDialog, AdwAlertDialog, etc. are supported (1.5+) +pub inline fn supportsDialogs() bool { + return atLeast(1, 5, 0); +} + +pub inline fn supportsTabOverview() bool { + return atLeast(1, 4, 0); +} + +pub inline fn supportsSwitchRow() bool { + return atLeast(1, 4, 0); +} + +pub inline fn supportsToolbarView() bool { + return atLeast(1, 4, 0); +} + +pub inline fn supportsBanner() bool { + return atLeast(1, 3, 0); +} diff --git a/src/apprt/gtk-ng/class/application.zig b/src/apprt/gtk-ng/class/application.zig new file mode 100644 index 000000000..5ce6568b8 --- /dev/null +++ b/src/apprt/gtk-ng/class/application.zig @@ -0,0 +1,283 @@ +const std = @import("std"); +const assert = std.debug.assert; +const builtin = @import("builtin"); +const adw = @import("adw"); +const gio = @import("gio"); +const glib = @import("glib"); +const gobject = @import("gobject"); + +const build_config = @import("../../../build_config.zig"); +const apprt = @import("../../../apprt.zig"); +const CoreApp = @import("../../../App.zig"); +const configpkg = @import("../../../config.zig"); +const Config = configpkg.Config; + +const log = std.log.scoped(.gtk); + +/// The primary entrypoint for the Ghostty GTK application. +/// +/// This requires a `ghostty.App` and `ghostty.Config` and takes +/// care of the rest. Call `run` to run the application to completion. +pub const GhosttyApplication = extern struct { + /// This type creates a new GObject class. Since the Application is + /// the primary entrypoint I'm going to use this as a place to document + /// how this all works and where you can find resources for it, but + /// this applies to any other GObject class within this apprt. + /// + /// The various fields (parent_instance) and constants (Parent, + /// getGObjectType, etc.) are mandatory "interfaces" for zig-gobject + /// to create a GObject class. + /// + /// I found these to be the best resources: + /// + /// * https://github.com/ianprime0509/zig-gobject/blob/d7f1edaf50193d49b56c60568dfaa9f23195565b/extensions/gobject2.zig + /// * https://github.com/ianprime0509/zig-gobject/blob/d7f1edaf50193d49b56c60568dfaa9f23195565b/example/src/custom_class.zig + /// + const Self = @This(); + + parent_instance: Parent, + pub const Parent = adw.Application; + pub const getGObjectType = gobject.ext.defineClass(Self, .{ + .classInit = &Class.init, + .parent_class = &Class.parent, + .private = .{ .Type = Private, .offset = &Private.offset }, + }); + + const Private = struct { + /// The libghostty App instance. + core_app: *CoreApp, + + /// The configuration for the application. + config: *Config, + + /// This is set to false internally when the event loop + /// should exit and the application should quit. This must + /// only be set by the main loop thread. + running: bool = false, + + var offset: c_int = 0; + }; + + /// Creates a new GhosttyApplication instance. + /// + /// Takes ownership of the `config` argument. + pub fn new(core_app: *CoreApp, config: *Config) *Self { + const single_instance = switch (config.@"gtk-single-instance") { + .true => true, + .false => false, + .desktop => switch (config.@"launched-from".?) { + .desktop, .systemd, .dbus => true, + .cli => false, + }, + }; + + // Setup the flags for our application. + const app_flags: gio.ApplicationFlags = app_flags: { + var flags: gio.ApplicationFlags = .flags_default_flags; + if (!single_instance) flags.non_unique = true; + break :app_flags flags; + }; + + // Our app ID determines uniqueness and maps to our desktop file. + // We append "-debug" to the ID if we're in debug mode so that we + // can develop Ghostty in Ghostty. + const app_id: [:0]const u8 = app_id: { + if (config.class) |class| { + if (gio.Application.idIsValid(class) != 0) { + break :app_id class; + } else { + log.warn("invalid 'class' in config, ignoring", .{}); + } + } + + const default_id = comptime build_config.bundle_id; + break :app_id if (builtin.mode == .Debug) default_id ++ "-debug" else default_id; + }; + + // Create our GTK Application which encapsulates our process. + log.debug("creating GTK application id={s} single-instance={}", .{ + app_id, + single_instance, + }); + + const self = gobject.ext.newInstance(Self, .{ + .application_id = app_id.ptr, + .flags = app_flags, + }); + + const priv = self.private(); + priv.core_app = core_app; + priv.config = config; + + return self; + } + + /// Force deinitialize the application. + /// + /// Normally in a GObject lifecycle, this would be called by the + /// finalizer. But applications are never fully unreferenced so this + /// ensures that our memory is cleaned up properly. + pub fn deinit(self: *Self) void { + const alloc = self.allocator(); + const priv = self.private(); + priv.config.deinit(); + alloc.destroy(priv.config); + } + + /// Run the application. This is a replacement for `gio.Application.run` + /// because we want more tight control over our event loop so we can + /// integrate it with libghostty. + pub fn run(self: *Self, rt_app: *apprt.gtk_ng.App) !void { + // Based on the actual `gio.Application.run` implementation: + // https://github.com/GNOME/glib/blob/a8e8b742e7926e33eb635a8edceac74cf239d6ed/gio/gapplication.c#L2533 + + // Acquire the default context for the application + const ctx = glib.MainContext.default(); + if (glib.MainContext.acquire(ctx) == 0) return error.ContextAcquireFailed; + + // The final cleanup that is always required at the end of running. + defer { + // Sync any remaining settings + gio.Settings.sync(); + + // Clear out the event loop, don't block. + while (glib.MainContext.iteration(ctx, 0) != 0) {} + + // Release the context so something else can use it. + defer glib.MainContext.release(ctx); + } + + // Register the application + var err_: ?*glib.Error = null; + if (self.as(gio.Application).register( + null, + &err_, + ) == 0) { + if (err_) |err| { + defer err.free(); + log.warn( + "error registering application: {s}", + .{err.f_message orelse "(unknown)"}, + ); + } + + return error.ApplicationRegisterFailed; + } + assert(err_ == null); + + // This just calls the `activate` signal but its part of the normal startup + // routine so we just call it, but only if the config allows it (this allows + // for launching Ghostty in the "background" without immediately opening + // a window). An initial window will not be immediately created if we were + // launched by D-Bus activation or systemd. D-Bus activation will send it's + // own `activate` or `new-window` signal later. + // + // https://gitlab.gnome.org/GNOME/glib/-/blob/bd2ccc2f69ecfd78ca3f34ab59e42e2b462bad65/gio/gapplication.c#L2302 + const priv = self.private(); + const config = priv.config; + if (config.@"initial-window") switch (config.@"launched-from".?) { + .desktop, .cli => self.as(gio.Application).activate(), + .dbus, .systemd => {}, + }; + + // If we are NOT the primary instance, then we never want to run. + // This means that another instance of the GTK app is running and + // our "activate" call above will open a window. + if (self.as(gio.Application).getIsRemote() != 0) { + log.debug( + "application is remote, exiting run loop after activation", + .{}, + ); + return; + } + + priv.running = true; + while (priv.running) { + _ = glib.MainContext.iteration(ctx, 1); + + // Tick the core Ghostty terminal app + try priv.core_app.tick(rt_app); + + // Check if we must quit based on the current state. + const must_quit = q: { + // If we are configured to always stay running, don't quit. + if (!config.@"quit-after-last-window-closed") break :q false; + + // If the quit timer has expired, quit. + // if (self.quit_timer == .expired) break :q true; + + // There's no quit timer running, or it hasn't expired, don't quit. + break :q false; + }; + + if (must_quit) { + //self.quit(); + priv.running = false; + } + } + } + + pub fn as(app: *Self, comptime T: type) *T { + return gobject.ext.as(T, app); + } + + pub fn unref(self: *Self) void { + gobject.Object.unref(self.as(gobject.Object)); + } + + fn private(self: *GhosttyApplication) *Private { + return gobject.ext.impl_helpers.getPrivate( + self, + Private, + Private.offset, + ); + } + + fn startup(self: *GhosttyApplication) callconv(.C) void { + // This is where we would initialize the application, but we + // do that in the `run` method instead. + log.debug("GhosttyApplication started", .{}); + + gio.Application.virtual_methods.startup.call( + Class.parent, + self.as(Parent), + ); + } + + fn activate(self: *GhosttyApplication) callconv(.C) void { + // This is called when the application is activated, but we + // don't need to do anything here since we handle activation + // in the `run` method. + log.debug("GhosttyApplication activated", .{}); + + // Call the parent activate method. + gio.Application.virtual_methods.activate.call( + Class.parent, + self.as(Parent), + ); + } + + fn finalize(self: *GhosttyApplication) callconv(.C) void { + self.deinit(); + gobject.Object.virtual_methods.finalize.call( + Class.parent, + self.as(Parent), + ); + } + + fn allocator(self: *GhosttyApplication) std.mem.Allocator { + return self.private().core_app.alloc; + } + + pub const Class = extern struct { + parent_class: Parent.Class, + var parent: *Parent.Class = undefined; + pub const Instance = Self; + + fn init(class: *Class) callconv(.C) void { + gio.Application.virtual_methods.activate.implement(class, &activate); + gio.Application.virtual_methods.startup.implement(class, &startup); + gobject.Object.virtual_methods.finalize.implement(class, &finalize); + } + }; +}; diff --git a/src/apprt/gtk-ng/gtk_version.zig b/src/apprt/gtk-ng/gtk_version.zig new file mode 100644 index 000000000..6f3d733a5 --- /dev/null +++ b/src/apprt/gtk-ng/gtk_version.zig @@ -0,0 +1,140 @@ +const std = @import("std"); + +// Until the gobject bindings are built at the same time we are building +// Ghostty, we need to import `gtk/gtk.h` directly to ensure that the version +// macros match the version of `gtk4` that we are building/linking against. +const c = @cImport({ + @cInclude("gtk/gtk.h"); +}); + +const gtk = @import("gtk"); + +const log = std.log.scoped(.gtk); + +pub const comptime_version: std.SemanticVersion = .{ + .major = c.GTK_MAJOR_VERSION, + .minor = c.GTK_MINOR_VERSION, + .patch = c.GTK_MICRO_VERSION, +}; + +pub fn getRuntimeVersion() std.SemanticVersion { + return .{ + .major = gtk.getMajorVersion(), + .minor = gtk.getMinorVersion(), + .patch = gtk.getMicroVersion(), + }; +} + +pub fn logVersion() void { + log.info("GTK version build={} runtime={}", .{ + comptime_version, + getRuntimeVersion(), + }); +} + +/// Verifies that the GTK version is at least the given version. +/// +/// This can be run in both a comptime and runtime context. If it is run in a +/// comptime context, it will only check the version in the headers. If it is +/// run in a runtime context, it will check the actual version of the library we +/// are linked against. +/// +/// This function should be used in cases where the version check would affect +/// code generation, such as using symbols that are only available beyond a +/// certain version. For checks which only depend on GTK's runtime behavior, +/// use `runtimeAtLeast`. +/// +/// This is inlined so that the comptime checks will disable the runtime checks +/// if the comptime checks fail. +pub inline fn atLeast( + comptime major: u16, + comptime minor: u16, + comptime micro: u16, +) bool { + // If our header has lower versions than the given version, + // we can return false immediately. This prevents us from + // compiling against unknown symbols and makes runtime checks + // very slightly faster. + if (comptime comptime_version.order(.{ + .major = major, + .minor = minor, + .patch = micro, + }) == .lt) return false; + + // If we're in comptime then we can't check the runtime version. + if (@inComptime()) return true; + + return runtimeAtLeast(major, minor, micro); +} + +/// Verifies that the GTK version at runtime is at least the given version. +/// +/// This function should be used in cases where the only the runtime behavior +/// is affected by the version check. For checks which would affect code +/// generation, use `atLeast`. +pub inline fn runtimeAtLeast( + comptime major: u16, + comptime minor: u16, + comptime micro: u16, +) bool { + // We use the functions instead of the constants such as c.GTK_MINOR_VERSION + // because the function gets the actual runtime version. + const runtime_version = getRuntimeVersion(); + return runtime_version.order(.{ + .major = major, + .minor = minor, + .patch = micro, + }) != .lt; +} + +pub inline fn runtimeUntil( + comptime major: u16, + comptime minor: u16, + comptime micro: u16, +) bool { + const runtime_version = getRuntimeVersion(); + return runtime_version.order(.{ + .major = major, + .minor = minor, + .patch = micro, + }) == .lt; +} + +test "atLeast" { + const testing = std.testing; + + const funs = &.{ atLeast, runtimeAtLeast }; + inline for (funs) |fun| { + try testing.expect(fun(c.GTK_MAJOR_VERSION, c.GTK_MINOR_VERSION, c.GTK_MICRO_VERSION)); + + try testing.expect(!fun(c.GTK_MAJOR_VERSION, c.GTK_MINOR_VERSION, c.GTK_MICRO_VERSION + 1)); + try testing.expect(!fun(c.GTK_MAJOR_VERSION, c.GTK_MINOR_VERSION + 1, c.GTK_MICRO_VERSION)); + try testing.expect(!fun(c.GTK_MAJOR_VERSION + 1, c.GTK_MINOR_VERSION, c.GTK_MICRO_VERSION)); + + try testing.expect(fun(c.GTK_MAJOR_VERSION - 1, c.GTK_MINOR_VERSION, c.GTK_MICRO_VERSION)); + try testing.expect(fun(c.GTK_MAJOR_VERSION - 1, c.GTK_MINOR_VERSION + 1, c.GTK_MICRO_VERSION)); + try testing.expect(fun(c.GTK_MAJOR_VERSION - 1, c.GTK_MINOR_VERSION, c.GTK_MICRO_VERSION + 1)); + + try testing.expect(fun(c.GTK_MAJOR_VERSION, c.GTK_MINOR_VERSION - 1, c.GTK_MICRO_VERSION + 1)); + } +} + +test "runtimeUntil" { + const testing = std.testing; + + // This is an array in case we add a comptime variant. + const funs = &.{runtimeUntil}; + inline for (funs) |fun| { + try testing.expect(!fun(c.GTK_MAJOR_VERSION, c.GTK_MINOR_VERSION, c.GTK_MICRO_VERSION)); + + try testing.expect(fun(c.GTK_MAJOR_VERSION, c.GTK_MINOR_VERSION, c.GTK_MICRO_VERSION + 1)); + try testing.expect(fun(c.GTK_MAJOR_VERSION, c.GTK_MINOR_VERSION + 1, c.GTK_MICRO_VERSION)); + try testing.expect(fun(c.GTK_MAJOR_VERSION + 1, c.GTK_MINOR_VERSION, c.GTK_MICRO_VERSION)); + + try testing.expect(!fun(c.GTK_MAJOR_VERSION - 1, c.GTK_MINOR_VERSION, c.GTK_MICRO_VERSION)); + try testing.expect(!fun(c.GTK_MAJOR_VERSION - 1, c.GTK_MINOR_VERSION + 1, c.GTK_MICRO_VERSION)); + try testing.expect(!fun(c.GTK_MAJOR_VERSION - 1, c.GTK_MINOR_VERSION, c.GTK_MICRO_VERSION + 1)); + + try testing.expect(!fun(c.GTK_MAJOR_VERSION, c.GTK_MINOR_VERSION - 1, c.GTK_MICRO_VERSION + 1)); + } +}