diff --git a/build.zig b/build.zig index a7f42383e..80aebd22a 100644 --- a/build.zig +++ b/build.zig @@ -3,6 +3,7 @@ const builtin = @import("builtin"); const fs = std.fs; const Builder = std.build.Builder; const LibExeObjStep = std.build.LibExeObjStep; +const apprt = @import("src/apprt.zig"); const glfw = @import("vendor/mach/libs/glfw/build.zig"); const fontconfig = @import("pkg/fontconfig/build.zig"); const freetype = @import("pkg/freetype/build.zig"); @@ -45,6 +46,7 @@ comptime { var tracy: bool = false; var enable_coretext: bool = false; var enable_fontconfig: bool = false; +var app_runtime: apprt.Runtime = .none; pub fn build(b: *std.build.Builder) !void { const optimize = b.standardOptimizeOption(.{}); @@ -77,6 +79,12 @@ pub fn build(b: *std.build.Builder) !void { "Enable fontconfig for font discovery (default true on Linux)", ) orelse target.isLinux(); + app_runtime = b.option( + apprt.Runtime, + "app-runtime", + "The app runtime to use. Not all values supported on all platforms.", + ) orelse apprt.Runtime.default(target); + const static = b.option( bool, "static", @@ -111,6 +119,7 @@ pub fn build(b: *std.build.Builder) !void { exe_options.addOption(bool, "tracy_enabled", tracy); exe_options.addOption(bool, "coretext", enable_coretext); exe_options.addOption(bool, "fontconfig", enable_fontconfig); + exe_options.addOption(apprt.Runtime, "app_runtime", app_runtime); // Exe { @@ -120,7 +129,7 @@ pub fn build(b: *std.build.Builder) !void { } exe.addOptions("build_options", exe_options); - exe.install(); + if (app_runtime != .none) exe.install(); // Add the shared dependencies _ = try addDeps(b, exe, static); @@ -134,7 +143,7 @@ pub fn build(b: *std.build.Builder) !void { b.installFile("dist/macos/Ghostty.icns", "Ghostty.app/Contents/Resources/Ghostty.icns"); } - // On Mac we can build the app. + // On Mac we can build the embedding library. if (builtin.target.isDarwin()) { const static_lib_aarch64 = lib: { const lib = b.addStaticLibrary(.{ @@ -539,22 +548,30 @@ fn addDeps( } if (!lib) { - step.addModule("glfw", glfw.module(b)); - // We always statically compile glad step.addIncludePath("vendor/glad/include/"); step.addCSourceFile("vendor/glad/src/gl.c", &.{}); - // Glfw - const glfw_opts: glfw.Options = .{ - .metal = step.target.isDarwin(), - .opengl = false, - }; - try glfw.link(b, step, glfw_opts); + switch (app_runtime) { + .none => {}, - // Imgui - const imgui_step = try imgui.link(b, step, imgui_opts); - try glfw.link(b, imgui_step, glfw_opts); + .glfw => { + step.addModule("glfw", glfw.module(b)); + const glfw_opts: glfw.Options = .{ + .metal = step.target.isDarwin(), + .opengl = false, + }; + try glfw.link(b, step, glfw_opts); + + // Must also link to imgui + const imgui_step = try imgui.link(b, step, imgui_opts); + try glfw.link(b, imgui_step, glfw_opts); + }, + + .gtk => { + step.linkSystemLibrary("gtk4"); + }, + } } return static_libs; diff --git a/nix/devshell.nix b/nix/devshell.nix index 343546312..e612f58fa 100644 --- a/nix/devshell.nix +++ b/nix/devshell.nix @@ -22,6 +22,7 @@ , expat , fontconfig , freetype +, gtk4 , harfbuzz , libpng , libGL @@ -53,6 +54,8 @@ let libXcursor libXi libXrandr + + gtk4 ]; in mkShell rec { name = "ghostty"; @@ -102,6 +105,9 @@ in mkShell rec { libXi libXinerama libXrandr + + # Only needed for GTK builds + gtk4 ]; # This should be set onto the rpath of the ghostty binary if you want diff --git a/src/apprt.zig b/src/apprt.zig index 36e2a2571..7ec13d21b 100644 --- a/src/apprt.zig +++ b/src/apprt.zig @@ -8,21 +8,49 @@ //! The goal is to have different implementations share as much of the core //! logic as possible, and to only reach out to platform-specific implementation //! code when absolutely necessary. +const std = @import("std"); const builtin = @import("builtin"); const build_config = @import("build_config.zig"); pub usingnamespace @import("apprt/structs.zig"); pub const glfw = @import("apprt/glfw.zig"); +pub const gtk = @import("apprt/gtk.zig"); pub const browser = @import("apprt/browser.zig"); pub const embedded = @import("apprt/embedded.zig"); pub const Window = @import("apprt/Window.zig"); +/// Runtime is the runtime to use for Ghostty. All runtimes do not provide +/// equivalent feature sets. For example, GTK offers tabbing and more features +/// that glfw does not provide. However, glfw may require many less +/// dependencies. +pub const Runtime = enum { + /// Will not produce an executable at all when `zig build` is called. + /// This is only useful if you're only interested in the lib only (macOS). + none, + + /// Glfw-backed. Very simple. Glfw is statically linked. Tabbing and + /// other rich windowing features are not supported. + glfw, + + /// GTK-backed. Rich windowed application. GTK is dynamically linked. + gtk, + + pub fn default(target: std.zig.CrossTarget) Runtime { + _ = target; + return .glfw; + } +}; + /// The implementation to use for the app runtime. This is comptime chosen /// so that every build has exactly one application runtime implementation. /// Note: it is very rare to use Runtime directly; most usage will use /// Window or something. pub const runtime = switch (build_config.artifact) { - .exe => glfw, + .exe => switch (build_config.app_runtime) { + .none => @compileError("exe with no runtime not allowed"), + .glfw => glfw, + .gtk => gtk, + }, .lib => embedded, .wasm_module => browser, }; diff --git a/src/apprt/gtk.zig b/src/apprt/gtk.zig new file mode 100644 index 000000000..b678ba180 --- /dev/null +++ b/src/apprt/gtk.zig @@ -0,0 +1,45 @@ +//! Application runtime that uses GTK4. + +const std = @import("std"); +const builtin = @import("builtin"); +const assert = std.debug.assert; +const Allocator = std.mem.Allocator; + +pub const c = @cImport({ + @cInclude("gtk/gtk.h"); +}); + +const log = std.log.scoped(.gtk); + +pub const App = struct { + pub const Options = struct { + /// GTK app ID + id: [:0]const u8 = "com.mitchellh.ghostty", + }; + + pub fn init(opts: Options) !App { + const app = c.gtk_application_new(opts.id.ptr, c.G_APPLICATION_DEFAULT_FLAGS); + errdefer c.g_object_unref(app); + return .{}; + } + + pub fn terminate(self: App) void { + _ = self; + } + + pub fn wakeup(self: App) !void { + _ = self; + } + + pub fn wait(self: App) !void { + _ = self; + } +}; + +pub const Window = struct { + pub const Options = struct {}; + + pub fn deinit(self: *Window) void { + _ = self; + } +}; diff --git a/src/build_config.zig b/src/build_config.zig index 2baaa48b8..ddaf9c14c 100644 --- a/src/build_config.zig +++ b/src/build_config.zig @@ -4,15 +4,19 @@ //! to shim logic and values into them later. const std = @import("std"); const builtin = @import("builtin"); +const options = @import("build_options"); const assert = std.debug.assert; /// The artifact we're producing. This can be used to determine if we're /// building a standalone exe, an embedded lib, etc. pub const artifact = Artifact.detect(); +/// The runtime to back exe artifacts with. +pub const app_runtime = options.app_runtime; + /// Whether our devmode UI is enabled or not. This requires imgui to be /// compiled. -pub const devmode_enabled = artifact == .exe; +pub const devmode_enabled = artifact == .exe and app_runtime == .glfw; pub const Artifact = enum { /// Standalone executable diff --git a/src/main.zig b/src/main.zig index 48c4d2a71..ef75d1f74 100644 --- a/src/main.zig +++ b/src/main.zig @@ -1,5 +1,6 @@ const std = @import("std"); const builtin = @import("builtin"); +const build_config = @import("build_config.zig"); const options = @import("build_options"); const glfw = @import("glfw"); const macos = @import("macos"); @@ -88,12 +89,19 @@ pub fn main() !void { try config.finalize(); std.log.debug("config={}", .{config}); - // We want to log all our errors - glfw.setErrorCallback(glfwErrorCallback); + switch (build_config.app_runtime) { + .none => {}, + .glfw => { + // We want to log all our errors + glfw.setErrorCallback(glfwErrorCallback); + }, + .gtk => {}, + } // Run our app with a single initial window to start. var app = try App.create(alloc, .{}, &config); defer app.destroy(); + if (build_config.app_runtime == .gtk) return; _ = try app.newWindow(.{}); try app.run(); } diff --git a/src/renderer/OpenGL.zig b/src/renderer/OpenGL.zig index c8cdf6a26..b3fa6b99d 100644 --- a/src/renderer/OpenGL.zig +++ b/src/renderer/OpenGL.zig @@ -420,6 +420,7 @@ pub fn deinitDevMode(self: *const OpenGL) void { /// Callback called by renderer.Thread when it begins. pub fn threadEnter(self: *const OpenGL, win: apprt.runtime.Window) !void { _ = self; + if (apprt.runtime == apprt.gtk) @panic("TODO"); // We need to make the OpenGL context current. OpenGL requires // that a single thread own the a single OpenGL context (if any). This @@ -1066,7 +1067,8 @@ pub fn setScreenSize(self: *OpenGL, dim: renderer.ScreenSize) !void { // Apply our padding const padding = self.padding.explicit.add(if (self.padding.balance) renderer.Padding.balanced(dim, grid_size, self.cell_size) - else .{}); + else + .{}); const padded_dim = dim.subPadding(padding); log.debug("screen size padded={} screen={} grid={} cell={} padding={}", .{