apprt refactor in progress, launches glfw no window

This commit is contained in:
Mitchell Hashimoto
2023-02-22 12:24:22 -08:00
parent 807c7fc64d
commit 3d8c62c41f
5 changed files with 188 additions and 116 deletions

View File

@ -30,9 +30,6 @@ pub const Mailbox = BlockingQueue(Message, 64);
/// General purpose allocator /// General purpose allocator
alloc: Allocator, alloc: Allocator,
/// The runtime for this app.
runtime: apprt.runtime.App,
/// The list of windows that are currently open /// The list of windows that are currently open
windows: WindowList, windows: WindowList,
@ -46,35 +43,16 @@ mailbox: *Mailbox,
/// Set to true once we're quitting. This never goes false again. /// Set to true once we're quitting. This never goes false again.
quit: bool, quit: bool,
/// Mac settings /// App will call this when tick should be called.
darwin: if (Darwin.enabled) Darwin else void, wakeup_cb: ?*const fn () void = null,
/// 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;
}
};
/// Initialize the main app instance. This creates the main window, sets /// Initialize the main app instance. This creates the main window, sets
/// up the renderer state, compiles the shaders, etc. This is the primary /// up the renderer state, compiles the shaders, etc. This is the primary
/// "startup" logic. /// "startup" logic.
pub fn create( pub fn create(
alloc: Allocator, alloc: Allocator,
rt_opts: apprt.runtime.App.Options,
config: *const Config, config: *const Config,
) !*App { ) !*App {
// Initialize app runtime
var app_backend = try apprt.runtime.App.init(rt_opts);
errdefer app_backend.terminate();
// The mailbox for messaging this thread // The mailbox for messaging this thread
var mailbox = try Mailbox.create(alloc); var mailbox = try Mailbox.create(alloc);
errdefer mailbox.destroy(alloc); errdefer mailbox.destroy(alloc);
@ -86,36 +64,13 @@ pub fn create(
errdefer alloc.destroy(app); errdefer alloc.destroy(app);
app.* = .{ app.* = .{
.alloc = alloc, .alloc = alloc,
.runtime = app_backend,
.windows = .{}, .windows = .{},
.config = config, .config = config,
.mailbox = mailbox, .mailbox = mailbox,
.quit = false, .quit = false,
.darwin = if (Darwin.enabled) undefined else {},
}; };
errdefer app.windows.deinit(alloc); 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; return app;
} }
@ -123,35 +78,22 @@ pub fn destroy(self: *App) void {
// Clean up all our windows // Clean up all our windows
for (self.windows.items) |window| window.destroy(); for (self.windows.items) |window| window.destroy();
self.windows.deinit(self.alloc); self.windows.deinit(self.alloc);
if (Darwin.enabled) self.darwin.deinit();
self.mailbox.destroy(self.alloc); self.mailbox.destroy(self.alloc);
// Close our windowing runtime
self.runtime.terminate();
self.alloc.destroy(self); self.alloc.destroy(self);
} }
/// Wake up the app event loop. This should be called after any messages /// Request the app runtime to process app events via tick.
/// are sent to the mailbox.
pub fn wakeup(self: App) void { pub fn wakeup(self: App) void {
self.runtime.wakeup() catch return; if (self.wakeup_cb) |cb| cb();
}
/// 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();
}
} }
/// Tick ticks the app loop. This will drain our mailbox and process those /// Tick ticks the app loop. This will drain our mailbox and process those
/// events. /// events. This should be called by the application runtime on every loop
pub fn tick(self: *App) !void { /// tick.
// Block for any events. ///
try self.runtime.wait(); /// 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 // If any windows are closing, destroy them
var i: usize = 0; var i: usize = 0;
while (i < self.windows.items.len) { while (i < self.windows.items.len) {
@ -165,8 +107,11 @@ pub fn tick(self: *App) !void {
i += 1; i += 1;
} }
// // Drain our mailbox only if we're not quitting. // Drain our mailbox only if we're not quitting.
if (!self.quit) try self.drainMailbox(); 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 /// 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. /// 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| { while (self.mailbox.pop()) |message| {
log.debug("mailbox message={s}", .{@tagName(message)}); log.debug("mailbox message={s}", .{@tagName(message)});
switch (message) { switch (message) {

View File

@ -19,6 +19,22 @@ pub const browser = @import("apprt/browser.zig");
pub const embedded = @import("apprt/embedded.zig"); pub const embedded = @import("apprt/embedded.zig");
pub const Window = @import("apprt/Window.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 /// Runtime is the runtime to use for Ghostty. All runtimes do not provide
/// equivalent feature sets. For example, GTK offers tabbing and more features /// equivalent feature sets. For example, GTK offers tabbing and more features
/// that glfw does not provide. However, glfw may require many less /// 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 { test {
@import("std").testing.refAllDecls(@This()); @import("std").testing.refAllDecls(@This());
} }

View File

@ -5,10 +5,12 @@
const std = @import("std"); const std = @import("std");
const builtin = @import("builtin"); const builtin = @import("builtin");
const build_config = @import("../build_config.zig");
const assert = std.debug.assert; const assert = std.debug.assert;
const Allocator = std.mem.Allocator; const Allocator = std.mem.Allocator;
const trace = @import("tracy").trace; const trace = @import("tracy").trace;
const glfw = @import("glfw"); const glfw = @import("glfw");
const macos = @import("macos");
const objc = @import("objc"); const objc = @import("objc");
const input = @import("../input.zig"); const input = @import("../input.zig");
const internal_os = @import("../os/main.zig"); const internal_os = @import("../os/main.zig");
@ -26,11 +28,28 @@ const glfwNative = glfw.Native(.{
const log = std.log.scoped(.glfw); const log = std.log.scoped(.glfw);
pub const App = struct { pub const App = struct {
app: *CoreApp,
/// Mac-specific state.
darwin: if (Darwin.enabled) Darwin else void,
pub const Options = struct {}; pub const Options = struct {};
pub fn init(_: Options) !App { pub fn init(core_app: *CoreApp, _: Options) !App {
if (!glfw.init(.{})) return error.GlfwInitFailed; 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 { pub fn terminate(self: App) void {
@ -38,17 +57,67 @@ pub const App = struct {
glfw.terminate(); 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. /// Wakeup the event loop. This should be able to be called from any thread.
pub fn wakeup(self: App) !void { pub fn wakeup() void {
_ = self;
glfw.postEmptyEvent(); glfw.postEmptyEvent();
} }
/// Wait for events in the event loop to process. fn glfwErrorCallback(code: glfw.ErrorCode, desc: [:0]const u8) void {
pub fn wait(self: App) !void { std.log.warn("glfw error={} message={s}", .{ code, desc });
_ = self;
glfw.waitEvents(); // 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 { pub const Window = struct {

View File

@ -14,6 +14,13 @@ pub const c = @cImport({
const log = std.log.scoped(.gtk); 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 App = struct {
pub const Options = struct { pub const Options = struct {
/// GTK app ID /// GTK app ID
@ -23,14 +30,15 @@ pub const App = struct {
app: *c.GtkApplication, app: *c.GtkApplication,
ctx: *c.GMainContext, 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( const app = @ptrCast(?*c.GtkApplication, c.gtk_application_new(
opts.id.ptr, opts.id.ptr,
c.G_APPLICATION_DEFAULT_FLAGS, c.G_APPLICATION_DEFAULT_FLAGS,
)) orelse return error.GtkInitFailed; )) orelse return error.GtkInitFailed;
errdefer c.g_object_unref(app); errdefer c.g_object_unref(app);
// Setup our callbacks
_ = c.g_signal_connect_data( _ = c.g_signal_connect_data(
app, app,
"activate", "activate",
@ -69,6 +77,8 @@ pub const App = struct {
return .{ .app = app, .ctx = ctx }; 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 { pub fn terminate(self: App) void {
c.g_settings_sync(); c.g_settings_sync();
while (c.g_main_context_iteration(self.ctx, 0) != 0) {} 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); _ = 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 { fn activate(app: *c.GtkApplication, ud: ?*anyopaque) callconv(.C) void {
_ = app; _ = app;
_ = ud; _ = 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", .{}); 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 { pub const Window = struct {

View File

@ -11,6 +11,7 @@ const fontconfig = @import("fontconfig");
const harfbuzz = @import("harfbuzz"); const harfbuzz = @import("harfbuzz");
const renderer = @import("renderer.zig"); const renderer = @import("renderer.zig");
const xdg = @import("xdg.zig"); const xdg = @import("xdg.zig");
const apprt = @import("apprt.zig");
const App = @import("App.zig"); const App = @import("App.zig");
const cli_args = @import("cli_args.zig"); const cli_args = @import("cli_args.zig");
@ -89,18 +90,30 @@ pub fn main() !void {
try config.finalize(); try config.finalize();
std.log.debug("config={}", .{config}); std.log.debug("config={}", .{config});
switch (build_config.app_runtime) { if (true) {
.none => {}, // Create our app state
.glfw => { var app = try App.create(alloc, &config);
// We want to log all our errors defer app.destroy();
glfw.setErrorCallback(glfwErrorCallback);
}, // Create our runtime app
.gtk => {}, 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. // Run our app with a single initial window to start.
var app = try App.create(alloc, .{}, &config); var app = try App.create(alloc, .{}, &config);
defer app.destroy(); 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.newWindow(.{});
try app.run(); 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 /// This represents the global process state. There should only
/// be one of these at any given moment. This is extracted into a dedicated /// 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. /// struct because it is reused by main and the static C lib.