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
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) {

View File

@ -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());
}

View File

@ -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 {

View File

@ -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 {

View File

@ -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.