From 3d8c62c41ff673a5156388863db8f70680d3e05e Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 22 Feb 2023 12:24:22 -0800 Subject: [PATCH] apprt refactor in progress, launches glfw no window --- src/App.zig | 87 +++++++++------------------------------------- src/apprt.zig | 30 ++++++++-------- src/apprt/glfw.zig | 85 +++++++++++++++++++++++++++++++++++++++----- src/apprt/gtk.zig | 61 ++++++++++++++++++++++++++++++-- src/main.zig | 41 +++++++++++----------- 5 files changed, 188 insertions(+), 116 deletions(-) diff --git a/src/App.zig b/src/App.zig index 267b4f984..4f8fdc85a 100644 --- a/src/App.zig +++ b/src/App.zig @@ -30,9 +30,6 @@ pub const Mailbox = BlockingQueue(Message, 64); /// General purpose allocator alloc: Allocator, -/// The runtime for this app. -runtime: apprt.runtime.App, - /// The list of windows that are currently open windows: WindowList, @@ -46,35 +43,16 @@ mailbox: *Mailbox, /// Set to true once we're quitting. This never goes false again. quit: bool, -/// Mac settings -darwin: if (Darwin.enabled) Darwin else void, - -/// Mac-specific settings. This is only enabled when the target is -/// Mac and the artifact is a standalone exe. We don't target libs because -/// the embedded API doesn't do windowing. -pub const Darwin = struct { - pub const enabled = builtin.target.isDarwin() and build_config.artifact == .exe; - - tabbing_id: *macos.foundation.String, - - pub fn deinit(self: *Darwin) void { - self.tabbing_id.release(); - self.* = undefined; - } -}; +/// App will call this when tick should be called. +wakeup_cb: ?*const fn () void = null, /// Initialize the main app instance. This creates the main window, sets /// up the renderer state, compiles the shaders, etc. This is the primary /// "startup" logic. pub fn create( alloc: Allocator, - rt_opts: apprt.runtime.App.Options, config: *const Config, ) !*App { - // Initialize app runtime - var app_backend = try apprt.runtime.App.init(rt_opts); - errdefer app_backend.terminate(); - // The mailbox for messaging this thread var mailbox = try Mailbox.create(alloc); errdefer mailbox.destroy(alloc); @@ -86,36 +64,13 @@ pub fn create( errdefer alloc.destroy(app); app.* = .{ .alloc = alloc, - .runtime = app_backend, .windows = .{}, .config = config, .mailbox = mailbox, .quit = false, - .darwin = if (Darwin.enabled) undefined else {}, }; errdefer app.windows.deinit(alloc); - // On Mac, we enable window tabbing. We only do this if we're building - // a standalone exe. In embedded mode the host app handles this for us. - if (Darwin.enabled) { - const NSWindow = objc.Class.getClass("NSWindow").?; - NSWindow.msgSend(void, objc.sel("setAllowsAutomaticWindowTabbing:"), .{true}); - - // Our tabbing ID allows all of our windows to group together - const tabbing_id = try macos.foundation.String.createWithBytes( - "dev.ghostty.window", - .utf8, - false, - ); - errdefer tabbing_id.release(); - - // Setup our Mac settings - app.darwin = .{ - .tabbing_id = tabbing_id, - }; - } - errdefer if (comptime builtin.target.isDarwin()) app.darwin.deinit(); - return app; } @@ -123,35 +78,22 @@ pub fn destroy(self: *App) void { // Clean up all our windows for (self.windows.items) |window| window.destroy(); self.windows.deinit(self.alloc); - if (Darwin.enabled) self.darwin.deinit(); self.mailbox.destroy(self.alloc); - // Close our windowing runtime - self.runtime.terminate(); - self.alloc.destroy(self); } -/// Wake up the app event loop. This should be called after any messages -/// are sent to the mailbox. +/// Request the app runtime to process app events via tick. pub fn wakeup(self: App) void { - self.runtime.wakeup() catch return; -} - -/// Run the main event loop for the application. This blocks until the -/// application quits or every window is closed. -pub fn run(self: *App) !void { - while (!self.quit and self.windows.items.len > 0) { - try self.tick(); - } + if (self.wakeup_cb) |cb| cb(); } /// Tick ticks the app loop. This will drain our mailbox and process those -/// events. -pub fn tick(self: *App) !void { - // Block for any events. - try self.runtime.wait(); - +/// events. This should be called by the application runtime on every loop +/// tick. +/// +/// This returns whether the app should quit or not. +pub fn tick(self: *App, rt_app: *apprt.runtime.App) !bool { // If any windows are closing, destroy them var i: usize = 0; while (i < self.windows.items.len) { @@ -165,8 +107,11 @@ pub fn tick(self: *App) !void { i += 1; } - // // Drain our mailbox only if we're not quitting. - if (!self.quit) try self.drainMailbox(); + // Drain our mailbox only if we're not quitting. + if (!self.quit) try self.drainMailbox(rt_app); + + // We quit if our quit flag is on or if we have closed all windows. + return self.quit or self.windows.items.len == 0; } /// Create a new window. This can be called only on the main thread. This @@ -201,7 +146,9 @@ pub fn closeWindow(self: *App, window: *Window) void { } /// Drain the mailbox. -fn drainMailbox(self: *App) !void { +fn drainMailbox(self: *App, rt_app: *apprt.runtime.App) !void { + _ = rt_app; + while (self.mailbox.pop()) |message| { log.debug("mailbox message={s}", .{@tagName(message)}); switch (message) { diff --git a/src/apprt.zig b/src/apprt.zig index 7ec13d21b..b2f4c93b1 100644 --- a/src/apprt.zig +++ b/src/apprt.zig @@ -19,6 +19,22 @@ pub const browser = @import("apprt/browser.zig"); pub const embedded = @import("apprt/embedded.zig"); pub const Window = @import("apprt/Window.zig"); +/// 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 => switch (build_config.app_runtime) { + .none => @compileError("exe with no runtime not allowed"), + .glfw => glfw, + .gtk => gtk, + }, + .lib => embedded, + .wasm_module => browser, +}; + +pub const App = runtime.App; + /// 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 @@ -41,20 +57,6 @@ pub const Runtime = enum { } }; -/// 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 => switch (build_config.app_runtime) { - .none => @compileError("exe with no runtime not allowed"), - .glfw => glfw, - .gtk => gtk, - }, - .lib => embedded, - .wasm_module => browser, -}; - test { @import("std").testing.refAllDecls(@This()); } diff --git a/src/apprt/glfw.zig b/src/apprt/glfw.zig index 1e03f30e9..5473e13d4 100644 --- a/src/apprt/glfw.zig +++ b/src/apprt/glfw.zig @@ -5,10 +5,12 @@ const std = @import("std"); const builtin = @import("builtin"); +const build_config = @import("../build_config.zig"); const assert = std.debug.assert; const Allocator = std.mem.Allocator; const trace = @import("tracy").trace; const glfw = @import("glfw"); +const macos = @import("macos"); const objc = @import("objc"); const input = @import("../input.zig"); const internal_os = @import("../os/main.zig"); @@ -26,11 +28,28 @@ const glfwNative = glfw.Native(.{ const log = std.log.scoped(.glfw); pub const App = struct { + app: *CoreApp, + + /// Mac-specific state. + darwin: if (Darwin.enabled) Darwin else void, + pub const Options = struct {}; - pub fn init(_: Options) !App { + pub fn init(core_app: *CoreApp, _: Options) !App { if (!glfw.init(.{})) return error.GlfwInitFailed; - return .{}; + glfw.setErrorCallback(glfwErrorCallback); + + // Mac-specific state. For example, on Mac we enable window tabbing. + var darwin = if (Darwin.enabled) try Darwin.init() else {}; + errdefer if (Darwin.enabled) darwin.deinit(); + + // Set our callback for being woken up + core_app.wakeup_cb = wakeup; + + return .{ + .app = core_app, + .darwin = darwin, + }; } pub fn terminate(self: App) void { @@ -38,17 +57,67 @@ pub const App = struct { glfw.terminate(); } + /// Run the event loop. This doesn't return until the app exits. + pub fn run(self: *App) !void { + while (true) { + // Wait for any events from the app event loop. wakeup will post + // an empty event so that this will return. + glfw.waitEvents(); + + // Tick the terminal app + const should_quit = try self.app.tick(self); + if (should_quit) return; + } + } + /// Wakeup the event loop. This should be able to be called from any thread. - pub fn wakeup(self: App) !void { - _ = self; + pub fn wakeup() void { glfw.postEmptyEvent(); } - /// Wait for events in the event loop to process. - pub fn wait(self: App) !void { - _ = self; - glfw.waitEvents(); + fn glfwErrorCallback(code: glfw.ErrorCode, desc: [:0]const u8) void { + std.log.warn("glfw error={} message={s}", .{ code, desc }); + + // Workaround for: https://github.com/ocornut/imgui/issues/5908 + // If we get an invalid value with "scancode" in the message we assume + // it is from the glfw key callback that imgui sets and we clear the + // error so that our future code doesn't crash. + if (code == glfw.ErrorCode.InvalidValue and + std.mem.indexOf(u8, desc, "scancode") != null) + { + _ = glfw.getError(); + } } + + /// Mac-specific settings. This is only enabled when the target is + /// Mac and the artifact is a standalone exe. We don't target libs because + /// the embedded API doesn't do windowing. + const Darwin = struct { + const enabled = builtin.target.isDarwin() and build_config.artifact == .exe; + + tabbing_id: *macos.foundation.String, + + pub fn init() !Darwin { + const NSWindow = objc.Class.getClass("NSWindow").?; + NSWindow.msgSend(void, objc.sel("setAllowsAutomaticWindowTabbing:"), .{true}); + + // Our tabbing ID allows all of our windows to group together + const tabbing_id = try macos.foundation.String.createWithBytes( + "com.mitchellh.ghostty.window", + .utf8, + false, + ); + errdefer tabbing_id.release(); + + // Setup our Mac settings + return .{ .tabbing_id = tabbing_id }; + } + + pub fn deinit(self: *Darwin) void { + self.tabbing_id.release(); + self.* = undefined; + } + }; }; pub const Window = struct { diff --git a/src/apprt/gtk.zig b/src/apprt/gtk.zig index 258da9970..a60ac6af4 100644 --- a/src/apprt/gtk.zig +++ b/src/apprt/gtk.zig @@ -14,6 +14,13 @@ pub const c = @cImport({ const log = std.log.scoped(.gtk); +/// App is the entrypoint for the application. This is called after all +/// of the runtime-agnostic initialization is complete and we're ready +/// to start. +/// +/// There is only ever one App instance per process. This is because most +/// application frameworks also have this restriction so it simplifies +/// the assumptions. pub const App = struct { pub const Options = struct { /// GTK app ID @@ -23,14 +30,15 @@ pub const App = struct { app: *c.GtkApplication, ctx: *c.GMainContext, - pub fn init(opts: Options) !App { + pub fn init(core_app: *CoreApp, opts: Options) !App { + _ = core_app; + + // Create our GTK Application which encapsulates our process. const app = @ptrCast(?*c.GtkApplication, c.gtk_application_new( opts.id.ptr, c.G_APPLICATION_DEFAULT_FLAGS, )) orelse return error.GtkInitFailed; errdefer c.g_object_unref(app); - - // Setup our callbacks _ = c.g_signal_connect_data( app, "activate", @@ -69,6 +77,8 @@ pub const App = struct { return .{ .app = app, .ctx = ctx }; } + // Terminate the application. The application will not be restarted after + // this so all global state can be cleaned up. pub fn terminate(self: App) void { c.g_settings_sync(); while (c.g_main_context_iteration(self.ctx, 0) != 0) {} @@ -85,11 +95,56 @@ pub const App = struct { _ = c.g_main_context_iteration(self.ctx, 1); } + pub fn newWindow(self: App) !void { + const window = c.gtk_application_window_new(self.app); + c.gtk_window_set_title(@ptrCast(*c.GtkWindow, window), "Ghostty"); + c.gtk_window_set_default_size(@ptrCast(*c.GtkWindow, window), 200, 200); + + const surface = c.gtk_gl_area_new(); + c.gtk_window_set_child(@ptrCast(*c.GtkWindow, window), surface); + _ = c.g_signal_connect_data( + surface, + "realize", + c.G_CALLBACK(&onSurfaceRealize), + null, + null, + c.G_CONNECT_DEFAULT, + ); + _ = c.g_signal_connect_data( + surface, + "render", + c.G_CALLBACK(&onSurfaceRender), + null, + null, + c.G_CONNECT_DEFAULT, + ); + + c.gtk_widget_show(window); + } + fn activate(app: *c.GtkApplication, ud: ?*anyopaque) callconv(.C) void { _ = app; _ = ud; + + // We purposely don't do anything on activation right now. We have + // this callback because if we don't then GTK emits a warning to + // stderr that we don't want. We emit a debug log just so that we know + // we reached this point. log.debug("application activated", .{}); } + + fn onSurfaceRealize(area: *c.GtkGLArea, ud: ?*anyopaque) callconv(.C) void { + _ = area; + _ = ud; + log.debug("gl surface realized", .{}); + } + + fn onSurfaceRender(area: *c.GtkGLArea, ctx: *c.GdkGLContext, ud: ?*anyopaque) callconv(.C) void { + _ = area; + _ = ctx; + _ = ud; + log.debug("gl render", .{}); + } }; pub const Window = struct { diff --git a/src/main.zig b/src/main.zig index a29f15057..35c24ccb5 100644 --- a/src/main.zig +++ b/src/main.zig @@ -11,6 +11,7 @@ const fontconfig = @import("fontconfig"); const harfbuzz = @import("harfbuzz"); const renderer = @import("renderer.zig"); const xdg = @import("xdg.zig"); +const apprt = @import("apprt.zig"); const App = @import("App.zig"); const cli_args = @import("cli_args.zig"); @@ -89,18 +90,30 @@ pub fn main() !void { try config.finalize(); std.log.debug("config={}", .{config}); - switch (build_config.app_runtime) { - .none => {}, - .glfw => { - // We want to log all our errors - glfw.setErrorCallback(glfwErrorCallback); - }, - .gtk => {}, + if (true) { + // Create our app state + var app = try App.create(alloc, &config); + defer app.destroy(); + + // Create our runtime app + var app_runtime = try apprt.App.init(app, .{}); + defer app_runtime.terminate(); + + // Create an initial window + + // Run the GUI event loop + try app_runtime.run(); + return; } // 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) { + try app.runtime.newWindow(); + while (true) try app.runtime.wait(); + return; + } _ = try app.newWindow(.{}); try app.run(); } @@ -158,20 +171,6 @@ pub const std_options = struct { } }; -fn glfwErrorCallback(code: glfw.ErrorCode, desc: [:0]const u8) void { - std.log.warn("glfw error={} message={s}", .{ code, desc }); - - // Workaround for: https://github.com/ocornut/imgui/issues/5908 - // If we get an invalid value with "scancode" in the message we assume - // it is from the glfw key callback that imgui sets and we clear the - // error so that our future code doesn't crash. - if (code == glfw.ErrorCode.InvalidValue and - std.mem.indexOf(u8, desc, "scancode") != null) - { - _ = glfw.getError(); - } -} - /// This represents the global process state. There should only /// be one of these at any given moment. This is extracted into a dedicated /// struct because it is reused by main and the static C lib.