From 48c9c6591522c92296c3bf16d3dbdcec436378db Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 20 Feb 2023 15:13:06 -0800 Subject: [PATCH 01/37] add app runtime option, add gtk backend --- build.zig | 43 +++++++++++++++++++++++++++------------ nix/devshell.nix | 6 ++++++ src/apprt.zig | 30 ++++++++++++++++++++++++++- src/apprt/gtk.zig | 45 +++++++++++++++++++++++++++++++++++++++++ src/build_config.zig | 6 +++++- src/main.zig | 12 +++++++++-- src/renderer/OpenGL.zig | 4 +++- 7 files changed, 128 insertions(+), 18 deletions(-) create mode 100644 src/apprt/gtk.zig 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={}", .{ From f268f3955e4b64fcc201c1aec5a40a390ed35bf5 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 21 Feb 2023 08:20:13 -0800 Subject: [PATCH 02/37] init gtk app --- nix/devshell.nix | 3 ++ src/App.zig | 5 ++- src/apprt/gtk.zig | 93 +++++++++++++++++++++++++++++++++++++++-- src/main.zig | 2 +- src/renderer/OpenGL.zig | 22 ++++++---- 5 files changed, 110 insertions(+), 15 deletions(-) diff --git a/nix/devshell.nix b/nix/devshell.nix index e612f58fa..e343682bd 100644 --- a/nix/devshell.nix +++ b/nix/devshell.nix @@ -22,6 +22,7 @@ , expat , fontconfig , freetype +, glib , gtk4 , harfbuzz , libpng @@ -56,6 +57,7 @@ let libXrandr gtk4 + glib ]; in mkShell rec { name = "ghostty"; @@ -108,6 +110,7 @@ in mkShell rec { # Only needed for GTK builds gtk4 + glib ]; # This should be set onto the rpath of the ghostty binary if you want diff --git a/src/App.zig b/src/App.zig index 4efbda3b2..267b4f984 100644 --- a/src/App.zig +++ b/src/App.zig @@ -125,10 +125,11 @@ pub fn destroy(self: *App) void { self.windows.deinit(self.alloc); if (Darwin.enabled) self.darwin.deinit(); self.mailbox.destroy(self.alloc); - self.alloc.destroy(self); // Close our windowing runtime self.runtime.terminate(); + + self.alloc.destroy(self); } /// Wake up the app event loop. This should be called after any messages @@ -164,7 +165,7 @@ pub fn tick(self: *App) !void { 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(); } diff --git a/src/apprt/gtk.zig b/src/apprt/gtk.zig index b678ba180..c8d8c5142 100644 --- a/src/apprt/gtk.zig +++ b/src/apprt/gtk.zig @@ -4,6 +4,9 @@ const std = @import("std"); const builtin = @import("builtin"); const assert = std.debug.assert; const Allocator = std.mem.Allocator; +const apprt = @import("../apprt.zig"); +const CoreApp = @import("../App.zig"); +const CoreWindow = @import("../Window.zig"); pub const c = @cImport({ @cInclude("gtk/gtk.h"); @@ -17,29 +20,111 @@ pub const App = struct { id: [:0]const u8 = "com.mitchellh.ghostty", }; + app: *c.GtkApplication, + ctx: *c.GMainContext, + pub fn init(opts: Options) !App { - const app = c.gtk_application_new(opts.id.ptr, c.G_APPLICATION_DEFAULT_FLAGS); + 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); - return .{}; + + // We don't use g_application_run, we want to manually control the + // loop so we have to do the same things the run function does: + // https://github.com/GNOME/glib/blob/a8e8b742e7926e33eb635a8edceac74cf239d6ed/gio/gapplication.c#L2533 + const ctx = c.g_main_context_default() orelse return error.GtkContextFailed; + if (c.g_main_context_acquire(ctx) == 0) return error.GtkContextAcquireFailed; + errdefer c.g_main_context_release(ctx); + + const gapp = @ptrCast(*c.GApplication, app); + var err_: ?*c.GError = null; + if (c.g_application_register( + gapp, + null, + @ptrCast([*c][*c]c.GError, &err_), + ) == 0) { + if (err_) |err| { + log.warn("error registering application: {s}", .{err.message}); + c.g_error_free(err); + } + return error.GtkApplicationRegisterFailed; + } + + c.g_application_activate(gapp); + + return .{ .app = app, .ctx = ctx }; } pub fn terminate(self: App) void { - _ = self; + c.g_settings_sync(); + while (c.g_main_context_iteration(self.ctx, 0) != 0) {} + c.g_main_context_release(self.ctx); + c.g_object_unref(self.app); } pub fn wakeup(self: App) !void { _ = self; + c.g_main_context_wakeup(null); } pub fn wait(self: App) !void { - _ = self; + _ = c.g_main_context_iteration(self.ctx, 1); } }; pub const Window = struct { pub const Options = struct {}; + pub fn init(app: *const CoreApp, core_win: *CoreWindow, opts: Options) !Window { + _ = app; + _ = core_win; + _ = opts; + + return .{}; + } + pub fn deinit(self: *Window) void { _ = self; } + + pub fn setShouldClose(self: *Window) void { + _ = self; + } + + pub fn shouldClose(self: *const Window) bool { + _ = self; + return false; + } + + pub fn getContentScale(self: *const Window) !apprt.ContentScale { + _ = self; + return .{ .x = 1, .y = 1 }; + } + + pub fn getSize(self: *const Window) !apprt.WindowSize { + _ = self; + return .{ .width = 800, .height = 600 }; + } + + pub fn setSizeLimits(self: *Window, min: apprt.WindowSize, max_: ?apprt.WindowSize) !void { + _ = self; + _ = min; + _ = max_; + } + + pub fn setTitle(self: *Window, slice: [:0]const u8) !void { + _ = self; + _ = slice; + } + + pub fn getClipboardString(self: *const Window) ![:0]const u8 { + _ = self; + return ""; + } + + pub fn setClipboardString(self: *const Window, val: [:0]const u8) !void { + _ = self; + _ = val; + } }; diff --git a/src/main.zig b/src/main.zig index ef75d1f74..b880c2116 100644 --- a/src/main.zig +++ b/src/main.zig @@ -101,9 +101,9 @@ pub fn main() !void { // Run our app with a single initial window to start. var app = try App.create(alloc, .{}, &config); defer app.destroy(); + try app.run(); if (build_config.app_runtime == .gtk) return; _ = try app.newWindow(.{}); - try app.run(); } // Required by tracy/tracy.zig to enable/disable tracy support. diff --git a/src/renderer/OpenGL.zig b/src/renderer/OpenGL.zig index b3fa6b99d..2f100e7c3 100644 --- a/src/renderer/OpenGL.zig +++ b/src/renderer/OpenGL.zig @@ -444,6 +444,7 @@ pub fn threadEnter(self: *const OpenGL, win: apprt.runtime.Window) !void { /// Callback called by renderer.Thread when it exits. pub fn threadExit(self: *const OpenGL) void { _ = self; + if (apprt.runtime == apprt.gtk) @panic("TODO"); gl.glad.unload(); glfw.makeContextCurrent(null); @@ -569,12 +570,14 @@ pub fn render( // Build our devmode draw data const devmode_data = devmode_data: { - if (state.devmode) |dm| { - if (dm.visible) { - imgui.ImplOpenGL3.newFrame(); - imgui.ImplGlfw.newFrame(); - try dm.update(); - break :devmode_data try dm.render(); + if (DevMode.enabled) { + if (state.devmode) |dm| { + if (dm.visible) { + imgui.ImplOpenGL3.newFrame(); + imgui.ImplGlfw.newFrame(); + try dm.update(); + break :devmode_data try dm.render(); + } } } @@ -639,11 +642,14 @@ pub fn render( try self.draw(); // If we have devmode, then render that - if (critical.devmode_data) |data| { - imgui.ImplOpenGL3.renderDrawData(data); + if (DevMode.enabled) { + if (critical.devmode_data) |data| { + imgui.ImplOpenGL3.renderDrawData(data); + } } // Swap our window buffers + if (apprt.runtime == apprt.gtk) @panic("TODO"); win.window.swapBuffers(); } From d368b8e7278a0c6a844be592d5c7977bb88ddaf7 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 21 Feb 2023 10:18:04 -0800 Subject: [PATCH 03/37] setup app, run works but crashes in OpenGL --- src/apprt/gtk.zig | 19 +++++++++++++ src/main.zig | 3 +-- src/renderer/OpenGL.zig | 59 +++++++++++++++++++++++++++-------------- 3 files changed, 59 insertions(+), 22 deletions(-) diff --git a/src/apprt/gtk.zig b/src/apprt/gtk.zig index c8d8c5142..258da9970 100644 --- a/src/apprt/gtk.zig +++ b/src/apprt/gtk.zig @@ -30,6 +30,16 @@ pub const App = struct { )) orelse return error.GtkInitFailed; errdefer c.g_object_unref(app); + // Setup our callbacks + _ = c.g_signal_connect_data( + app, + "activate", + c.G_CALLBACK(&activate), + null, + null, + c.G_CONNECT_DEFAULT, + ); + // We don't use g_application_run, we want to manually control the // loop so we have to do the same things the run function does: // https://github.com/GNOME/glib/blob/a8e8b742e7926e33eb635a8edceac74cf239d6ed/gio/gapplication.c#L2533 @@ -51,6 +61,9 @@ pub const App = struct { return error.GtkApplicationRegisterFailed; } + // This just calls the "activate" signal but its part of the normal + // startup routine so we just call it: + // https://gitlab.gnome.org/GNOME/glib/-/blob/bd2ccc2f69ecfd78ca3f34ab59e42e2b462bad65/gio/gapplication.c#L2302 c.g_application_activate(gapp); return .{ .app = app, .ctx = ctx }; @@ -71,6 +84,12 @@ pub const App = struct { pub fn wait(self: App) !void { _ = c.g_main_context_iteration(self.ctx, 1); } + + fn activate(app: *c.GtkApplication, ud: ?*anyopaque) callconv(.C) void { + _ = app; + _ = ud; + log.debug("application activated", .{}); + } }; pub const Window = struct { diff --git a/src/main.zig b/src/main.zig index b880c2116..a29f15057 100644 --- a/src/main.zig +++ b/src/main.zig @@ -101,9 +101,8 @@ pub fn main() !void { // Run our app with a single initial window to start. var app = try App.create(alloc, .{}, &config); defer app.destroy(); - try app.run(); - if (build_config.app_runtime == .gtk) return; _ = try app.newWindow(.{}); + try app.run(); } // Required by tracy/tracy.zig to enable/disable tracy support. diff --git a/src/renderer/OpenGL.zig b/src/renderer/OpenGL.zig index 2f100e7c3..f631ec81e 100644 --- a/src/renderer/OpenGL.zig +++ b/src/renderer/OpenGL.zig @@ -420,34 +420,53 @@ 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 - // ensures that the context switches over to our thread. Important: - // the prior thread MUST have detached the context prior to calling - // this entrypoint. - glfw.makeContextCurrent(win.window); - errdefer glfw.makeContextCurrent(null); - glfw.swapInterval(1); + switch (apprt.runtime) { + else => @compileError("unsupported app runtime for OpenGL"), - // Load OpenGL bindings. This API is context-aware so this sets - // a threadlocal context for these pointers. - const version = try gl.glad.load(&glfw.getProcAddress); - errdefer gl.glad.unload(); - log.info("loaded OpenGL {}.{}", .{ - gl.glad.versionMajor(@intCast(c_uint, version)), - gl.glad.versionMinor(@intCast(c_uint, version)), - }); + apprt.gtk => { + // GTK doesn't support threaded OpenGL operations as far as I can + // tell, so we use the renderer thread to setup all the state + // but then do the actual draws and texture syncs and all that + // on the main thread. As such, we don't do anything here. + }, + + apprt.glfw => { + // We need to make the OpenGL context current. OpenGL requires + // that a single thread own the a single OpenGL context (if any). This + // ensures that the context switches over to our thread. Important: + // the prior thread MUST have detached the context prior to calling + // this entrypoint. + glfw.makeContextCurrent(win.window); + errdefer glfw.makeContextCurrent(null); + glfw.swapInterval(1); + + // Load OpenGL bindings. This API is context-aware so this sets + // a threadlocal context for these pointers. + const version = try gl.glad.load(&glfw.getProcAddress); + errdefer gl.glad.unload(); + log.info("loaded OpenGL {}.{}", .{ + gl.glad.versionMajor(@intCast(c_uint, version)), + gl.glad.versionMinor(@intCast(c_uint, version)), + }); + }, + } } /// Callback called by renderer.Thread when it exits. pub fn threadExit(self: *const OpenGL) void { _ = self; - if (apprt.runtime == apprt.gtk) @panic("TODO"); - gl.glad.unload(); - glfw.makeContextCurrent(null); + switch (apprt.runtime) { + else => @compileError("unsupported app runtime for OpenGL"), + + apprt.gtk => {}, + + apprt.glfw => { + gl.glad.unload(); + glfw.makeContextCurrent(null); + }, + } } /// Callback when the focus changes for the terminal this is rendering. From 807c7fc64d488085de2011ba40069b8f0252959c Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 21 Feb 2023 14:03:20 -0800 Subject: [PATCH 04/37] opengl: support loading global gl functions --- src/renderer/opengl/glad.zig | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/renderer/opengl/glad.zig b/src/renderer/opengl/glad.zig index 3ee52431e..e1e763da6 100644 --- a/src/renderer/opengl/glad.zig +++ b/src/renderer/opengl/glad.zig @@ -23,6 +23,10 @@ pub fn load(getProcAddress: anytype) !c_int { getProcAddress, )), + // null proc address means that we are just loading the globally + // pointed gl functions + @TypeOf(null) => c.gladLoaderLoadGLContext(&context), + // try as-is. If this introduces a compiler error, then add a new case. else => c.gladLoadGLContext(&context, getProcAddress), }; From 3d8c62c41ff673a5156388863db8f70680d3e05e Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 22 Feb 2023 12:24:22 -0800 Subject: [PATCH 05/37] 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. From fbe35c226bf354d7c256d2bb5970f434d567889a Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 22 Feb 2023 14:37:37 -0800 Subject: [PATCH 06/37] Integrating new surface --- src/App.zig | 169 +-- src/DevMode.zig | 4 +- src/Surface.zig | 1635 +++++++++++++++++++++++++ src/apprt.zig | 3 +- src/apprt/{Window.zig => Surface.zig} | 10 +- src/apprt/glfw.zig | 100 +- src/main.zig | 1 + src/renderer/OpenGL.zig | 27 +- src/renderer/Options.zig | 4 +- src/renderer/Thread.zig | 12 +- src/termio/Exec.zig | 8 +- src/termio/Options.zig | 4 +- 12 files changed, 1831 insertions(+), 146 deletions(-) create mode 100644 src/Surface.zig rename src/apprt/{Window.zig => Surface.zig} (86%) diff --git a/src/App.zig b/src/App.zig index 4f8fdc85a..6819d5059 100644 --- a/src/App.zig +++ b/src/App.zig @@ -9,7 +9,7 @@ const assert = std.debug.assert; const Allocator = std.mem.Allocator; const build_config = @import("build_config.zig"); const apprt = @import("apprt.zig"); -const Window = @import("Window.zig"); +const Surface = @import("Surface.zig"); const tracy = @import("tracy"); const input = @import("input.zig"); const Config = @import("config.zig").Config; @@ -22,7 +22,8 @@ const DevMode = @import("DevMode.zig"); const log = std.log.scoped(.app); -const WindowList = std.ArrayListUnmanaged(*Window); +const SurfaceList = std.ArrayListUnmanaged(*apprt.Surface); +const SurfacePool = std.heap.MemoryPool(apprt.Surface); /// The type used for sending messages to the app thread. pub const Mailbox = BlockingQueue(Message, 64); @@ -30,8 +31,14 @@ pub const Mailbox = BlockingQueue(Message, 64); /// General purpose allocator alloc: Allocator, -/// The list of windows that are currently open -windows: WindowList, +/// The list of surfaces that are currently active. +surfaces: SurfaceList, + +/// The memory pool to request surfaces. We use a memory pool because surfaces +/// typically require stable pointers due to runtime GUI callbacks. Centralizing +/// all the allocations in this pool makes it so that all our pools remain +/// close in memory. +surface_pool: SurfacePool, // The configuration for the app. config: *const Config, @@ -64,20 +71,23 @@ pub fn create( errdefer alloc.destroy(app); app.* = .{ .alloc = alloc, - .windows = .{}, + .surfaces = .{}, + .surface_pool = try SurfacePool.initPreheated(alloc, 2), .config = config, .mailbox = mailbox, .quit = false, }; - errdefer app.windows.deinit(alloc); + errdefer app.surfaces.deinit(alloc); + errdefer app.surface_pool.deinit(); return app; } pub fn destroy(self: *App) void { - // Clean up all our windows - for (self.windows.items) |window| window.destroy(); - self.windows.deinit(self.alloc); + // Clean up all our surfaces + for (self.surfaces.items) |surface| surface.deinit(); + self.surfaces.deinit(self.alloc); + self.surface_pool.deinit(); self.mailbox.destroy(self.alloc); self.alloc.destroy(self); @@ -94,13 +104,14 @@ pub fn wakeup(self: App) void { /// /// 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 surfaces are closing, destroy them var i: usize = 0; - while (i < self.windows.items.len) { - const window = self.windows.items[i]; - if (window.shouldClose()) { - window.destroy(); - _ = self.windows.swapRemove(i); + while (i < self.surfaces.items.len) { + const surface = self.surfaces.items[i]; + if (surface.shouldClose()) { + surface.deinit(); + _ = self.surfaces.swapRemove(i); + self.surface_pool.destroy(surface); continue; } @@ -110,52 +121,56 @@ pub fn tick(self: *App, rt_app: *apprt.runtime.App) !bool { // 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; + // We quit if our quit flag is on or if we have closed all surfaces. + return self.quit or self.surfaces.items.len == 0; } -/// Create a new window. This can be called only on the main thread. This -/// can be called prior to ever running the app loop. -pub fn newWindow(self: *App, msg: Message.NewWindow) !*Window { - var window = try Window.create(self.alloc, self, self.config, msg.runtime); - errdefer window.destroy(); +/// Add an initialized surface. This is really only for the runtime +/// implementations to call and should NOT be called by general app users. +/// The surface must be from the pool. +pub fn addSurface(self: *App, rt_surface: *apprt.Surface) !void { + try self.surfaces.append(self.alloc, rt_surface); +} - try self.windows.append(self.alloc, window); - errdefer _ = self.windows.pop(); - - // Set initial font size if given - if (msg.font_size) |size| window.setFontSize(size); - - return window; +/// Delete the surface from the known surface list. This will NOT call the +/// destructor or free the memory. +pub fn deleteSurface(self: *App, rt_surface: *apprt.Surface) void { + var i: usize = 0; + while (i < self.surfaces.items.len) { + if (self.surfaces.items[i] == rt_surface) { + _ = self.surfaces.swapRemove(i); + } + } } /// Close a window and free all resources associated with it. This can /// only be called from the main thread. -pub fn closeWindow(self: *App, window: *Window) void { - var i: usize = 0; - while (i < self.windows.items.len) { - const current = self.windows.items[i]; - if (window == current) { - window.destroy(); - _ = self.windows.swapRemove(i); - return; - } - - i += 1; - } -} +// pub fn closeWindow(self: *App, window: *Window) void { +// var i: usize = 0; +// while (i < self.surfaces.items.len) { +// const current = self.surfaces.items[i]; +// if (window == current) { +// window.destroy(); +// _ = self.surfaces.swapRemove(i); +// return; +// } +// +// i += 1; +// } +// } /// Drain the mailbox. 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) { - .new_window => |msg| _ = try self.newWindow(msg), + .new_window => |msg| { + _ = msg; // TODO + try rt_app.newWindow(); + }, .new_tab => |msg| try self.newTab(msg), .quit => try self.setQuit(), - .window_message => |msg| try self.windowMessage(msg.window, msg.message), + .surface_message => |msg| try self.surfaceMessage(msg.surface, msg.message), } } } @@ -180,7 +195,7 @@ fn newTab(self: *App, msg: Message.NewWindow) !void { }; // If the parent was closed prior to us handling the message, we do nothing. - if (!self.hasWindow(parent)) { + if (!self.hasSurface(parent)) { log.warn("new_tab parent is gone, not launching a new tab", .{}); return; } @@ -197,28 +212,28 @@ fn setQuit(self: *App) !void { if (self.quit) return; self.quit = true; - // Mark that all our windows should close - for (self.windows.items) |window| { - window.window.setShouldClose(); + // Mark that all our surfaces should close + for (self.surfaces.items) |surface| { + surface.setShouldClose(); } } /// Handle a window message -fn windowMessage(self: *App, win: *Window, msg: Window.Message) !void { +fn surfaceMessage(self: *App, surface: *Surface, msg: apprt.surface.Message) !void { // We want to ensure our window is still active. Window messages // are quite rare and we normally don't have many windows so we do // a simple linear search here. - if (self.hasWindow(win)) { - try win.handleMessage(msg); + if (self.hasSurface(surface)) { + try surface.handleMessage(msg); } // Window was not found, it probably quit before we handled the message. // Not a problem. } -fn hasWindow(self: *App, win: *Window) bool { - for (self.windows.items) |window| { - if (window == win) return true; +fn hasSurface(self: *App, surface: *Surface) bool { + for (self.surfaces.items) |v| { + if (&v.core_surface == surface) return true; } return false; @@ -237,18 +252,18 @@ pub const Message = union(enum) { /// Quit quit: void, - /// A message for a specific window - window_message: struct { - window: *Window, - message: Window.Message, + /// A message for a specific surface. + surface_message: struct { + surface: *Surface, + message: apprt.surface.Message, }, const NewWindow = struct { /// Runtime-specific window options. - runtime: apprt.runtime.Window.Options = .{}, + runtime: apprt.runtime.Surface.Options = .{}, - /// The parent window, only used for new tabs. - parent: ?*Window = null, + /// The parent surface, only used for new tabs. + parent: ?*Surface = null, /// The font size to create the window with or null to default to /// the configuration amount. @@ -332,7 +347,7 @@ pub const CAPI = struct { export fn ghostty_surface_new( app: *App, opts: *const apprt.runtime.Window.Options, - ) ?*Window { + ) ?*Surface { return surface_new_(app, opts) catch |err| { log.err("error initializing surface err={}", .{err}); return null; @@ -342,46 +357,46 @@ pub const CAPI = struct { fn surface_new_( app: *App, opts: *const apprt.runtime.Window.Options, - ) !*Window { + ) !*Surface { const w = try app.newWindow(.{ .runtime = opts.*, }); return w; } - export fn ghostty_surface_free(ptr: ?*Window) void { + export fn ghostty_surface_free(ptr: ?*Surface) void { if (ptr) |v| v.app.closeWindow(v); } /// Returns the app associated with a surface. - export fn ghostty_surface_app(win: *Window) *App { + export fn ghostty_surface_app(win: *Surface) *App { return win.app; } /// Tell the surface that it needs to schedule a render - export fn ghostty_surface_refresh(win: *Window) void { + export fn ghostty_surface_refresh(win: *Surface) void { win.window.refresh(); } /// Update the size of a surface. This will trigger resize notifications /// to the pty and the renderer. - export fn ghostty_surface_set_size(win: *Window, w: u32, h: u32) void { + export fn ghostty_surface_set_size(win: *Surface, w: u32, h: u32) void { win.window.updateSize(w, h); } /// Update the content scale of the surface. - export fn ghostty_surface_set_content_scale(win: *Window, x: f64, y: f64) void { + export fn ghostty_surface_set_content_scale(win: *Surface, x: f64, y: f64) void { win.window.updateContentScale(x, y); } /// Update the focused state of a surface. - export fn ghostty_surface_set_focus(win: *Window, focused: bool) void { + export fn ghostty_surface_set_focus(win: *Surface, focused: bool) void { win.window.focusCallback(focused); } /// Tell the surface that it needs to schedule a render export fn ghostty_surface_key( - win: *Window, + win: *Surface, action: input.Action, key: input.Key, mods: c_int, @@ -394,13 +409,13 @@ pub const CAPI = struct { } /// Tell the surface that it needs to schedule a render - export fn ghostty_surface_char(win: *Window, codepoint: u32) void { + export fn ghostty_surface_char(win: *Surface, codepoint: u32) void { win.window.charCallback(codepoint); } /// Tell the surface that it needs to schedule a render export fn ghostty_surface_mouse_button( - win: *Window, + win: *Surface, action: input.MouseButtonState, button: input.MouseButton, mods: c_int, @@ -413,15 +428,15 @@ pub const CAPI = struct { } /// Update the mouse position within the view. - export fn ghostty_surface_mouse_pos(win: *Window, x: f64, y: f64) void { + export fn ghostty_surface_mouse_pos(win: *Surface, x: f64, y: f64) void { win.window.cursorPosCallback(x, y); } - export fn ghostty_surface_mouse_scroll(win: *Window, x: f64, y: f64) void { + export fn ghostty_surface_mouse_scroll(win: *Surface, x: f64, y: f64) void { win.window.scrollCallback(x, y); } - export fn ghostty_surface_ime_point(win: *Window, x: *f64, y: *f64) void { + export fn ghostty_surface_ime_point(win: *Surface, x: *f64, y: *f64) void { const pos = win.imePoint(); x.* = pos.x; y.* = pos.y; diff --git a/src/DevMode.zig b/src/DevMode.zig index 2b037ce2a..931c04dea 100644 --- a/src/DevMode.zig +++ b/src/DevMode.zig @@ -10,7 +10,7 @@ const Allocator = std.mem.Allocator; const assert = std.debug.assert; const font = @import("font/main.zig"); -const Window = @import("Window.zig"); +const Surface = @import("Surface.zig"); const renderer = @import("renderer.zig"); const Config = @import("config.zig").Config; @@ -30,7 +30,7 @@ visible: bool = false, config: ?*const Config = null, /// The window we're tracking. -window: ?*Window = null, +window: ?*Surface = null, /// Update the state associated with the dev mode. This should generally /// only be called paired with a render since it otherwise wastes CPU diff --git a/src/Surface.zig b/src/Surface.zig new file mode 100644 index 000000000..04da5d0a8 --- /dev/null +++ b/src/Surface.zig @@ -0,0 +1,1635 @@ +//! Surface represents a single terminal "surface". A terminal surface is +//! a minimal "widget" where the terminal is drawn and responds to events +//! such as keyboard and mouse. Each surface also creates and owns its pty +//! session. +//! +//! The word "surface" is used because it is left to the higher level +//! application runtime to determine if the surface is a window, a tab, +//! a split, a preview pane in a larger window, etc. This struct doesn't care: +//! it just draws and responds to events. The events come from the application +//! runtime so the runtime can determine when and how those are delivered +//! (i.e. with focus, without focus, and so on). +const Surface = @This(); + +const apprt = @import("apprt.zig"); +pub const Mailbox = apprt.surface.Mailbox; +pub const Message = apprt.surface.Message; + +const std = @import("std"); +const builtin = @import("builtin"); +const assert = std.debug.assert; +const Allocator = std.mem.Allocator; +const renderer = @import("renderer.zig"); +const termio = @import("termio.zig"); +const objc = @import("objc"); +const imgui = @import("imgui"); +const Pty = @import("Pty.zig"); +const font = @import("font/main.zig"); +const Command = @import("Command.zig"); +const trace = @import("tracy").trace; +const terminal = @import("terminal/main.zig"); +const Config = @import("config.zig").Config; +const input = @import("input.zig"); +const DevMode = @import("DevMode.zig"); +const App = @import("App.zig"); +const internal_os = @import("os/main.zig"); + +const log = std.log.scoped(.window); + +// The renderer implementation to use. +const Renderer = renderer.Renderer; + +/// Allocator +alloc: Allocator, + +/// The app that this window is a part of. +app: *App, + +/// The windowing system surface +rt_surface: *apprt.runtime.Surface, + +/// The font structures +font_lib: font.Library, +font_group: *font.GroupCache, +font_size: font.face.DesiredSize, + +/// Imgui context +imgui_ctx: if (DevMode.enabled) *imgui.Context else void, + +/// The renderer for this window. +renderer: Renderer, + +/// The render state +renderer_state: renderer.State, + +/// The renderer thread manager +renderer_thread: renderer.Thread, + +/// The actual thread +renderer_thr: std.Thread, + +/// Mouse state. +mouse: Mouse, +mouse_interval: u64, + +/// The terminal IO handler. +io: termio.Impl, +io_thread: termio.Thread, +io_thr: std.Thread, + +/// All the cached sizes since we need them at various times. +screen_size: renderer.ScreenSize, +grid_size: renderer.GridSize, +cell_size: renderer.CellSize, + +/// Explicit padding due to configuration +padding: renderer.Padding, + +/// The app configuration +config: *const Config, + +/// Set to true for a single GLFW key/char callback cycle to cause the +/// char callback to ignore. GLFW seems to always do key followed by char +/// callbacks so we abuse that here. This is to solve an issue where commands +/// like such as "control-v" will write a "v" even if they're intercepted. +ignore_char: bool = false, + +/// Mouse state for the window. +const Mouse = struct { + /// The last tracked mouse button state by button. + click_state: [input.MouseButton.max]input.MouseButtonState = .{.release} ** input.MouseButton.max, + + /// The last mods state when the last mouse button (whatever it was) was + /// pressed or release. + mods: input.Mods = .{}, + + /// The point at which the left mouse click happened. This is in screen + /// coordinates so that scrolling preserves the location. + left_click_point: terminal.point.ScreenPoint = .{}, + + /// The starting xpos/ypos of the left click. Note that if scrolling occurs, + /// these will point to different "cells", but the xpos/ypos will stay + /// stable during scrolling relative to the window. + left_click_xpos: f64 = 0, + left_click_ypos: f64 = 0, + + /// The count of clicks to count double and triple clicks and so on. + /// The left click time was the last time the left click was done. This + /// is always set on the first left click. + left_click_count: u8 = 0, + left_click_time: std.time.Instant = undefined, + + /// The last x/y sent for mouse reports. + event_point: terminal.point.Viewport = .{}, +}; + +/// Create a new surface. This must be called from the main thread. The +/// pointer to the memory for the surface must be provided and must be +/// stable due to interfacing with various callbacks. +pub fn init( + self: *Surface, + app: *App, + config: *const Config, + rt_surface: *apprt.runtime.Surface, +) !void { + const alloc = app.alloc; + + // Initialize our renderer with our initialized surface. + try Renderer.surfaceInit(rt_surface); + + // Determine our DPI configurations so we can properly configure + // font points to pixels and handle other high-DPI scaling factors. + const content_scale = try rt_surface.getContentScale(); + const x_dpi = content_scale.x * font.face.default_dpi; + const y_dpi = content_scale.y * font.face.default_dpi; + log.debug("xscale={} yscale={} xdpi={} ydpi={}", .{ + content_scale.x, + content_scale.y, + x_dpi, + y_dpi, + }); + + // The font size we desire along with the DPI determined for the surface + const font_size: font.face.DesiredSize = .{ + .points = config.@"font-size", + .xdpi = @floatToInt(u16, x_dpi), + .ydpi = @floatToInt(u16, y_dpi), + }; + + // Find all the fonts for this surface + // + // Future: we can share the font group amongst all surfaces to save + // some new surface init time and some memory. This will require making + // thread-safe changes to font structs. + var font_lib = try font.Library.init(); + errdefer font_lib.deinit(); + var font_group = try alloc.create(font.GroupCache); + errdefer alloc.destroy(font_group); + font_group.* = try font.GroupCache.init(alloc, group: { + var group = try font.Group.init(alloc, font_lib, font_size); + errdefer group.deinit(); + + // Search for fonts + if (font.Discover != void) { + var disco = font.Discover.init(); + group.discover = disco; + + if (config.@"font-family") |family| { + var disco_it = try disco.discover(.{ + .family = family, + .size = font_size.points, + }); + defer disco_it.deinit(); + if (try disco_it.next()) |face| { + log.info("font regular: {s}", .{try face.name()}); + try group.addFace(alloc, .regular, face); + } + } + if (config.@"font-family-bold") |family| { + var disco_it = try disco.discover(.{ + .family = family, + .size = font_size.points, + .bold = true, + }); + defer disco_it.deinit(); + if (try disco_it.next()) |face| { + log.info("font bold: {s}", .{try face.name()}); + try group.addFace(alloc, .bold, face); + } + } + if (config.@"font-family-italic") |family| { + var disco_it = try disco.discover(.{ + .family = family, + .size = font_size.points, + .italic = true, + }); + defer disco_it.deinit(); + if (try disco_it.next()) |face| { + log.info("font italic: {s}", .{try face.name()}); + try group.addFace(alloc, .italic, face); + } + } + if (config.@"font-family-bold-italic") |family| { + var disco_it = try disco.discover(.{ + .family = family, + .size = font_size.points, + .bold = true, + .italic = true, + }); + defer disco_it.deinit(); + if (try disco_it.next()) |face| { + log.info("font bold+italic: {s}", .{try face.name()}); + try group.addFace(alloc, .bold_italic, face); + } + } + } + + // Our built-in font will be used as a backup + try group.addFace( + alloc, + .regular, + font.DeferredFace.initLoaded(try font.Face.init(font_lib, face_ttf, font_size)), + ); + try group.addFace( + alloc, + .bold, + font.DeferredFace.initLoaded(try font.Face.init(font_lib, face_bold_ttf, font_size)), + ); + + // Emoji fallback. We don't include this on Mac since Mac is expected + // to always have the Apple Emoji available. + if (builtin.os.tag != .macos or font.Discover == void) { + try group.addFace( + alloc, + .regular, + font.DeferredFace.initLoaded(try font.Face.init(font_lib, face_emoji_ttf, font_size)), + ); + try group.addFace( + alloc, + .regular, + font.DeferredFace.initLoaded(try font.Face.init(font_lib, face_emoji_text_ttf, font_size)), + ); + } + + // If we're on Mac, then we try to use the Apple Emoji font for Emoji. + if (builtin.os.tag == .macos and font.Discover != void) { + var disco = font.Discover.init(); + defer disco.deinit(); + var disco_it = try disco.discover(.{ + .family = "Apple Color Emoji", + .size = font_size.points, + }); + defer disco_it.deinit(); + if (try disco_it.next()) |face| { + log.debug("font emoji: {s}", .{try face.name()}); + try group.addFace(alloc, .regular, face); + } + } + + break :group group; + }); + errdefer font_group.deinit(alloc); + + // Pre-calculate our initial cell size ourselves. + const cell_size = try renderer.CellSize.init(alloc, font_group); + + // Convert our padding from points to pixels + const padding_x = (@intToFloat(f32, config.@"window-padding-x") * x_dpi) / 72; + const padding_y = (@intToFloat(f32, config.@"window-padding-y") * y_dpi) / 72; + const padding: renderer.Padding = .{ + .top = padding_y, + .bottom = padding_y, + .right = padding_x, + .left = padding_x, + }; + + // Create our terminal grid with the initial window size + var renderer_impl = try Renderer.init(alloc, .{ + .config = config, + .font_group = font_group, + .padding = .{ + .explicit = padding, + .balance = config.@"window-padding-balance", + }, + .window_mailbox = .{ .window = self, .app = app.mailbox }, + }); + errdefer renderer_impl.deinit(); + + // Calculate our grid size based on known dimensions. + const window_size = try rt_surface.getSize(); + const screen_size: renderer.ScreenSize = .{ + .width = window_size.width, + .height = window_size.height, + }; + const grid_size = renderer.GridSize.init( + screen_size.subPadding(padding), + cell_size, + ); + + // The mutex used to protect our renderer state. + var mutex = try alloc.create(std.Thread.Mutex); + mutex.* = .{}; + errdefer alloc.destroy(mutex); + + // Create the renderer thread + var render_thread = try renderer.Thread.init( + alloc, + rt_surface, + &self.renderer, + &self.renderer_state, + ); + errdefer render_thread.deinit(); + + // Start our IO implementation + var io = try termio.Impl.init(alloc, .{ + .grid_size = grid_size, + .screen_size = screen_size, + .config = config, + .renderer_state = &self.renderer_state, + .renderer_wakeup = render_thread.wakeup, + .renderer_mailbox = render_thread.mailbox, + .window_mailbox = .{ .window = self, .app = app.mailbox }, + }); + errdefer io.deinit(); + + // Create the IO thread + var io_thread = try termio.Thread.init(alloc, &self.io); + errdefer io_thread.deinit(); + + // True if this window is hosting devmode. We only host devmode on + // the first window since imgui is not threadsafe. We need to do some + // work to make DevMode work with multiple threads. + const host_devmode = DevMode.enabled and DevMode.instance.window == null; + + self.* = .{ + .alloc = alloc, + .app = app, + .rt_surface = rt_surface, + .font_lib = font_lib, + .font_group = font_group, + .font_size = font_size, + .renderer = renderer_impl, + .renderer_thread = render_thread, + .renderer_state = .{ + .mutex = mutex, + .cursor = .{ + .style = .blinking_block, + .visible = true, + }, + .terminal = &self.io.terminal, + .devmode = if (!host_devmode) null else &DevMode.instance, + }, + .renderer_thr = undefined, + .mouse = .{}, + .mouse_interval = config.@"click-repeat-interval" * 1_000_000, // 500ms + .io = io, + .io_thread = io_thread, + .io_thr = undefined, + .screen_size = screen_size, + .grid_size = grid_size, + .cell_size = cell_size, + .padding = padding, + .config = config, + + .imgui_ctx = if (!DevMode.enabled) {} else try imgui.Context.create(), + }; + errdefer if (DevMode.enabled) self.imgui_ctx.destroy(); + + // Set a minimum size that is cols=10 h=4. This matches Mac's Terminal.app + // but is otherwise somewhat arbitrary. + try rt_surface.setSizeLimits(.{ + .width = @floatToInt(u32, cell_size.width * 10), + .height = @floatToInt(u32, cell_size.height * 4), + }, null); + + // Call our size callback which handles all our retina setup + // Note: this shouldn't be necessary and when we clean up the window + // init stuff we should get rid of this. But this is required because + // sizeCallback does retina-aware stuff we don't do here and don't want + // to duplicate. + try self.sizeCallback(window_size); + + // Load imgui. This must be done LAST because it has to be done after + // all our GLFW setup is complete. + if (DevMode.enabled and DevMode.instance.window == null) { + const dev_io = try imgui.IO.get(); + dev_io.cval().IniFilename = "ghostty_dev_mode.ini"; + + // Add our built-in fonts so it looks slightly better + const dev_atlas = @ptrCast(*imgui.FontAtlas, dev_io.cval().Fonts); + dev_atlas.addFontFromMemoryTTF( + face_ttf, + @intToFloat(f32, font_size.pixels()), + ); + + // Default dark style + const style = try imgui.Style.get(); + style.colorsDark(); + + // Add our window to the instance if it isn't set. + DevMode.instance.window = self; + + // Let our renderer setup + try renderer_impl.initDevMode(rt_surface); + } + + // Give the renderer one more opportunity to finalize any window + // setup on the main thread prior to spinning up the rendering thread. + try renderer_impl.finalizeSurfaceInit(rt_surface); + + // Start our renderer thread + self.renderer_thr = try std.Thread.spawn( + .{}, + renderer.Thread.threadMain, + .{&self.renderer_thread}, + ); + self.renderer_thr.setName("renderer") catch {}; + + // Start our IO thread + self.io_thr = try std.Thread.spawn( + .{}, + termio.Thread.threadMain, + .{&self.io_thread}, + ); + self.io_thr.setName("io") catch {}; +} + +pub fn destroy(self: *Surface) void { + // Stop rendering thread + { + self.renderer_thread.stop.notify() catch |err| + log.err("error notifying renderer thread to stop, may stall err={}", .{err}); + self.renderer_thr.join(); + + // We need to become the active rendering thread again + self.renderer.threadEnter(self.rt_surface) catch unreachable; + + // If we are devmode-owning, clean that up. + if (DevMode.enabled and DevMode.instance.window == self) { + // Let our renderer clean up + self.renderer.deinitDevMode(); + + // Clear the window + DevMode.instance.window = null; + + // Uninitialize imgui + self.imgui_ctx.destroy(); + } + } + + // Stop our IO thread + { + self.io_thread.stop.notify() catch |err| + log.err("error notifying io thread to stop, may stall err={}", .{err}); + self.io_thr.join(); + } + + // We need to deinit AFTER everything is stopped, since there are + // shared values between the two threads. + self.renderer_thread.deinit(); + self.renderer.deinit(); + self.io_thread.deinit(); + self.io.deinit(); + + self.rt_surface.deinit(); + + self.font_group.deinit(self.alloc); + self.font_lib.deinit(); + self.alloc.destroy(self.font_group); + + self.alloc.destroy(self.renderer_state.mutex); + + self.alloc.destroy(self); +} + +/// Called from the app thread to handle mailbox messages to our specific +/// window. +pub fn handleMessage(self: *Surface, msg: Message) !void { + switch (msg) { + .set_title => |*v| { + // The ptrCast just gets sliceTo to return the proper type. + // We know that our title should end in 0. + const slice = std.mem.sliceTo(@ptrCast([*:0]const u8, v), 0); + log.debug("changing title \"{s}\"", .{slice}); + try self.rt_surface.setTitle(slice); + }, + + .cell_size => |size| try self.setCellSize(size), + + .clipboard_read => |kind| try self.clipboardRead(kind), + + .clipboard_write => |req| switch (req) { + .small => |v| try self.clipboardWrite(v.data[0..v.len]), + .stable => |v| try self.clipboardWrite(v), + .alloc => |v| { + defer v.alloc.free(v.data); + try self.clipboardWrite(v.data); + }, + }, + } +} + +/// Returns the x/y coordinate of where the IME (Input Method Editor) +/// keyboard should be rendered. +pub fn imePoint(self: *const Surface) apprt.IMEPos { + self.renderer_state.mutex.lock(); + const cursor = self.renderer_state.terminal.screen.cursor; + self.renderer_state.mutex.unlock(); + + // TODO: need to handle when scrolling and the cursor is not + // in the visible portion of the screen. + + // Our sizes are all scaled so we need to send the unscaled values back. + const content_scale = self.rt_surface.getContentScale() catch .{ .x = 1, .y = 1 }; + + const x: f64 = x: { + // Simple x * cell width gives the top-left corner + var x: f64 = @floatCast(f64, @intToFloat(f32, cursor.x) * self.cell_size.width); + + // We want the midpoint + x += self.cell_size.width / 2; + + // And scale it + x /= content_scale.x; + + break :x x; + }; + + const y: f64 = y: { + // Simple x * cell width gives the top-left corner + var y: f64 = @floatCast(f64, @intToFloat(f32, cursor.y) * self.cell_size.height); + + // We want the bottom + y += self.cell_size.height; + + // And scale it + y /= content_scale.y; + + break :y y; + }; + + return .{ .x = x, .y = y }; +} + +fn clipboardRead(self: *const Surface, kind: u8) !void { + if (!self.config.@"clipboard-read") { + log.info("application attempted to read clipboard, but 'clipboard-read' setting is off", .{}); + return; + } + + const data = self.rt_surface.getClipboardString() catch |err| { + log.warn("error reading clipboard: {}", .{err}); + return; + }; + + // Even if the clipboard data is empty we reply, since presumably + // the client app is expecting a reply. We first allocate our buffer. + // This must hold the base64 encoded data PLUS the OSC code surrounding it. + const enc = std.base64.standard.Encoder; + const size = enc.calcSize(data.len); + var buf = try self.alloc.alloc(u8, size + 9); // const for OSC + defer self.alloc.free(buf); + + // Wrap our data with the OSC code + const prefix = try std.fmt.bufPrint(buf, "\x1b]52;{c};", .{kind}); + assert(prefix.len == 7); + buf[buf.len - 2] = '\x1b'; + buf[buf.len - 1] = '\\'; + + // Do the base64 encoding + const encoded = enc.encode(buf[prefix.len..], data); + assert(encoded.len == size); + + _ = self.io_thread.mailbox.push(try termio.Message.writeReq( + self.alloc, + buf, + ), .{ .forever = {} }); + self.io_thread.wakeup.notify() catch {}; +} + +fn clipboardWrite(self: *const Surface, data: []const u8) !void { + if (!self.config.@"clipboard-write") { + log.info("application attempted to write clipboard, but 'clipboard-write' setting is off", .{}); + return; + } + + const dec = std.base64.standard.Decoder; + + // Build buffer + const size = try dec.calcSizeForSlice(data); + var buf = try self.alloc.allocSentinel(u8, size, 0); + defer self.alloc.free(buf); + buf[buf.len] = 0; + + // Decode + try dec.decode(buf, data); + assert(buf[buf.len] == 0); + + self.rt_surface.setClipboardString(buf) catch |err| { + log.err("error setting clipboard string err={}", .{err}); + return; + }; +} + +/// Change the cell size for the terminal grid. This can happen as +/// a result of changing the font size at runtime. +fn setCellSize(self: *Surface, size: renderer.CellSize) !void { + // Update our new cell size for future calcs + self.cell_size = size; + + // Update our grid_size + self.grid_size = renderer.GridSize.init( + self.screen_size.subPadding(self.padding), + self.cell_size, + ); + + // Notify the terminal + _ = self.io_thread.mailbox.push(.{ + .resize = .{ + .grid_size = self.grid_size, + .screen_size = self.screen_size, + .padding = self.padding, + }, + }, .{ .forever = {} }); + self.io_thread.wakeup.notify() catch {}; +} + +/// Change the font size. +/// +/// This can only be called from the main thread. +pub fn setFontSize(self: *Surface, size: font.face.DesiredSize) void { + // Update our font size so future changes work + self.font_size = size; + + // Notify our render thread of the font size. This triggers everything else. + _ = self.renderer_thread.mailbox.push(.{ + .font_size = size, + }, .{ .forever = {} }); + + // Schedule render which also drains our mailbox + self.queueRender() catch unreachable; +} + +/// This queues a render operation with the renderer thread. The render +/// isn't guaranteed to happen immediately but it will happen as soon as +/// practical. +fn queueRender(self: *const Surface) !void { + try self.renderer_thread.wakeup.notify(); +} + +pub fn sizeCallback(self: *Surface, size: apprt.WindowSize) !void { + const tracy = trace(@src()); + defer tracy.end(); + + // TODO: if our screen size didn't change, then we should avoid the + // overhead of inter-thread communication + + // Save our screen size + self.screen_size = .{ + .width = size.width, + .height = size.height, + }; + + // Recalculate our grid size + self.grid_size = renderer.GridSize.init( + self.screen_size.subPadding(self.padding), + self.cell_size, + ); + if (self.grid_size.columns < 5 and (self.padding.left > 0 or self.padding.right > 0)) { + log.warn("WARNING: very small terminal grid detected with padding " ++ + "set. Is your padding reasonable?", .{}); + } + if (self.grid_size.rows < 2 and (self.padding.top > 0 or self.padding.bottom > 0)) { + log.warn("WARNING: very small terminal grid detected with padding " ++ + "set. Is your padding reasonable?", .{}); + } + + // Mail the renderer + _ = self.renderer_thread.mailbox.push(.{ + .screen_size = self.screen_size, + }, .{ .forever = {} }); + try self.queueRender(); + + // Mail the IO thread + _ = self.io_thread.mailbox.push(.{ + .resize = .{ + .grid_size = self.grid_size, + .screen_size = self.screen_size, + .padding = self.padding, + }, + }, .{ .forever = {} }); + try self.io_thread.wakeup.notify(); +} + +pub fn charCallback(self: *Surface, codepoint: u21) !void { + const tracy = trace(@src()); + defer tracy.end(); + + // Dev Mode + if (DevMode.enabled and DevMode.instance.visible) { + // If the event was handled by imgui, ignore it. + if (imgui.IO.get()) |io| { + if (io.cval().WantCaptureKeyboard) { + try self.queueRender(); + } + } else |_| {} + } + + // Ignore if requested. See field docs for more information. + if (self.ignore_char) { + self.ignore_char = false; + return; + } + + // Critical area + { + self.renderer_state.mutex.lock(); + defer self.renderer_state.mutex.unlock(); + + // Clear the selction if we have one. + if (self.io.terminal.selection != null) { + self.io.terminal.selection = null; + try self.queueRender(); + } + + // We want to scroll to the bottom + // TODO: detect if we're at the bottom to avoid the render call here. + try self.io.terminal.scrollViewport(.{ .bottom = {} }); + } + + // Ask our IO thread to write the data + var data: termio.Message.WriteReq.Small.Array = undefined; + const len = try std.unicode.utf8Encode(codepoint, &data); + _ = self.io_thread.mailbox.push(.{ + .write_small = .{ + .data = data, + .len = len, + }, + }, .{ .forever = {} }); + + // After sending all our messages we have to notify our IO thread + try self.io_thread.wakeup.notify(); +} + +pub fn keyCallback( + self: *Surface, + action: input.Action, + key: input.Key, + mods: input.Mods, +) !void { + const tracy = trace(@src()); + defer tracy.end(); + + // Dev Mode + if (DevMode.enabled and DevMode.instance.visible) { + // If the event was handled by imgui, ignore it. + if (imgui.IO.get()) |io| { + if (io.cval().WantCaptureKeyboard) { + try self.queueRender(); + } + } else |_| {} + } + + // Reset the ignore char setting. If we didn't handle the char + // by here, we aren't going to get it so we just reset this. + self.ignore_char = false; + + if (action == .press or action == .repeat) { + const trigger: input.Binding.Trigger = .{ + .mods = mods, + .key = key, + }; + + //log.warn("BINDING TRIGGER={}", .{trigger}); + if (self.config.keybind.set.get(trigger)) |binding_action| { + //log.warn("BINDING ACTION={}", .{binding_action}); + + switch (binding_action) { + .unbind => unreachable, + .ignore => {}, + + .csi => |data| { + _ = self.io_thread.mailbox.push(.{ + .write_stable = "\x1B[", + }, .{ .forever = {} }); + _ = self.io_thread.mailbox.push(.{ + .write_stable = data, + }, .{ .forever = {} }); + try self.io_thread.wakeup.notify(); + }, + + .cursor_key => |ck| { + // We send a different sequence depending on if we're + // in cursor keys mode. We're in "normal" mode if cursor + // keys mdoe is NOT set. + const normal = normal: { + self.renderer_state.mutex.lock(); + defer self.renderer_state.mutex.unlock(); + break :normal !self.io.terminal.modes.cursor_keys; + }; + + if (normal) { + _ = self.io_thread.mailbox.push(.{ + .write_stable = ck.normal, + }, .{ .forever = {} }); + } else { + _ = self.io_thread.mailbox.push(.{ + .write_stable = ck.application, + }, .{ .forever = {} }); + } + + try self.io_thread.wakeup.notify(); + }, + + .copy_to_clipboard => { + // We can read from the renderer state without holding + // the lock because only we will write to this field. + if (self.io.terminal.selection) |sel| { + var buf = self.io.terminal.screen.selectionString( + self.alloc, + sel, + self.config.@"clipboard-trim-trailing-spaces", + ) catch |err| { + log.err("error reading selection string err={}", .{err}); + return; + }; + defer self.alloc.free(buf); + + self.rt_surface.setClipboardString(buf) catch |err| { + log.err("error setting clipboard string err={}", .{err}); + return; + }; + } + }, + + .paste_from_clipboard => { + const data = self.rt_surface.getClipboardString() catch |err| { + log.warn("error reading clipboard: {}", .{err}); + return; + }; + + if (data.len > 0) { + const bracketed = bracketed: { + self.renderer_state.mutex.lock(); + defer self.renderer_state.mutex.unlock(); + break :bracketed self.io.terminal.modes.bracketed_paste; + }; + + if (bracketed) { + _ = self.io_thread.mailbox.push(.{ + .write_stable = "\x1B[200~", + }, .{ .forever = {} }); + } + + _ = self.io_thread.mailbox.push(try termio.Message.writeReq( + self.alloc, + data, + ), .{ .forever = {} }); + + if (bracketed) { + _ = self.io_thread.mailbox.push(.{ + .write_stable = "\x1B[201~", + }, .{ .forever = {} }); + } + + try self.io_thread.wakeup.notify(); + } + }, + + .increase_font_size => |delta| { + log.debug("increase font size={}", .{delta}); + + var size = self.font_size; + size.points +|= delta; + self.setFontSize(size); + }, + + .decrease_font_size => |delta| { + log.debug("decrease font size={}", .{delta}); + + var size = self.font_size; + size.points = @max(1, size.points -| delta); + self.setFontSize(size); + }, + + .reset_font_size => { + log.debug("reset font size", .{}); + + var size = self.font_size; + size.points = self.config.@"font-size"; + self.setFontSize(size); + }, + + .toggle_dev_mode => if (DevMode.enabled) { + DevMode.instance.visible = !DevMode.instance.visible; + try self.queueRender(); + } else log.warn("dev mode was not compiled into this binary", .{}), + + .new_window => { + _ = self.app.mailbox.push(.{ + .new_window = .{ + .font_size = if (self.config.@"window-inherit-font-size") + self.font_size + else + null, + }, + }, .{ .instant = {} }); + self.app.wakeup(); + }, + + .new_tab => { + _ = self.app.mailbox.push(.{ + .new_tab = .{ + .parent = self, + + .font_size = if (self.config.@"window-inherit-font-size") + self.font_size + else + null, + }, + }, .{ .instant = {} }); + self.app.wakeup(); + }, + + .close_window => self.rt_surface.setShouldClose(), + + .quit => { + _ = self.app.mailbox.push(.{ + .quit = {}, + }, .{ .instant = {} }); + self.app.wakeup(); + }, + } + + // Bindings always result in us ignoring the char if printable + self.ignore_char = true; + + // No matter what, if there is a binding then we are done. + return; + } + + // Handle non-printables + const char: u8 = char: { + const mods_int = @bitCast(u8, mods); + const ctrl_only = @bitCast(u8, input.Mods{ .ctrl = true }); + + // If we're only pressing control, check if this is a character + // we convert to a non-printable. + if (mods_int == ctrl_only) { + const val: u8 = switch (key) { + .a => 0x01, + .b => 0x02, + .c => 0x03, + .d => 0x04, + .e => 0x05, + .f => 0x06, + .g => 0x07, + .h => 0x08, + .i => 0x09, + .j => 0x0A, + .k => 0x0B, + .l => 0x0C, + .m => 0x0D, + .n => 0x0E, + .o => 0x0F, + .p => 0x10, + .q => 0x11, + .r => 0x12, + .s => 0x13, + .t => 0x14, + .u => 0x15, + .v => 0x16, + .w => 0x17, + .x => 0x18, + .y => 0x19, + .z => 0x1A, + else => 0, + }; + + if (val > 0) break :char val; + } + + // Otherwise, we don't care what modifiers we press we do this. + break :char @as(u8, switch (key) { + .backspace => 0x7F, + .enter => '\r', + .tab => '\t', + .escape => 0x1B, + else => 0, + }); + }; + if (char > 0) { + // Ask our IO thread to write the data + var data: termio.Message.WriteReq.Small.Array = undefined; + data[0] = @intCast(u8, char); + _ = self.io_thread.mailbox.push(.{ + .write_small = .{ + .data = data, + .len = 1, + }, + }, .{ .forever = {} }); + + // After sending all our messages we have to notify our IO thread + try self.io_thread.wakeup.notify(); + } + } +} + +pub fn focusCallback(self: *Surface, focused: bool) !void { + // Notify our render thread of the new state + _ = self.renderer_thread.mailbox.push(.{ + .focus = focused, + }, .{ .forever = {} }); + + // Schedule render which also drains our mailbox + try self.queueRender(); +} + +pub fn refreshCallback(self: *Surface) !void { + // The point of this callback is to schedule a render, so do that. + try self.queueRender(); +} + +pub fn scrollCallback(self: *Surface, xoff: f64, yoff: f64) !void { + const tracy = trace(@src()); + defer tracy.end(); + + // If our dev mode window is visible then we always schedule a render on + // cursor move because the cursor might touch our windows. + if (DevMode.enabled and DevMode.instance.visible) { + try self.queueRender(); + + // If the mouse event was handled by imgui, ignore it. + if (imgui.IO.get()) |io| { + if (io.cval().WantCaptureMouse) return; + } else |_| {} + } + + //log.info("SCROLL: {} {}", .{ xoff, yoff }); + _ = xoff; + + // Positive is up + const sign: isize = if (yoff > 0) -1 else 1; + const delta: isize = sign * @max(@divFloor(self.grid_size.rows, 15), 1); + log.info("scroll: delta={}", .{delta}); + + { + self.renderer_state.mutex.lock(); + defer self.renderer_state.mutex.unlock(); + + // Modify our viewport, this requires a lock since it affects rendering + try self.io.terminal.scrollViewport(.{ .delta = delta }); + + // If we're scrolling up or down, then send a mouse event. This requires + // a lock since we read terminal state. + if (yoff != 0) { + const pos = try self.rt_surface.getCursorPos(); + try self.mouseReport(if (yoff < 0) .five else .four, .press, self.mouse.mods, pos); + } + } + + try self.queueRender(); +} + +/// The type of action to report for a mouse event. +const MouseReportAction = enum { press, release, motion }; + +fn mouseReport( + self: *Surface, + button: ?input.MouseButton, + action: MouseReportAction, + mods: input.Mods, + pos: apprt.CursorPos, +) !void { + // TODO: posToViewport currently clamps to the window boundary, + // do we want to not report mouse events at all outside the window? + + // Depending on the event, we may do nothing at all. + switch (self.io.terminal.modes.mouse_event) { + .none => return, + + // X10 only reports clicks with mouse button 1, 2, 3. We verify + // the button later. + .x10 => if (action != .press or + button == null or + !(button.? == .left or + button.? == .right or + button.? == .middle)) return, + + // Doesn't report motion + .normal => if (action == .motion) return, + + // Button must be pressed + .button => if (button == null) return, + + // Everything + .any => {}, + } + + // This format reports X/Y + const viewport_point = self.posToViewport(pos.x, pos.y); + + // Record our new point + self.mouse.event_point = viewport_point; + + // Get the code we'll actually write + const button_code: u8 = code: { + var acc: u8 = 0; + + // Determine our initial button value + if (button == null) { + // Null button means motion without a button pressed + acc = 3; + } else if (action == .release and self.io.terminal.modes.mouse_format != .sgr) { + // Release is 3. It is NOT 3 in SGR mode because SGR can tell + // the application what button was released. + acc = 3; + } else { + acc = switch (button.?) { + .left => 0, + .right => 1, + .middle => 2, + .four => 64, + .five => 65, + else => return, // unsupported + }; + } + + // X10 doesn't have modifiers + if (self.io.terminal.modes.mouse_event != .x10) { + if (mods.shift) acc += 4; + if (mods.super) acc += 8; + if (mods.ctrl) acc += 16; + } + + // Motion adds another bit + if (action == .motion) acc += 32; + + break :code acc; + }; + + switch (self.io.terminal.modes.mouse_format) { + .x10 => { + if (viewport_point.x > 222 or viewport_point.y > 222) { + log.info("X10 mouse format can only encode X/Y up to 223", .{}); + return; + } + + // + 1 below is because our x/y is 0-indexed and proto wants 1 + var data: termio.Message.WriteReq.Small.Array = undefined; + assert(data.len >= 5); + data[0] = '\x1b'; + data[1] = '['; + data[2] = 'M'; + data[3] = 32 + button_code; + data[4] = 32 + @intCast(u8, viewport_point.x) + 1; + data[5] = 32 + @intCast(u8, viewport_point.y) + 1; + + // Ask our IO thread to write the data + _ = self.io_thread.mailbox.push(.{ + .write_small = .{ + .data = data, + .len = 5, + }, + }, .{ .forever = {} }); + }, + + .utf8 => { + // Maximum of 12 because at most we have 2 fully UTF-8 encoded chars + var data: termio.Message.WriteReq.Small.Array = undefined; + assert(data.len >= 12); + data[0] = '\x1b'; + data[1] = '['; + data[2] = 'M'; + + // The button code will always fit in a single u8 + data[3] = 32 + button_code; + + // UTF-8 encode the x/y + var i: usize = 4; + i += try std.unicode.utf8Encode(@intCast(u21, 32 + viewport_point.x + 1), data[i..]); + i += try std.unicode.utf8Encode(@intCast(u21, 32 + viewport_point.y + 1), data[i..]); + + // Ask our IO thread to write the data + _ = self.io_thread.mailbox.push(.{ + .write_small = .{ + .data = data, + .len = @intCast(u8, i), + }, + }, .{ .forever = {} }); + }, + + .sgr => { + // Final character to send in the CSI + const final: u8 = if (action == .release) 'm' else 'M'; + + // Response always is at least 4 chars, so this leaves the + // remainder for numbers which are very large... + var data: termio.Message.WriteReq.Small.Array = undefined; + const resp = try std.fmt.bufPrint(&data, "\x1B[<{d};{d};{d}{c}", .{ + button_code, + viewport_point.x + 1, + viewport_point.y + 1, + final, + }); + + // Ask our IO thread to write the data + _ = self.io_thread.mailbox.push(.{ + .write_small = .{ + .data = data, + .len = @intCast(u8, resp.len), + }, + }, .{ .forever = {} }); + }, + + .urxvt => { + // Response always is at least 4 chars, so this leaves the + // remainder for numbers which are very large... + var data: termio.Message.WriteReq.Small.Array = undefined; + const resp = try std.fmt.bufPrint(&data, "\x1B[{d};{d};{d}M", .{ + 32 + button_code, + viewport_point.x + 1, + viewport_point.y + 1, + }); + + // Ask our IO thread to write the data + _ = self.io_thread.mailbox.push(.{ + .write_small = .{ + .data = data, + .len = @intCast(u8, resp.len), + }, + }, .{ .forever = {} }); + }, + + .sgr_pixels => { + // Final character to send in the CSI + const final: u8 = if (action == .release) 'm' else 'M'; + + // Response always is at least 4 chars, so this leaves the + // remainder for numbers which are very large... + var data: termio.Message.WriteReq.Small.Array = undefined; + const resp = try std.fmt.bufPrint(&data, "\x1B[<{d};{d};{d}{c}", .{ + button_code, + pos.x, + pos.y, + final, + }); + + // Ask our IO thread to write the data + _ = self.io_thread.mailbox.push(.{ + .write_small = .{ + .data = data, + .len = @intCast(u8, resp.len), + }, + }, .{ .forever = {} }); + }, + } + + // After sending all our messages we have to notify our IO thread + try self.io_thread.wakeup.notify(); +} + +pub fn mouseButtonCallback( + self: *Surface, + action: input.MouseButtonState, + button: input.MouseButton, + mods: input.Mods, +) !void { + const tracy = trace(@src()); + defer tracy.end(); + + // If our dev mode window is visible then we always schedule a render on + // cursor move because the cursor might touch our windows. + if (DevMode.enabled and DevMode.instance.visible) { + try self.queueRender(); + + // If the mouse event was handled by imgui, ignore it. + if (imgui.IO.get()) |io| { + if (io.cval().WantCaptureMouse) return; + } else |_| {} + } + + // Always record our latest mouse state + self.mouse.click_state[@intCast(usize, @enumToInt(button))] = action; + self.mouse.mods = @bitCast(input.Mods, mods); + + self.renderer_state.mutex.lock(); + defer self.renderer_state.mutex.unlock(); + + // Report mouse events if enabled + if (self.io.terminal.modes.mouse_event != .none) { + const pos = try self.rt_surface.getCursorPos(); + + const report_action: MouseReportAction = switch (action) { + .press => .press, + .release => .release, + }; + + try self.mouseReport( + button, + report_action, + self.mouse.mods, + pos, + ); + } + + // For left button clicks we always record some information for + // selection/highlighting purposes. + if (button == .left and action == .press) { + const pos = try self.rt_surface.getCursorPos(); + + // If we move our cursor too much between clicks then we reset + // the multi-click state. + if (self.mouse.left_click_count > 0) { + const max_distance = self.cell_size.width; + const distance = @sqrt( + std.math.pow(f64, pos.x - self.mouse.left_click_xpos, 2) + + std.math.pow(f64, pos.y - self.mouse.left_click_ypos, 2), + ); + + if (distance > max_distance) self.mouse.left_click_count = 0; + } + + // Store it + const point = self.posToViewport(pos.x, pos.y); + self.mouse.left_click_point = point.toScreen(&self.io.terminal.screen); + self.mouse.left_click_xpos = pos.x; + self.mouse.left_click_ypos = pos.y; + + // Setup our click counter and timer + if (std.time.Instant.now()) |now| { + // If we have mouse clicks, then we check if the time elapsed + // is less than and our interval and if so, increase the count. + if (self.mouse.left_click_count > 0) { + const since = now.since(self.mouse.left_click_time); + if (since > self.mouse_interval) { + self.mouse.left_click_count = 0; + } + } + + self.mouse.left_click_time = now; + self.mouse.left_click_count += 1; + + // We only support up to triple-clicks. + if (self.mouse.left_click_count > 3) self.mouse.left_click_count = 1; + } else |err| { + self.mouse.left_click_count = 1; + log.err("error reading time, mouse multi-click won't work err={}", .{err}); + } + + switch (self.mouse.left_click_count) { + // First mouse click, clear selection + 1 => if (self.io.terminal.selection != null) { + self.io.terminal.selection = null; + try self.queueRender(); + }, + + // Double click, select the word under our mouse + 2 => { + const sel_ = self.io.terminal.screen.selectWord(self.mouse.left_click_point); + if (sel_) |sel| { + self.io.terminal.selection = sel; + try self.queueRender(); + } + }, + + // Triple click, select the line under our mouse + 3 => { + const sel_ = self.io.terminal.screen.selectLine(self.mouse.left_click_point); + if (sel_) |sel| { + self.io.terminal.selection = sel; + try self.queueRender(); + } + }, + + // We should be bounded by 1 to 3 + else => unreachable, + } + } +} + +pub fn cursorPosCallback( + self: *Surface, + pos: apprt.CursorPos, +) !void { + const tracy = trace(@src()); + defer tracy.end(); + + // If our dev mode window is visible then we always schedule a render on + // cursor move because the cursor might touch our windows. + if (DevMode.enabled and DevMode.instance.visible) { + try self.queueRender(); + + // If the mouse event was handled by imgui, ignore it. + if (imgui.IO.get()) |io| { + if (io.cval().WantCaptureMouse) return; + } else |_| {} + } + + // We are reading/writing state for the remainder + self.renderer_state.mutex.lock(); + defer self.renderer_state.mutex.unlock(); + + // Do a mouse report + if (self.io.terminal.modes.mouse_event != .none) { + // We use the first mouse button we find pressed in order to report + // since the spec (afaict) does not say... + const button: ?input.MouseButton = button: for (self.mouse.click_state) |state, i| { + if (state == .press) + break :button @intToEnum(input.MouseButton, i); + } else null; + + try self.mouseReport(button, .motion, self.mouse.mods, pos); + + // If we're doing mouse motion tracking, we do not support text + // selection. + return; + } + + // If the cursor isn't clicked currently, it doesn't matter + if (self.mouse.click_state[@enumToInt(input.MouseButton.left)] != .press) return; + + // All roads lead to requiring a re-render at this pont. + try self.queueRender(); + + // Convert to pixels from screen coords + const xpos = pos.x; + const ypos = pos.y; + + // Convert to points + const viewport_point = self.posToViewport(xpos, ypos); + const screen_point = viewport_point.toScreen(&self.io.terminal.screen); + + // Handle dragging depending on click count + switch (self.mouse.left_click_count) { + 1 => self.dragLeftClickSingle(screen_point, xpos), + 2 => self.dragLeftClickDouble(screen_point), + 3 => self.dragLeftClickTriple(screen_point), + else => unreachable, + } +} + +/// Double-click dragging moves the selection one "word" at a time. +fn dragLeftClickDouble( + self: *Surface, + screen_point: terminal.point.ScreenPoint, +) void { + // Get the word under our current point. If there isn't a word, do nothing. + const word = self.io.terminal.screen.selectWord(screen_point) orelse return; + + // Get our selection to grow it. If we don't have a selection, start it now. + // We may not have a selection if we started our dbl-click in an area + // that had no data, then we dragged our mouse into an area with data. + var sel = self.io.terminal.screen.selectWord(self.mouse.left_click_point) orelse { + self.io.terminal.selection = word; + return; + }; + + // Grow our selection + if (screen_point.before(self.mouse.left_click_point)) { + sel.start = word.start; + } else { + sel.end = word.end; + } + self.io.terminal.selection = sel; +} + +/// Triple-click dragging moves the selection one "line" at a time. +fn dragLeftClickTriple( + self: *Surface, + screen_point: terminal.point.ScreenPoint, +) void { + // Get the word under our current point. If there isn't a word, do nothing. + const word = self.io.terminal.screen.selectLine(screen_point) orelse return; + + // Get our selection to grow it. If we don't have a selection, start it now. + // We may not have a selection if we started our dbl-click in an area + // that had no data, then we dragged our mouse into an area with data. + var sel = self.io.terminal.screen.selectLine(self.mouse.left_click_point) orelse { + self.io.terminal.selection = word; + return; + }; + + // Grow our selection + if (screen_point.before(self.mouse.left_click_point)) { + sel.start = word.start; + } else { + sel.end = word.end; + } + self.io.terminal.selection = sel; +} + +fn dragLeftClickSingle( + self: *Surface, + screen_point: terminal.point.ScreenPoint, + xpos: f64, +) void { + // NOTE(mitchellh): This logic super sucks. There has to be an easier way + // to calculate this, but this is good for a v1. Selection isn't THAT + // common so its not like this performance heavy code is running that + // often. + // TODO: unit test this, this logic sucks + + // If we were selecting, and we switched directions, then we restart + // calculations because it forces us to reconsider if the first cell is + // selected. + if (self.io.terminal.selection) |sel| { + const reset: bool = if (sel.end.before(sel.start)) + sel.start.before(screen_point) + else + screen_point.before(sel.start); + + if (reset) self.io.terminal.selection = null; + } + + // Our logic for determing if the starting cell is selected: + // + // - The "xboundary" is 60% the width of a cell from the left. We choose + // 60% somewhat arbitrarily based on feeling. + // - If we started our click left of xboundary, backwards selections + // can NEVER select the current char. + // - If we started our click right of xboundary, backwards selections + // ALWAYS selected the current char, but we must move the cursor + // left of the xboundary. + // - Inverted logic for forwards selections. + // + + // the boundary point at which we consider selection or non-selection + const cell_xboundary = self.cell_size.width * 0.6; + + // first xpos of the clicked cell + const cell_xstart = @intToFloat(f32, self.mouse.left_click_point.x) * self.cell_size.width; + const cell_start_xpos = self.mouse.left_click_xpos - cell_xstart; + + // If this is the same cell, then we only start the selection if weve + // moved past the boundary point the opposite direction from where we + // started. + if (std.meta.eql(screen_point, self.mouse.left_click_point)) { + const cell_xpos = xpos - cell_xstart; + const selected: bool = if (cell_start_xpos < cell_xboundary) + cell_xpos >= cell_xboundary + else + cell_xpos < cell_xboundary; + + self.io.terminal.selection = if (selected) .{ + .start = screen_point, + .end = screen_point, + } else null; + + return; + } + + // If this is a different cell and we haven't started selection, + // we determine the starting cell first. + if (self.io.terminal.selection == null) { + // - If we're moving to a point before the start, then we select + // the starting cell if we started after the boundary, else + // we start selection of the prior cell. + // - Inverse logic for a point after the start. + const click_point = self.mouse.left_click_point; + const start: terminal.point.ScreenPoint = if (screen_point.before(click_point)) start: { + if (self.mouse.left_click_xpos > cell_xboundary) { + break :start click_point; + } else { + break :start if (click_point.x > 0) terminal.point.ScreenPoint{ + .y = click_point.y, + .x = click_point.x - 1, + } else terminal.point.ScreenPoint{ + .x = self.io.terminal.screen.cols - 1, + .y = click_point.y -| 1, + }; + } + } else start: { + if (self.mouse.left_click_xpos < cell_xboundary) { + break :start click_point; + } else { + break :start if (click_point.x < self.io.terminal.screen.cols - 1) terminal.point.ScreenPoint{ + .y = click_point.y, + .x = click_point.x + 1, + } else terminal.point.ScreenPoint{ + .y = click_point.y + 1, + .x = 0, + }; + } + }; + + self.io.terminal.selection = .{ .start = start, .end = screen_point }; + return; + } + + // TODO: detect if selection point is passed the point where we've + // actually written data before and disallow it. + + // We moved! Set the selection end point. The start point should be + // set earlier. + assert(self.io.terminal.selection != null); + self.io.terminal.selection.?.end = screen_point; +} + +fn posToViewport(self: Surface, xpos: f64, ypos: f64) terminal.point.Viewport { + // xpos and ypos can be negative if while dragging, the user moves the + // mouse off the window. Likewise, they can be larger than our window + // width if the user drags out of the window positively. + return .{ + .x = if (xpos < 0) 0 else x: { + // Our cell is the mouse divided by cell width + const cell_width = @floatCast(f64, self.cell_size.width); + const x = @floatToInt(usize, xpos / cell_width); + + // Can be off the screen if the user drags it out, so max + // it out on our available columns + break :x @min(x, self.grid_size.columns - 1); + }, + + .y = if (ypos < 0) 0 else y: { + const cell_height = @floatCast(f64, self.cell_size.height); + const y = @floatToInt(usize, ypos / cell_height); + break :y @min(y, self.grid_size.rows - 1); + }, + }; +} + +const face_ttf = @embedFile("font/res/FiraCode-Regular.ttf"); +const face_bold_ttf = @embedFile("font/res/FiraCode-Bold.ttf"); +const face_emoji_ttf = @embedFile("font/res/NotoColorEmoji.ttf"); +const face_emoji_text_ttf = @embedFile("font/res/NotoEmoji-Regular.ttf"); diff --git a/src/apprt.zig b/src/apprt.zig index b2f4c93b1..24b85b00b 100644 --- a/src/apprt.zig +++ b/src/apprt.zig @@ -17,7 +17,7 @@ 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"); +pub const surface = @import("apprt/Surface.zig"); /// The implementation to use for the app runtime. This is comptime chosen /// so that every build has exactly one application runtime implementation. @@ -34,6 +34,7 @@ pub const runtime = switch (build_config.artifact) { }; pub const App = runtime.App; +pub const Surface = runtime.Surface; /// Runtime is the runtime to use for Ghostty. All runtimes do not provide /// equivalent feature sets. For example, GTK offers tabbing and more features diff --git a/src/apprt/Window.zig b/src/apprt/Surface.zig similarity index 86% rename from src/apprt/Window.zig rename to src/apprt/Surface.zig index 159b6e92a..fff956504 100644 --- a/src/apprt/Window.zig +++ b/src/apprt/Surface.zig @@ -1,5 +1,5 @@ const App = @import("../App.zig"); -const Window = @import("../Window.zig"); +const Surface = @import("../Surface.zig"); const renderer = @import("../renderer.zig"); const termio = @import("../termio.zig"); @@ -27,17 +27,17 @@ pub const Message = union(enum) { /// A window mailbox. pub const Mailbox = struct { - window: *Window, + window: *Surface, app: *App.Mailbox, /// Send a message to the window. pub fn push(self: Mailbox, msg: Message, timeout: App.Mailbox.Timeout) App.Mailbox.Size { - // Window message sending is actually implemented on the app + // Surface message sending is actually implemented on the app // thread, so we have to rewrap the message with our window // pointer and send it to the app thread. const result = self.app.push(.{ - .window_message = .{ - .window = self.window, + .surface_message = .{ + .surface = self.window, .message = msg, }, }, timeout); diff --git a/src/apprt/glfw.zig b/src/apprt/glfw.zig index 5473e13d4..2150711d5 100644 --- a/src/apprt/glfw.zig +++ b/src/apprt/glfw.zig @@ -18,7 +18,7 @@ const renderer = @import("../renderer.zig"); const Renderer = renderer.Renderer; const apprt = @import("../apprt.zig"); const CoreApp = @import("../App.zig"); -const CoreWindow = @import("../Window.zig"); +const CoreSurface = @import("../Surface.zig"); // Get native API access on certain platforms so we can do more customization. const glfwNative = glfw.Native(.{ @@ -75,6 +75,16 @@ pub const App = struct { glfw.postEmptyEvent(); } + /// Create a new window for the app. + pub fn newWindow(self: *App) !void { + // Grab a surface allocation because we're going to need it. + const surface = try self.app.surface_pool.create(); + errdefer self.app.surface_pool.destroy(surface); + + // Create the surface -- because windows are surfaces for glfw. + try surface.init(self); + } + fn glfwErrorCallback(code: glfw.ErrorCode, desc: [:0]const u8) void { std.log.warn("glfw error={} message={s}", .{ code, desc }); @@ -120,18 +130,32 @@ pub const App = struct { }; }; -pub const Window = struct { +/// Surface represents the drawable surface for glfw. In glfw, a surface +/// is always a window because that is the only abstraction that glfw exposes. +/// +/// This means that there is no way for the glfw runtime to support tabs, +/// splits, etc. without considerable effort. In fact, on Darwin, we do +/// support tabs because the minimal tabbing interface is a window abstraction, +/// but this is a bit of a hack. The native Swift runtime should be used instead +/// which uses real native tabbing. +/// +/// Other runtimes a surface usually represents the equivalent of a "view" +/// or "widget" level granularity. +pub const Surface = struct { /// The glfw window handle window: glfw.Window, /// The glfw mouse cursor handle. cursor: glfw.Cursor, + /// A core surface + core_surface: CoreSurface, + pub const Options = struct {}; - pub fn init(app: *const CoreApp, core_win: *CoreWindow, opts: Options) !Window { - _ = opts; - + /// Initialize the surface into the given self pointer. This gives a + /// stable pointer to the destination that can be used for callbacks. + pub fn init(self: *Surface, app: *App) !void { // Create our window const win = glfw.Window.create( 640, @@ -143,9 +167,9 @@ pub const Window = struct { ) orelse return glfw.mustGetErrorCode(); errdefer win.destroy(); + // Get our physical DPI - debug only because we don't have a use for + // this but the logging of it may be useful if (builtin.mode == .Debug) { - // Get our physical DPI - debug only because we don't have a use for - // this but the logging of it may be useful const monitor = win.getMonitor() orelse monitor: { log.warn("window had null monitor, getting primary monitor", .{}); break :monitor glfw.Monitor.getPrimary().?; @@ -160,8 +184,8 @@ pub const Window = struct { }); } - // On Mac, enable tabbing - if (comptime builtin.target.isDarwin()) { + // On Mac, enable window tabbing + if (App.Darwin.enabled) { const NSWindowTabbingMode = enum(usize) { automatic = 0, preferred = 1, disallowed = 2 }; const nswindow = objc.Object.fromId(glfwNative.getCocoaWindow(win).?); @@ -184,7 +208,7 @@ pub const Window = struct { } // Set our callbacks - win.setUserPointer(core_win); + win.setUserPointer(&self.core_surface); win.setSizeCallback(sizeCallback); win.setCharCallback(charCallback); win.setKeyCallback(keyCallback); @@ -195,13 +219,23 @@ pub const Window = struct { win.setMouseButtonCallback(mouseButtonCallback); // Build our result - return Window{ + self.* = .{ .window = win, .cursor = cursor, + .core_surface = undefined, }; + errdefer self.* = undefined; + + // Add ourselves to the list of surfaces on the app. + try app.app.addSurface(self); + errdefer app.app.deleteSurface(self); + + // Initialize our surface now that we have the stable pointer. + try self.core_surface.init(app.app, app.app.config, self); + errdefer self.core_surface.destroy(); } - pub fn deinit(self: *Window) void { + pub fn deinit(self: *Surface) void { var tabgroup_opt: if (builtin.target.isDarwin()) ?objc.Object else void = undefined; if (comptime builtin.target.isDarwin()) { const nswindow = objc.Object.fromId(glfwNative.getCocoaWindow(self.window).?); @@ -240,7 +274,7 @@ pub const Window = struct { // If we have a tabgroup set, we want to manually focus the next window. // We should NOT have to do this usually, see the comments above. - if (comptime builtin.target.isDarwin()) { + if (App.Darwin.enabled) { if (tabgroup_opt) |tabgroup| { const selected = tabgroup.getProperty(objc.Object, "selectedWindow"); selected.msgSend(void, objc.sel("makeKeyWindow"), .{}); @@ -252,7 +286,7 @@ pub const Window = struct { /// Note: this interface is not good, we should redo it if we plan /// to use this more. i.e. you can't set max width but no max height, /// or no mins. - pub fn setSizeLimits(self: *Window, min: apprt.WindowSize, max_: ?apprt.WindowSize) !void { + pub fn setSizeLimits(self: *Surface, min: apprt.WindowSize, max_: ?apprt.WindowSize) !void { self.window.setSizeLimits(.{ .width = min.width, .height = min.height, @@ -266,7 +300,7 @@ pub const Window = struct { } /// Returns the content scale for the created window. - pub fn getContentScale(self: *const Window) !apprt.ContentScale { + pub fn getContentScale(self: *const Surface) !apprt.ContentScale { const scale = self.window.getContentScale(); return apprt.ContentScale{ .x = scale.x_scale, .y = scale.y_scale }; } @@ -274,14 +308,14 @@ pub const Window = struct { /// Returns the size of the window in pixels. The pixel size may /// not match screen coordinate size but we should be able to convert /// back and forth using getContentScale. - pub fn getSize(self: *const Window) !apprt.WindowSize { + pub fn getSize(self: *const Surface) !apprt.WindowSize { const size = self.window.getFramebufferSize(); return apprt.WindowSize{ .width = size.width, .height = size.height }; } /// Returns the cursor position in scaled pixels relative to the /// upper-left of the window. - pub fn getCursorPos(self: *const Window) !apprt.CursorPos { + pub fn getCursorPos(self: *const Surface) !apprt.CursorPos { const unscaled_pos = self.window.getCursorPos(); const pos = try self.cursorPosToPixels(unscaled_pos); return apprt.CursorPos{ @@ -292,37 +326,37 @@ pub const Window = struct { /// Set the flag that notes this window should be closed for the next /// iteration of the event loop. - pub fn setShouldClose(self: *Window) void { + pub fn setShouldClose(self: *Surface) void { self.window.setShouldClose(true); } /// Returns true if the window is flagged to close. - pub fn shouldClose(self: *const Window) bool { + pub fn shouldClose(self: *const Surface) bool { return self.window.shouldClose(); } /// Set the title of the window. - pub fn setTitle(self: *Window, slice: [:0]const u8) !void { + pub fn setTitle(self: *Surface, slice: [:0]const u8) !void { self.window.setTitle(slice.ptr); } /// Read the clipboard. The windowing system is responsible for allocating /// a buffer as necessary. This should be a stable pointer until the next /// time getClipboardString is called. - pub fn getClipboardString(self: *const Window) ![:0]const u8 { + pub fn getClipboardString(self: *const Surface) ![:0]const u8 { _ = self; return glfw.getClipboardString() orelse return glfw.mustGetErrorCode(); } /// Set the clipboard. - pub fn setClipboardString(self: *const Window, val: [:0]const u8) !void { + pub fn setClipboardString(self: *const Surface, val: [:0]const u8) !void { _ = self; glfw.setClipboardString(val); } /// The cursor position from glfw directly is in screen coordinates but /// all our interface works in pixels. - fn cursorPosToPixels(self: *const Window, pos: glfw.Window.CursorPos) !glfw.Window.CursorPos { + fn cursorPosToPixels(self: *const Surface, pos: glfw.Window.CursorPos) !glfw.Window.CursorPos { // The cursor position is in screen coordinates but we // want it in pixels. we need to get both the size of the // window in both to get the ratio to make the conversion. @@ -349,8 +383,8 @@ pub const Window = struct { // Get the size. We are given a width/height but this is in screen // coordinates and we want raw pixels. The core window uses the content // scale to scale appropriately. - const core_win = window.getUserPointer(CoreWindow) orelse return; - const size = core_win.window.getSize() catch |err| { + const core_win = window.getUserPointer(CoreSurface) orelse return; + const size = core_win.rt_surface.getSize() catch |err| { log.err("error querying window size for size callback err={}", .{err}); return; }; @@ -366,7 +400,7 @@ pub const Window = struct { const tracy = trace(@src()); defer tracy.end(); - const core_win = window.getUserPointer(CoreWindow) orelse return; + const core_win = window.getUserPointer(CoreSurface) orelse return; core_win.charCallback(codepoint) catch |err| { log.err("error in char callback err={}", .{err}); return; @@ -518,7 +552,7 @@ pub const Window = struct { => .invalid, }; - const core_win = window.getUserPointer(CoreWindow) orelse return; + const core_win = window.getUserPointer(CoreSurface) orelse return; core_win.keyCallback(action, key, mods) catch |err| { log.err("error in key callback err={}", .{err}); return; @@ -529,7 +563,7 @@ pub const Window = struct { const tracy = trace(@src()); defer tracy.end(); - const core_win = window.getUserPointer(CoreWindow) orelse return; + const core_win = window.getUserPointer(CoreSurface) orelse return; core_win.focusCallback(focused) catch |err| { log.err("error in focus callback err={}", .{err}); return; @@ -540,7 +574,7 @@ pub const Window = struct { const tracy = trace(@src()); defer tracy.end(); - const core_win = window.getUserPointer(CoreWindow) orelse return; + const core_win = window.getUserPointer(CoreSurface) orelse return; core_win.refreshCallback() catch |err| { log.err("error in refresh callback err={}", .{err}); return; @@ -551,7 +585,7 @@ pub const Window = struct { const tracy = trace(@src()); defer tracy.end(); - const core_win = window.getUserPointer(CoreWindow) orelse return; + const core_win = window.getUserPointer(CoreSurface) orelse return; core_win.scrollCallback(xoff, yoff) catch |err| { log.err("error in scroll callback err={}", .{err}); return; @@ -566,10 +600,10 @@ pub const Window = struct { const tracy = trace(@src()); defer tracy.end(); - const core_win = window.getUserPointer(CoreWindow) orelse return; + const core_win = window.getUserPointer(CoreSurface) orelse return; // Convert our unscaled x/y to scaled. - const pos = core_win.window.cursorPosToPixels(.{ + const pos = core_win.rt_surface.cursorPosToPixels(.{ .xpos = unscaled_xpos, .ypos = unscaled_ypos, }) catch |err| { @@ -598,7 +632,7 @@ pub const Window = struct { const tracy = trace(@src()); defer tracy.end(); - const core_win = window.getUserPointer(CoreWindow) orelse return; + const core_win = window.getUserPointer(CoreSurface) orelse return; // Convert glfw button to input button const mods = @bitCast(input.Mods, glfw_mods); diff --git a/src/main.zig b/src/main.zig index 35c24ccb5..8d50d3045 100644 --- a/src/main.zig +++ b/src/main.zig @@ -100,6 +100,7 @@ pub fn main() !void { defer app_runtime.terminate(); // Create an initial window + try app_runtime.newWindow(); // Run the GUI event loop try app_runtime.run(); diff --git a/src/renderer/OpenGL.zig b/src/renderer/OpenGL.zig index f631ec81e..951835998 100644 --- a/src/renderer/OpenGL.zig +++ b/src/renderer/OpenGL.zig @@ -18,7 +18,7 @@ const trace = @import("tracy").trace; const math = @import("../math.zig"); const lru = @import("../lru.zig"); const DevMode = @import("../DevMode.zig"); -const Window = @import("../Window.zig"); +const Surface = @import("../Surface.zig"); const log = std.log.scoped(.grid); @@ -89,7 +89,7 @@ focused: bool, padding: renderer.Options.Padding, /// The mailbox for communicating with the window. -window_mailbox: Window.Mailbox, +window_mailbox: apprt.surface.Mailbox, /// The raw structure that maps directly to the buffer sent to the vertex shader. /// This must be "extern" so that the field order is not reordered by the @@ -362,12 +362,11 @@ pub fn glfwWindowHints() glfw.Window.Hints { }; } -/// This is called early right after window creation to setup our -/// window surface as necessary. -pub fn windowInit(win: apprt.runtime.Window) !void { +/// This is called early right after surface creation. +pub fn surfaceInit(surface: *apprt.Surface) !void { // Treat this like a thread entry const self: OpenGL = undefined; - try self.threadEnter(win); + try self.threadEnter(surface); // Blending for text. We use GL_ONE here because we should be using // premultiplied alpha for all our colors in our fragment shaders. @@ -388,19 +387,19 @@ pub fn windowInit(win: apprt.runtime.Window) !void { /// This is called just prior to spinning up the renderer thread for /// final main thread setup requirements. -pub fn finalizeWindowInit(self: *const OpenGL, win: apprt.runtime.Window) !void { +pub fn finalizeSurfaceInit(self: *const OpenGL, surface: *apprt.Surface) !void { _ = self; - _ = win; + _ = surface; } /// This is called if this renderer runs DevMode. -pub fn initDevMode(self: *const OpenGL, win: apprt.runtime.Window) !void { +pub fn initDevMode(self: *const OpenGL, surface: *apprt.Surface) !void { _ = self; if (DevMode.enabled) { // Initialize for our window assert(imgui.ImplGlfw.initForOpenGL( - @ptrCast(*imgui.ImplGlfw.GLFWWindow, win.window.handle), + @ptrCast(*imgui.ImplGlfw.GLFWWindow, surface.window.handle), true, )); assert(imgui.ImplOpenGL3.init("#version 330 core")); @@ -418,7 +417,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 { +pub fn threadEnter(self: *const OpenGL, surface: *apprt.Surface) !void { _ = self; switch (apprt.runtime) { @@ -437,7 +436,7 @@ pub fn threadEnter(self: *const OpenGL, win: apprt.runtime.Window) !void { // ensures that the context switches over to our thread. Important: // the prior thread MUST have detached the context prior to calling // this entrypoint. - glfw.makeContextCurrent(win.window); + glfw.makeContextCurrent(surface.window); errdefer glfw.makeContextCurrent(null); glfw.swapInterval(1); @@ -548,7 +547,7 @@ fn resetFontMetrics( /// The primary render callback that is completely thread-safe. pub fn render( self: *OpenGL, - win: apprt.runtime.Window, + surface: *apprt.Surface, state: *renderer.State, ) !void { // Data we extract out of the critical area. @@ -669,7 +668,7 @@ pub fn render( // Swap our window buffers if (apprt.runtime == apprt.gtk) @panic("TODO"); - win.window.swapBuffers(); + surface.window.swapBuffers(); } /// rebuildCells rebuilds all the GPU cells from our CPU state. This is a diff --git a/src/renderer/Options.zig b/src/renderer/Options.zig index c456c287d..0687d4e99 100644 --- a/src/renderer/Options.zig +++ b/src/renderer/Options.zig @@ -1,8 +1,8 @@ //! The options that are used to configure a renderer. +const apprt = @import("../apprt.zig"); const font = @import("../font/main.zig"); const renderer = @import("../renderer.zig"); -const Window = @import("../Window.zig"); const Config = @import("../config.zig").Config; /// The app configuration. @@ -16,7 +16,7 @@ padding: Padding, /// The mailbox for sending the window messages. This is only valid /// once the thread has started and should not be used outside of the thread. -window_mailbox: Window.Mailbox, +window_mailbox: apprt.surface.Mailbox, pub const Padding = struct { // Explicit padding options, in pixels. The windowing thread is diff --git a/src/renderer/Thread.zig b/src/renderer/Thread.zig index 534561fa1..2ffd0a1df 100644 --- a/src/renderer/Thread.zig +++ b/src/renderer/Thread.zig @@ -47,8 +47,8 @@ cursor_h: xev.Timer, cursor_c: xev.Completion = .{}, cursor_c_cancel: xev.Completion = .{}, -/// The window we're rendering to. -window: apprt.runtime.Window, +/// The surface we're rendering to. +surface: *apprt.Surface, /// The underlying renderer implementation. renderer: *renderer.Renderer, @@ -65,7 +65,7 @@ mailbox: *Mailbox, /// is up to the caller to start the thread with the threadMain entrypoint. pub fn init( alloc: Allocator, - win: apprt.runtime.Window, + surface: *apprt.Surface, renderer_impl: *renderer.Renderer, state: *renderer.State, ) !Thread { @@ -100,7 +100,7 @@ pub fn init( .stop = stop_h, .render_h = render_h, .cursor_h = cursor_timer, - .window = win, + .surface = surface, .renderer = renderer_impl, .state = state, .mailbox = mailbox, @@ -135,7 +135,7 @@ fn threadMain_(self: *Thread) !void { // Run our thread start/end callbacks. This is important because some // renderers have to do per-thread setup. For example, OpenGL has to set // some thread-local state since that is how it works. - try self.renderer.threadEnter(self.window); + try self.renderer.threadEnter(self.surface); defer self.renderer.threadExit(); // Start the async handlers @@ -305,7 +305,7 @@ fn renderCallback( return .disarm; }; - t.renderer.render(t.window, t.state) catch |err| + t.renderer.render(t.surface, t.state) catch |err| log.warn("error rendering err={}", .{err}); return .disarm; } diff --git a/src/termio/Exec.zig b/src/termio/Exec.zig index 01e0d300a..a6a56e90b 100644 --- a/src/termio/Exec.zig +++ b/src/termio/Exec.zig @@ -8,7 +8,6 @@ const assert = std.debug.assert; const Allocator = std.mem.Allocator; const termio = @import("../termio.zig"); const Command = @import("../Command.zig"); -const Window = @import("../Window.zig"); const Pty = @import("../Pty.zig"); const SegmentedPool = @import("../segmented_pool.zig").SegmentedPool; const terminal = @import("../terminal/main.zig"); @@ -16,6 +15,7 @@ const xev = @import("xev"); const renderer = @import("../renderer.zig"); const tracy = @import("tracy"); const trace = tracy.trace; +const apprt = @import("../apprt.zig"); const fastmem = @import("../fastmem.zig"); const log = std.log.scoped(.io_exec); @@ -52,7 +52,7 @@ renderer_wakeup: xev.Async, renderer_mailbox: *renderer.Thread.Mailbox, /// The mailbox for communicating with the window. -window_mailbox: Window.Mailbox, +window_mailbox: apprt.surface.Mailbox, /// The cached grid size whenever a resize is called. grid_size: renderer.GridSize, @@ -638,7 +638,7 @@ const StreamHandler = struct { alloc: Allocator, grid_size: *renderer.GridSize, terminal: *terminal.Terminal, - window_mailbox: Window.Mailbox, + window_mailbox: apprt.surface.Mailbox, /// This is set to true when a message was written to the writer /// mailbox. This can be used by callers to determine if they need @@ -1003,7 +1003,7 @@ const StreamHandler = struct { // Write clipboard contents _ = self.window_mailbox.push(.{ - .clipboard_write = try Window.Message.WriteReq.init( + .clipboard_write = try apprt.surface.Message.WriteReq.init( self.alloc, data, ), diff --git a/src/termio/Options.zig b/src/termio/Options.zig index d571d0fac..df90a8369 100644 --- a/src/termio/Options.zig +++ b/src/termio/Options.zig @@ -1,9 +1,9 @@ //! The options that are used to configure a terminal IO implementation. const xev = @import("xev"); +const apprt = @import("../apprt.zig"); const renderer = @import("../renderer.zig"); const Config = @import("../config.zig").Config; -const Window = @import("../Window.zig"); /// The size of the terminal grid. grid_size: renderer.GridSize, @@ -28,4 +28,4 @@ renderer_wakeup: xev.Async, renderer_mailbox: *renderer.Thread.Mailbox, /// The mailbox for sending the window messages. -window_mailbox: Window.Mailbox, +window_mailbox: apprt.surface.Mailbox, From 913131c8f1bce61008d5e95eab933ef848a93054 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 22 Feb 2023 14:52:38 -0800 Subject: [PATCH 07/37] rename more stuff --- src/DevMode.zig | 22 ++++---- src/Surface.zig | 74 ++++++++++++-------------- src/apprt.zig | 2 +- src/apprt/glfw.zig | 16 +++--- src/apprt/structs.zig | 4 +- src/apprt/{Surface.zig => surface.zig} | 16 +++--- src/renderer/OpenGL.zig | 6 +-- src/renderer/Options.zig | 6 +-- src/termio/Exec.zig | 16 +++--- src/termio/Options.zig | 6 +-- 10 files changed, 84 insertions(+), 84 deletions(-) rename src/apprt/{Surface.zig => surface.zig} (79%) diff --git a/src/DevMode.zig b/src/DevMode.zig index 931c04dea..bd28366ca 100644 --- a/src/DevMode.zig +++ b/src/DevMode.zig @@ -29,8 +29,8 @@ visible: bool = false, /// Our app config config: ?*const Config = null, -/// The window we're tracking. -window: ?*Surface = null, +/// The surface we're tracking. +surface: ?*Surface = null, /// Update the state associated with the dev mode. This should generally /// only be called paired with a render since it otherwise wastes CPU @@ -86,20 +86,20 @@ pub fn update(self: *const DevMode) !void { } } - if (self.window) |window| { + if (self.surface) |surface| { if (imgui.collapsingHeader("Font Manager", null, .{})) { - imgui.text("Glyphs: %d", window.font_group.glyphs.count()); + imgui.text("Glyphs: %d", surface.font_group.glyphs.count()); imgui.sameLine(0, -1); helpMarker("The number of glyphs loaded and rendered into a " ++ "font atlas currently."); - const Renderer = @TypeOf(window.renderer); + const Renderer = @TypeOf(surface.renderer); if (imgui.treeNode("Atlas: Greyscale", .{ .default_open = true })) { defer imgui.treePop(); - const atlas = &window.font_group.atlas_greyscale; + const atlas = &surface.font_group.atlas_greyscale; const tex = switch (Renderer) { - renderer.OpenGL => @intCast(usize, window.renderer.texture.id), - renderer.Metal => @ptrToInt(window.renderer.texture_greyscale.value), + renderer.OpenGL => @intCast(usize, surface.renderer.texture.id), + renderer.Metal => @ptrToInt(surface.renderer.texture_greyscale.value), else => @compileError("renderer unsupported, add it!"), }; try self.atlasInfo(atlas, tex); @@ -107,10 +107,10 @@ pub fn update(self: *const DevMode) !void { if (imgui.treeNode("Atlas: Color (Emoji)", .{ .default_open = true })) { defer imgui.treePop(); - const atlas = &window.font_group.atlas_color; + const atlas = &surface.font_group.atlas_color; const tex = switch (Renderer) { - renderer.OpenGL => @intCast(usize, window.renderer.texture_color.id), - renderer.Metal => @ptrToInt(window.renderer.texture_color.value), + renderer.OpenGL => @intCast(usize, surface.renderer.texture_color.id), + renderer.Metal => @ptrToInt(surface.renderer.texture_color.value), else => @compileError("renderer unsupported, add it!"), }; try self.atlasInfo(atlas, tex); diff --git a/src/Surface.zig b/src/Surface.zig index 04da5d0a8..6329a8760 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -34,7 +34,7 @@ const DevMode = @import("DevMode.zig"); const App = @import("App.zig"); const internal_os = @import("os/main.zig"); -const log = std.log.scoped(.window); +const log = std.log.scoped(.surface); // The renderer implementation to use. const Renderer = renderer.Renderer; @@ -56,7 +56,7 @@ font_size: font.face.DesiredSize, /// Imgui context imgui_ctx: if (DevMode.enabled) *imgui.Context else void, -/// The renderer for this window. +/// The renderer for this surface. renderer: Renderer, /// The render state @@ -94,7 +94,7 @@ config: *const Config, /// like such as "control-v" will write a "v" even if they're intercepted. ignore_char: bool = false, -/// Mouse state for the window. +/// Mouse state for the surface. const Mouse = struct { /// The last tracked mouse button state by button. click_state: [input.MouseButton.max]input.MouseButtonState = .{.release} ** input.MouseButton.max, @@ -109,7 +109,7 @@ const Mouse = struct { /// The starting xpos/ypos of the left click. Note that if scrolling occurs, /// these will point to different "cells", but the xpos/ypos will stay - /// stable during scrolling relative to the window. + /// stable during scrolling relative to the surface. left_click_xpos: f64 = 0, left_click_ypos: f64 = 0, @@ -283,7 +283,7 @@ pub fn init( .left = padding_x, }; - // Create our terminal grid with the initial window size + // Create our terminal grid with the initial size var renderer_impl = try Renderer.init(alloc, .{ .config = config, .font_group = font_group, @@ -291,15 +291,15 @@ pub fn init( .explicit = padding, .balance = config.@"window-padding-balance", }, - .window_mailbox = .{ .window = self, .app = app.mailbox }, + .surface_mailbox = .{ .surface = self, .app = app.mailbox }, }); errdefer renderer_impl.deinit(); // Calculate our grid size based on known dimensions. - const window_size = try rt_surface.getSize(); + const surface_size = try rt_surface.getSize(); const screen_size: renderer.ScreenSize = .{ - .width = window_size.width, - .height = window_size.height, + .width = surface_size.width, + .height = surface_size.height, }; const grid_size = renderer.GridSize.init( screen_size.subPadding(padding), @@ -328,7 +328,7 @@ pub fn init( .renderer_state = &self.renderer_state, .renderer_wakeup = render_thread.wakeup, .renderer_mailbox = render_thread.mailbox, - .window_mailbox = .{ .window = self, .app = app.mailbox }, + .surface_mailbox = .{ .surface = self, .app = app.mailbox }, }); errdefer io.deinit(); @@ -336,10 +336,10 @@ pub fn init( var io_thread = try termio.Thread.init(alloc, &self.io); errdefer io_thread.deinit(); - // True if this window is hosting devmode. We only host devmode on - // the first window since imgui is not threadsafe. We need to do some + // True if this surface is hosting devmode. We only host devmode on + // the first surface since imgui is not threadsafe. We need to do some // work to make DevMode work with multiple threads. - const host_devmode = DevMode.enabled and DevMode.instance.window == null; + const host_devmode = DevMode.enabled and DevMode.instance.surface == null; self.* = .{ .alloc = alloc, @@ -383,15 +383,15 @@ pub fn init( }, null); // Call our size callback which handles all our retina setup - // Note: this shouldn't be necessary and when we clean up the window + // Note: this shouldn't be necessary and when we clean up the surface // init stuff we should get rid of this. But this is required because // sizeCallback does retina-aware stuff we don't do here and don't want // to duplicate. - try self.sizeCallback(window_size); + try self.sizeCallback(surface_size); // Load imgui. This must be done LAST because it has to be done after // all our GLFW setup is complete. - if (DevMode.enabled and DevMode.instance.window == null) { + if (DevMode.enabled and DevMode.instance.surface == null) { const dev_io = try imgui.IO.get(); dev_io.cval().IniFilename = "ghostty_dev_mode.ini"; @@ -406,14 +406,14 @@ pub fn init( const style = try imgui.Style.get(); style.colorsDark(); - // Add our window to the instance if it isn't set. - DevMode.instance.window = self; + // Add our surface to the instance if it isn't set. + DevMode.instance.surface = self; // Let our renderer setup try renderer_impl.initDevMode(rt_surface); } - // Give the renderer one more opportunity to finalize any window + // Give the renderer one more opportunity to finalize any surface // setup on the main thread prior to spinning up the rendering thread. try renderer_impl.finalizeSurfaceInit(rt_surface); @@ -434,7 +434,7 @@ pub fn init( self.io_thr.setName("io") catch {}; } -pub fn destroy(self: *Surface) void { +pub fn deinit(self: *Surface) void { // Stop rendering thread { self.renderer_thread.stop.notify() catch |err| @@ -445,12 +445,12 @@ pub fn destroy(self: *Surface) void { self.renderer.threadEnter(self.rt_surface) catch unreachable; // If we are devmode-owning, clean that up. - if (DevMode.enabled and DevMode.instance.window == self) { + if (DevMode.enabled and DevMode.instance.surface == self) { // Let our renderer clean up self.renderer.deinitDevMode(); - // Clear the window - DevMode.instance.window = null; + // Clear the surface + DevMode.instance.surface = null; // Uninitialize imgui self.imgui_ctx.destroy(); @@ -471,19 +471,15 @@ pub fn destroy(self: *Surface) void { self.io_thread.deinit(); self.io.deinit(); - self.rt_surface.deinit(); - self.font_group.deinit(self.alloc); self.font_lib.deinit(); self.alloc.destroy(self.font_group); self.alloc.destroy(self.renderer_state.mutex); - - self.alloc.destroy(self); } /// Called from the app thread to handle mailbox messages to our specific -/// window. +/// surface. pub fn handleMessage(self: *Surface, msg: Message) !void { switch (msg) { .set_title => |*v| { @@ -657,7 +653,7 @@ fn queueRender(self: *const Surface) !void { try self.renderer_thread.wakeup.notify(); } -pub fn sizeCallback(self: *Surface, size: apprt.WindowSize) !void { +pub fn sizeCallback(self: *Surface, size: apprt.SurfaceSize) !void { const tracy = trace(@src()); defer tracy.end(); @@ -1034,8 +1030,8 @@ pub fn scrollCallback(self: *Surface, xoff: f64, yoff: f64) !void { const tracy = trace(@src()); defer tracy.end(); - // If our dev mode window is visible then we always schedule a render on - // cursor move because the cursor might touch our windows. + // If our dev mode surface is visible then we always schedule a render on + // cursor move because the cursor might touch our surfaces. if (DevMode.enabled and DevMode.instance.visible) { try self.queueRender(); @@ -1081,8 +1077,8 @@ fn mouseReport( mods: input.Mods, pos: apprt.CursorPos, ) !void { - // TODO: posToViewport currently clamps to the window boundary, - // do we want to not report mouse events at all outside the window? + // TODO: posToViewport currently clamps to the surface boundary, + // do we want to not report mouse events at all outside the surface? // Depending on the event, we may do nothing at all. switch (self.io.terminal.modes.mouse_event) { @@ -1278,8 +1274,8 @@ pub fn mouseButtonCallback( const tracy = trace(@src()); defer tracy.end(); - // If our dev mode window is visible then we always schedule a render on - // cursor move because the cursor might touch our windows. + // If our dev mode surface is visible then we always schedule a render on + // cursor move because the cursor might touch our surfaces. if (DevMode.enabled and DevMode.instance.visible) { try self.queueRender(); @@ -1395,8 +1391,8 @@ pub fn cursorPosCallback( const tracy = trace(@src()); defer tracy.end(); - // If our dev mode window is visible then we always schedule a render on - // cursor move because the cursor might touch our windows. + // If our dev mode surface is visible then we always schedule a render on + // cursor move because the cursor might touch our surfaces. if (DevMode.enabled and DevMode.instance.visible) { try self.queueRender(); @@ -1608,8 +1604,8 @@ fn dragLeftClickSingle( fn posToViewport(self: Surface, xpos: f64, ypos: f64) terminal.point.Viewport { // xpos and ypos can be negative if while dragging, the user moves the - // mouse off the window. Likewise, they can be larger than our window - // width if the user drags out of the window positively. + // mouse off the surface. Likewise, they can be larger than our surface + // width if the user drags out of the surface positively. return .{ .x = if (xpos < 0) 0 else x: { // Our cell is the mouse divided by cell width diff --git a/src/apprt.zig b/src/apprt.zig index 24b85b00b..01aea84e8 100644 --- a/src/apprt.zig +++ b/src/apprt.zig @@ -17,7 +17,7 @@ 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 surface = @import("apprt/Surface.zig"); +pub const surface = @import("apprt/surface.zig"); /// The implementation to use for the app runtime. This is comptime chosen /// so that every build has exactly one application runtime implementation. diff --git a/src/apprt/glfw.zig b/src/apprt/glfw.zig index 2150711d5..beb560622 100644 --- a/src/apprt/glfw.zig +++ b/src/apprt/glfw.zig @@ -83,6 +83,7 @@ pub const App = struct { // Create the surface -- because windows are surfaces for glfw. try surface.init(self); + errdefer surface.deinit(); } fn glfwErrorCallback(code: glfw.ErrorCode, desc: [:0]const u8) void { @@ -232,12 +233,15 @@ pub const Surface = struct { // Initialize our surface now that we have the stable pointer. try self.core_surface.init(app.app, app.app.config, self); - errdefer self.core_surface.destroy(); + errdefer self.core_surface.deinit(); } pub fn deinit(self: *Surface) void { - var tabgroup_opt: if (builtin.target.isDarwin()) ?objc.Object else void = undefined; - if (comptime builtin.target.isDarwin()) { + // First clean up our core surface so that all the rendering and IO stop. + self.core_surface.deinit(); + + var tabgroup_opt: if (App.Darwin.enabled) ?objc.Object else void = undefined; + if (App.Darwin.enabled) { const nswindow = objc.Object.fromId(glfwNative.getCocoaWindow(self.window).?); const tabgroup = nswindow.getProperty(objc.Object, "tabGroup"); @@ -286,7 +290,7 @@ pub const Surface = struct { /// Note: this interface is not good, we should redo it if we plan /// to use this more. i.e. you can't set max width but no max height, /// or no mins. - pub fn setSizeLimits(self: *Surface, min: apprt.WindowSize, max_: ?apprt.WindowSize) !void { + pub fn setSizeLimits(self: *Surface, min: apprt.SurfaceSize, max_: ?apprt.SurfaceSize) !void { self.window.setSizeLimits(.{ .width = min.width, .height = min.height, @@ -308,9 +312,9 @@ pub const Surface = struct { /// Returns the size of the window in pixels. The pixel size may /// not match screen coordinate size but we should be able to convert /// back and forth using getContentScale. - pub fn getSize(self: *const Surface) !apprt.WindowSize { + pub fn getSize(self: *const Surface) !apprt.SurfaceSize { const size = self.window.getFramebufferSize(); - return apprt.WindowSize{ .width = size.width, .height = size.height }; + return apprt.SurfaceSize{ .width = size.width, .height = size.height }; } /// Returns the cursor position in scaled pixels relative to the diff --git a/src/apprt/structs.zig b/src/apprt/structs.zig index 2cdb8be9e..9ca2434d4 100644 --- a/src/apprt/structs.zig +++ b/src/apprt/structs.zig @@ -6,8 +6,8 @@ pub const ContentScale = struct { y: f32, }; -/// The size of the window in pixels. -pub const WindowSize = struct { +/// The size of the surface in pixels. +pub const SurfaceSize = struct { width: u32, height: u32, }; diff --git a/src/apprt/Surface.zig b/src/apprt/surface.zig similarity index 79% rename from src/apprt/Surface.zig rename to src/apprt/surface.zig index fff956504..f76a4ea06 100644 --- a/src/apprt/Surface.zig +++ b/src/apprt/surface.zig @@ -3,13 +3,13 @@ const Surface = @import("../Surface.zig"); const renderer = @import("../renderer.zig"); const termio = @import("../termio.zig"); -/// The message types that can be sent to a single window. +/// The message types that can be sent to a single surface. pub const Message = union(enum) { /// Represents a write request. Magic number comes from the max size /// we want this union to be. pub const WriteReq = termio.MessageData(u8, 256); - /// Set the title of the window. + /// Set the title of the surface. /// TODO: we should change this to a "WriteReq" style structure in /// the termio message so that we can more efficiently send strings /// of any length @@ -25,25 +25,25 @@ pub const Message = union(enum) { clipboard_write: WriteReq, }; -/// A window mailbox. +/// A surface mailbox. pub const Mailbox = struct { - window: *Surface, + surface: *Surface, app: *App.Mailbox, - /// Send a message to the window. + /// Send a message to the surface. pub fn push(self: Mailbox, msg: Message, timeout: App.Mailbox.Timeout) App.Mailbox.Size { // Surface message sending is actually implemented on the app - // thread, so we have to rewrap the message with our window + // thread, so we have to rewrap the message with our surface // pointer and send it to the app thread. const result = self.app.push(.{ .surface_message = .{ - .surface = self.window, + .surface = self.surface, .message = msg, }, }, timeout); // Wake up our app loop - self.window.app.wakeup(); + self.surface.app.wakeup(); return result; } diff --git a/src/renderer/OpenGL.zig b/src/renderer/OpenGL.zig index 951835998..3c3554fdc 100644 --- a/src/renderer/OpenGL.zig +++ b/src/renderer/OpenGL.zig @@ -89,7 +89,7 @@ focused: bool, padding: renderer.Options.Padding, /// The mailbox for communicating with the window. -window_mailbox: apprt.surface.Mailbox, +surface_mailbox: apprt.surface.Mailbox, /// The raw structure that maps directly to the buffer sent to the vertex shader. /// This must be "extern" so that the field order is not reordered by the @@ -311,7 +311,7 @@ pub fn init(alloc: Allocator, options: renderer.Options) !OpenGL { null, .focused = true, .padding = options.padding, - .window_mailbox = options.window_mailbox, + .surface_mailbox = options.surface_mailbox, }; } @@ -504,7 +504,7 @@ pub fn setFontSize(self: *OpenGL, size: font.face.DesiredSize) !void { self.cell_size = new_cell_size; // Notify the window that the cell size changed. - _ = self.window_mailbox.push(.{ + _ = self.surface_mailbox.push(.{ .cell_size = new_cell_size, }, .{ .forever = {} }); } diff --git a/src/renderer/Options.zig b/src/renderer/Options.zig index 0687d4e99..c8b58c8c7 100644 --- a/src/renderer/Options.zig +++ b/src/renderer/Options.zig @@ -14,12 +14,12 @@ font_group: *font.GroupCache, /// Padding options for the viewport. padding: Padding, -/// The mailbox for sending the window messages. This is only valid +/// The mailbox for sending the surface messages. This is only valid /// once the thread has started and should not be used outside of the thread. -window_mailbox: apprt.surface.Mailbox, +surface_mailbox: apprt.surface.Mailbox, pub const Padding = struct { - // Explicit padding options, in pixels. The windowing thread is + // Explicit padding options, in pixels. The surface thread is // expected to convert points to pixels for a given DPI. explicit: renderer.Padding, diff --git a/src/termio/Exec.zig b/src/termio/Exec.zig index a6a56e90b..86dcf71e8 100644 --- a/src/termio/Exec.zig +++ b/src/termio/Exec.zig @@ -51,8 +51,8 @@ renderer_wakeup: xev.Async, /// The mailbox for notifying the renderer of things. renderer_mailbox: *renderer.Thread.Mailbox, -/// The mailbox for communicating with the window. -window_mailbox: apprt.surface.Mailbox, +/// The mailbox for communicating with the surface. +surface_mailbox: apprt.surface.Mailbox, /// The cached grid size whenever a resize is called. grid_size: renderer.GridSize, @@ -83,7 +83,7 @@ pub fn init(alloc: Allocator, opts: termio.Options) !Exec { .renderer_state = opts.renderer_state, .renderer_wakeup = opts.renderer_wakeup, .renderer_mailbox = opts.renderer_mailbox, - .window_mailbox = opts.window_mailbox, + .surface_mailbox = opts.surface_mailbox, .grid_size = opts.grid_size, .data = null, }; @@ -131,7 +131,7 @@ pub fn threadEnter(self: *Exec, thread: *termio.Thread) !ThreadData { .ev = ev_data_ptr, .terminal = &self.terminal, .grid_size = &self.grid_size, - .window_mailbox = self.window_mailbox, + .surface_mailbox = self.surface_mailbox, }, }, }; @@ -638,7 +638,7 @@ const StreamHandler = struct { alloc: Allocator, grid_size: *renderer.GridSize, terminal: *terminal.Terminal, - window_mailbox: apprt.surface.Mailbox, + surface_mailbox: apprt.surface.Mailbox, /// This is set to true when a message was written to the writer /// mailbox. This can be used by callers to determine if they need @@ -983,7 +983,7 @@ const StreamHandler = struct { std.mem.copy(u8, &buf, title); buf[title.len] = 0; - _ = self.window_mailbox.push(.{ + _ = self.surface_mailbox.push(.{ .set_title = buf, }, .{ .forever = {} }); } @@ -995,14 +995,14 @@ const StreamHandler = struct { // Get clipboard contents if (data.len == 1 and data[0] == '?') { - _ = self.window_mailbox.push(.{ + _ = self.surface_mailbox.push(.{ .clipboard_read = kind, }, .{ .forever = {} }); return; } // Write clipboard contents - _ = self.window_mailbox.push(.{ + _ = self.surface_mailbox.push(.{ .clipboard_write = try apprt.surface.Message.WriteReq.init( self.alloc, data, diff --git a/src/termio/Options.zig b/src/termio/Options.zig index df90a8369..06af6b4d0 100644 --- a/src/termio/Options.zig +++ b/src/termio/Options.zig @@ -15,7 +15,7 @@ screen_size: renderer.ScreenSize, config: *const Config, /// The render state. The IO implementation can modify anything here. The -/// window thread will setup the initial "terminal" pointer but the IO impl +/// surface thread will setup the initial "terminal" pointer but the IO impl /// is free to change that if that is useful (i.e. doing some sort of dual /// terminal implementation.) renderer_state: *renderer.State, @@ -27,5 +27,5 @@ renderer_wakeup: xev.Async, /// The mailbox for renderer messages. renderer_mailbox: *renderer.Thread.Mailbox, -/// The mailbox for sending the window messages. -window_mailbox: apprt.surface.Mailbox, +/// The mailbox for sending the surface messages. +surface_mailbox: apprt.surface.Mailbox, From 9e4560043a58199ff6d4fd2702c85b7a3a0aa81e Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 22 Feb 2023 14:58:20 -0800 Subject: [PATCH 08/37] fix crashes on close --- src/App.zig | 4 +--- src/apprt/glfw.zig | 11 ++++++++++- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/App.zig b/src/App.zig index 6819d5059..c28faebf7 100644 --- a/src/App.zig +++ b/src/App.zig @@ -109,9 +109,7 @@ pub fn tick(self: *App, rt_app: *apprt.runtime.App) !bool { while (i < self.surfaces.items.len) { const surface = self.surfaces.items[i]; if (surface.shouldClose()) { - surface.deinit(); - _ = self.surfaces.swapRemove(i); - self.surface_pool.destroy(surface); + rt_app.closeSurface(surface); continue; } diff --git a/src/apprt/glfw.zig b/src/apprt/glfw.zig index beb560622..0aa2b5f44 100644 --- a/src/apprt/glfw.zig +++ b/src/apprt/glfw.zig @@ -86,6 +86,12 @@ pub const App = struct { errdefer surface.deinit(); } + /// Close the given surface. + pub fn closeSurface(self: *App, surface: *Surface) void { + surface.deinit(); + self.app.surface_pool.destroy(surface); + } + fn glfwErrorCallback(code: glfw.ErrorCode, desc: [:0]const u8) void { std.log.warn("glfw error={} message={s}", .{ code, desc }); @@ -237,7 +243,10 @@ pub const Surface = struct { } pub fn deinit(self: *Surface) void { - // First clean up our core surface so that all the rendering and IO stop. + // Remove ourselves from the list of known surfaces in the app. + self.core_surface.app.deleteSurface(self); + + // Clean up our core surface so that all the rendering and IO stop. self.core_surface.deinit(); var tabgroup_opt: if (App.Darwin.enabled) ?objc.Object else void = undefined; From 053748481aed950536692f4a2e2b5e86a3391489 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 22 Feb 2023 15:16:17 -0800 Subject: [PATCH 09/37] more crap --- src/App.zig | 24 +- src/Window.zig | 1679 ---------------------------------------- src/apprt/embedded.zig | 50 +- src/apprt/glfw.zig | 38 +- src/apprt/gtk.zig | 24 +- src/main.zig | 2 +- src/renderer/Metal.zig | 40 +- 7 files changed, 99 insertions(+), 1758 deletions(-) delete mode 100644 src/Window.zig diff --git a/src/App.zig b/src/App.zig index c28faebf7..94efe5fd1 100644 --- a/src/App.zig +++ b/src/App.zig @@ -164,9 +164,9 @@ fn drainMailbox(self: *App, rt_app: *apprt.runtime.App) !void { switch (message) { .new_window => |msg| { _ = msg; // TODO - try rt_app.newWindow(); + _ = try rt_app.newWindow(); }, - .new_tab => |msg| try self.newTab(msg), + .new_tab => |msg| try self.newTab(rt_app, msg), .quit => try self.setQuit(), .surface_message => |msg| try self.surfaceMessage(msg.surface, msg.message), } @@ -174,19 +174,7 @@ fn drainMailbox(self: *App, rt_app: *apprt.runtime.App) !void { } /// Create a new tab in the parent window -fn newTab(self: *App, msg: Message.NewWindow) !void { - if (comptime !builtin.target.isDarwin()) { - log.warn("tabbing is not supported on this platform", .{}); - return; - } - - // In embedded mode, it is up to the embedder to implement tabbing - // on their own. - if (comptime build_config.artifact != .exe) { - log.warn("tabbing is not supported in embedded mode", .{}); - return; - } - +fn newTab(self: *App, rt_app: *apprt.runtime.App, msg: Message.NewWindow) !void { const parent = msg.parent orelse { log.warn("parent must be set in new_tab message", .{}); return; @@ -198,11 +186,7 @@ fn newTab(self: *App, msg: Message.NewWindow) !void { return; } - // Create the new window - const window = try self.newWindow(msg); - - // Add the window to our parent tab group - parent.addWindow(window); + try rt_app.newTab(parent); } /// Start quitting diff --git a/src/Window.zig b/src/Window.zig deleted file mode 100644 index a769343fc..000000000 --- a/src/Window.zig +++ /dev/null @@ -1,1679 +0,0 @@ -//! Window represents a single OS window. -//! -//! NOTE(multi-window): This may be premature, but this abstraction is here -//! to pave the way One Day(tm) for multi-window support. At the time of -//! writing, we support exactly one window. -const Window = @This(); - -// TODO: eventually, I want to extract Window.zig into the "window" package -// so we can also have alternate implementations (i.e. not glfw). -const apprt = @import("apprt.zig"); -pub const Mailbox = apprt.Window.Mailbox; -pub const Message = apprt.Window.Message; - -const std = @import("std"); -const builtin = @import("builtin"); -const assert = std.debug.assert; -const Allocator = std.mem.Allocator; -const renderer = @import("renderer.zig"); -const termio = @import("termio.zig"); -const objc = @import("objc"); -const glfw = @import("glfw"); -const imgui = @import("imgui"); -const Pty = @import("Pty.zig"); -const font = @import("font/main.zig"); -const Command = @import("Command.zig"); -const trace = @import("tracy").trace; -const terminal = @import("terminal/main.zig"); -const Config = @import("config.zig").Config; -const input = @import("input.zig"); -const DevMode = @import("DevMode.zig"); -const App = @import("App.zig"); -const internal_os = @import("os/main.zig"); - -// Get native API access on certain platforms so we can do more customization. -const glfwNative = glfw.Native(.{ - .cocoa = builtin.target.isDarwin(), -}); - -const log = std.log.scoped(.window); - -// The renderer implementation to use. -const Renderer = renderer.Renderer; - -/// Allocator -alloc: Allocator, - -/// The app that this window is a part of. -app: *App, - -/// The windowing system state -window: apprt.runtime.Window, - -/// The font structures -font_lib: font.Library, -font_group: *font.GroupCache, -font_size: font.face.DesiredSize, - -/// Imgui context -imgui_ctx: if (DevMode.enabled) *imgui.Context else void, - -/// The renderer for this window. -renderer: Renderer, - -/// The render state -renderer_state: renderer.State, - -/// The renderer thread manager -renderer_thread: renderer.Thread, - -/// The actual thread -renderer_thr: std.Thread, - -/// Mouse state. -mouse: Mouse, -mouse_interval: u64, - -/// The terminal IO handler. -io: termio.Impl, -io_thread: termio.Thread, -io_thr: std.Thread, - -/// All the cached sizes since we need them at various times. -screen_size: renderer.ScreenSize, -grid_size: renderer.GridSize, -cell_size: renderer.CellSize, - -/// Explicit padding due to configuration -padding: renderer.Padding, - -/// The app configuration -config: *const Config, - -/// Set to true for a single GLFW key/char callback cycle to cause the -/// char callback to ignore. GLFW seems to always do key followed by char -/// callbacks so we abuse that here. This is to solve an issue where commands -/// like such as "control-v" will write a "v" even if they're intercepted. -ignore_char: bool = false, - -/// Mouse state for the window. -const Mouse = struct { - /// The last tracked mouse button state by button. - click_state: [input.MouseButton.max]input.MouseButtonState = .{.release} ** input.MouseButton.max, - - /// The last mods state when the last mouse button (whatever it was) was - /// pressed or release. - mods: input.Mods = .{}, - - /// The point at which the left mouse click happened. This is in screen - /// coordinates so that scrolling preserves the location. - left_click_point: terminal.point.ScreenPoint = .{}, - - /// The starting xpos/ypos of the left click. Note that if scrolling occurs, - /// these will point to different "cells", but the xpos/ypos will stay - /// stable during scrolling relative to the window. - left_click_xpos: f64 = 0, - left_click_ypos: f64 = 0, - - /// The count of clicks to count double and triple clicks and so on. - /// The left click time was the last time the left click was done. This - /// is always set on the first left click. - left_click_count: u8 = 0, - left_click_time: std.time.Instant = undefined, - - /// The last x/y sent for mouse reports. - event_point: terminal.point.Viewport = .{}, -}; - -/// Create a new window. This allocates and returns a pointer because we -/// need a stable pointer for user data callbacks. Therefore, a stack-only -/// initialization is not currently possible. -pub fn create( - alloc: Allocator, - app: *App, - config: *const Config, - rt_opts: apprt.runtime.Window.Options, -) !*Window { - var self = try alloc.create(Window); - errdefer alloc.destroy(self); - - // Create the windowing system - var window = try apprt.runtime.Window.init(app, self, rt_opts); - errdefer window.deinit(); - - // Initialize our renderer with our initialized windowing system. - try Renderer.windowInit(window); - - // Determine our DPI configurations so we can properly configure - // font points to pixels and handle other high-DPI scaling factors. - const content_scale = try window.getContentScale(); - const x_dpi = content_scale.x * font.face.default_dpi; - const y_dpi = content_scale.y * font.face.default_dpi; - log.debug("xscale={} yscale={} xdpi={} ydpi={}", .{ - content_scale.x, - content_scale.y, - x_dpi, - y_dpi, - }); - - // The font size we desire along with the DPI determiend for the window - const font_size: font.face.DesiredSize = .{ - .points = config.@"font-size", - .xdpi = @floatToInt(u16, x_dpi), - .ydpi = @floatToInt(u16, y_dpi), - }; - - // Find all the fonts for this window - // - // Future: we can share the font group amongst all windows to save - // some new window init time and some memory. This will require making - // thread-safe changes to font structs. - var font_lib = try font.Library.init(); - errdefer font_lib.deinit(); - var font_group = try alloc.create(font.GroupCache); - errdefer alloc.destroy(font_group); - font_group.* = try font.GroupCache.init(alloc, group: { - var group = try font.Group.init(alloc, font_lib, font_size); - errdefer group.deinit(); - - // Search for fonts - if (font.Discover != void) { - var disco = font.Discover.init(); - group.discover = disco; - - if (config.@"font-family") |family| { - var disco_it = try disco.discover(.{ - .family = family, - .size = font_size.points, - }); - defer disco_it.deinit(); - if (try disco_it.next()) |face| { - log.info("font regular: {s}", .{try face.name()}); - try group.addFace(alloc, .regular, face); - } - } - if (config.@"font-family-bold") |family| { - var disco_it = try disco.discover(.{ - .family = family, - .size = font_size.points, - .bold = true, - }); - defer disco_it.deinit(); - if (try disco_it.next()) |face| { - log.info("font bold: {s}", .{try face.name()}); - try group.addFace(alloc, .bold, face); - } - } - if (config.@"font-family-italic") |family| { - var disco_it = try disco.discover(.{ - .family = family, - .size = font_size.points, - .italic = true, - }); - defer disco_it.deinit(); - if (try disco_it.next()) |face| { - log.info("font italic: {s}", .{try face.name()}); - try group.addFace(alloc, .italic, face); - } - } - if (config.@"font-family-bold-italic") |family| { - var disco_it = try disco.discover(.{ - .family = family, - .size = font_size.points, - .bold = true, - .italic = true, - }); - defer disco_it.deinit(); - if (try disco_it.next()) |face| { - log.info("font bold+italic: {s}", .{try face.name()}); - try group.addFace(alloc, .bold_italic, face); - } - } - } - - // Our built-in font will be used as a backup - try group.addFace( - alloc, - .regular, - font.DeferredFace.initLoaded(try font.Face.init(font_lib, face_ttf, font_size)), - ); - try group.addFace( - alloc, - .bold, - font.DeferredFace.initLoaded(try font.Face.init(font_lib, face_bold_ttf, font_size)), - ); - - // Emoji fallback. We don't include this on Mac since Mac is expected - // to always have the Apple Emoji available. - if (builtin.os.tag != .macos or font.Discover == void) { - try group.addFace( - alloc, - .regular, - font.DeferredFace.initLoaded(try font.Face.init(font_lib, face_emoji_ttf, font_size)), - ); - try group.addFace( - alloc, - .regular, - font.DeferredFace.initLoaded(try font.Face.init(font_lib, face_emoji_text_ttf, font_size)), - ); - } - - // If we're on Mac, then we try to use the Apple Emoji font for Emoji. - if (builtin.os.tag == .macos and font.Discover != void) { - var disco = font.Discover.init(); - defer disco.deinit(); - var disco_it = try disco.discover(.{ - .family = "Apple Color Emoji", - .size = font_size.points, - }); - defer disco_it.deinit(); - if (try disco_it.next()) |face| { - log.debug("font emoji: {s}", .{try face.name()}); - try group.addFace(alloc, .regular, face); - } - } - - break :group group; - }); - errdefer font_group.deinit(alloc); - - // Pre-calculate our initial cell size ourselves. - const cell_size = try renderer.CellSize.init(alloc, font_group); - - // Convert our padding from points to pixels - const padding_x = (@intToFloat(f32, config.@"window-padding-x") * x_dpi) / 72; - const padding_y = (@intToFloat(f32, config.@"window-padding-y") * y_dpi) / 72; - const padding: renderer.Padding = .{ - .top = padding_y, - .bottom = padding_y, - .right = padding_x, - .left = padding_x, - }; - - // Create our terminal grid with the initial window size - var renderer_impl = try Renderer.init(alloc, .{ - .config = config, - .font_group = font_group, - .padding = .{ - .explicit = padding, - .balance = config.@"window-padding-balance", - }, - .window_mailbox = .{ .window = self, .app = app.mailbox }, - }); - errdefer renderer_impl.deinit(); - - // Calculate our grid size based on known dimensions. - const window_size = try window.getSize(); - const screen_size: renderer.ScreenSize = .{ - .width = window_size.width, - .height = window_size.height, - }; - const grid_size = renderer.GridSize.init( - screen_size.subPadding(padding), - cell_size, - ); - - // The mutex used to protect our renderer state. - var mutex = try alloc.create(std.Thread.Mutex); - mutex.* = .{}; - errdefer alloc.destroy(mutex); - - // Create the renderer thread - var render_thread = try renderer.Thread.init( - alloc, - window, - &self.renderer, - &self.renderer_state, - ); - errdefer render_thread.deinit(); - - // Start our IO implementation - var io = try termio.Impl.init(alloc, .{ - .grid_size = grid_size, - .screen_size = screen_size, - .config = config, - .renderer_state = &self.renderer_state, - .renderer_wakeup = render_thread.wakeup, - .renderer_mailbox = render_thread.mailbox, - .window_mailbox = .{ .window = self, .app = app.mailbox }, - }); - errdefer io.deinit(); - - // Create the IO thread - var io_thread = try termio.Thread.init(alloc, &self.io); - errdefer io_thread.deinit(); - - // True if this window is hosting devmode. We only host devmode on - // the first window since imgui is not threadsafe. We need to do some - // work to make DevMode work with multiple threads. - const host_devmode = DevMode.enabled and DevMode.instance.window == null; - - self.* = .{ - .alloc = alloc, - .app = app, - .window = window, - .font_lib = font_lib, - .font_group = font_group, - .font_size = font_size, - .renderer = renderer_impl, - .renderer_thread = render_thread, - .renderer_state = .{ - .mutex = mutex, - .cursor = .{ - .style = .blinking_block, - .visible = true, - }, - .terminal = &self.io.terminal, - .devmode = if (!host_devmode) null else &DevMode.instance, - }, - .renderer_thr = undefined, - .mouse = .{}, - .mouse_interval = config.@"click-repeat-interval" * 1_000_000, // 500ms - .io = io, - .io_thread = io_thread, - .io_thr = undefined, - .screen_size = screen_size, - .grid_size = grid_size, - .cell_size = cell_size, - .padding = padding, - .config = config, - - .imgui_ctx = if (!DevMode.enabled) {} else try imgui.Context.create(), - }; - errdefer if (DevMode.enabled) self.imgui_ctx.destroy(); - - // Set a minimum size that is cols=10 h=4. This matches Mac's Terminal.app - // but is otherwise somewhat arbitrary. - try window.setSizeLimits(.{ - .width = @floatToInt(u32, cell_size.width * 10), - .height = @floatToInt(u32, cell_size.height * 4), - }, null); - - // Call our size callback which handles all our retina setup - // Note: this shouldn't be necessary and when we clean up the window - // init stuff we should get rid of this. But this is required because - // sizeCallback does retina-aware stuff we don't do here and don't want - // to duplicate. - try self.sizeCallback(window_size); - - // Load imgui. This must be done LAST because it has to be done after - // all our GLFW setup is complete. - if (DevMode.enabled and DevMode.instance.window == null) { - const dev_io = try imgui.IO.get(); - dev_io.cval().IniFilename = "ghostty_dev_mode.ini"; - - // Add our built-in fonts so it looks slightly better - const dev_atlas = @ptrCast(*imgui.FontAtlas, dev_io.cval().Fonts); - dev_atlas.addFontFromMemoryTTF( - face_ttf, - @intToFloat(f32, font_size.pixels()), - ); - - // Default dark style - const style = try imgui.Style.get(); - style.colorsDark(); - - // Add our window to the instance if it isn't set. - DevMode.instance.window = self; - - // Let our renderer setup - try renderer_impl.initDevMode(window); - } - - // Give the renderer one more opportunity to finalize any window - // setup on the main thread prior to spinning up the rendering thread. - try renderer_impl.finalizeWindowInit(window); - - // Start our renderer thread - self.renderer_thr = try std.Thread.spawn( - .{}, - renderer.Thread.threadMain, - .{&self.renderer_thread}, - ); - self.renderer_thr.setName("renderer") catch {}; - - // Start our IO thread - self.io_thr = try std.Thread.spawn( - .{}, - termio.Thread.threadMain, - .{&self.io_thread}, - ); - self.io_thr.setName("io") catch {}; - - return self; -} - -pub fn destroy(self: *Window) void { - // Stop rendering thread - { - self.renderer_thread.stop.notify() catch |err| - log.err("error notifying renderer thread to stop, may stall err={}", .{err}); - self.renderer_thr.join(); - - // We need to become the active rendering thread again - self.renderer.threadEnter(self.window) catch unreachable; - - // If we are devmode-owning, clean that up. - if (DevMode.enabled and DevMode.instance.window == self) { - // Let our renderer clean up - self.renderer.deinitDevMode(); - - // Clear the window - DevMode.instance.window = null; - - // Uninitialize imgui - self.imgui_ctx.destroy(); - } - } - - // Stop our IO thread - { - self.io_thread.stop.notify() catch |err| - log.err("error notifying io thread to stop, may stall err={}", .{err}); - self.io_thr.join(); - } - - // We need to deinit AFTER everything is stopped, since there are - // shared values between the two threads. - self.renderer_thread.deinit(); - self.renderer.deinit(); - self.io_thread.deinit(); - self.io.deinit(); - - self.window.deinit(); - - self.font_group.deinit(self.alloc); - self.font_lib.deinit(); - self.alloc.destroy(self.font_group); - - self.alloc.destroy(self.renderer_state.mutex); - - self.alloc.destroy(self); -} - -pub fn shouldClose(self: Window) bool { - return self.window.shouldClose(); -} - -/// Add a window to the tab group of this window. -pub fn addWindow(self: *Window, other: *Window) void { - assert(builtin.target.isDarwin()); - - // This has a hard dependency on GLFW currently. If we want to support - // this in other windowing systems we should abstract this. This is NOT - // the right interface. - const self_win = glfwNative.getCocoaWindow(self.window.window).?; - const other_win = glfwNative.getCocoaWindow(other.window.window).?; - - const NSWindowOrderingMode = enum(isize) { below = -1, out = 0, above = 1 }; - const nswindow = objc.Object.fromId(self_win); - nswindow.msgSend(void, objc.sel("addTabbedWindow:ordered:"), .{ - objc.Object.fromId(other_win), - NSWindowOrderingMode.above, - }); - - // Adding a new tab can cause the tab bar to appear which changes - // our viewport size. We need to call the size callback in order to - // update values. For example, we need this to set the proper mouse selection - // point in the grid. - const size = self.window.getSize() catch |err| { - log.err("error querying window size for size callback on new tab err={}", .{err}); - return; - }; - self.sizeCallback(size) catch |err| { - log.err("error in size callback from new tab err={}", .{err}); - return; - }; -} - -/// Called from the app thread to handle mailbox messages to our specific -/// window. -pub fn handleMessage(self: *Window, msg: Message) !void { - switch (msg) { - .set_title => |*v| { - // The ptrCast just gets sliceTo to return the proper type. - // We know that our title should end in 0. - const slice = std.mem.sliceTo(@ptrCast([*:0]const u8, v), 0); - log.debug("changing title \"{s}\"", .{slice}); - try self.window.setTitle(slice); - }, - - .cell_size => |size| try self.setCellSize(size), - - .clipboard_read => |kind| try self.clipboardRead(kind), - - .clipboard_write => |req| switch (req) { - .small => |v| try self.clipboardWrite(v.data[0..v.len]), - .stable => |v| try self.clipboardWrite(v), - .alloc => |v| { - defer v.alloc.free(v.data); - try self.clipboardWrite(v.data); - }, - }, - } -} - -/// Returns the x/y coordinate of where the IME (Input Method Editor) -/// keyboard should be rendered. -pub fn imePoint(self: *const Window) apprt.IMEPos { - self.renderer_state.mutex.lock(); - const cursor = self.renderer_state.terminal.screen.cursor; - self.renderer_state.mutex.unlock(); - - // TODO: need to handle when scrolling and the cursor is not - // in the visible portion of the screen. - - // Our sizes are all scaled so we need to send the unscaled values back. - const content_scale = self.window.getContentScale() catch .{ .x = 1, .y = 1 }; - - const x: f64 = x: { - // Simple x * cell width gives the top-left corner - var x: f64 = @floatCast(f64, @intToFloat(f32, cursor.x) * self.cell_size.width); - - // We want the midpoint - x += self.cell_size.width / 2; - - // And scale it - x /= content_scale.x; - - break :x x; - }; - - const y: f64 = y: { - // Simple x * cell width gives the top-left corner - var y: f64 = @floatCast(f64, @intToFloat(f32, cursor.y) * self.cell_size.height); - - // We want the bottom - y += self.cell_size.height; - - // And scale it - y /= content_scale.y; - - break :y y; - }; - - return .{ .x = x, .y = y }; -} - -fn clipboardRead(self: *const Window, kind: u8) !void { - if (!self.config.@"clipboard-read") { - log.info("application attempted to read clipboard, but 'clipboard-read' setting is off", .{}); - return; - } - - const data = self.window.getClipboardString() catch |err| { - log.warn("error reading clipboard: {}", .{err}); - return; - }; - - // Even if the clipboard data is empty we reply, since presumably - // the client app is expecting a reply. We first allocate our buffer. - // This must hold the base64 encoded data PLUS the OSC code surrounding it. - const enc = std.base64.standard.Encoder; - const size = enc.calcSize(data.len); - var buf = try self.alloc.alloc(u8, size + 9); // const for OSC - defer self.alloc.free(buf); - - // Wrap our data with the OSC code - const prefix = try std.fmt.bufPrint(buf, "\x1b]52;{c};", .{kind}); - assert(prefix.len == 7); - buf[buf.len - 2] = '\x1b'; - buf[buf.len - 1] = '\\'; - - // Do the base64 encoding - const encoded = enc.encode(buf[prefix.len..], data); - assert(encoded.len == size); - - _ = self.io_thread.mailbox.push(try termio.Message.writeReq( - self.alloc, - buf, - ), .{ .forever = {} }); - self.io_thread.wakeup.notify() catch {}; -} - -fn clipboardWrite(self: *const Window, data: []const u8) !void { - if (!self.config.@"clipboard-write") { - log.info("application attempted to write clipboard, but 'clipboard-write' setting is off", .{}); - return; - } - - const dec = std.base64.standard.Decoder; - - // Build buffer - const size = try dec.calcSizeForSlice(data); - var buf = try self.alloc.allocSentinel(u8, size, 0); - defer self.alloc.free(buf); - buf[buf.len] = 0; - - // Decode - try dec.decode(buf, data); - assert(buf[buf.len] == 0); - - self.window.setClipboardString(buf) catch |err| { - log.err("error setting clipboard string err={}", .{err}); - return; - }; -} - -/// Change the cell size for the terminal grid. This can happen as -/// a result of changing the font size at runtime. -fn setCellSize(self: *Window, size: renderer.CellSize) !void { - // Update our new cell size for future calcs - self.cell_size = size; - - // Update our grid_size - self.grid_size = renderer.GridSize.init( - self.screen_size.subPadding(self.padding), - self.cell_size, - ); - - // Notify the terminal - _ = self.io_thread.mailbox.push(.{ - .resize = .{ - .grid_size = self.grid_size, - .screen_size = self.screen_size, - .padding = self.padding, - }, - }, .{ .forever = {} }); - self.io_thread.wakeup.notify() catch {}; -} - -/// Change the font size. -/// -/// This can only be called from the main thread. -pub fn setFontSize(self: *Window, size: font.face.DesiredSize) void { - // Update our font size so future changes work - self.font_size = size; - - // Notify our render thread of the font size. This triggers everything else. - _ = self.renderer_thread.mailbox.push(.{ - .font_size = size, - }, .{ .forever = {} }); - - // Schedule render which also drains our mailbox - self.queueRender() catch unreachable; -} - -/// This queues a render operation with the renderer thread. The render -/// isn't guaranteed to happen immediately but it will happen as soon as -/// practical. -fn queueRender(self: *const Window) !void { - try self.renderer_thread.wakeup.notify(); -} - -pub fn sizeCallback(self: *Window, size: apprt.WindowSize) !void { - const tracy = trace(@src()); - defer tracy.end(); - - // TODO: if our screen size didn't change, then we should avoid the - // overhead of inter-thread communication - - // Save our screen size - self.screen_size = .{ - .width = size.width, - .height = size.height, - }; - - // Recalculate our grid size - self.grid_size = renderer.GridSize.init( - self.screen_size.subPadding(self.padding), - self.cell_size, - ); - if (self.grid_size.columns < 5 and (self.padding.left > 0 or self.padding.right > 0)) { - log.warn("WARNING: very small terminal grid detected with padding " ++ - "set. Is your padding reasonable?", .{}); - } - if (self.grid_size.rows < 2 and (self.padding.top > 0 or self.padding.bottom > 0)) { - log.warn("WARNING: very small terminal grid detected with padding " ++ - "set. Is your padding reasonable?", .{}); - } - - // Mail the renderer - _ = self.renderer_thread.mailbox.push(.{ - .screen_size = self.screen_size, - }, .{ .forever = {} }); - try self.queueRender(); - - // Mail the IO thread - _ = self.io_thread.mailbox.push(.{ - .resize = .{ - .grid_size = self.grid_size, - .screen_size = self.screen_size, - .padding = self.padding, - }, - }, .{ .forever = {} }); - try self.io_thread.wakeup.notify(); -} - -pub fn charCallback(self: *Window, codepoint: u21) !void { - const tracy = trace(@src()); - defer tracy.end(); - - // Dev Mode - if (DevMode.enabled and DevMode.instance.visible) { - // If the event was handled by imgui, ignore it. - if (imgui.IO.get()) |io| { - if (io.cval().WantCaptureKeyboard) { - try self.queueRender(); - } - } else |_| {} - } - - // Ignore if requested. See field docs for more information. - if (self.ignore_char) { - self.ignore_char = false; - return; - } - - // Critical area - { - self.renderer_state.mutex.lock(); - defer self.renderer_state.mutex.unlock(); - - // Clear the selction if we have one. - if (self.io.terminal.selection != null) { - self.io.terminal.selection = null; - try self.queueRender(); - } - - // We want to scroll to the bottom - // TODO: detect if we're at the bottom to avoid the render call here. - try self.io.terminal.scrollViewport(.{ .bottom = {} }); - } - - // Ask our IO thread to write the data - var data: termio.Message.WriteReq.Small.Array = undefined; - const len = try std.unicode.utf8Encode(codepoint, &data); - _ = self.io_thread.mailbox.push(.{ - .write_small = .{ - .data = data, - .len = len, - }, - }, .{ .forever = {} }); - - // After sending all our messages we have to notify our IO thread - try self.io_thread.wakeup.notify(); -} - -pub fn keyCallback( - self: *Window, - action: input.Action, - key: input.Key, - mods: input.Mods, -) !void { - const tracy = trace(@src()); - defer tracy.end(); - - // Dev Mode - if (DevMode.enabled and DevMode.instance.visible) { - // If the event was handled by imgui, ignore it. - if (imgui.IO.get()) |io| { - if (io.cval().WantCaptureKeyboard) { - try self.queueRender(); - } - } else |_| {} - } - - // Reset the ignore char setting. If we didn't handle the char - // by here, we aren't going to get it so we just reset this. - self.ignore_char = false; - - if (action == .press or action == .repeat) { - const trigger: input.Binding.Trigger = .{ - .mods = mods, - .key = key, - }; - - //log.warn("BINDING TRIGGER={}", .{trigger}); - if (self.config.keybind.set.get(trigger)) |binding_action| { - //log.warn("BINDING ACTION={}", .{binding_action}); - - switch (binding_action) { - .unbind => unreachable, - .ignore => {}, - - .csi => |data| { - _ = self.io_thread.mailbox.push(.{ - .write_stable = "\x1B[", - }, .{ .forever = {} }); - _ = self.io_thread.mailbox.push(.{ - .write_stable = data, - }, .{ .forever = {} }); - try self.io_thread.wakeup.notify(); - }, - - .cursor_key => |ck| { - // We send a different sequence depending on if we're - // in cursor keys mode. We're in "normal" mode if cursor - // keys mdoe is NOT set. - const normal = normal: { - self.renderer_state.mutex.lock(); - defer self.renderer_state.mutex.unlock(); - break :normal !self.io.terminal.modes.cursor_keys; - }; - - if (normal) { - _ = self.io_thread.mailbox.push(.{ - .write_stable = ck.normal, - }, .{ .forever = {} }); - } else { - _ = self.io_thread.mailbox.push(.{ - .write_stable = ck.application, - }, .{ .forever = {} }); - } - - try self.io_thread.wakeup.notify(); - }, - - .copy_to_clipboard => { - // We can read from the renderer state without holding - // the lock because only we will write to this field. - if (self.io.terminal.selection) |sel| { - var buf = self.io.terminal.screen.selectionString( - self.alloc, - sel, - self.config.@"clipboard-trim-trailing-spaces", - ) catch |err| { - log.err("error reading selection string err={}", .{err}); - return; - }; - defer self.alloc.free(buf); - - self.window.setClipboardString(buf) catch |err| { - log.err("error setting clipboard string err={}", .{err}); - return; - }; - } - }, - - .paste_from_clipboard => { - const data = self.window.getClipboardString() catch |err| { - log.warn("error reading clipboard: {}", .{err}); - return; - }; - - if (data.len > 0) { - const bracketed = bracketed: { - self.renderer_state.mutex.lock(); - defer self.renderer_state.mutex.unlock(); - break :bracketed self.io.terminal.modes.bracketed_paste; - }; - - if (bracketed) { - _ = self.io_thread.mailbox.push(.{ - .write_stable = "\x1B[200~", - }, .{ .forever = {} }); - } - - _ = self.io_thread.mailbox.push(try termio.Message.writeReq( - self.alloc, - data, - ), .{ .forever = {} }); - - if (bracketed) { - _ = self.io_thread.mailbox.push(.{ - .write_stable = "\x1B[201~", - }, .{ .forever = {} }); - } - - try self.io_thread.wakeup.notify(); - } - }, - - .increase_font_size => |delta| { - log.debug("increase font size={}", .{delta}); - - var size = self.font_size; - size.points +|= delta; - self.setFontSize(size); - }, - - .decrease_font_size => |delta| { - log.debug("decrease font size={}", .{delta}); - - var size = self.font_size; - size.points = @max(1, size.points -| delta); - self.setFontSize(size); - }, - - .reset_font_size => { - log.debug("reset font size", .{}); - - var size = self.font_size; - size.points = self.config.@"font-size"; - self.setFontSize(size); - }, - - .toggle_dev_mode => if (DevMode.enabled) { - DevMode.instance.visible = !DevMode.instance.visible; - try self.queueRender(); - } else log.warn("dev mode was not compiled into this binary", .{}), - - .new_window => { - _ = self.app.mailbox.push(.{ - .new_window = .{ - .font_size = if (self.config.@"window-inherit-font-size") - self.font_size - else - null, - }, - }, .{ .instant = {} }); - self.app.wakeup(); - }, - - .new_tab => { - _ = self.app.mailbox.push(.{ - .new_tab = .{ - .parent = self, - - .font_size = if (self.config.@"window-inherit-font-size") - self.font_size - else - null, - }, - }, .{ .instant = {} }); - self.app.wakeup(); - }, - - .close_window => self.window.setShouldClose(), - - .quit => { - _ = self.app.mailbox.push(.{ - .quit = {}, - }, .{ .instant = {} }); - self.app.wakeup(); - }, - } - - // Bindings always result in us ignoring the char if printable - self.ignore_char = true; - - // No matter what, if there is a binding then we are done. - return; - } - - // Handle non-printables - const char: u8 = char: { - const mods_int = @bitCast(u8, mods); - const ctrl_only = @bitCast(u8, input.Mods{ .ctrl = true }); - - // If we're only pressing control, check if this is a character - // we convert to a non-printable. - if (mods_int == ctrl_only) { - const val: u8 = switch (key) { - .a => 0x01, - .b => 0x02, - .c => 0x03, - .d => 0x04, - .e => 0x05, - .f => 0x06, - .g => 0x07, - .h => 0x08, - .i => 0x09, - .j => 0x0A, - .k => 0x0B, - .l => 0x0C, - .m => 0x0D, - .n => 0x0E, - .o => 0x0F, - .p => 0x10, - .q => 0x11, - .r => 0x12, - .s => 0x13, - .t => 0x14, - .u => 0x15, - .v => 0x16, - .w => 0x17, - .x => 0x18, - .y => 0x19, - .z => 0x1A, - else => 0, - }; - - if (val > 0) break :char val; - } - - // Otherwise, we don't care what modifiers we press we do this. - break :char @as(u8, switch (key) { - .backspace => 0x7F, - .enter => '\r', - .tab => '\t', - .escape => 0x1B, - else => 0, - }); - }; - if (char > 0) { - // Ask our IO thread to write the data - var data: termio.Message.WriteReq.Small.Array = undefined; - data[0] = @intCast(u8, char); - _ = self.io_thread.mailbox.push(.{ - .write_small = .{ - .data = data, - .len = 1, - }, - }, .{ .forever = {} }); - - // After sending all our messages we have to notify our IO thread - try self.io_thread.wakeup.notify(); - } - } -} - -pub fn focusCallback(self: *Window, focused: bool) !void { - // Notify our render thread of the new state - _ = self.renderer_thread.mailbox.push(.{ - .focus = focused, - }, .{ .forever = {} }); - - // Schedule render which also drains our mailbox - try self.queueRender(); -} - -pub fn refreshCallback(self: *Window) !void { - // The point of this callback is to schedule a render, so do that. - try self.queueRender(); -} - -pub fn scrollCallback(self: *Window, xoff: f64, yoff: f64) !void { - const tracy = trace(@src()); - defer tracy.end(); - - // If our dev mode window is visible then we always schedule a render on - // cursor move because the cursor might touch our windows. - if (DevMode.enabled and DevMode.instance.visible) { - try self.queueRender(); - - // If the mouse event was handled by imgui, ignore it. - if (imgui.IO.get()) |io| { - if (io.cval().WantCaptureMouse) return; - } else |_| {} - } - - //log.info("SCROLL: {} {}", .{ xoff, yoff }); - _ = xoff; - - // Positive is up - const sign: isize = if (yoff > 0) -1 else 1; - const delta: isize = sign * @max(@divFloor(self.grid_size.rows, 15), 1); - log.info("scroll: delta={}", .{delta}); - - { - self.renderer_state.mutex.lock(); - defer self.renderer_state.mutex.unlock(); - - // Modify our viewport, this requires a lock since it affects rendering - try self.io.terminal.scrollViewport(.{ .delta = delta }); - - // If we're scrolling up or down, then send a mouse event. This requires - // a lock since we read terminal state. - if (yoff != 0) { - const pos = try self.window.getCursorPos(); - try self.mouseReport(if (yoff < 0) .five else .four, .press, self.mouse.mods, pos); - } - } - - try self.queueRender(); -} - -/// The type of action to report for a mouse event. -const MouseReportAction = enum { press, release, motion }; - -fn mouseReport( - self: *Window, - button: ?input.MouseButton, - action: MouseReportAction, - mods: input.Mods, - pos: apprt.CursorPos, -) !void { - // TODO: posToViewport currently clamps to the window boundary, - // do we want to not report mouse events at all outside the window? - - // Depending on the event, we may do nothing at all. - switch (self.io.terminal.modes.mouse_event) { - .none => return, - - // X10 only reports clicks with mouse button 1, 2, 3. We verify - // the button later. - .x10 => if (action != .press or - button == null or - !(button.? == .left or - button.? == .right or - button.? == .middle)) return, - - // Doesn't report motion - .normal => if (action == .motion) return, - - // Button must be pressed - .button => if (button == null) return, - - // Everything - .any => {}, - } - - // This format reports X/Y - const viewport_point = self.posToViewport(pos.x, pos.y); - - // Record our new point - self.mouse.event_point = viewport_point; - - // Get the code we'll actually write - const button_code: u8 = code: { - var acc: u8 = 0; - - // Determine our initial button value - if (button == null) { - // Null button means motion without a button pressed - acc = 3; - } else if (action == .release and self.io.terminal.modes.mouse_format != .sgr) { - // Release is 3. It is NOT 3 in SGR mode because SGR can tell - // the application what button was released. - acc = 3; - } else { - acc = switch (button.?) { - .left => 0, - .right => 1, - .middle => 2, - .four => 64, - .five => 65, - else => return, // unsupported - }; - } - - // X10 doesn't have modifiers - if (self.io.terminal.modes.mouse_event != .x10) { - if (mods.shift) acc += 4; - if (mods.super) acc += 8; - if (mods.ctrl) acc += 16; - } - - // Motion adds another bit - if (action == .motion) acc += 32; - - break :code acc; - }; - - switch (self.io.terminal.modes.mouse_format) { - .x10 => { - if (viewport_point.x > 222 or viewport_point.y > 222) { - log.info("X10 mouse format can only encode X/Y up to 223", .{}); - return; - } - - // + 1 below is because our x/y is 0-indexed and proto wants 1 - var data: termio.Message.WriteReq.Small.Array = undefined; - assert(data.len >= 5); - data[0] = '\x1b'; - data[1] = '['; - data[2] = 'M'; - data[3] = 32 + button_code; - data[4] = 32 + @intCast(u8, viewport_point.x) + 1; - data[5] = 32 + @intCast(u8, viewport_point.y) + 1; - - // Ask our IO thread to write the data - _ = self.io_thread.mailbox.push(.{ - .write_small = .{ - .data = data, - .len = 5, - }, - }, .{ .forever = {} }); - }, - - .utf8 => { - // Maximum of 12 because at most we have 2 fully UTF-8 encoded chars - var data: termio.Message.WriteReq.Small.Array = undefined; - assert(data.len >= 12); - data[0] = '\x1b'; - data[1] = '['; - data[2] = 'M'; - - // The button code will always fit in a single u8 - data[3] = 32 + button_code; - - // UTF-8 encode the x/y - var i: usize = 4; - i += try std.unicode.utf8Encode(@intCast(u21, 32 + viewport_point.x + 1), data[i..]); - i += try std.unicode.utf8Encode(@intCast(u21, 32 + viewport_point.y + 1), data[i..]); - - // Ask our IO thread to write the data - _ = self.io_thread.mailbox.push(.{ - .write_small = .{ - .data = data, - .len = @intCast(u8, i), - }, - }, .{ .forever = {} }); - }, - - .sgr => { - // Final character to send in the CSI - const final: u8 = if (action == .release) 'm' else 'M'; - - // Response always is at least 4 chars, so this leaves the - // remainder for numbers which are very large... - var data: termio.Message.WriteReq.Small.Array = undefined; - const resp = try std.fmt.bufPrint(&data, "\x1B[<{d};{d};{d}{c}", .{ - button_code, - viewport_point.x + 1, - viewport_point.y + 1, - final, - }); - - // Ask our IO thread to write the data - _ = self.io_thread.mailbox.push(.{ - .write_small = .{ - .data = data, - .len = @intCast(u8, resp.len), - }, - }, .{ .forever = {} }); - }, - - .urxvt => { - // Response always is at least 4 chars, so this leaves the - // remainder for numbers which are very large... - var data: termio.Message.WriteReq.Small.Array = undefined; - const resp = try std.fmt.bufPrint(&data, "\x1B[{d};{d};{d}M", .{ - 32 + button_code, - viewport_point.x + 1, - viewport_point.y + 1, - }); - - // Ask our IO thread to write the data - _ = self.io_thread.mailbox.push(.{ - .write_small = .{ - .data = data, - .len = @intCast(u8, resp.len), - }, - }, .{ .forever = {} }); - }, - - .sgr_pixels => { - // Final character to send in the CSI - const final: u8 = if (action == .release) 'm' else 'M'; - - // Response always is at least 4 chars, so this leaves the - // remainder for numbers which are very large... - var data: termio.Message.WriteReq.Small.Array = undefined; - const resp = try std.fmt.bufPrint(&data, "\x1B[<{d};{d};{d}{c}", .{ - button_code, - pos.x, - pos.y, - final, - }); - - // Ask our IO thread to write the data - _ = self.io_thread.mailbox.push(.{ - .write_small = .{ - .data = data, - .len = @intCast(u8, resp.len), - }, - }, .{ .forever = {} }); - }, - } - - // After sending all our messages we have to notify our IO thread - try self.io_thread.wakeup.notify(); -} - -pub fn mouseButtonCallback( - self: *Window, - action: input.MouseButtonState, - button: input.MouseButton, - mods: input.Mods, -) !void { - const tracy = trace(@src()); - defer tracy.end(); - - // If our dev mode window is visible then we always schedule a render on - // cursor move because the cursor might touch our windows. - if (DevMode.enabled and DevMode.instance.visible) { - try self.queueRender(); - - // If the mouse event was handled by imgui, ignore it. - if (imgui.IO.get()) |io| { - if (io.cval().WantCaptureMouse) return; - } else |_| {} - } - - // Always record our latest mouse state - self.mouse.click_state[@intCast(usize, @enumToInt(button))] = action; - self.mouse.mods = @bitCast(input.Mods, mods); - - self.renderer_state.mutex.lock(); - defer self.renderer_state.mutex.unlock(); - - // Report mouse events if enabled - if (self.io.terminal.modes.mouse_event != .none) { - const pos = try self.window.getCursorPos(); - - const report_action: MouseReportAction = switch (action) { - .press => .press, - .release => .release, - }; - - try self.mouseReport( - button, - report_action, - self.mouse.mods, - pos, - ); - } - - // For left button clicks we always record some information for - // selection/highlighting purposes. - if (button == .left and action == .press) { - const pos = try self.window.getCursorPos(); - - // If we move our cursor too much between clicks then we reset - // the multi-click state. - if (self.mouse.left_click_count > 0) { - const max_distance = self.cell_size.width; - const distance = @sqrt( - std.math.pow(f64, pos.x - self.mouse.left_click_xpos, 2) + - std.math.pow(f64, pos.y - self.mouse.left_click_ypos, 2), - ); - - if (distance > max_distance) self.mouse.left_click_count = 0; - } - - // Store it - const point = self.posToViewport(pos.x, pos.y); - self.mouse.left_click_point = point.toScreen(&self.io.terminal.screen); - self.mouse.left_click_xpos = pos.x; - self.mouse.left_click_ypos = pos.y; - - // Setup our click counter and timer - if (std.time.Instant.now()) |now| { - // If we have mouse clicks, then we check if the time elapsed - // is less than and our interval and if so, increase the count. - if (self.mouse.left_click_count > 0) { - const since = now.since(self.mouse.left_click_time); - if (since > self.mouse_interval) { - self.mouse.left_click_count = 0; - } - } - - self.mouse.left_click_time = now; - self.mouse.left_click_count += 1; - - // We only support up to triple-clicks. - if (self.mouse.left_click_count > 3) self.mouse.left_click_count = 1; - } else |err| { - self.mouse.left_click_count = 1; - log.err("error reading time, mouse multi-click won't work err={}", .{err}); - } - - switch (self.mouse.left_click_count) { - // First mouse click, clear selection - 1 => if (self.io.terminal.selection != null) { - self.io.terminal.selection = null; - try self.queueRender(); - }, - - // Double click, select the word under our mouse - 2 => { - const sel_ = self.io.terminal.screen.selectWord(self.mouse.left_click_point); - if (sel_) |sel| { - self.io.terminal.selection = sel; - try self.queueRender(); - } - }, - - // Triple click, select the line under our mouse - 3 => { - const sel_ = self.io.terminal.screen.selectLine(self.mouse.left_click_point); - if (sel_) |sel| { - self.io.terminal.selection = sel; - try self.queueRender(); - } - }, - - // We should be bounded by 1 to 3 - else => unreachable, - } - } -} - -pub fn cursorPosCallback( - self: *Window, - pos: apprt.CursorPos, -) !void { - const tracy = trace(@src()); - defer tracy.end(); - - // If our dev mode window is visible then we always schedule a render on - // cursor move because the cursor might touch our windows. - if (DevMode.enabled and DevMode.instance.visible) { - try self.queueRender(); - - // If the mouse event was handled by imgui, ignore it. - if (imgui.IO.get()) |io| { - if (io.cval().WantCaptureMouse) return; - } else |_| {} - } - - // We are reading/writing state for the remainder - self.renderer_state.mutex.lock(); - defer self.renderer_state.mutex.unlock(); - - // Do a mouse report - if (self.io.terminal.modes.mouse_event != .none) { - // We use the first mouse button we find pressed in order to report - // since the spec (afaict) does not say... - const button: ?input.MouseButton = button: for (self.mouse.click_state) |state, i| { - if (state == .press) - break :button @intToEnum(input.MouseButton, i); - } else null; - - try self.mouseReport(button, .motion, self.mouse.mods, pos); - - // If we're doing mouse motion tracking, we do not support text - // selection. - return; - } - - // If the cursor isn't clicked currently, it doesn't matter - if (self.mouse.click_state[@enumToInt(input.MouseButton.left)] != .press) return; - - // All roads lead to requiring a re-render at this pont. - try self.queueRender(); - - // Convert to pixels from screen coords - const xpos = pos.x; - const ypos = pos.y; - - // Convert to points - const viewport_point = self.posToViewport(xpos, ypos); - const screen_point = viewport_point.toScreen(&self.io.terminal.screen); - - // Handle dragging depending on click count - switch (self.mouse.left_click_count) { - 1 => self.dragLeftClickSingle(screen_point, xpos), - 2 => self.dragLeftClickDouble(screen_point), - 3 => self.dragLeftClickTriple(screen_point), - else => unreachable, - } -} - -/// Double-click dragging moves the selection one "word" at a time. -fn dragLeftClickDouble( - self: *Window, - screen_point: terminal.point.ScreenPoint, -) void { - // Get the word under our current point. If there isn't a word, do nothing. - const word = self.io.terminal.screen.selectWord(screen_point) orelse return; - - // Get our selection to grow it. If we don't have a selection, start it now. - // We may not have a selection if we started our dbl-click in an area - // that had no data, then we dragged our mouse into an area with data. - var sel = self.io.terminal.screen.selectWord(self.mouse.left_click_point) orelse { - self.io.terminal.selection = word; - return; - }; - - // Grow our selection - if (screen_point.before(self.mouse.left_click_point)) { - sel.start = word.start; - } else { - sel.end = word.end; - } - self.io.terminal.selection = sel; -} - -/// Triple-click dragging moves the selection one "line" at a time. -fn dragLeftClickTriple( - self: *Window, - screen_point: terminal.point.ScreenPoint, -) void { - // Get the word under our current point. If there isn't a word, do nothing. - const word = self.io.terminal.screen.selectLine(screen_point) orelse return; - - // Get our selection to grow it. If we don't have a selection, start it now. - // We may not have a selection if we started our dbl-click in an area - // that had no data, then we dragged our mouse into an area with data. - var sel = self.io.terminal.screen.selectLine(self.mouse.left_click_point) orelse { - self.io.terminal.selection = word; - return; - }; - - // Grow our selection - if (screen_point.before(self.mouse.left_click_point)) { - sel.start = word.start; - } else { - sel.end = word.end; - } - self.io.terminal.selection = sel; -} - -fn dragLeftClickSingle( - self: *Window, - screen_point: terminal.point.ScreenPoint, - xpos: f64, -) void { - // NOTE(mitchellh): This logic super sucks. There has to be an easier way - // to calculate this, but this is good for a v1. Selection isn't THAT - // common so its not like this performance heavy code is running that - // often. - // TODO: unit test this, this logic sucks - - // If we were selecting, and we switched directions, then we restart - // calculations because it forces us to reconsider if the first cell is - // selected. - if (self.io.terminal.selection) |sel| { - const reset: bool = if (sel.end.before(sel.start)) - sel.start.before(screen_point) - else - screen_point.before(sel.start); - - if (reset) self.io.terminal.selection = null; - } - - // Our logic for determing if the starting cell is selected: - // - // - The "xboundary" is 60% the width of a cell from the left. We choose - // 60% somewhat arbitrarily based on feeling. - // - If we started our click left of xboundary, backwards selections - // can NEVER select the current char. - // - If we started our click right of xboundary, backwards selections - // ALWAYS selected the current char, but we must move the cursor - // left of the xboundary. - // - Inverted logic for forwards selections. - // - - // the boundary point at which we consider selection or non-selection - const cell_xboundary = self.cell_size.width * 0.6; - - // first xpos of the clicked cell - const cell_xstart = @intToFloat(f32, self.mouse.left_click_point.x) * self.cell_size.width; - const cell_start_xpos = self.mouse.left_click_xpos - cell_xstart; - - // If this is the same cell, then we only start the selection if weve - // moved past the boundary point the opposite direction from where we - // started. - if (std.meta.eql(screen_point, self.mouse.left_click_point)) { - const cell_xpos = xpos - cell_xstart; - const selected: bool = if (cell_start_xpos < cell_xboundary) - cell_xpos >= cell_xboundary - else - cell_xpos < cell_xboundary; - - self.io.terminal.selection = if (selected) .{ - .start = screen_point, - .end = screen_point, - } else null; - - return; - } - - // If this is a different cell and we haven't started selection, - // we determine the starting cell first. - if (self.io.terminal.selection == null) { - // - If we're moving to a point before the start, then we select - // the starting cell if we started after the boundary, else - // we start selection of the prior cell. - // - Inverse logic for a point after the start. - const click_point = self.mouse.left_click_point; - const start: terminal.point.ScreenPoint = if (screen_point.before(click_point)) start: { - if (self.mouse.left_click_xpos > cell_xboundary) { - break :start click_point; - } else { - break :start if (click_point.x > 0) terminal.point.ScreenPoint{ - .y = click_point.y, - .x = click_point.x - 1, - } else terminal.point.ScreenPoint{ - .x = self.io.terminal.screen.cols - 1, - .y = click_point.y -| 1, - }; - } - } else start: { - if (self.mouse.left_click_xpos < cell_xboundary) { - break :start click_point; - } else { - break :start if (click_point.x < self.io.terminal.screen.cols - 1) terminal.point.ScreenPoint{ - .y = click_point.y, - .x = click_point.x + 1, - } else terminal.point.ScreenPoint{ - .y = click_point.y + 1, - .x = 0, - }; - } - }; - - self.io.terminal.selection = .{ .start = start, .end = screen_point }; - return; - } - - // TODO: detect if selection point is passed the point where we've - // actually written data before and disallow it. - - // We moved! Set the selection end point. The start point should be - // set earlier. - assert(self.io.terminal.selection != null); - self.io.terminal.selection.?.end = screen_point; -} - -fn posToViewport(self: Window, xpos: f64, ypos: f64) terminal.point.Viewport { - // xpos and ypos can be negative if while dragging, the user moves the - // mouse off the window. Likewise, they can be larger than our window - // width if the user drags out of the window positively. - return .{ - .x = if (xpos < 0) 0 else x: { - // Our cell is the mouse divided by cell width - const cell_width = @floatCast(f64, self.cell_size.width); - const x = @floatToInt(usize, xpos / cell_width); - - // Can be off the screen if the user drags it out, so max - // it out on our available columns - break :x @min(x, self.grid_size.columns - 1); - }, - - .y = if (ypos < 0) 0 else y: { - const cell_height = @floatCast(f64, self.cell_size.height); - const y = @floatToInt(usize, ypos / cell_height); - break :y @min(y, self.grid_size.rows - 1); - }, - }; -} - -const face_ttf = @embedFile("font/res/FiraCode-Regular.ttf"); -const face_bold_ttf = @embedFile("font/res/FiraCode-Bold.ttf"); -const face_emoji_ttf = @embedFile("font/res/NotoColorEmoji.ttf"); -const face_emoji_text_ttf = @embedFile("font/res/NotoEmoji-Regular.ttf"); diff --git a/src/apprt/embedded.zig b/src/apprt/embedded.zig index 663860977..617850149 100644 --- a/src/apprt/embedded.zig +++ b/src/apprt/embedded.zig @@ -12,7 +12,7 @@ const objc = @import("objc"); const apprt = @import("../apprt.zig"); const input = @import("../input.zig"); const CoreApp = @import("../App.zig"); -const CoreWindow = @import("../Window.zig"); +const CoreSurface = @import("../Surface.zig"); const log = std.log.scoped(.embedded_window); @@ -65,11 +65,11 @@ pub const App = struct { } }; -pub const Window = struct { +pub const Surface = struct { nsview: objc.Object, - core_win: *CoreWindow, + core_win: *CoreSurface, content_scale: apprt.ContentScale, - size: apprt.WindowSize, + size: apprt.SurfaceSize, cursor_pos: apprt.CursorPos, opts: Options, @@ -84,7 +84,7 @@ pub const Window = struct { scale_factor: f64 = 1, }; - pub fn init(app: *const CoreApp, core_win: *CoreWindow, opts: Options) !Window { + pub fn init(app: *const CoreApp, core_win: *CoreSurface, opts: Options) !Surface { _ = app; return .{ @@ -100,68 +100,68 @@ pub const Window = struct { }; } - pub fn deinit(self: *Window) void { + pub fn deinit(self: *Surface) void { _ = self; } - pub fn getContentScale(self: *const Window) !apprt.ContentScale { + pub fn getContentScale(self: *const Surface) !apprt.ContentScale { return self.content_scale; } - pub fn getSize(self: *const Window) !apprt.WindowSize { + pub fn getSize(self: *const Surface) !apprt.SurfaceSize { return self.size; } - pub fn setSizeLimits(self: *Window, min: apprt.WindowSize, max_: ?apprt.WindowSize) !void { + pub fn setSizeLimits(self: *Surface, min: apprt.SurfaceSize, max_: ?apprt.SurfaceSize) !void { _ = self; _ = min; _ = max_; } - pub fn setTitle(self: *Window, slice: [:0]const u8) !void { + pub fn setTitle(self: *Surface, slice: [:0]const u8) !void { self.core_win.app.runtime.opts.set_title( self.opts.userdata, slice.ptr, ); } - pub fn getClipboardString(self: *const Window) ![:0]const u8 { + pub fn getClipboardString(self: *const Surface) ![:0]const u8 { const ptr = self.core_win.app.runtime.opts.read_clipboard(self.opts.userdata); return std.mem.sliceTo(ptr, 0); } - pub fn setClipboardString(self: *const Window, val: [:0]const u8) !void { + pub fn setClipboardString(self: *const Surface, val: [:0]const u8) !void { self.core_win.app.runtime.opts.write_clipboard(self.opts.userdata, val.ptr); } - pub fn setShouldClose(self: *Window) void { + pub fn setShouldClose(self: *Surface) void { _ = self; } - pub fn shouldClose(self: *const Window) bool { + pub fn shouldClose(self: *const Surface) bool { _ = self; return false; } - pub fn getCursorPos(self: *const Window) !apprt.CursorPos { + pub fn getCursorPos(self: *const Surface) !apprt.CursorPos { return self.cursor_pos; } - pub fn refresh(self: *Window) void { + pub fn refresh(self: *Surface) void { self.core_win.refreshCallback() catch |err| { log.err("error in refresh callback err={}", .{err}); return; }; } - pub fn updateContentScale(self: *Window, x: f64, y: f64) void { + pub fn updateContentScale(self: *Surface, x: f64, y: f64) void { self.content_scale = .{ .x = @floatCast(f32, x), .y = @floatCast(f32, y), }; } - pub fn updateSize(self: *Window, width: u32, height: u32) void { + pub fn updateSize(self: *Surface, width: u32, height: u32) void { self.size = .{ .width = width, .height = height, @@ -175,7 +175,7 @@ pub const Window = struct { } pub fn mouseButtonCallback( - self: *const Window, + self: *const Surface, action: input.MouseButtonState, button: input.MouseButton, mods: input.Mods, @@ -186,14 +186,14 @@ pub const Window = struct { }; } - pub fn scrollCallback(self: *const Window, xoff: f64, yoff: f64) void { + pub fn scrollCallback(self: *const Surface, xoff: f64, yoff: f64) void { self.core_win.scrollCallback(xoff, yoff) catch |err| { log.err("error in scroll callback err={}", .{err}); return; }; } - pub fn cursorPosCallback(self: *Window, x: f64, y: f64) void { + pub fn cursorPosCallback(self: *Surface, x: f64, y: f64) void { // Convert our unscaled x/y to scaled. self.cursor_pos = self.core_win.window.cursorPosToPixels(.{ .x = @floatCast(f32, x), @@ -213,7 +213,7 @@ pub const Window = struct { } pub fn keyCallback( - self: *const Window, + self: *const Surface, action: input.Action, key: input.Key, mods: input.Mods, @@ -225,7 +225,7 @@ pub const Window = struct { }; } - pub fn charCallback(self: *const Window, cp_: u32) void { + pub fn charCallback(self: *const Surface, cp_: u32) void { const cp = std.math.cast(u21, cp_) orelse return; self.core_win.charCallback(cp) catch |err| { log.err("error in char callback err={}", .{err}); @@ -233,7 +233,7 @@ pub const Window = struct { }; } - pub fn focusCallback(self: *const Window, focused: bool) void { + pub fn focusCallback(self: *const Surface, focused: bool) void { self.core_win.focusCallback(focused) catch |err| { log.err("error in focus callback err={}", .{err}); return; @@ -242,7 +242,7 @@ pub const Window = struct { /// The cursor position from the host directly is in screen coordinates but /// all our interface works in pixels. - fn cursorPosToPixels(self: *const Window, pos: apprt.CursorPos) !apprt.CursorPos { + fn cursorPosToPixels(self: *const Surface, pos: apprt.CursorPos) !apprt.CursorPos { const scale = try self.getContentScale(); return .{ .x = pos.x * scale.x, .y = pos.y * scale.y }; } diff --git a/src/apprt/glfw.zig b/src/apprt/glfw.zig index 0aa2b5f44..23c8bd42e 100644 --- a/src/apprt/glfw.zig +++ b/src/apprt/glfw.zig @@ -76,7 +76,7 @@ pub const App = struct { } /// Create a new window for the app. - pub fn newWindow(self: *App) !void { + pub fn newWindow(self: *App) !*Surface { // Grab a surface allocation because we're going to need it. const surface = try self.app.surface_pool.create(); errdefer self.app.surface_pool.destroy(surface); @@ -84,6 +84,42 @@ pub const App = struct { // Create the surface -- because windows are surfaces for glfw. try surface.init(self); errdefer surface.deinit(); + + return surface; + } + + /// Create a new tab in the parent surface. + pub fn newTab(self: *App, parent: *CoreSurface) !void { + if (!Darwin.enabled) { + log.warn("tabbing is not supported on this platform", .{}); + return; + } + + // Create the new window + const window = try self.newWindow(); + + // Add the new window the parent window + const parent_win = glfwNative.getCocoaWindow(parent.rt_surface.window).?; + const other_win = glfwNative.getCocoaWindow(window.window).?; + const NSWindowOrderingMode = enum(isize) { below = -1, out = 0, above = 1 }; + const nswindow = objc.Object.fromId(parent_win); + nswindow.msgSend(void, objc.sel("addTabbedWindow:ordered:"), .{ + objc.Object.fromId(other_win), + NSWindowOrderingMode.above, + }); + + // Adding a new tab can cause the tab bar to appear which changes + // our viewport size. We need to call the size callback in order to + // update values. For example, we need this to set the proper mouse selection + // point in the grid. + const size = parent.rt_surface.getSize() catch |err| { + log.err("error querying window size for size callback on new tab err={}", .{err}); + return; + }; + parent.sizeCallback(size) catch |err| { + log.err("error in size callback from new tab err={}", .{err}); + return; + }; } /// Close the given surface. diff --git a/src/apprt/gtk.zig b/src/apprt/gtk.zig index a60ac6af4..4bf3bff8e 100644 --- a/src/apprt/gtk.zig +++ b/src/apprt/gtk.zig @@ -6,7 +6,7 @@ const assert = std.debug.assert; const Allocator = std.mem.Allocator; const apprt = @import("../apprt.zig"); const CoreApp = @import("../App.zig"); -const CoreWindow = @import("../Window.zig"); +const CoreSurface = @import("../Surface.zig"); pub const c = @cImport({ @cInclude("gtk/gtk.h"); @@ -147,10 +147,10 @@ pub const App = struct { } }; -pub const Window = struct { +pub const Surface = struct { pub const Options = struct {}; - pub fn init(app: *const CoreApp, core_win: *CoreWindow, opts: Options) !Window { + pub fn init(app: *const CoreApp, core_win: *CoreSurface, opts: Options) !Surface { _ = app; _ = core_win; _ = opts; @@ -158,46 +158,46 @@ pub const Window = struct { return .{}; } - pub fn deinit(self: *Window) void { + pub fn deinit(self: *Surface) void { _ = self; } - pub fn setShouldClose(self: *Window) void { + pub fn setShouldClose(self: *Surface) void { _ = self; } - pub fn shouldClose(self: *const Window) bool { + pub fn shouldClose(self: *const Surface) bool { _ = self; return false; } - pub fn getContentScale(self: *const Window) !apprt.ContentScale { + pub fn getContentScale(self: *const Surface) !apprt.ContentScale { _ = self; return .{ .x = 1, .y = 1 }; } - pub fn getSize(self: *const Window) !apprt.WindowSize { + pub fn getSize(self: *const Surface) !apprt.SurfaceSize { _ = self; return .{ .width = 800, .height = 600 }; } - pub fn setSizeLimits(self: *Window, min: apprt.WindowSize, max_: ?apprt.WindowSize) !void { + pub fn setSizeLimits(self: *Surface, min: apprt.SurfaceSize, max_: ?apprt.SurfaceSize) !void { _ = self; _ = min; _ = max_; } - pub fn setTitle(self: *Window, slice: [:0]const u8) !void { + pub fn setTitle(self: *Surface, slice: [:0]const u8) !void { _ = self; _ = slice; } - pub fn getClipboardString(self: *const Window) ![:0]const u8 { + pub fn getClipboardString(self: *const Surface) ![:0]const u8 { _ = self; return ""; } - pub fn setClipboardString(self: *const Window, val: [:0]const u8) !void { + pub fn setClipboardString(self: *const Surface, val: [:0]const u8) !void { _ = self; _ = val; } diff --git a/src/main.zig b/src/main.zig index 8d50d3045..e7d0e7b1e 100644 --- a/src/main.zig +++ b/src/main.zig @@ -100,7 +100,7 @@ pub fn main() !void { defer app_runtime.terminate(); // Create an initial window - try app_runtime.newWindow(); + _ = try app_runtime.newWindow(); // Run the GUI event loop try app_runtime.run(); diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index abd8ad06a..8082eeed9 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -16,7 +16,7 @@ const terminal = @import("../terminal/main.zig"); const renderer = @import("../renderer.zig"); const math = @import("../math.zig"); const DevMode = @import("../DevMode.zig"); -const Window = @import("../Window.zig"); +const Surface = @import("../Surface.zig"); const assert = std.debug.assert; const Allocator = std.mem.Allocator; const Terminal = terminal.Terminal; @@ -32,7 +32,7 @@ const log = std.log.scoped(.metal); alloc: std.mem.Allocator, /// The mailbox for communicating with the window. -window_mailbox: Window.Mailbox, +surface_mailbox: apprt.surface.Mailbox, /// Current cell dimensions for this grid. cell_size: renderer.CellSize, @@ -135,8 +135,8 @@ pub fn glfwWindowHints() glfw.Window.Hints { /// This is called early right after window creation to setup our /// window surface as necessary. -pub fn windowInit(win: apprt.runtime.Window) !void { - _ = win; +pub fn surfaceInit(surface: *apprt.Surface) !void { + _ = surface; // We don't do anything else here because we want to set everything // else up during actual initialization. @@ -240,7 +240,7 @@ pub fn init(alloc: Allocator, options: renderer.Options) !Metal { return Metal{ .alloc = alloc, - .window_mailbox = options.window_mailbox, + .surface_mailbox = options.surface_mailbox, .cell_size = .{ .width = metrics.cell_width, .height = metrics.cell_height }, .padding = options.padding, .focused = true, @@ -304,7 +304,7 @@ pub fn deinit(self: *Metal) void { /// This is called just prior to spinning up the renderer thread for /// final main thread setup requirements. -pub fn finalizeWindowInit(self: *const Metal, win: apprt.runtime.Window) !void { +pub fn finalizeSurfaceInit(self: *const Metal, surface: *apprt.Surface) !void { const Info = struct { view: objc.Object, scaleFactor: f64, @@ -315,7 +315,7 @@ pub fn finalizeWindowInit(self: *const Metal, win: apprt.runtime.Window) !void { apprt.glfw => info: { // Everything in glfw is window-oriented so we grab the backing // window, then derive everything from that. - const nswindow = objc.Object.fromId(glfwNative.getCocoaWindow(win.window).?); + const nswindow = objc.Object.fromId(glfwNative.getCocoaWindow(surface.window).?); const contentView = objc.Object.fromId(nswindow.getProperty(?*anyopaque, "contentView").?); const scaleFactor = nswindow.getProperty(macos.graphics.c.CGFloat, "backingScaleFactor"); break :info .{ @@ -325,8 +325,8 @@ pub fn finalizeWindowInit(self: *const Metal, win: apprt.runtime.Window) !void { }, apprt.embedded => .{ - .view = win.nsview, - .scaleFactor = @floatCast(f64, win.content_scale.x), + .view = surface.nsview, + .scaleFactor = @floatCast(f64, surface.content_scale.x), }, else => @compileError("unsupported apprt for metal"), @@ -344,11 +344,11 @@ pub fn finalizeWindowInit(self: *const Metal, win: apprt.runtime.Window) !void { } /// This is called if this renderer runs DevMode. -pub fn initDevMode(self: *const Metal, win: apprt.runtime.Window) !void { +pub fn initDevMode(self: *const Metal, surface: *apprt.Surface) !void { if (DevMode.enabled) { // Initialize for our window assert(imgui.ImplGlfw.initForOther( - @ptrCast(*imgui.ImplGlfw.GLFWWindow, win.window.handle), + @ptrCast(*imgui.ImplGlfw.GLFWWindow, surface.window.handle), true, )); assert(imgui.ImplMetal.init(self.device.value)); @@ -366,9 +366,9 @@ pub fn deinitDevMode(self: *const Metal) void { } /// Callback called by renderer.Thread when it begins. -pub fn threadEnter(self: *const Metal, win: apprt.runtime.Window) !void { +pub fn threadEnter(self: *const Metal, surface: *apprt.Surface) !void { _ = self; - _ = win; + _ = surface; // Metal requires no per-thread state. } @@ -442,7 +442,7 @@ pub fn setFontSize(self: *Metal, size: font.face.DesiredSize) !void { }; // Notify the window that the cell size changed. - _ = self.window_mailbox.push(.{ + _ = self.surface_mailbox.push(.{ .cell_size = new_cell_size, }, .{ .forever = {} }); } @@ -450,10 +450,10 @@ pub fn setFontSize(self: *Metal, size: font.face.DesiredSize) !void { /// The primary render callback that is completely thread-safe. pub fn render( self: *Metal, - win: apprt.runtime.Window, + surface: *apprt.Surface, state: *renderer.State, ) !void { - _ = win; + _ = surface; // Data we extract out of the critical area. const Critical = struct { @@ -533,8 +533,8 @@ pub fn render( critical.draw_cursor, ); - // Get our surface (CAMetalDrawable) - const surface = self.swapchain.msgSend(objc.Object, objc.sel("nextDrawable"), .{}); + // Get our drawable (CAMetalDrawable) + const drawable = self.swapchain.msgSend(objc.Object, objc.sel("nextDrawable"), .{}); // If our font atlas changed, sync the texture data if (self.font_group.atlas_greyscale.modified) { @@ -572,7 +572,7 @@ pub fn render( // Ghostty in XCode in debug mode it returns a CaptureMTLDrawable // which ironically doesn't implement CAMetalDrawable as a // property so we just send a message. - const texture = surface.msgSend(objc.c.id, objc.sel("texture"), .{}); + const texture = drawable.msgSend(objc.c.id, objc.sel("texture"), .{}); attachment.setProperty("loadAction", @enumToInt(MTLLoadAction.clear)); attachment.setProperty("storeAction", @enumToInt(MTLStoreAction.store)); attachment.setProperty("texture", texture); @@ -656,7 +656,7 @@ pub fn render( } } - buffer.msgSend(void, objc.sel("presentDrawable:"), .{surface.value}); + buffer.msgSend(void, objc.sel("presentDrawable:"), .{drawable.value}); buffer.msgSend(void, objc.sel("commit"), .{}); } From 8c18e1ee48b9e1ff2ad114051b8ebfd309dcaacc Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 22 Feb 2023 15:32:30 -0800 Subject: [PATCH 10/37] remove memory pool usage for mac --- src/App.zig | 10 ---------- src/apprt/glfw.zig | 6 +++--- 2 files changed, 3 insertions(+), 13 deletions(-) diff --git a/src/App.zig b/src/App.zig index 94efe5fd1..0dee7d3e3 100644 --- a/src/App.zig +++ b/src/App.zig @@ -23,7 +23,6 @@ const DevMode = @import("DevMode.zig"); const log = std.log.scoped(.app); const SurfaceList = std.ArrayListUnmanaged(*apprt.Surface); -const SurfacePool = std.heap.MemoryPool(apprt.Surface); /// The type used for sending messages to the app thread. pub const Mailbox = BlockingQueue(Message, 64); @@ -34,12 +33,6 @@ alloc: Allocator, /// The list of surfaces that are currently active. surfaces: SurfaceList, -/// The memory pool to request surfaces. We use a memory pool because surfaces -/// typically require stable pointers due to runtime GUI callbacks. Centralizing -/// all the allocations in this pool makes it so that all our pools remain -/// close in memory. -surface_pool: SurfacePool, - // The configuration for the app. config: *const Config, @@ -72,13 +65,11 @@ pub fn create( app.* = .{ .alloc = alloc, .surfaces = .{}, - .surface_pool = try SurfacePool.initPreheated(alloc, 2), .config = config, .mailbox = mailbox, .quit = false, }; errdefer app.surfaces.deinit(alloc); - errdefer app.surface_pool.deinit(); return app; } @@ -87,7 +78,6 @@ pub fn destroy(self: *App) void { // Clean up all our surfaces for (self.surfaces.items) |surface| surface.deinit(); self.surfaces.deinit(self.alloc); - self.surface_pool.deinit(); self.mailbox.destroy(self.alloc); self.alloc.destroy(self); diff --git a/src/apprt/glfw.zig b/src/apprt/glfw.zig index 23c8bd42e..560d7fd0c 100644 --- a/src/apprt/glfw.zig +++ b/src/apprt/glfw.zig @@ -78,8 +78,8 @@ pub const App = struct { /// Create a new window for the app. pub fn newWindow(self: *App) !*Surface { // Grab a surface allocation because we're going to need it. - const surface = try self.app.surface_pool.create(); - errdefer self.app.surface_pool.destroy(surface); + var surface = try self.app.alloc.create(Surface); + errdefer self.app.alloc.destroy(surface); // Create the surface -- because windows are surfaces for glfw. try surface.init(self); @@ -125,7 +125,7 @@ pub const App = struct { /// Close the given surface. pub fn closeSurface(self: *App, surface: *Surface) void { surface.deinit(); - self.app.surface_pool.destroy(surface); + self.app.alloc.destroy(surface); } fn glfwErrorCallback(code: glfw.ErrorCode, desc: [:0]const u8) void { From ac772c2d2d1c54534d156abca21074ce016d7516 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 22 Feb 2023 19:31:12 -0800 Subject: [PATCH 11/37] inherit font size works again --- src/App.zig | 33 ++++++++++++++++++++++----------- src/Surface.zig | 10 +--------- src/apprt/glfw.zig | 6 ++++-- 3 files changed, 27 insertions(+), 22 deletions(-) diff --git a/src/App.zig b/src/App.zig index 0dee7d3e3..bf89a6847 100644 --- a/src/App.zig +++ b/src/App.zig @@ -152,10 +152,7 @@ fn drainMailbox(self: *App, rt_app: *apprt.runtime.App) !void { while (self.mailbox.pop()) |message| { log.debug("mailbox message={s}", .{@tagName(message)}); switch (message) { - .new_window => |msg| { - _ = msg; // TODO - _ = try rt_app.newWindow(); - }, + .new_window => |msg| try self.newWindow(rt_app, msg), .new_tab => |msg| try self.newTab(rt_app, msg), .quit => try self.setQuit(), .surface_message => |msg| try self.surfaceMessage(msg.surface, msg.message), @@ -163,8 +160,20 @@ fn drainMailbox(self: *App, rt_app: *apprt.runtime.App) !void { } } +/// Create a new window +fn newWindow(self: *App, rt_app: *apprt.runtime.App, msg: Message.NewWindow) !void { + const window = try rt_app.newWindow(); + if (self.config.@"window-inherit-font-size") { + if (msg.parent) |parent| { + if (self.hasSurface(parent)) { + window.core_surface.setFontSize(parent.font_size); + } + } + } +} + /// Create a new tab in the parent window -fn newTab(self: *App, rt_app: *apprt.runtime.App, msg: Message.NewWindow) !void { +fn newTab(self: *App, rt_app: *apprt.runtime.App, msg: Message.NewTab) !void { const parent = msg.parent orelse { log.warn("parent must be set in new_tab message", .{}); return; @@ -176,7 +185,8 @@ fn newTab(self: *App, rt_app: *apprt.runtime.App, msg: Message.NewWindow) !void return; } - try rt_app.newTab(parent); + const window = try rt_app.newTab(parent); + if (self.config.@"window-inherit-font-size") window.core_surface.setFontSize(parent.font_size); } /// Start quitting @@ -219,7 +229,7 @@ pub const Message = union(enum) { /// Create a new tab within the tab group of the focused window. /// This does nothing if we're on a platform or using a window /// environment that doesn't support tabs. - new_tab: NewWindow, + new_tab: NewTab, /// Quit quit: void, @@ -234,12 +244,13 @@ pub const Message = union(enum) { /// Runtime-specific window options. runtime: apprt.runtime.Surface.Options = .{}, - /// The parent surface, only used for new tabs. + /// The parent surface parent: ?*Surface = null, + }; - /// The font size to create the window with or null to default to - /// the configuration amount. - font_size: ?font.face.DesiredSize = null, + const NewTab = struct { + /// The parent surface + parent: ?*Surface = null, }; }; diff --git a/src/Surface.zig b/src/Surface.zig index 6329a8760..a09b34609 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -904,10 +904,7 @@ pub fn keyCallback( .new_window => { _ = self.app.mailbox.push(.{ .new_window = .{ - .font_size = if (self.config.@"window-inherit-font-size") - self.font_size - else - null, + .parent = self, }, }, .{ .instant = {} }); self.app.wakeup(); @@ -917,11 +914,6 @@ pub fn keyCallback( _ = self.app.mailbox.push(.{ .new_tab = .{ .parent = self, - - .font_size = if (self.config.@"window-inherit-font-size") - self.font_size - else - null, }, }, .{ .instant = {} }); self.app.wakeup(); diff --git a/src/apprt/glfw.zig b/src/apprt/glfw.zig index 560d7fd0c..8fa6a6b0d 100644 --- a/src/apprt/glfw.zig +++ b/src/apprt/glfw.zig @@ -89,10 +89,10 @@ pub const App = struct { } /// Create a new tab in the parent surface. - pub fn newTab(self: *App, parent: *CoreSurface) !void { + pub fn newTab(self: *App, parent: *CoreSurface) !*Surface { if (!Darwin.enabled) { log.warn("tabbing is not supported on this platform", .{}); - return; + return error.TabbingNotSupported; } // Create the new window @@ -120,6 +120,8 @@ pub const App = struct { log.err("error in size callback from new tab err={}", .{err}); return; }; + + return window; } /// Close the given surface. From 705d56d18e362f7402b220cb8a9c6a826719907a Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 22 Feb 2023 20:08:48 -0800 Subject: [PATCH 12/37] surface no longer has reference to app --- src/App.zig | 72 +++++++++++++++++++++--------------------- src/Surface.zig | 28 ++++++++-------- src/apprt/embedded.zig | 54 +++++++++++++++++++------------ src/apprt/glfw.zig | 23 +++++++++----- src/apprt/surface.zig | 15 ++++----- 5 files changed, 104 insertions(+), 88 deletions(-) diff --git a/src/App.zig b/src/App.zig index bf89a6847..1afb82e31 100644 --- a/src/App.zig +++ b/src/App.zig @@ -24,9 +24,6 @@ const log = std.log.scoped(.app); const SurfaceList = std.ArrayListUnmanaged(*apprt.Surface); -/// The type used for sending messages to the app thread. -pub const Mailbox = BlockingQueue(Message, 64); - /// General purpose allocator alloc: Allocator, @@ -38,14 +35,11 @@ config: *const Config, /// The mailbox that can be used to send this thread messages. Note /// this is a blocking queue so if it is full you will get errors (or block). -mailbox: *Mailbox, +mailbox: Mailbox.Queue, /// Set to true once we're quitting. This never goes false again. quit: bool, -/// 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. @@ -53,10 +47,6 @@ pub fn create( alloc: Allocator, config: *const Config, ) !*App { - // The mailbox for messaging this thread - var mailbox = try Mailbox.create(alloc); - errdefer mailbox.destroy(alloc); - // If we have DevMode on, store the config so we can show it if (DevMode.enabled) DevMode.instance.config = config; @@ -66,7 +56,7 @@ pub fn create( .alloc = alloc, .surfaces = .{}, .config = config, - .mailbox = mailbox, + .mailbox = .{}, .quit = false, }; errdefer app.surfaces.deinit(alloc); @@ -78,16 +68,10 @@ pub fn destroy(self: *App) void { // Clean up all our surfaces for (self.surfaces.items) |surface| surface.deinit(); self.surfaces.deinit(self.alloc); - self.mailbox.destroy(self.alloc); self.alloc.destroy(self); } -/// Request the app runtime to process app events via tick. -pub fn wakeup(self: App) void { - if (self.wakeup_cb) |cb| cb(); -} - /// Tick ticks the app loop. This will drain our mailbox and process those /// events. This should be called by the application runtime on every loop /// tick. @@ -127,26 +111,13 @@ pub fn deleteSurface(self: *App, rt_surface: *apprt.Surface) void { while (i < self.surfaces.items.len) { if (self.surfaces.items[i] == rt_surface) { _ = self.surfaces.swapRemove(i); + continue; } + + i += 1; } } -/// Close a window and free all resources associated with it. This can -/// only be called from the main thread. -// pub fn closeWindow(self: *App, window: *Window) void { -// var i: usize = 0; -// while (i < self.surfaces.items.len) { -// const current = self.surfaces.items[i]; -// if (window == current) { -// window.destroy(); -// _ = self.surfaces.swapRemove(i); -// return; -// } -// -// i += 1; -// } -// } - /// Drain the mailbox. fn drainMailbox(self: *App, rt_app: *apprt.runtime.App) !void { while (self.mailbox.pop()) |message| { @@ -154,12 +125,18 @@ fn drainMailbox(self: *App, rt_app: *apprt.runtime.App) !void { switch (message) { .new_window => |msg| try self.newWindow(rt_app, msg), .new_tab => |msg| try self.newTab(rt_app, msg), + .close => |surface| try self.closeSurface(rt_app, surface), .quit => try self.setQuit(), .surface_message => |msg| try self.surfaceMessage(msg.surface, msg.message), } } } +fn closeSurface(self: *App, rt_app: *apprt.App, surface: *Surface) !void { + if (!self.hasSurface(surface)) return; + rt_app.closeSurface(surface.rt_surface); +} + /// Create a new window fn newWindow(self: *App, rt_app: *apprt.runtime.App, msg: Message.NewWindow) !void { const window = try rt_app.newWindow(); @@ -231,6 +208,10 @@ pub const Message = union(enum) { /// environment that doesn't support tabs. new_tab: NewTab, + /// Close a surface. This notifies the runtime that a surface + /// should close. + close: *Surface, + /// Quit quit: void, @@ -254,6 +235,25 @@ pub const Message = union(enum) { }; }; +/// Mailbox is the way that other threads send the app thread messages. +pub const Mailbox = struct { + /// The type used for sending messages to the app thread. + pub const Queue = BlockingQueue(Message, 64); + + rt_app: *apprt.App, + mailbox: *Queue, + + /// Send a message to the surface. + pub fn push(self: Mailbox, msg: Message, timeout: Queue.Timeout) Queue.Size { + const result = self.mailbox.push(msg, timeout); + + // Wake up our app loop + self.rt_app.wakeup(); + + return result; + } +}; + // Wasm API. pub const Wasm = if (!builtin.target.isWasm()) struct {} else struct { const wasm = @import("os/wasm.zig"); @@ -329,7 +329,7 @@ pub const CAPI = struct { /// Create a new surface as part of an app. export fn ghostty_surface_new( app: *App, - opts: *const apprt.runtime.Window.Options, + opts: *const apprt.Surface.Options, ) ?*Surface { return surface_new_(app, opts) catch |err| { log.err("error initializing surface err={}", .{err}); @@ -339,7 +339,7 @@ pub const CAPI = struct { fn surface_new_( app: *App, - opts: *const apprt.runtime.Window.Options, + opts: *const apprt.Surface.Options, ) !*Surface { const w = try app.newWindow(.{ .runtime = opts.*, diff --git a/src/Surface.zig b/src/Surface.zig index a09b34609..65740340e 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -42,8 +42,8 @@ const Renderer = renderer.Renderer; /// Allocator alloc: Allocator, -/// The app that this window is a part of. -app: *App, +/// The mailbox for sending messages to the main app thread. +app_mailbox: App.Mailbox, /// The windowing system surface rt_surface: *apprt.runtime.Surface, @@ -128,12 +128,11 @@ const Mouse = struct { /// stable due to interfacing with various callbacks. pub fn init( self: *Surface, - app: *App, + alloc: Allocator, config: *const Config, + app_mailbox: App.Mailbox, rt_surface: *apprt.runtime.Surface, ) !void { - const alloc = app.alloc; - // Initialize our renderer with our initialized surface. try Renderer.surfaceInit(rt_surface); @@ -291,7 +290,7 @@ pub fn init( .explicit = padding, .balance = config.@"window-padding-balance", }, - .surface_mailbox = .{ .surface = self, .app = app.mailbox }, + .surface_mailbox = .{ .surface = self, .app = app_mailbox }, }); errdefer renderer_impl.deinit(); @@ -328,7 +327,7 @@ pub fn init( .renderer_state = &self.renderer_state, .renderer_wakeup = render_thread.wakeup, .renderer_mailbox = render_thread.mailbox, - .surface_mailbox = .{ .surface = self, .app = app.mailbox }, + .surface_mailbox = .{ .surface = self, .app = app_mailbox }, }); errdefer io.deinit(); @@ -343,7 +342,7 @@ pub fn init( self.* = .{ .alloc = alloc, - .app = app, + .app_mailbox = app_mailbox, .rt_surface = rt_surface, .font_lib = font_lib, .font_group = font_group, @@ -902,30 +901,29 @@ pub fn keyCallback( } else log.warn("dev mode was not compiled into this binary", .{}), .new_window => { - _ = self.app.mailbox.push(.{ + _ = self.app_mailbox.push(.{ .new_window = .{ .parent = self, }, }, .{ .instant = {} }); - self.app.wakeup(); }, .new_tab => { - _ = self.app.mailbox.push(.{ + _ = self.app_mailbox.push(.{ .new_tab = .{ .parent = self, }, }, .{ .instant = {} }); - self.app.wakeup(); }, - .close_window => self.rt_surface.setShouldClose(), + .close_window => { + _ = self.app_mailbox.push(.{ .close = self }, .{ .instant = {} }); + }, .quit => { - _ = self.app.mailbox.push(.{ + _ = self.app_mailbox.push(.{ .quit = {}, }, .{ .instant = {} }); - self.app.wakeup(); }, } diff --git a/src/apprt/embedded.zig b/src/apprt/embedded.zig index 617850149..c5b85705e 100644 --- a/src/apprt/embedded.zig +++ b/src/apprt/embedded.zig @@ -46,10 +46,11 @@ pub const App = struct { write_clipboard: *const fn (SurfaceUD, [*:0]const u8) callconv(.C) void, }; + core_app: *CoreApp, opts: Options, - pub fn init(opts: Options) !App { - return .{ .opts = opts }; + pub fn init(core_app: *CoreApp, opts: Options) !App { + return .{ .core_app = core_app, .opts = opts }; } pub fn terminate(self: App) void { @@ -67,7 +68,7 @@ pub const App = struct { pub const Surface = struct { nsview: objc.Object, - core_win: *CoreSurface, + core_surface: *CoreSurface, content_scale: apprt.ContentScale, size: apprt.SurfaceSize, cursor_pos: apprt.CursorPos, @@ -84,11 +85,9 @@ pub const Surface = struct { scale_factor: f64 = 1, }; - pub fn init(app: *const CoreApp, core_win: *CoreSurface, opts: Options) !Surface { - _ = app; - - return .{ - .core_win = core_win, + pub fn init(self: *Surface, app: *App, opts: Options) !void { + self.* = .{ + .core_surface = undefined, .nsview = objc.Object.fromId(opts.nsview), .content_scale = .{ .x = @floatCast(f32, opts.scale_factor), @@ -98,10 +97,23 @@ pub const Surface = struct { .cursor_pos = .{ .x = 0, .y = 0 }, .opts = opts, }; + + // Add ourselves to the list of surfaces on the app. + try app.app.addSurface(self); + errdefer app.app.deleteSurface(self); + + // Initialize our surface right away. We're given a view that is + // ready to use. + try self.core_surface.init(app.app, app.app.config, self); + errdefer self.core_surface.deinit(); } pub fn deinit(self: *Surface) void { - _ = self; + // Remove ourselves from the list of known surfaces in the app. + self.core_surface.app.deleteSurface(self); + + // Clean up our core surface so that all the rendering and IO stop. + self.core_surface.deinit(); } pub fn getContentScale(self: *const Surface) !apprt.ContentScale { @@ -119,19 +131,19 @@ pub const Surface = struct { } pub fn setTitle(self: *Surface, slice: [:0]const u8) !void { - self.core_win.app.runtime.opts.set_title( + self.core_surface.app.runtime.opts.set_title( self.opts.userdata, slice.ptr, ); } pub fn getClipboardString(self: *const Surface) ![:0]const u8 { - const ptr = self.core_win.app.runtime.opts.read_clipboard(self.opts.userdata); + const ptr = self.core_surface.app.runtime.opts.read_clipboard(self.opts.userdata); return std.mem.sliceTo(ptr, 0); } pub fn setClipboardString(self: *const Surface, val: [:0]const u8) !void { - self.core_win.app.runtime.opts.write_clipboard(self.opts.userdata, val.ptr); + self.core_surface.app.runtime.opts.write_clipboard(self.opts.userdata, val.ptr); } pub fn setShouldClose(self: *Surface) void { @@ -148,7 +160,7 @@ pub const Surface = struct { } pub fn refresh(self: *Surface) void { - self.core_win.refreshCallback() catch |err| { + self.core_surface.refreshCallback() catch |err| { log.err("error in refresh callback err={}", .{err}); return; }; @@ -168,7 +180,7 @@ pub const Surface = struct { }; // Call the primary callback. - self.core_win.sizeCallback(self.size) catch |err| { + self.core_surface.sizeCallback(self.size) catch |err| { log.err("error in size callback err={}", .{err}); return; }; @@ -180,14 +192,14 @@ pub const Surface = struct { button: input.MouseButton, mods: input.Mods, ) void { - self.core_win.mouseButtonCallback(action, button, mods) catch |err| { + self.core_surface.mouseButtonCallback(action, button, mods) catch |err| { log.err("error in mouse button callback err={}", .{err}); return; }; } pub fn scrollCallback(self: *const Surface, xoff: f64, yoff: f64) void { - self.core_win.scrollCallback(xoff, yoff) catch |err| { + self.core_surface.scrollCallback(xoff, yoff) catch |err| { log.err("error in scroll callback err={}", .{err}); return; }; @@ -195,7 +207,7 @@ pub const Surface = struct { pub fn cursorPosCallback(self: *Surface, x: f64, y: f64) void { // Convert our unscaled x/y to scaled. - self.cursor_pos = self.core_win.window.cursorPosToPixels(.{ + self.cursor_pos = self.core_surface.window.cursorPosToPixels(.{ .x = @floatCast(f32, x), .y = @floatCast(f32, y), }) catch |err| { @@ -206,7 +218,7 @@ pub const Surface = struct { return; }; - self.core_win.cursorPosCallback(self.cursor_pos) catch |err| { + self.core_surface.cursorPosCallback(self.cursor_pos) catch |err| { log.err("error in cursor pos callback err={}", .{err}); return; }; @@ -219,7 +231,7 @@ pub const Surface = struct { mods: input.Mods, ) void { // log.warn("key action={} key={} mods={}", .{ action, key, mods }); - self.core_win.keyCallback(action, key, mods) catch |err| { + self.core_surface.keyCallback(action, key, mods) catch |err| { log.err("error in key callback err={}", .{err}); return; }; @@ -227,14 +239,14 @@ pub const Surface = struct { pub fn charCallback(self: *const Surface, cp_: u32) void { const cp = std.math.cast(u21, cp_) orelse return; - self.core_win.charCallback(cp) catch |err| { + self.core_surface.charCallback(cp) catch |err| { log.err("error in char callback err={}", .{err}); return; }; } pub fn focusCallback(self: *const Surface, focused: bool) void { - self.core_win.focusCallback(focused) catch |err| { + self.core_surface.focusCallback(focused) catch |err| { log.err("error in focus callback err={}", .{err}); return; }; diff --git a/src/apprt/glfw.zig b/src/apprt/glfw.zig index 8fa6a6b0d..59c79dfa5 100644 --- a/src/apprt/glfw.zig +++ b/src/apprt/glfw.zig @@ -43,9 +43,6 @@ pub const App = struct { 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, @@ -71,7 +68,8 @@ pub const App = struct { } /// Wakeup the event loop. This should be able to be called from any thread. - pub fn wakeup() void { + pub fn wakeup(self: *const App) void { + _ = self; glfw.postEmptyEvent(); } @@ -114,11 +112,11 @@ pub const App = struct { // point in the grid. const size = parent.rt_surface.getSize() catch |err| { log.err("error querying window size for size callback on new tab err={}", .{err}); - return; + return window; }; parent.sizeCallback(size) catch |err| { log.err("error in size callback from new tab err={}", .{err}); - return; + return window; }; return window; @@ -193,6 +191,9 @@ pub const Surface = struct { /// The glfw mouse cursor handle. cursor: glfw.Cursor, + /// The app we're part of + app: *App, + /// A core surface core_surface: CoreSurface, @@ -265,6 +266,7 @@ pub const Surface = struct { // Build our result self.* = .{ + .app = app, .window = win, .cursor = cursor, .core_surface = undefined, @@ -276,13 +278,18 @@ pub const Surface = struct { errdefer app.app.deleteSurface(self); // Initialize our surface now that we have the stable pointer. - try self.core_surface.init(app.app, app.app.config, self); + try self.core_surface.init( + app.app.alloc, + app.app.config, + .{ .rt_app = app, .mailbox = &app.app.mailbox }, + self, + ); errdefer self.core_surface.deinit(); } pub fn deinit(self: *Surface) void { // Remove ourselves from the list of known surfaces in the app. - self.core_surface.app.deleteSurface(self); + self.app.app.deleteSurface(self); // Clean up our core surface so that all the rendering and IO stop. self.core_surface.deinit(); diff --git a/src/apprt/surface.zig b/src/apprt/surface.zig index f76a4ea06..0e6439a41 100644 --- a/src/apprt/surface.zig +++ b/src/apprt/surface.zig @@ -28,23 +28,22 @@ pub const Message = union(enum) { /// A surface mailbox. pub const Mailbox = struct { surface: *Surface, - app: *App.Mailbox, + app: App.Mailbox, /// Send a message to the surface. - pub fn push(self: Mailbox, msg: Message, timeout: App.Mailbox.Timeout) App.Mailbox.Size { + pub fn push( + self: Mailbox, + msg: Message, + timeout: App.Mailbox.Queue.Timeout, + ) App.Mailbox.Queue.Size { // Surface message sending is actually implemented on the app // thread, so we have to rewrap the message with our surface // pointer and send it to the app thread. - const result = self.app.push(.{ + return self.app.push(.{ .surface_message = .{ .surface = self.surface, .message = msg, }, }, timeout); - - // Wake up our app loop - self.surface.app.wakeup(); - - return result; } }; From 2adb0c9234e934acd1a27349440fdf37a5d1c84f Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 22 Feb 2023 21:10:20 -0800 Subject: [PATCH 13/37] apprt: C API for embedded updated to new style --- src/App.zig | 162 +++----------------------------- src/apprt/embedded.zig | 205 ++++++++++++++++++++++++++++++++++++++--- src/main_c.zig | 4 +- 3 files changed, 206 insertions(+), 165 deletions(-) diff --git a/src/App.zig b/src/App.zig index 1afb82e31..7986ed8d5 100644 --- a/src/App.zig +++ b/src/App.zig @@ -77,7 +77,7 @@ pub fn destroy(self: *App) void { /// tick. /// /// This returns whether the app should quit or not. -pub fn tick(self: *App, rt_app: *apprt.runtime.App) !bool { +pub fn tick(self: *App, rt_app: *apprt.App) !bool { // If any surfaces are closing, destroy them var i: usize = 0; while (i < self.surfaces.items.len) { @@ -119,7 +119,7 @@ pub fn deleteSurface(self: *App, rt_surface: *apprt.Surface) void { } /// Drain the mailbox. -fn drainMailbox(self: *App, rt_app: *apprt.runtime.App) !void { +fn drainMailbox(self: *App, rt_app: *apprt.App) !void { while (self.mailbox.pop()) |message| { log.debug("mailbox message={s}", .{@tagName(message)}); switch (message) { @@ -138,7 +138,12 @@ fn closeSurface(self: *App, rt_app: *apprt.App, surface: *Surface) !void { } /// Create a new window -fn newWindow(self: *App, rt_app: *apprt.runtime.App, msg: Message.NewWindow) !void { +fn newWindow(self: *App, rt_app: *apprt.App, msg: Message.NewWindow) !void { + if (!@hasDecl(apprt.App, "newWindow")) { + log.warn("newWindow is not supported by this runtime", .{}); + return; + } + const window = try rt_app.newWindow(); if (self.config.@"window-inherit-font-size") { if (msg.parent) |parent| { @@ -150,7 +155,12 @@ fn newWindow(self: *App, rt_app: *apprt.runtime.App, msg: Message.NewWindow) !vo } /// Create a new tab in the parent window -fn newTab(self: *App, rt_app: *apprt.runtime.App, msg: Message.NewTab) !void { +fn newTab(self: *App, rt_app: *apprt.App, msg: Message.NewTab) !void { + if (!@hasDecl(apprt.App, "newTab")) { + log.warn("newTab is not supported by this runtime", .{}); + return; + } + const parent = msg.parent orelse { log.warn("parent must be set in new_tab message", .{}); return; @@ -281,147 +291,3 @@ pub const Wasm = if (!builtin.target.isWasm()) struct {} else struct { // } // } }; - -// C API -pub const CAPI = struct { - const global = &@import("main.zig").state; - - /// Create a new app. - export fn ghostty_app_new( - opts: *const apprt.runtime.App.Options, - config: *const Config, - ) ?*App { - return app_new_(opts, config) catch |err| { - log.err("error initializing app err={}", .{err}); - return null; - }; - } - - fn app_new_( - opts: *const apprt.runtime.App.Options, - config: *const Config, - ) !*App { - const app = try App.create(global.alloc, opts.*, config); - errdefer app.destroy(); - return app; - } - - /// Tick the event loop. This should be called whenever the "wakeup" - /// callback is invoked for the runtime. - export fn ghostty_app_tick(v: *App) void { - v.tick() catch |err| { - log.err("error app tick err={}", .{err}); - }; - } - - /// Return the userdata associated with the app. - export fn ghostty_app_userdata(v: *App) ?*anyopaque { - return v.runtime.opts.userdata; - } - - export fn ghostty_app_free(ptr: ?*App) void { - if (ptr) |v| { - v.destroy(); - v.alloc.destroy(v); - } - } - - /// Create a new surface as part of an app. - export fn ghostty_surface_new( - app: *App, - opts: *const apprt.Surface.Options, - ) ?*Surface { - return surface_new_(app, opts) catch |err| { - log.err("error initializing surface err={}", .{err}); - return null; - }; - } - - fn surface_new_( - app: *App, - opts: *const apprt.Surface.Options, - ) !*Surface { - const w = try app.newWindow(.{ - .runtime = opts.*, - }); - return w; - } - - export fn ghostty_surface_free(ptr: ?*Surface) void { - if (ptr) |v| v.app.closeWindow(v); - } - - /// Returns the app associated with a surface. - export fn ghostty_surface_app(win: *Surface) *App { - return win.app; - } - - /// Tell the surface that it needs to schedule a render - export fn ghostty_surface_refresh(win: *Surface) void { - win.window.refresh(); - } - - /// Update the size of a surface. This will trigger resize notifications - /// to the pty and the renderer. - export fn ghostty_surface_set_size(win: *Surface, w: u32, h: u32) void { - win.window.updateSize(w, h); - } - - /// Update the content scale of the surface. - export fn ghostty_surface_set_content_scale(win: *Surface, x: f64, y: f64) void { - win.window.updateContentScale(x, y); - } - - /// Update the focused state of a surface. - export fn ghostty_surface_set_focus(win: *Surface, focused: bool) void { - win.window.focusCallback(focused); - } - - /// Tell the surface that it needs to schedule a render - export fn ghostty_surface_key( - win: *Surface, - action: input.Action, - key: input.Key, - mods: c_int, - ) void { - win.window.keyCallback( - action, - key, - @bitCast(input.Mods, @truncate(u8, @bitCast(c_uint, mods))), - ); - } - - /// Tell the surface that it needs to schedule a render - export fn ghostty_surface_char(win: *Surface, codepoint: u32) void { - win.window.charCallback(codepoint); - } - - /// Tell the surface that it needs to schedule a render - export fn ghostty_surface_mouse_button( - win: *Surface, - action: input.MouseButtonState, - button: input.MouseButton, - mods: c_int, - ) void { - win.window.mouseButtonCallback( - action, - button, - @bitCast(input.Mods, @truncate(u8, @bitCast(c_uint, mods))), - ); - } - - /// Update the mouse position within the view. - export fn ghostty_surface_mouse_pos(win: *Surface, x: f64, y: f64) void { - win.window.cursorPosCallback(x, y); - } - - export fn ghostty_surface_mouse_scroll(win: *Surface, x: f64, y: f64) void { - win.window.scrollCallback(x, y); - } - - export fn ghostty_surface_ime_point(win: *Surface, x: *f64, y: *f64) void { - const pos = win.imePoint(); - x.* = pos.x; - y.* = pos.y; - } -}; diff --git a/src/apprt/embedded.zig b/src/apprt/embedded.zig index c5b85705e..8c1180a87 100644 --- a/src/apprt/embedded.zig +++ b/src/apprt/embedded.zig @@ -57,18 +57,38 @@ pub const App = struct { _ = self; } - pub fn wakeup(self: App) !void { + pub fn wakeup(self: App) void { self.opts.wakeup(self.opts.userdata); } pub fn wait(self: App) !void { _ = self; } + + /// Create a new surface for the app. + fn newSurface(self: *App, opts: Surface.Options) !*Surface { + // Grab a surface allocation because we're going to need it. + var surface = try self.core_app.alloc.create(Surface); + errdefer self.core_app.alloc.destroy(surface); + + // Create the surface -- because windows are surfaces for glfw. + try surface.init(self, opts); + errdefer surface.deinit(); + + return surface; + } + + /// Close the given surface. + pub fn closeSurface(self: *App, surface: *Surface) void { + surface.deinit(); + self.core_app.alloc.destroy(surface); + } }; pub const Surface = struct { + app: *App, nsview: objc.Object, - core_surface: *CoreSurface, + core_surface: CoreSurface, content_scale: apprt.ContentScale, size: apprt.SurfaceSize, cursor_pos: apprt.CursorPos, @@ -87,6 +107,7 @@ pub const Surface = struct { pub fn init(self: *Surface, app: *App, opts: Options) !void { self.* = .{ + .app = app, .core_surface = undefined, .nsview = objc.Object.fromId(opts.nsview), .content_scale = .{ @@ -99,18 +120,23 @@ pub const Surface = struct { }; // Add ourselves to the list of surfaces on the app. - try app.app.addSurface(self); - errdefer app.app.deleteSurface(self); + try app.core_app.addSurface(self); + errdefer app.core_app.deleteSurface(self); // Initialize our surface right away. We're given a view that is // ready to use. - try self.core_surface.init(app.app, app.app.config, self); + try self.core_surface.init( + app.core_app.alloc, + app.core_app.config, + .{ .rt_app = app, .mailbox = &app.core_app.mailbox }, + self, + ); errdefer self.core_surface.deinit(); } pub fn deinit(self: *Surface) void { // Remove ourselves from the list of known surfaces in the app. - self.core_surface.app.deleteSurface(self); + self.app.core_app.deleteSurface(self); // Clean up our core surface so that all the rendering and IO stop. self.core_surface.deinit(); @@ -131,19 +157,19 @@ pub const Surface = struct { } pub fn setTitle(self: *Surface, slice: [:0]const u8) !void { - self.core_surface.app.runtime.opts.set_title( + self.app.opts.set_title( self.opts.userdata, slice.ptr, ); } pub fn getClipboardString(self: *const Surface) ![:0]const u8 { - const ptr = self.core_surface.app.runtime.opts.read_clipboard(self.opts.userdata); + const ptr = self.app.opts.read_clipboard(self.opts.userdata); return std.mem.sliceTo(ptr, 0); } pub fn setClipboardString(self: *const Surface, val: [:0]const u8) !void { - self.core_surface.app.runtime.opts.write_clipboard(self.opts.userdata, val.ptr); + self.app.opts.write_clipboard(self.opts.userdata, val.ptr); } pub fn setShouldClose(self: *Surface) void { @@ -187,7 +213,7 @@ pub const Surface = struct { } pub fn mouseButtonCallback( - self: *const Surface, + self: *Surface, action: input.MouseButtonState, button: input.MouseButton, mods: input.Mods, @@ -198,7 +224,7 @@ pub const Surface = struct { }; } - pub fn scrollCallback(self: *const Surface, xoff: f64, yoff: f64) void { + pub fn scrollCallback(self: *Surface, xoff: f64, yoff: f64) void { self.core_surface.scrollCallback(xoff, yoff) catch |err| { log.err("error in scroll callback err={}", .{err}); return; @@ -207,7 +233,7 @@ pub const Surface = struct { pub fn cursorPosCallback(self: *Surface, x: f64, y: f64) void { // Convert our unscaled x/y to scaled. - self.cursor_pos = self.core_surface.window.cursorPosToPixels(.{ + self.cursor_pos = self.cursorPosToPixels(.{ .x = @floatCast(f32, x), .y = @floatCast(f32, y), }) catch |err| { @@ -225,7 +251,7 @@ pub const Surface = struct { } pub fn keyCallback( - self: *const Surface, + self: *Surface, action: input.Action, key: input.Key, mods: input.Mods, @@ -237,7 +263,7 @@ pub const Surface = struct { }; } - pub fn charCallback(self: *const Surface, cp_: u32) void { + pub fn charCallback(self: *Surface, cp_: u32) void { const cp = std.math.cast(u21, cp_) orelse return; self.core_surface.charCallback(cp) catch |err| { log.err("error in char callback err={}", .{err}); @@ -245,7 +271,7 @@ pub const Surface = struct { }; } - pub fn focusCallback(self: *const Surface, focused: bool) void { + pub fn focusCallback(self: *Surface, focused: bool) void { self.core_surface.focusCallback(focused) catch |err| { log.err("error in focus callback err={}", .{err}); return; @@ -259,3 +285,152 @@ pub const Surface = struct { return .{ .x = pos.x * scale.x, .y = pos.y * scale.y }; } }; + +// C API +pub const CAPI = struct { + const global = &@import("../main.zig").state; + const Config = @import("../config.zig").Config; + + /// Create a new app. + export fn ghostty_app_new( + opts: *const apprt.runtime.App.Options, + config: *const Config, + ) ?*App { + return app_new_(opts, config) catch |err| { + log.err("error initializing app err={}", .{err}); + return null; + }; + } + + fn app_new_( + opts: *const apprt.runtime.App.Options, + config: *const Config, + ) !*App { + var core_app = try CoreApp.create(global.alloc, config); + errdefer core_app.destroy(); + + // Create our runtime app + var app = try global.alloc.create(App); + errdefer global.alloc.destroy(app); + app.* = try App.init(core_app, opts.*); + errdefer app.terminate(); + + return app; + } + + /// Tick the event loop. This should be called whenever the "wakeup" + /// callback is invoked for the runtime. + export fn ghostty_app_tick(v: *App) void { + _ = v.core_app.tick(v) catch |err| { + log.err("error app tick err={}", .{err}); + }; + } + + /// Return the userdata associated with the app. + export fn ghostty_app_userdata(v: *App) ?*anyopaque { + return v.opts.userdata; + } + + export fn ghostty_app_free(v: *App) void { + const core_app = v.core_app; + v.terminate(); + global.alloc.destroy(v); + core_app.destroy(); + } + + /// Create a new surface as part of an app. + export fn ghostty_surface_new( + app: *App, + opts: *const apprt.Surface.Options, + ) ?*Surface { + return surface_new_(app, opts) catch |err| { + log.err("error initializing surface err={}", .{err}); + return null; + }; + } + + fn surface_new_( + app: *App, + opts: *const apprt.Surface.Options, + ) !*Surface { + return try app.newSurface(opts.*); + } + + export fn ghostty_surface_free(ptr: *Surface) void { + ptr.app.closeSurface(ptr); + } + + /// Returns the app associated with a surface. + export fn ghostty_surface_app(surface: *Surface) *App { + return surface.app; + } + + /// Tell the surface that it needs to schedule a render + export fn ghostty_surface_refresh(surface: *Surface) void { + surface.refresh(); + } + + /// Update the size of a surface. This will trigger resize notifications + /// to the pty and the renderer. + export fn ghostty_surface_set_size(surface: *Surface, w: u32, h: u32) void { + surface.updateSize(w, h); + } + + /// Update the content scale of the surface. + export fn ghostty_surface_set_content_scale(surface: *Surface, x: f64, y: f64) void { + surface.updateContentScale(x, y); + } + + /// Update the focused state of a surface. + export fn ghostty_surface_set_focus(surface: *Surface, focused: bool) void { + surface.focusCallback(focused); + } + + /// Tell the surface that it needs to schedule a render + export fn ghostty_surface_key( + surface: *Surface, + action: input.Action, + key: input.Key, + mods: c_int, + ) void { + surface.keyCallback( + action, + key, + @bitCast(input.Mods, @truncate(u8, @bitCast(c_uint, mods))), + ); + } + + /// Tell the surface that it needs to schedule a render + export fn ghostty_surface_char(surface: *Surface, codepoint: u32) void { + surface.charCallback(codepoint); + } + + /// Tell the surface that it needs to schedule a render + export fn ghostty_surface_mouse_button( + surface: *Surface, + action: input.MouseButtonState, + button: input.MouseButton, + mods: c_int, + ) void { + surface.mouseButtonCallback( + action, + button, + @bitCast(input.Mods, @truncate(u8, @bitCast(c_uint, mods))), + ); + } + + /// Update the mouse position within the view. + export fn ghostty_surface_mouse_pos(surface: *Surface, x: f64, y: f64) void { + surface.cursorPosCallback(x, y); + } + + export fn ghostty_surface_mouse_scroll(surface: *Surface, x: f64, y: f64) void { + surface.scrollCallback(x, y); + } + + export fn ghostty_surface_ime_point(surface: *Surface, x: *f64, y: *f64) void { + const pos = surface.core_surface.imePoint(); + x.* = pos.x; + y.* = pos.y; + } +}; diff --git a/src/main_c.zig b/src/main_c.zig index 344d477bb..05cf43897 100644 --- a/src/main_c.zig +++ b/src/main_c.zig @@ -10,10 +10,10 @@ const std = @import("std"); const assert = std.debug.assert; const builtin = @import("builtin"); const main = @import("main.zig"); +const apprt = @import("apprt.zig"); // Some comptime assertions that our C API depends on. comptime { - const apprt = @import("apprt.zig"); assert(apprt.runtime == apprt.embedded); } @@ -21,7 +21,7 @@ comptime { pub const std_options = main.std_options; pub usingnamespace @import("config.zig").CAPI; -pub usingnamespace @import("App.zig").CAPI; +pub usingnamespace apprt.runtime.CAPI; /// Initialize ghostty global state. It is possible to have more than /// one global state but it has zero practical benefit. From 2dda1d65a41093f43d34125ceee5d9249325346d Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 22 Feb 2023 21:11:59 -0800 Subject: [PATCH 14/37] main update to new runtime API --- src/main.zig | 38 ++++++++++++-------------------------- 1 file changed, 12 insertions(+), 26 deletions(-) diff --git a/src/main.zig b/src/main.zig index e7d0e7b1e..143fa0cca 100644 --- a/src/main.zig +++ b/src/main.zig @@ -90,33 +90,19 @@ pub fn main() !void { try config.finalize(); std.log.debug("config={}", .{config}); - 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 - _ = try app_runtime.newWindow(); - - // 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); + // Create our app state + 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(); + + // Create our runtime app + var app_runtime = try apprt.App.init(app, .{}); + defer app_runtime.terminate(); + + // Create an initial window + _ = try app_runtime.newWindow(); + + // Run the GUI event loop + try app_runtime.run(); } // Required by tracy/tracy.zig to enable/disable tracy support. From 153004eb6f90200d70c20d203933a1673f502a4b Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 22 Feb 2023 21:25:42 -0800 Subject: [PATCH 15/37] apprt/gtk: compiles again -- does nothing --- src/apprt/gtk.zig | 60 ++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 49 insertions(+), 11 deletions(-) diff --git a/src/apprt/gtk.zig b/src/apprt/gtk.zig index 4bf3bff8e..c3444655e 100644 --- a/src/apprt/gtk.zig +++ b/src/apprt/gtk.zig @@ -27,12 +27,11 @@ pub const App = struct { id: [:0]const u8 = "com.mitchellh.ghostty", }; + core_app: *CoreApp, app: *c.GtkApplication, ctx: *c.GMainContext, 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, @@ -74,7 +73,11 @@ pub const App = struct { // https://gitlab.gnome.org/GNOME/glib/-/blob/bd2ccc2f69ecfd78ca3f34ab59e42e2b462bad65/gio/gapplication.c#L2302 c.g_application_activate(gapp); - return .{ .app = app, .ctx = ctx }; + return .{ + .core_app = core_app, + .app = app, + .ctx = ctx, + }; } // Terminate the application. The application will not be restarted after @@ -91,11 +94,24 @@ pub const App = struct { c.g_main_context_wakeup(null); } - pub fn wait(self: App) !void { - _ = c.g_main_context_iteration(self.ctx, 1); + /// Run the event loop. This doesn't return until the app exits. + pub fn run(self: *App) !void { + while (true) { + _ = c.g_main_context_iteration(self.ctx, 1); + + // Tick the terminal app + const should_quit = try self.core_app.tick(self); + if (false and should_quit) return; + } } - pub fn newWindow(self: App) !void { + /// Close the given surface. + pub fn closeSurface(self: *App, surface: *Surface) void { + surface.deinit(); + self.core_app.alloc.destroy(surface); + } + + pub fn newWindow(self: App) !*Surface { 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); @@ -120,6 +136,8 @@ pub const App = struct { ); c.gtk_widget_show(window); + + return undefined; } fn activate(app: *c.GtkApplication, ud: ?*anyopaque) callconv(.C) void { @@ -150,12 +168,32 @@ pub const App = struct { pub const Surface = struct { pub const Options = struct {}; - pub fn init(app: *const CoreApp, core_win: *CoreSurface, opts: Options) !Surface { - _ = app; - _ = core_win; - _ = opts; + /// The app we're part of + app: *App, - return .{}; + /// The core surface backing this surface + core_surface: CoreSurface, + + pub fn init(self: *Surface, app: *App) !void { + // Build our result + self.* = .{ + .app = app, + .core_surface = undefined, + }; + errdefer self.* = undefined; + + // Add ourselves to the list of surfaces on the app. + try app.app.addSurface(self); + errdefer app.app.deleteSurface(self); + + // Initialize our surface now that we have the stable pointer. + try self.core_surface.init( + app.app.alloc, + app.app.config, + .{ .rt_app = app, .mailbox = &app.app.mailbox }, + self, + ); + errdefer self.core_surface.deinit(); } pub fn deinit(self: *Surface) void { From fb13838532fed17ad0695728c776e4f2aefec32a Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 23 Feb 2023 08:44:01 -0800 Subject: [PATCH 16/37] apprt newWindow/newTab do not have to return a surface --- src/App.zig | 19 +++++++++---------- src/apprt/glfw.zig | 41 +++++++++++++++++++++++++---------------- src/main.zig | 2 +- 3 files changed, 35 insertions(+), 27 deletions(-) diff --git a/src/App.zig b/src/App.zig index 7986ed8d5..7776dc49d 100644 --- a/src/App.zig +++ b/src/App.zig @@ -144,14 +144,14 @@ fn newWindow(self: *App, rt_app: *apprt.App, msg: Message.NewWindow) !void { return; } - const window = try rt_app.newWindow(); - if (self.config.@"window-inherit-font-size") { - if (msg.parent) |parent| { - if (self.hasSurface(parent)) { - window.core_surface.setFontSize(parent.font_size); - } - } - } + const parent = if (msg.parent) |parent| parent: { + break :parent if (self.hasSurface(parent)) + parent + else + null; + } else null; + + try rt_app.newWindow(parent); } /// Create a new tab in the parent window @@ -172,8 +172,7 @@ fn newTab(self: *App, rt_app: *apprt.App, msg: Message.NewTab) !void { return; } - const window = try rt_app.newTab(parent); - if (self.config.@"window-inherit-font-size") window.core_surface.setFontSize(parent.font_size); + try rt_app.newTab(parent); } /// Start quitting diff --git a/src/apprt/glfw.zig b/src/apprt/glfw.zig index 59c79dfa5..3faab933d 100644 --- a/src/apprt/glfw.zig +++ b/src/apprt/glfw.zig @@ -74,27 +74,19 @@ pub const App = struct { } /// Create a new window for the app. - pub fn newWindow(self: *App) !*Surface { - // Grab a surface allocation because we're going to need it. - var surface = try self.app.alloc.create(Surface); - errdefer self.app.alloc.destroy(surface); - - // Create the surface -- because windows are surfaces for glfw. - try surface.init(self); - errdefer surface.deinit(); - - return surface; + pub fn newWindow(self: *App, parent_: ?*CoreSurface) !void { + _ = try self.newSurface(parent_); } /// Create a new tab in the parent surface. - pub fn newTab(self: *App, parent: *CoreSurface) !*Surface { + pub fn newTab(self: *App, parent: *CoreSurface) !void { if (!Darwin.enabled) { log.warn("tabbing is not supported on this platform", .{}); - return error.TabbingNotSupported; + return; } // Create the new window - const window = try self.newWindow(); + const window = try self.newWindow(parent); // Add the new window the parent window const parent_win = glfwNative.getCocoaWindow(parent.rt_surface.window).?; @@ -112,14 +104,31 @@ pub const App = struct { // point in the grid. const size = parent.rt_surface.getSize() catch |err| { log.err("error querying window size for size callback on new tab err={}", .{err}); - return window; + return; }; parent.sizeCallback(size) catch |err| { log.err("error in size callback from new tab err={}", .{err}); - return window; + return; }; + } - return window; + fn newSurface(self: *App, parent_: ?*CoreSurface) !*Surface { + // Grab a surface allocation because we're going to need it. + var surface = try self.app.alloc.create(Surface); + errdefer self.app.alloc.destroy(surface); + + // Create the surface -- because windows are surfaces for glfw. + try surface.init(self); + errdefer surface.deinit(); + + // If we have a parent, inherit some properties + if (self.app.config.@"window-inherit-font-size") { + if (parent_) |parent| { + surface.core_surface.setFontSize(parent.font_size); + } + } + + return surface; } /// Close the given surface. diff --git a/src/main.zig b/src/main.zig index 143fa0cca..52447f456 100644 --- a/src/main.zig +++ b/src/main.zig @@ -99,7 +99,7 @@ pub fn main() !void { defer app_runtime.terminate(); // Create an initial window - _ = try app_runtime.newWindow(); + try app_runtime.newWindow(null); // Run the GUI event loop try app_runtime.run(); From 7991e6e495a3350330b304f63918d54cabf13103 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 23 Feb 2023 08:46:52 -0800 Subject: [PATCH 17/37] apprt/glfw: fix macos build --- src/apprt/glfw.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/apprt/glfw.zig b/src/apprt/glfw.zig index 3faab933d..6ba10656d 100644 --- a/src/apprt/glfw.zig +++ b/src/apprt/glfw.zig @@ -86,7 +86,7 @@ pub const App = struct { } // Create the new window - const window = try self.newWindow(parent); + const window = try self.newSurface(parent); // Add the new window the parent window const parent_win = glfwNative.getCocoaWindow(parent.rt_surface.window).?; From 7f34afa3953289d33f95150c5f7d6a6d458a6993 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 23 Feb 2023 09:43:52 -0800 Subject: [PATCH 18/37] gtk: hook up GL area and render a color --- src/App.zig | 3 - src/apprt/gtk.zig | 160 +++++++++++++++++++++++++++++++--------------- 2 files changed, 108 insertions(+), 55 deletions(-) diff --git a/src/App.zig b/src/App.zig index 7776dc49d..1154cf86d 100644 --- a/src/App.zig +++ b/src/App.zig @@ -231,9 +231,6 @@ pub const Message = union(enum) { }, const NewWindow = struct { - /// Runtime-specific window options. - runtime: apprt.runtime.Surface.Options = .{}, - /// The parent surface parent: ?*Surface = null, }; diff --git a/src/apprt/gtk.zig b/src/apprt/gtk.zig index c3444655e..69c2dcd52 100644 --- a/src/apprt/gtk.zig +++ b/src/apprt/gtk.zig @@ -107,37 +107,37 @@ pub const App = struct { /// Close the given surface. pub fn closeSurface(self: *App, surface: *Surface) void { - surface.deinit(); - self.core_app.alloc.destroy(surface); + _ = self; + _ = surface; + + // This shouldn't be called because we should be working within + // the GTK lifecycle and we can't just deallocate surfaces here. + @panic("This should not be called with GTK."); } - pub fn newWindow(self: App) !*Surface { + pub fn newWindow(self: *App, parent_: ?*CoreSurface) !void { + _ = parent_; + + // Grab a surface allocation we'll need it later. + var surface = try self.core_app.alloc.create(Surface); + errdefer self.core_app.alloc.destroy(surface); + 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, - ); - + const gtk_window = @ptrCast(*c.GtkWindow, window); + c.gtk_window_set_title(gtk_window, "Ghostty"); + c.gtk_window_set_default_size(gtk_window, 200, 200); c.gtk_widget_show(window); - return undefined; + // Initialize the GtkGLArea and attach it to our surface. + // The surface starts in the "unrealized" state because we have to + // wait for the "realize" callback from GTK to know that the OpenGL + // context is ready. See Surface docs for more info. + const gl_area = c.gtk_gl_area_new(); + try surface.init(self, .{ + .gl_area = @ptrCast(*c.GtkGLArea, gl_area), + }); + errdefer surface.deinit(); + c.gtk_window_set_child(gtk_window, gl_area); } fn activate(app: *c.GtkApplication, ud: ?*anyopaque) callconv(.C) void { @@ -150,23 +150,17 @@ pub const App = struct { // 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 Surface = struct { - pub const Options = struct {}; + pub const Options = struct { + gl_area: *c.GtkGLArea, + }; + + /// Whether the surface has been realized or not yet. When a surface is + /// "realized" it means that the OpenGL context is ready and the core + /// surface has been initialized. + realized: bool = false, /// The app we're part of app: *App, @@ -174,7 +168,7 @@ pub const Surface = struct { /// The core surface backing this surface core_surface: CoreSurface, - pub fn init(self: *Surface, app: *App) !void { + pub fn init(self: *Surface, app: *App, opts: Options) !void { // Build our result self.* = .{ .app = app, @@ -182,18 +176,80 @@ pub const Surface = struct { }; errdefer self.* = undefined; - // Add ourselves to the list of surfaces on the app. - try app.app.addSurface(self); - errdefer app.app.deleteSurface(self); - - // Initialize our surface now that we have the stable pointer. - try self.core_surface.init( - app.app.alloc, - app.app.config, - .{ .rt_app = app, .mailbox = &app.app.mailbox }, - self, + // Create the GL area that will contain our surface + _ = c.g_signal_connect_data( + opts.gl_area, + "realize", + c.G_CALLBACK(>kRealize), + null, + null, + c.G_CONNECT_DEFAULT, ); - errdefer self.core_surface.deinit(); + _ = c.g_signal_connect_data( + opts.gl_area, + "render", + c.G_CALLBACK(>kRender), + null, + null, + c.G_CONNECT_DEFAULT, + ); + _ = c.g_signal_connect_data( + opts.gl_area, + "destroy", + c.G_CALLBACK(>kDestroy), + self, + null, + c.G_CONNECT_DEFAULT, + ); + + // // Add ourselves to the list of surfaces on the app. + // try app.app.addSurface(self); + // errdefer app.app.deleteSurface(self); + // + // // Initialize our surface now that we have the stable pointer. + // try self.core_surface.init( + // app.app.alloc, + // app.app.config, + // .{ .rt_app = app, .mailbox = &app.app.mailbox }, + // self, + // ); + // errdefer self.core_surface.deinit(); + } + + fn gtkRealize(area: *c.GtkGLArea, ud: ?*anyopaque) callconv(.C) void { + _ = area; + _ = ud; + + log.debug("gl surface realized", .{}); + const opengl = @import("../renderer/opengl/main.zig"); + log.warn("foo: {}", .{opengl.glad.load(null) catch 0}); + } + + fn gtkRender(area: *c.GtkGLArea, ctx: *c.GdkGLContext, ud: ?*anyopaque) callconv(.C) c.gboolean { + _ = area; + _ = ctx; + _ = ud; + log.debug("gl render", .{}); + + const opengl = @import("../renderer/opengl/main.zig"); + opengl.clearColor(0, 0.5, 1, 1); + opengl.clear(opengl.c.GL_COLOR_BUFFER_BIT); + + return 1; + } + + /// "destroy" signal for surface + fn gtkDestroy(v: *c.GtkWidget, ud: ?*anyopaque) callconv(.C) void { + _ = v; + + const self = userdataSelf(ud orelse return); + const alloc = self.app.core_app.alloc; + self.deinit(); + alloc.destroy(self); + } + + fn userdataSelf(ud: *anyopaque) *Surface { + return @ptrCast(*Surface, @alignCast(@alignOf(Surface), ud)); } pub fn deinit(self: *Surface) void { From 7eb7cae9e00111196677f4de867b87cd580d5c65 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 23 Feb 2023 10:15:58 -0800 Subject: [PATCH 19/37] opengl: move screen size GL context changes into draw path --- src/apprt/gtk.zig | 42 +++++++++------ src/renderer/OpenGL.zig | 117 +++++++++++++++++++++++++++------------- 2 files changed, 105 insertions(+), 54 deletions(-) diff --git a/src/apprt/gtk.zig b/src/apprt/gtk.zig index 69c2dcd52..b0426a061 100644 --- a/src/apprt/gtk.zig +++ b/src/apprt/gtk.zig @@ -89,7 +89,7 @@ pub const App = struct { c.g_object_unref(self.app); } - pub fn wakeup(self: App) !void { + pub fn wakeup(self: App) void { _ = self; c.g_main_context_wakeup(null); } @@ -181,7 +181,7 @@ pub const Surface = struct { opts.gl_area, "realize", c.G_CALLBACK(>kRealize), - null, + self, null, c.G_CONNECT_DEFAULT, ); @@ -201,28 +201,36 @@ pub const Surface = struct { null, c.G_CONNECT_DEFAULT, ); + } - // // Add ourselves to the list of surfaces on the app. - // try app.app.addSurface(self); - // errdefer app.app.deleteSurface(self); - // - // // Initialize our surface now that we have the stable pointer. - // try self.core_surface.init( - // app.app.alloc, - // app.app.config, - // .{ .rt_app = app, .mailbox = &app.app.mailbox }, - // self, - // ); - // errdefer self.core_surface.deinit(); + fn realize(self: *Surface) !void { + // Add ourselves to the list of surfaces on the app. + try self.app.core_app.addSurface(self); + errdefer self.app.core_app.deleteSurface(self); + + // Initialize our surface now that we have the stable pointer. + try self.core_surface.init( + self.app.core_app.alloc, + self.app.core_app.config, + .{ .rt_app = self.app, .mailbox = &self.app.core_app.mailbox }, + self, + ); + errdefer self.core_surface.deinit(); } fn gtkRealize(area: *c.GtkGLArea, ud: ?*anyopaque) callconv(.C) void { _ = area; - _ = ud; log.debug("gl surface realized", .{}); - const opengl = @import("../renderer/opengl/main.zig"); - log.warn("foo: {}", .{opengl.glad.load(null) catch 0}); + + // realize means that our OpenGL context is ready, so we can now + // initialize the core surface which will setup the renderer. + const self = userdataSelf(ud orelse return); + self.realize() catch |err| { + // TODO: we need to destroy the GL area here. + log.err("surface failed to realize: {}", .{err}); + return; + }; } fn gtkRender(area: *c.GtkGLArea, ctx: *c.GdkGLContext, ud: ?*anyopaque) callconv(.C) c.gboolean { diff --git a/src/renderer/OpenGL.zig b/src/renderer/OpenGL.zig index 3c3554fdc..8c8061028 100644 --- a/src/renderer/OpenGL.zig +++ b/src/renderer/OpenGL.zig @@ -91,6 +91,55 @@ padding: renderer.Options.Padding, /// The mailbox for communicating with the window. surface_mailbox: apprt.surface.Mailbox, +/// Deferred operations. This is used to apply changes to the OpenGL context. +/// Some runtimes (GTK) do not support multi-threading so to keep our logic +/// simple we apply all OpenGL context changes in the render() call. +deferred_screen_size: ?SetScreenSize = null, + +/// Defererred OpenGL operation to update the screen size. +const SetScreenSize = struct { + size: renderer.ScreenSize, + + fn apply(self: SetScreenSize, r: *const OpenGL) !void { + // Apply our padding + const padding = r.padding.explicit.add(if (r.padding.balance) + renderer.Padding.balanced(self.size, r.gridSize(self.size), r.cell_size) + else + .{}); + const padded_size = self.size.subPadding(padding); + + log.debug("GL api: screen size padded={} screen={} grid={} cell={} padding={}", .{ + padded_size, + self.size, + r.gridSize(self.size), + r.cell_size, + r.padding.explicit, + }); + + // Update our viewport for this context to be the entire window. + // OpenGL works in pixels, so we have to use the pixel size. + try gl.viewport( + 0, + 0, + @intCast(i32, self.size.width), + @intCast(i32, self.size.height), + ); + + // Update the projection uniform within our shader + try r.program.setUniform( + "projection", + + // 2D orthographic projection with the full w/h + math.ortho2d( + -1 * padding.left, + @intToFloat(f32, padded_size.width) + padding.right, + @intToFloat(f32, padded_size.height) + padding.bottom, + -1 * padding.top, + ), + ); + } +}; + /// The raw structure that maps directly to the buffer sent to the vertex shader. /// This must be "extern" so that the field order is not reordered by the /// Zig compiler. @@ -366,7 +415,22 @@ pub fn glfwWindowHints() glfw.Window.Hints { pub fn surfaceInit(surface: *apprt.Surface) !void { // Treat this like a thread entry const self: OpenGL = undefined; - try self.threadEnter(surface); + + switch (apprt.runtime) { + else => @compileError("unsupported app runtime for OpenGL"), + + apprt.gtk => { + // GTK uses global OpenGL context so we load from null. + const version = try gl.glad.load(null); + errdefer gl.glad.unload(); + log.info("loaded OpenGL {}.{}", .{ + gl.glad.versionMajor(@intCast(c_uint, version)), + gl.glad.versionMinor(@intCast(c_uint, version)), + }); + }, + + apprt.glfw => try self.threadEnter(surface), + } // Blending for text. We use GL_ONE here because we should be using // premultiplied alpha for all our colors in our fragment shaders. @@ -459,7 +523,10 @@ pub fn threadExit(self: *const OpenGL) void { switch (apprt.runtime) { else => @compileError("unsupported app runtime for OpenGL"), - apprt.gtk => {}, + apprt.gtk => { + // We don't need to do any unloading for GTK because we may + // be sharing the global bindings with other windows. + }, apprt.glfw => { gl.glad.unload(); @@ -550,6 +617,7 @@ pub fn render( surface: *apprt.Surface, state: *renderer.State, ) !void { + log.warn("RENDER", .{}); // Data we extract out of the critical area. const Critical = struct { gl_bg: terminal.color.RGB, @@ -1075,7 +1143,7 @@ pub fn updateCell( /// Returns the grid size for a given screen size. This is safe to call /// on any thread. -fn gridSize(self: *OpenGL, screen_size: renderer.ScreenSize) renderer.GridSize { +fn gridSize(self: *const OpenGL, screen_size: renderer.ScreenSize) renderer.GridSize { return renderer.GridSize.init( screen_size.subPadding(self.padding.explicit), self.cell_size, @@ -1088,15 +1156,7 @@ pub fn setScreenSize(self: *OpenGL, dim: renderer.ScreenSize) !void { // Recalculate the rows/columns. const grid_size = self.gridSize(dim); - // Apply our padding - const padding = self.padding.explicit.add(if (self.padding.balance) - renderer.Padding.balanced(dim, grid_size, self.cell_size) - else - .{}); - const padded_dim = dim.subPadding(padding); - - log.debug("screen size padded={} screen={} grid={} cell={} padding={}", .{ - padded_dim, + log.debug("screen size screen={} grid={} cell={} padding={}", .{ dim, grid_size, self.cell_size, @@ -1118,31 +1178,8 @@ pub fn setScreenSize(self: *OpenGL, dim: renderer.ScreenSize) !void { self.alloc.free(self.font_shaper.cell_buf); self.font_shaper.cell_buf = shape_buf; - // Update our viewport for this context to be the entire window. - // OpenGL works in pixels, so we have to use the pixel size. - try gl.viewport( - 0, - 0, - @intCast(i32, dim.width), - @intCast(i32, dim.height), - ); - - // Update the projection uniform within our shader - { - const bind = try self.program.use(); - defer bind.unbind(); - try self.program.setUniform( - "projection", - - // 2D orthographic projection with the full w/h - math.ortho2d( - -1 * padding.left, - @intToFloat(f32, padded_dim.width) + padding.right, - @intToFloat(f32, padded_dim.height) + padding.bottom, - -1 * padding.top, - ), - ); - } + // Defer our OpenGL updates + self.deferred_screen_size = .{ .size = dim }; } /// Updates the font texture atlas if it is dirty. @@ -1250,6 +1287,12 @@ pub fn draw(self: *OpenGL) !void { const pbind = try self.program.use(); defer pbind.unbind(); + // If we have deferred operations, run them. + if (self.deferred_screen_size) |v| { + try v.apply(self); + self.deferred_screen_size = null; + } + try self.drawCells(binding, self.cells_bg); try self.drawCells(binding, self.cells); } From b19f9b2aff86a219fd6c8a0096d1b39961b7abfe Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 23 Feb 2023 10:36:45 -0800 Subject: [PATCH 20/37] opengl: enable single-threaded draw --- src/apprt/gtk.zig | 4 ++ src/renderer/OpenGL.zig | 87 +++++++++++++++++++++++++++++------------ 2 files changed, 66 insertions(+), 25 deletions(-) diff --git a/src/apprt/gtk.zig b/src/apprt/gtk.zig index b0426a061..62020e4eb 100644 --- a/src/apprt/gtk.zig +++ b/src/apprt/gtk.zig @@ -153,6 +153,10 @@ pub const App = struct { }; pub const Surface = struct { + /// This is detected by the OpenGL renderer to move to a single-threaded + /// draw operation. This basically puts locks around our draw path. + pub const opengl_single_threaded_draw = true; + pub const Options = struct { gl_area: *c.GtkGLArea, }; diff --git a/src/renderer/OpenGL.zig b/src/renderer/OpenGL.zig index 8c8061028..9a2a2fc47 100644 --- a/src/renderer/OpenGL.zig +++ b/src/renderer/OpenGL.zig @@ -22,15 +22,25 @@ const Surface = @import("../Surface.zig"); const log = std.log.scoped(.grid); -// The LRU is keyed by (screen, row_id) since we need to cache rows -// separately for alt screens. By storing that in the key, we very likely -// have the cache already for when the primary screen is reactivated. +/// The LRU is keyed by (screen, row_id) since we need to cache rows +/// separately for alt screens. By storing that in the key, we very likely +/// have the cache already for when the primary screen is reactivated. const CellsLRU = lru.AutoHashMap(struct { selection: ?terminal.Selection, screen: terminal.Terminal.ScreenType, row_id: terminal.Screen.RowHeader.Id, }, std.ArrayListUnmanaged(GPUCell)); +/// The runtime can request a single-threaded draw by setting this boolean +/// to true. In this case, the renderer.draw() call is expected to be called +/// from the runtime. +const single_threaded_draw = if (@hasDecl(apprt.Surface, "opengl_single_threaded_draw")) + apprt.Surface.opengl_single_threaded_draw +else + false; +const DrawMutex = if (single_threaded_draw) std.Thread.Mutex else void; +const drawMutexZero = if (DrawMutex == void) void{} else .{}; + alloc: std.mem.Allocator, /// Current cell dimensions for this grid. @@ -96,6 +106,13 @@ surface_mailbox: apprt.surface.Mailbox, /// simple we apply all OpenGL context changes in the render() call. deferred_screen_size: ?SetScreenSize = null, +/// If we're drawing with single threaded operations +draw_mutex: DrawMutex = drawMutexZero, + +/// Current background to draw. This may not match self.background if the +/// terminal is in reversed mode. +draw_background: terminal.color.RGB, + /// Defererred OpenGL operation to update the screen size. const SetScreenSize = struct { size: renderer.ScreenSize, @@ -350,6 +367,7 @@ pub fn init(alloc: Allocator, options: renderer.Options) !OpenGL { .cursor_color = if (options.config.@"cursor-color") |col| col.toTerminalRGB() else null, .background = options.config.background.toTerminalRGB(), .foreground = options.config.foreground.toTerminalRGB(), + .draw_background = options.config.background.toTerminalRGB(), .selection_background = if (options.config.@"selection-background") |bg| bg.toTerminalRGB() else @@ -554,6 +572,7 @@ pub fn blinkCursor(self: *OpenGL, reset: bool) void { /// Must be called on the render thread. pub fn setFontSize(self: *OpenGL, size: font.face.DesiredSize) !void { log.info("set font size={}", .{size}); + if (apprt.runtime == apprt.gtk) @panic("TODO: make thread safe"); // Set our new size, this will also reset our font atlas. try self.font_group.setSize(size); @@ -617,7 +636,6 @@ pub fn render( surface: *apprt.Surface, state: *renderer.State, ) !void { - log.warn("RENDER", .{}); // Data we extract out of the critical area. const Critical = struct { gl_bg: terminal.color.RGB, @@ -703,28 +721,28 @@ pub fn render( }; defer critical.screen.deinit(); - // Build our GPU cells - try self.rebuildCells( - critical.active_screen, - critical.selection, - &critical.screen, - critical.draw_cursor, - ); + // Grab our draw mutex if we have it and update our data + { + if (single_threaded_draw) self.draw_mutex.lock(); + defer if (single_threaded_draw) self.draw_mutex.unlock(); - // Try to flush our atlas, this will only do something if there - // are changes to the atlas. - try self.flushAtlas(); + // Set our draw data + self.draw_background = critical.gl_bg; - // Clear the surface - gl.clearColor( - @intToFloat(f32, critical.gl_bg.r) / 255, - @intToFloat(f32, critical.gl_bg.g) / 255, - @intToFloat(f32, critical.gl_bg.b) / 255, - 1.0, - ); - gl.clear(gl.c.GL_COLOR_BUFFER_BIT); + // Build our GPU cells + try self.rebuildCells( + critical.active_screen, + critical.selection, + &critical.screen, + critical.draw_cursor, + ); + } + + // We're out of the critical path now. Let's render. We only render if + // we're not single threaded. If we're single threaded we expect the + // runtime to call draw. + if (single_threaded_draw) return; - // We're out of the critical path now. Let's first render our terminal. try self.draw(); // If we have devmode, then render that @@ -735,8 +753,10 @@ pub fn render( } // Swap our window buffers - if (apprt.runtime == apprt.gtk) @panic("TODO"); - surface.window.swapBuffers(); + switch (apprt.runtime) { + else => @compileError("unsupported runtime"), + apprt.glfw => surface.window.swapBuffers(), + } } /// rebuildCells rebuilds all the GPU cells from our CPU state. This is a @@ -1259,9 +1279,26 @@ pub fn draw(self: *OpenGL) !void { const t = trace(@src()); defer t.end(); + // If we're in single-threaded more we grab a lock since we use shared data. + if (single_threaded_draw) self.draw_mutex.lock(); + defer if (single_threaded_draw) self.draw_mutex.unlock(); + // If we have no cells to render, then we render nothing. if (self.cells.items.len == 0) return; + // Try to flush our atlas, this will only do something if there + // are changes to the atlas. + try self.flushAtlas(); + + // Clear the surface + gl.clearColor( + @intToFloat(f32, self.draw_background.r) / 255, + @intToFloat(f32, self.draw_background.g) / 255, + @intToFloat(f32, self.draw_background.b) / 255, + 1.0, + ); + gl.clear(gl.c.GL_COLOR_BUFFER_BIT); + // Setup our VAO try self.vao.bind(); defer gl.VertexArray.unbind() catch null; From 6acf67ec662c124a11debdf0669b822f84b1508b Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 23 Feb 2023 11:19:51 -0800 Subject: [PATCH 21/37] gtk: render! --- src/App.zig | 12 ++++ src/Surface.zig | 1 + src/apprt/glfw.zig | 7 ++ src/apprt/gtk.zig | 100 ++++++++++++++++++---------- src/renderer/OpenGL.zig | 2 +- src/renderer/Thread.zig | 15 +++++ src/renderer/opengl/Program.zig | 1 + src/renderer/opengl/Texture.zig | 2 + src/renderer/opengl/VertexArray.zig | 2 + 9 files changed, 106 insertions(+), 36 deletions(-) diff --git a/src/App.zig b/src/App.zig index 1154cf86d..683a70f47 100644 --- a/src/App.zig +++ b/src/App.zig @@ -128,6 +128,7 @@ fn drainMailbox(self: *App, rt_app: *apprt.App) !void { .close => |surface| try self.closeSurface(rt_app, surface), .quit => try self.setQuit(), .surface_message => |msg| try self.surfaceMessage(msg.surface, msg.message), + .redraw_surface => |surface| try self.redrawSurface(rt_app, surface), } } } @@ -137,6 +138,11 @@ fn closeSurface(self: *App, rt_app: *apprt.App, surface: *Surface) !void { rt_app.closeSurface(surface.rt_surface); } +fn redrawSurface(self: *App, rt_app: *apprt.App, surface: *apprt.Surface) !void { + if (!self.hasSurface(&surface.core_surface)) return; + rt_app.redrawSurface(surface); +} + /// Create a new window fn newWindow(self: *App, rt_app: *apprt.App, msg: Message.NewWindow) !void { if (!@hasDecl(apprt.App, "newWindow")) { @@ -230,6 +236,12 @@ pub const Message = union(enum) { message: apprt.surface.Message, }, + /// Redraw a surface. This only has an effect for runtimes that + /// use single-threaded draws. To redraw a surface for all runtimes, + /// wake up the renderer thread. The renderer thread will send this + /// message if it needs to. + redraw_surface: *apprt.Surface, + const NewWindow = struct { /// The parent surface parent: ?*Surface = null, diff --git a/src/Surface.zig b/src/Surface.zig index 65740340e..b7ce3ede7 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -316,6 +316,7 @@ pub fn init( rt_surface, &self.renderer, &self.renderer_state, + app_mailbox, ); errdefer render_thread.deinit(); diff --git a/src/apprt/glfw.zig b/src/apprt/glfw.zig index 6ba10656d..dbdb925d3 100644 --- a/src/apprt/glfw.zig +++ b/src/apprt/glfw.zig @@ -137,6 +137,13 @@ pub const App = struct { self.app.alloc.destroy(surface); } + pub fn redrawSurface(self: *App, surface: *Surface) void { + _ = self; + _ = surface; + + @panic("This should never be called for GLFW."); + } + fn glfwErrorCallback(code: glfw.ErrorCode, desc: [:0]const u8) void { std.log.warn("glfw error={} message={s}", .{ code, desc }); diff --git a/src/apprt/gtk.zig b/src/apprt/gtk.zig index 62020e4eb..4c4cb9429 100644 --- a/src/apprt/gtk.zig +++ b/src/apprt/gtk.zig @@ -115,6 +115,11 @@ pub const App = struct { @panic("This should not be called with GTK."); } + pub fn redrawSurface(self: *App, surface: *Surface) void { + _ = self; + surface.invalidate(); + } + pub fn newWindow(self: *App, parent_: ?*CoreSurface) !void { _ = parent_; @@ -169,42 +174,30 @@ pub const Surface = struct { /// The app we're part of app: *App, + /// Our GTK area + gl_area: *c.GtkGLArea, + /// The core surface backing this surface core_surface: CoreSurface, + /// Cached metrics about the surface from GTK callbacks. + size: apprt.SurfaceSize, + pub fn init(self: *Surface, app: *App, opts: Options) !void { // Build our result self.* = .{ .app = app, + .gl_area = opts.gl_area, .core_surface = undefined, + .size = .{ .width = 800, .height = 600 }, }; errdefer self.* = undefined; // Create the GL area that will contain our surface - _ = c.g_signal_connect_data( - opts.gl_area, - "realize", - c.G_CALLBACK(>kRealize), - self, - null, - c.G_CONNECT_DEFAULT, - ); - _ = c.g_signal_connect_data( - opts.gl_area, - "render", - c.G_CALLBACK(>kRender), - null, - null, - c.G_CONNECT_DEFAULT, - ); - _ = c.g_signal_connect_data( - opts.gl_area, - "destroy", - c.G_CALLBACK(>kDestroy), - self, - null, - c.G_CONNECT_DEFAULT, - ); + _ = c.g_signal_connect_data(opts.gl_area, "realize", c.G_CALLBACK(>kRealize), self, null, c.G_CONNECT_DEFAULT); + _ = c.g_signal_connect_data(opts.gl_area, "destroy", c.G_CALLBACK(>kDestroy), self, null, c.G_CONNECT_DEFAULT); + _ = c.g_signal_connect_data(opts.gl_area, "render", c.G_CALLBACK(>kRender), self, null, c.G_CONNECT_DEFAULT); + _ = c.g_signal_connect_data(opts.gl_area, "resize", c.G_CALLBACK(>kResize), self, null, c.G_CONNECT_DEFAULT); } fn realize(self: *Surface) !void { @@ -220,16 +213,33 @@ pub const Surface = struct { self, ); errdefer self.core_surface.deinit(); + + // Note we're realized + self.realized = true; + } + + fn render(self: *Surface) !void { + try self.core_surface.renderer.draw(); + } + + /// Invalidate the surface so that it forces a redraw on the next tick. + fn invalidate(self: *Surface) void { + c.gtk_gl_area_queue_render(self.gl_area); } fn gtkRealize(area: *c.GtkGLArea, ud: ?*anyopaque) callconv(.C) void { - _ = area; - log.debug("gl surface realized", .{}); + // We need to make the context current so we can call GL functions. + c.gtk_gl_area_make_current(area); + if (c.gtk_gl_area_get_error(area)) |err| { + log.err("surface failed to realize: {s}", .{err.*.message}); + return; + } + // realize means that our OpenGL context is ready, so we can now // initialize the core surface which will setup the renderer. - const self = userdataSelf(ud orelse return); + const self = userdataSelf(ud.?); self.realize() catch |err| { // TODO: we need to destroy the GL area here. log.err("surface failed to realize: {}", .{err}); @@ -237,24 +247,45 @@ pub const Surface = struct { }; } + /// render singal fn gtkRender(area: *c.GtkGLArea, ctx: *c.GdkGLContext, ud: ?*anyopaque) callconv(.C) c.gboolean { _ = area; _ = ctx; - _ = ud; - log.debug("gl render", .{}); - const opengl = @import("../renderer/opengl/main.zig"); - opengl.clearColor(0, 0.5, 1, 1); - opengl.clear(opengl.c.GL_COLOR_BUFFER_BIT); + const self = userdataSelf(ud.?); + self.render() catch |err| { + log.err("surface failed to render: {}", .{err}); + return 0; + }; return 1; } + /// render singal + fn gtkResize(area: *c.GtkGLArea, width: c.gint, height: c.gint, ud: ?*anyopaque) callconv(.C) void { + _ = area; + log.debug("gl resize {} {}", .{ width, height }); + + const self = userdataSelf(ud.?); + self.size = .{ + .width = @intCast(u32, width), + .height = @intCast(u32, height), + }; + + // Call the primary callback. + if (self.realized) { + self.core_surface.sizeCallback(self.size) catch |err| { + log.err("error in size callback err={}", .{err}); + return; + }; + } + } + /// "destroy" signal for surface fn gtkDestroy(v: *c.GtkWidget, ud: ?*anyopaque) callconv(.C) void { _ = v; - const self = userdataSelf(ud orelse return); + const self = userdataSelf(ud.?); const alloc = self.app.core_app.alloc; self.deinit(); alloc.destroy(self); @@ -283,8 +314,7 @@ pub const Surface = struct { } pub fn getSize(self: *const Surface) !apprt.SurfaceSize { - _ = self; - return .{ .width = 800, .height = 600 }; + return self.size; } pub fn setSizeLimits(self: *Surface, min: apprt.SurfaceSize, max_: ?apprt.SurfaceSize) !void { diff --git a/src/renderer/OpenGL.zig b/src/renderer/OpenGL.zig index 9a2a2fc47..b041f8d2f 100644 --- a/src/renderer/OpenGL.zig +++ b/src/renderer/OpenGL.zig @@ -34,7 +34,7 @@ const CellsLRU = lru.AutoHashMap(struct { /// The runtime can request a single-threaded draw by setting this boolean /// to true. In this case, the renderer.draw() call is expected to be called /// from the runtime. -const single_threaded_draw = if (@hasDecl(apprt.Surface, "opengl_single_threaded_draw")) +pub const single_threaded_draw = if (@hasDecl(apprt.Surface, "opengl_single_threaded_draw")) apprt.Surface.opengl_single_threaded_draw else false; diff --git a/src/renderer/Thread.zig b/src/renderer/Thread.zig index 2ffd0a1df..12424272d 100644 --- a/src/renderer/Thread.zig +++ b/src/renderer/Thread.zig @@ -10,6 +10,7 @@ const apprt = @import("../apprt.zig"); const BlockingQueue = @import("../blocking_queue.zig").BlockingQueue; const tracy = @import("tracy"); const trace = tracy.trace; +const App = @import("../App.zig"); const Allocator = std.mem.Allocator; const log = std.log.scoped(.renderer_thread); @@ -60,6 +61,9 @@ state: *renderer.State, /// this is a blocking queue so if it is full you will get errors (or block). mailbox: *Mailbox, +/// Mailbox to send messages to the app thread +app_mailbox: App.Mailbox, + /// Initialize the thread. This does not START the thread. This only sets /// up all the internal state necessary prior to starting the thread. It /// is up to the caller to start the thread with the threadMain entrypoint. @@ -68,6 +72,7 @@ pub fn init( surface: *apprt.Surface, renderer_impl: *renderer.Renderer, state: *renderer.State, + app_mailbox: App.Mailbox, ) !Thread { // Create our event loop. var loop = try xev.Loop.init(.{}); @@ -104,6 +109,7 @@ pub fn init( .renderer = renderer_impl, .state = state, .mailbox = mailbox, + .app_mailbox = app_mailbox, }; } @@ -307,6 +313,15 @@ fn renderCallback( t.renderer.render(t.surface, t.state) catch |err| log.warn("error rendering err={}", .{err}); + + // If we're doing single-threaded GPU calls then we also wake up the + // app thread to redraw at this point. + if (renderer.Renderer == renderer.OpenGL and + renderer.OpenGL.single_threaded_draw) + { + _ = t.app_mailbox.push(.{ .redraw_surface = t.surface }, .{ .instant = {} }); + } + return .disarm; } diff --git a/src/renderer/opengl/Program.zig b/src/renderer/opengl/Program.zig index 5140b7517..2e2978363 100644 --- a/src/renderer/opengl/Program.zig +++ b/src/renderer/opengl/Program.zig @@ -69,6 +69,7 @@ pub inline fn link(p: Program) !void { pub inline fn use(p: Program) !Binding { glad.context.UseProgram.?(p.id); + try errors.getError(); return Binding{}; } diff --git a/src/renderer/opengl/Texture.zig b/src/renderer/opengl/Texture.zig index 446f0a9ad..f52d12859 100644 --- a/src/renderer/opengl/Texture.zig +++ b/src/renderer/opengl/Texture.zig @@ -9,6 +9,7 @@ id: c.GLuint, pub inline fn active(target: c.GLenum) !void { glad.context.ActiveTexture.?(target); + try errors.getError(); } /// Enun for possible texture binding targets. @@ -153,6 +154,7 @@ pub inline fn create() !Texture { /// glBindTexture pub inline fn bind(v: Texture, target: Target) !Binding { glad.context.BindTexture.?(@enumToInt(target), v.id); + try errors.getError(); return Binding{ .target = target }; } diff --git a/src/renderer/opengl/VertexArray.zig b/src/renderer/opengl/VertexArray.zig index 6222a2b68..b86794042 100644 --- a/src/renderer/opengl/VertexArray.zig +++ b/src/renderer/opengl/VertexArray.zig @@ -2,6 +2,7 @@ const VertexArray = @This(); const c = @import("c.zig"); const glad = @import("glad.zig"); +const errors = @import("errors.zig"); id: c.GLuint, @@ -20,6 +21,7 @@ pub inline fn unbind() !void { /// glBindVertexArray pub inline fn bind(v: VertexArray) !void { glad.context.BindVertexArray.?(v.id); + try errors.getError(); } pub inline fn destroy(v: VertexArray) void { From 90bea1b7427984608fddcbd197a6334653511fd3 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 23 Feb 2023 11:58:50 -0800 Subject: [PATCH 22/37] gtk: get proper content scaling --- build.zig | 8 ++++++++ src/apprt/gtk.zig | 12 +++++++++++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/build.zig b/build.zig index 80aebd22a..1ac50bbf4 100644 --- a/build.zig +++ b/build.zig @@ -569,6 +569,14 @@ fn addDeps( }, .gtk => { + // We need glfw for GTK because we use GLFW to get DPI. + step.addModule("glfw", glfw.module(b)); + const glfw_opts: glfw.Options = .{ + .metal = step.target.isDarwin(), + .opengl = false, + }; + try glfw.link(b, step, glfw_opts); + step.linkSystemLibrary("gtk4"); }, } diff --git a/src/apprt/gtk.zig b/src/apprt/gtk.zig index 4c4cb9429..503368d16 100644 --- a/src/apprt/gtk.zig +++ b/src/apprt/gtk.zig @@ -4,6 +4,7 @@ const std = @import("std"); const builtin = @import("builtin"); const assert = std.debug.assert; const Allocator = std.mem.Allocator; +const glfw = @import("glfw"); const apprt = @import("../apprt.zig"); const CoreApp = @import("../App.zig"); const CoreSurface = @import("../Surface.zig"); @@ -32,6 +33,12 @@ pub const App = struct { ctx: *c.GMainContext, pub fn init(core_app: *CoreApp, opts: Options) !App { + // This is super weird, but we still use GLFW with GTK only so that + // we can tap into their folklore logic to get screen DPI. If we can + // figure out a reliable way to determine this ourselves, we can get + // rid of this dep. + if (!glfw.init(.{})) return error.GlfwInitFailed; + // Create our GTK Application which encapsulates our process. const app = @ptrCast(?*c.GtkApplication, c.gtk_application_new( opts.id.ptr, @@ -87,6 +94,7 @@ pub const App = struct { while (c.g_main_context_iteration(self.ctx, 0) != 0) {} c.g_main_context_release(self.ctx); c.g_object_unref(self.app); + glfw.terminate(); } pub fn wakeup(self: App) void { @@ -310,7 +318,9 @@ pub const Surface = struct { pub fn getContentScale(self: *const Surface) !apprt.ContentScale { _ = self; - return .{ .x = 1, .y = 1 }; + const monitor = glfw.Monitor.getPrimary() orelse return error.NoMonitor; + const scale = monitor.getContentScale(); + return apprt.ContentScale{ .x = scale.x_scale, .y = scale.y_scale }; } pub fn getSize(self: *const Surface) !apprt.SurfaceSize { From 793c19e1870571397a43dab0f2cf0508066dfeea Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 23 Feb 2023 12:01:21 -0800 Subject: [PATCH 23/37] gtk: proper surface cleanup --- src/apprt/gtk.zig | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/apprt/gtk.zig b/src/apprt/gtk.zig index 503368d16..572d64fbc 100644 --- a/src/apprt/gtk.zig +++ b/src/apprt/gtk.zig @@ -292,6 +292,7 @@ pub const Surface = struct { /// "destroy" signal for surface fn gtkDestroy(v: *c.GtkWidget, ud: ?*anyopaque) callconv(.C) void { _ = v; + log.debug("gl destroy", .{}); const self = userdataSelf(ud.?); const alloc = self.app.core_app.alloc; @@ -304,7 +305,14 @@ pub const Surface = struct { } pub fn deinit(self: *Surface) void { - _ = self; + // We don't allocate anything if we aren't realized. + if (!self.realized) return; + + // Remove ourselves from the list of known surfaces in the app. + self.app.core_app.deleteSurface(self); + + // Clean up our core surface so that all the rendering and IO stop. + self.core_surface.deinit(); } pub fn setShouldClose(self: *Surface) void { From 8fbafda3d3945a725a3e4fa4b653b6c7e4b3d1d1 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 23 Feb 2023 12:02:37 -0800 Subject: [PATCH 24/37] gtk: listen to should quit from app --- src/apprt/gtk.zig | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/apprt/gtk.zig b/src/apprt/gtk.zig index 572d64fbc..ceddb59eb 100644 --- a/src/apprt/gtk.zig +++ b/src/apprt/gtk.zig @@ -109,7 +109,7 @@ pub const App = struct { // Tick the terminal app const should_quit = try self.core_app.tick(self); - if (false and should_quit) return; + if (should_quit) return; } } @@ -313,6 +313,7 @@ pub const Surface = struct { // Clean up our core surface so that all the rendering and IO stop. self.core_surface.deinit(); + self.core_surface = undefined; } pub fn setShouldClose(self: *Surface) void { From 358ce5a24ecf37e17afb65069a02f76d073eb52d Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 23 Feb 2023 13:33:05 -0800 Subject: [PATCH 25/37] gtk: start hooking up event callbacks --- src/apprt/gtk.zig | 167 +++++++++++++++++++++++++++++++--------------- 1 file changed, 113 insertions(+), 54 deletions(-) diff --git a/src/apprt/gtk.zig b/src/apprt/gtk.zig index ceddb59eb..7fc5f2aee 100644 --- a/src/apprt/gtk.zig +++ b/src/apprt/gtk.zig @@ -151,6 +151,11 @@ pub const App = struct { }); errdefer surface.deinit(); c.gtk_window_set_child(gtk_window, gl_area); + + // We need to grab focus after it is added to the window. When + // creating a window we want to always focus on the widget. + const widget = @ptrCast(*c.GtkWidget, gl_area); + _ = c.gtk_widget_grab_focus(widget); } fn activate(app: *c.GtkApplication, ud: ?*anyopaque) callconv(.C) void { @@ -192,6 +197,23 @@ pub const Surface = struct { size: apprt.SurfaceSize, pub fn init(self: *Surface, app: *App, opts: Options) !void { + const widget = @ptrCast(*c.GtkWidget, opts.gl_area); + + // Add our event controllers + const ec_key = c.gtk_event_controller_key_new(); + errdefer c.g_object_unref(ec_key); + c.gtk_widget_add_controller(widget, ec_key); + errdefer c.gtk_widget_remove_controller(widget, ec_key); + + const ec_focus = c.gtk_event_controller_focus_new(); + errdefer c.g_object_unref(ec_focus); + c.gtk_widget_add_controller(widget, ec_focus); + errdefer c.gtk_widget_remove_controller(widget, ec_focus); + + // The GL area has to be focusable so that it can receive events + c.gtk_widget_set_focusable(widget, 1); + c.gtk_widget_set_focus_on_click(widget, 1); + // Build our result self.* = .{ .app = app, @@ -201,11 +223,15 @@ pub const Surface = struct { }; errdefer self.* = undefined; - // Create the GL area that will contain our surface + // GL events _ = c.g_signal_connect_data(opts.gl_area, "realize", c.G_CALLBACK(>kRealize), self, null, c.G_CONNECT_DEFAULT); _ = c.g_signal_connect_data(opts.gl_area, "destroy", c.G_CALLBACK(>kDestroy), self, null, c.G_CONNECT_DEFAULT); _ = c.g_signal_connect_data(opts.gl_area, "render", c.G_CALLBACK(>kRender), self, null, c.G_CONNECT_DEFAULT); _ = c.g_signal_connect_data(opts.gl_area, "resize", c.G_CALLBACK(>kResize), self, null, c.G_CONNECT_DEFAULT); + + _ = c.g_signal_connect_data(ec_key, "key-pressed", c.G_CALLBACK(>kKeyPressed), self, null, c.G_CONNECT_DEFAULT); + _ = c.g_signal_connect_data(ec_focus, "enter", c.G_CALLBACK(>kFocusEnter), self, null, c.G_CONNECT_DEFAULT); + _ = c.g_signal_connect_data(ec_focus, "leave", c.G_CALLBACK(>kFocusLeave), self, null, c.G_CONNECT_DEFAULT); } fn realize(self: *Surface) !void { @@ -226,6 +252,18 @@ pub const Surface = struct { self.realized = true; } + pub fn deinit(self: *Surface) void { + // We don't allocate anything if we aren't realized. + if (!self.realized) return; + + // Remove ourselves from the list of known surfaces in the app. + self.app.core_app.deleteSurface(self); + + // Clean up our core surface so that all the rendering and IO stop. + self.core_surface.deinit(); + self.core_surface = undefined; + } + fn render(self: *Surface) !void { try self.core_surface.renderer.draw(); } @@ -235,6 +273,47 @@ pub const Surface = struct { c.gtk_gl_area_queue_render(self.gl_area); } + pub fn setShouldClose(self: *Surface) void { + _ = self; + } + + pub fn shouldClose(self: *const Surface) bool { + _ = self; + return false; + } + + pub fn getContentScale(self: *const Surface) !apprt.ContentScale { + _ = self; + const monitor = glfw.Monitor.getPrimary() orelse return error.NoMonitor; + const scale = monitor.getContentScale(); + return apprt.ContentScale{ .x = scale.x_scale, .y = scale.y_scale }; + } + + pub fn getSize(self: *const Surface) !apprt.SurfaceSize { + return self.size; + } + + pub fn setSizeLimits(self: *Surface, min: apprt.SurfaceSize, max_: ?apprt.SurfaceSize) !void { + _ = self; + _ = min; + _ = max_; + } + + pub fn setTitle(self: *Surface, slice: [:0]const u8) !void { + _ = self; + _ = slice; + } + + pub fn getClipboardString(self: *const Surface) ![:0]const u8 { + _ = self; + return ""; + } + + pub fn setClipboardString(self: *const Surface, val: [:0]const u8) !void { + _ = self; + _ = val; + } + fn gtkRealize(area: *c.GtkGLArea, ud: ?*anyopaque) callconv(.C) void { log.debug("gl surface realized", .{}); @@ -300,60 +379,40 @@ pub const Surface = struct { alloc.destroy(self); } + fn gtkKeyPressed( + _: *c.GtkEventControllerKey, + keyval: c.guint, + keycode: c.guint, + state: c.GdkModifierType, + ud: ?*anyopaque, + ) callconv(.C) c.gboolean { + _ = ud; + log.warn("KEY PRESS val={} code={} state={}", .{ + keyval, + keycode, + state, + }); + + return 0; + } + + fn gtkFocusEnter(_: *c.GtkEventControllerFocus, ud: ?*anyopaque) callconv(.C) void { + const self = userdataSelf(ud.?); + self.core_surface.focusCallback(true) catch |err| { + log.err("error in focus callback err={}", .{err}); + return; + }; + } + + fn gtkFocusLeave(_: *c.GtkEventControllerFocus, ud: ?*anyopaque) callconv(.C) void { + const self = userdataSelf(ud.?); + self.core_surface.focusCallback(false) catch |err| { + log.err("error in focus callback err={}", .{err}); + return; + }; + } + fn userdataSelf(ud: *anyopaque) *Surface { return @ptrCast(*Surface, @alignCast(@alignOf(Surface), ud)); } - - pub fn deinit(self: *Surface) void { - // We don't allocate anything if we aren't realized. - if (!self.realized) return; - - // Remove ourselves from the list of known surfaces in the app. - self.app.core_app.deleteSurface(self); - - // Clean up our core surface so that all the rendering and IO stop. - self.core_surface.deinit(); - self.core_surface = undefined; - } - - pub fn setShouldClose(self: *Surface) void { - _ = self; - } - - pub fn shouldClose(self: *const Surface) bool { - _ = self; - return false; - } - - pub fn getContentScale(self: *const Surface) !apprt.ContentScale { - _ = self; - const monitor = glfw.Monitor.getPrimary() orelse return error.NoMonitor; - const scale = monitor.getContentScale(); - return apprt.ContentScale{ .x = scale.x_scale, .y = scale.y_scale }; - } - - pub fn getSize(self: *const Surface) !apprt.SurfaceSize { - return self.size; - } - - pub fn setSizeLimits(self: *Surface, min: apprt.SurfaceSize, max_: ?apprt.SurfaceSize) !void { - _ = self; - _ = min; - _ = max_; - } - - pub fn setTitle(self: *Surface, slice: [:0]const u8) !void { - _ = self; - _ = slice; - } - - pub fn getClipboardString(self: *const Surface) ![:0]const u8 { - _ = self; - return ""; - } - - pub fn setClipboardString(self: *const Surface, val: [:0]const u8) !void { - _ = self; - _ = val; - } }; From 4fe7b9b3f28ba0e4b5d5cc059ec3ccfd3e4edde0 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 23 Feb 2023 13:51:07 -0800 Subject: [PATCH 26/37] gtk: char input --- src/apprt/gtk.zig | 34 +++++++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/src/apprt/gtk.zig b/src/apprt/gtk.zig index 7fc5f2aee..39f0ed8f5 100644 --- a/src/apprt/gtk.zig +++ b/src/apprt/gtk.zig @@ -199,17 +199,27 @@ pub const Surface = struct { pub fn init(self: *Surface, app: *App, opts: Options) !void { const widget = @ptrCast(*c.GtkWidget, opts.gl_area); - // Add our event controllers + // Key event controller will tell us about raw keypress events. const ec_key = c.gtk_event_controller_key_new(); errdefer c.g_object_unref(ec_key); c.gtk_widget_add_controller(widget, ec_key); errdefer c.gtk_widget_remove_controller(widget, ec_key); + // Focus controller will tell us about focus enter/exit events const ec_focus = c.gtk_event_controller_focus_new(); errdefer c.g_object_unref(ec_focus); c.gtk_widget_add_controller(widget, ec_focus); errdefer c.gtk_widget_remove_controller(widget, ec_focus); + // Tell the key controller that we're interested in getting a full + // input method so raw characters/strings are given too. + const im_context = c.gtk_im_multicontext_new(); + errdefer c.g_object_unref(im_context); + c.gtk_event_controller_key_set_im_context( + @ptrCast(*c.GtkEventControllerKey, ec_key), + im_context, + ); + // The GL area has to be focusable so that it can receive events c.gtk_widget_set_focusable(widget, 1); c.gtk_widget_set_focus_on_click(widget, 1); @@ -232,6 +242,7 @@ pub const Surface = struct { _ = c.g_signal_connect_data(ec_key, "key-pressed", c.G_CALLBACK(>kKeyPressed), self, null, c.G_CONNECT_DEFAULT); _ = c.g_signal_connect_data(ec_focus, "enter", c.G_CALLBACK(>kFocusEnter), self, null, c.G_CONNECT_DEFAULT); _ = c.g_signal_connect_data(ec_focus, "leave", c.G_CALLBACK(>kFocusLeave), self, null, c.G_CONNECT_DEFAULT); + _ = c.g_signal_connect_data(im_context, "commit", c.G_CALLBACK(>kInputCommit), self, null, c.G_CONNECT_DEFAULT); } fn realize(self: *Surface) !void { @@ -396,6 +407,27 @@ pub const Surface = struct { return 0; } + fn gtkInputCommit( + _: *c.GtkIMContext, + bytes: [*:0]u8, + ud: ?*anyopaque, + ) callconv(.C) void { + const str = std.mem.sliceTo(bytes, 0); + const view = std.unicode.Utf8View.init(str) catch |err| { + log.warn("cannot build utf8 view over input: {}", .{err}); + return; + }; + + const self = userdataSelf(ud.?); + var it = view.iterator(); + while (it.nextCodepoint()) |cp| { + self.core_surface.charCallback(cp) catch |err| { + log.err("error in char callback err={}", .{err}); + return; + }; + } + } + fn gtkFocusEnter(_: *c.GtkEventControllerFocus, ud: ?*anyopaque) callconv(.C) void { const self = userdataSelf(ud.?); self.core_surface.focusCallback(true) catch |err| { From 3ab51f5643b047b105839d8dcefd76d5b0ac72f3 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 23 Feb 2023 14:11:06 -0800 Subject: [PATCH 27/37] gtk: key press/release --- src/apprt/gtk.zig | 179 ++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 173 insertions(+), 6 deletions(-) diff --git a/src/apprt/gtk.zig b/src/apprt/gtk.zig index 39f0ed8f5..e1fa9e0f2 100644 --- a/src/apprt/gtk.zig +++ b/src/apprt/gtk.zig @@ -6,6 +6,7 @@ const assert = std.debug.assert; const Allocator = std.mem.Allocator; const glfw = @import("glfw"); const apprt = @import("../apprt.zig"); +const input = @import("../input.zig"); const CoreApp = @import("../App.zig"); const CoreSurface = @import("../Surface.zig"); @@ -240,6 +241,7 @@ pub const Surface = struct { _ = c.g_signal_connect_data(opts.gl_area, "resize", c.G_CALLBACK(>kResize), self, null, c.G_CONNECT_DEFAULT); _ = c.g_signal_connect_data(ec_key, "key-pressed", c.G_CALLBACK(>kKeyPressed), self, null, c.G_CONNECT_DEFAULT); + _ = c.g_signal_connect_data(ec_key, "key-released", c.G_CALLBACK(>kKeyReleased), self, null, c.G_CONNECT_DEFAULT); _ = c.g_signal_connect_data(ec_focus, "enter", c.G_CALLBACK(>kFocusEnter), self, null, c.G_CONNECT_DEFAULT); _ = c.g_signal_connect_data(ec_focus, "leave", c.G_CALLBACK(>kFocusLeave), self, null, c.G_CONNECT_DEFAULT); _ = c.g_signal_connect_data(im_context, "commit", c.G_CALLBACK(>kInputCommit), self, null, c.G_CONNECT_DEFAULT); @@ -397,12 +399,35 @@ pub const Surface = struct { state: c.GdkModifierType, ud: ?*anyopaque, ) callconv(.C) c.gboolean { - _ = ud; - log.warn("KEY PRESS val={} code={} state={}", .{ - keyval, - keycode, - state, - }); + _ = keycode; + + const key = translateKey(keyval); + const mods = translateMods(state); + const self = userdataSelf(ud.?); + self.core_surface.keyCallback(.press, key, mods) catch |err| { + log.err("error in key callback err={}", .{err}); + return 0; + }; + + return 0; + } + + fn gtkKeyReleased( + _: *c.GtkEventControllerKey, + keyval: c.guint, + keycode: c.guint, + state: c.GdkModifierType, + ud: ?*anyopaque, + ) callconv(.C) c.gboolean { + _ = keycode; + + const key = translateKey(keyval); + const mods = translateMods(state); + const self = userdataSelf(ud.?); + self.core_surface.keyCallback(.release, key, mods) catch |err| { + log.err("error in key callback err={}", .{err}); + return 0; + }; return 0; } @@ -448,3 +473,145 @@ pub const Surface = struct { return @ptrCast(*Surface, @alignCast(@alignOf(Surface), ud)); } }; + +fn translateMods(state: c.GdkModifierType) input.Mods { + var mods: input.Mods = .{}; + if (state & c.GDK_SHIFT_MASK != 0) mods.shift = true; + if (state & c.GDK_CONTROL_MASK != 0) mods.ctrl = true; + if (state & c.GDK_ALT_MASK != 0) mods.alt = true; + if (state & c.GDK_SUPER_MASK != 0) mods.super = true; + + // Lock is dependent on the X settings but we just assume caps lock. + if (state & c.GDK_LOCK_MASK != 0) mods.caps_lock = true; + return mods; +} + +fn translateKey(keyval: c.guint) input.Key { + return switch (keyval) { + c.GDK_KEY_a => .a, + c.GDK_KEY_b => .b, + c.GDK_KEY_c => .c, + c.GDK_KEY_d => .d, + c.GDK_KEY_e => .e, + c.GDK_KEY_f => .f, + c.GDK_KEY_g => .g, + c.GDK_KEY_h => .h, + c.GDK_KEY_i => .i, + c.GDK_KEY_j => .j, + c.GDK_KEY_k => .k, + c.GDK_KEY_l => .l, + c.GDK_KEY_m => .m, + c.GDK_KEY_n => .n, + c.GDK_KEY_o => .o, + c.GDK_KEY_p => .p, + c.GDK_KEY_q => .q, + c.GDK_KEY_r => .r, + c.GDK_KEY_s => .s, + c.GDK_KEY_t => .t, + c.GDK_KEY_u => .u, + c.GDK_KEY_v => .v, + c.GDK_KEY_w => .w, + c.GDK_KEY_x => .x, + c.GDK_KEY_y => .y, + c.GDK_KEY_z => .z, + + c.GDK_KEY_0 => .zero, + c.GDK_KEY_1 => .one, + c.GDK_KEY_2 => .two, + c.GDK_KEY_3 => .three, + c.GDK_KEY_4 => .four, + c.GDK_KEY_5 => .five, + c.GDK_KEY_6 => .six, + c.GDK_KEY_7 => .seven, + c.GDK_KEY_8 => .eight, + c.GDK_KEY_9 => .nine, + + c.GDK_KEY_semicolon => .semicolon, + c.GDK_KEY_space => .space, + c.GDK_KEY_apostrophe => .apostrophe, + c.GDK_KEY_comma => .comma, + c.GDK_KEY_grave => .grave_accent, // ` + c.GDK_KEY_period => .period, + c.GDK_KEY_slash => .slash, + c.GDK_KEY_minus => .minus, + c.GDK_KEY_equal => .equal, + c.GDK_KEY_bracketleft => .left_bracket, // [ + c.GDK_KEY_bracketright => .right_bracket, // ] + c.GDK_KEY_backslash => .backslash, // / + + c.GDK_KEY_Up => .up, + c.GDK_KEY_Down => .down, + c.GDK_KEY_Right => .right, + c.GDK_KEY_Left => .left, + c.GDK_KEY_Home => .home, + c.GDK_KEY_End => .end, + c.GDK_KEY_Insert => .insert, + c.GDK_KEY_Delete => .delete, + c.GDK_KEY_Caps_Lock => .caps_lock, + c.GDK_KEY_Scroll_Lock => .scroll_lock, + c.GDK_KEY_Num_Lock => .num_lock, + c.GDK_KEY_Page_Up => .page_up, + c.GDK_KEY_Page_Down => .page_down, + c.GDK_KEY_Escape => .escape, + c.GDK_KEY_Return => .enter, + c.GDK_KEY_Tab => .tab, + c.GDK_KEY_BackSpace => .backspace, + c.GDK_KEY_Print => .print_screen, + c.GDK_KEY_Pause => .pause, + + c.GDK_KEY_F1 => .f1, + c.GDK_KEY_F2 => .f2, + c.GDK_KEY_F3 => .f3, + c.GDK_KEY_F4 => .f4, + c.GDK_KEY_F5 => .f5, + c.GDK_KEY_F6 => .f6, + c.GDK_KEY_F7 => .f7, + c.GDK_KEY_F8 => .f8, + c.GDK_KEY_F9 => .f9, + c.GDK_KEY_F10 => .f10, + c.GDK_KEY_F11 => .f11, + c.GDK_KEY_F12 => .f12, + c.GDK_KEY_F13 => .f13, + c.GDK_KEY_F14 => .f14, + c.GDK_KEY_F15 => .f15, + c.GDK_KEY_F16 => .f16, + c.GDK_KEY_F17 => .f17, + c.GDK_KEY_F18 => .f18, + c.GDK_KEY_F19 => .f19, + c.GDK_KEY_F20 => .f20, + c.GDK_KEY_F21 => .f21, + c.GDK_KEY_F22 => .f22, + c.GDK_KEY_F23 => .f23, + c.GDK_KEY_F24 => .f24, + c.GDK_KEY_F25 => .f25, + + c.GDK_KEY_KP_0 => .kp_0, + c.GDK_KEY_KP_1 => .kp_1, + c.GDK_KEY_KP_2 => .kp_2, + c.GDK_KEY_KP_3 => .kp_3, + c.GDK_KEY_KP_4 => .kp_4, + c.GDK_KEY_KP_5 => .kp_5, + c.GDK_KEY_KP_6 => .kp_6, + c.GDK_KEY_KP_7 => .kp_7, + c.GDK_KEY_KP_8 => .kp_8, + c.GDK_KEY_KP_9 => .kp_9, + c.GDK_KEY_KP_Decimal => .kp_decimal, + c.GDK_KEY_KP_Divide => .kp_divide, + c.GDK_KEY_KP_Multiply => .kp_multiply, + c.GDK_KEY_KP_Subtract => .kp_subtract, + c.GDK_KEY_KP_Add => .kp_add, + c.GDK_KEY_KP_Enter => .kp_enter, + c.GDK_KEY_KP_Equal => .kp_equal, + + c.GDK_KEY_Shift_L => .left_shift, + c.GDK_KEY_Control_L => .left_control, + c.GDK_KEY_Alt_L => .left_alt, + c.GDK_KEY_Super_L => .left_super, + c.GDK_KEY_Shift_R => .right_shift, + c.GDK_KEY_Control_R => .right_control, + c.GDK_KEY_Alt_R => .right_alt, + c.GDK_KEY_Super_R => .right_super, + + else => .invalid, + }; +} From adae05cf045e35cbe073673d77cc380b1241f89d Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 23 Feb 2023 17:08:09 -0800 Subject: [PATCH 28/37] gtk: mouse input --- include/ghostty.h | 3 +- src/apprt/gtk.zig | 80 +++++++++++++++++++++++++++++++++++++++++++++ src/input/mouse.zig | 1 + 3 files changed, 83 insertions(+), 1 deletion(-) diff --git a/include/ghostty.h b/include/ghostty.h index 1764ef445..6da1db805 100644 --- a/include/ghostty.h +++ b/include/ghostty.h @@ -51,7 +51,8 @@ typedef enum { } ghostty_input_mouse_state_e; typedef enum { - GHOSTTY_MOUSE_LEFT = 1, + GHOSTTY_MOUSE_UNKNOWN, + GHOSTTY_MOUSE_LEFT, GHOSTTY_MOUSE_RIGHT, GHOSTTY_MOUSE_MIDDLE, } ghostty_input_mouse_button_e; diff --git a/src/apprt/gtk.zig b/src/apprt/gtk.zig index e1fa9e0f2..c18635667 100644 --- a/src/apprt/gtk.zig +++ b/src/apprt/gtk.zig @@ -221,6 +221,18 @@ pub const Surface = struct { im_context, ); + // Clicks + const gesture_click = c.gtk_gesture_click_new(); + errdefer c.g_object_unref(gesture_click); + c.gtk_gesture_single_set_button(@ptrCast( + *c.GtkGestureSingle, + gesture_click, + ), 0); + c.gtk_widget_add_controller(widget, @ptrCast( + *c.GtkEventController, + gesture_click, + )); + // The GL area has to be focusable so that it can receive events c.gtk_widget_set_focusable(widget, 1); c.gtk_widget_set_focus_on_click(widget, 1); @@ -245,6 +257,8 @@ pub const Surface = struct { _ = c.g_signal_connect_data(ec_focus, "enter", c.G_CALLBACK(>kFocusEnter), self, null, c.G_CONNECT_DEFAULT); _ = c.g_signal_connect_data(ec_focus, "leave", c.G_CALLBACK(>kFocusLeave), self, null, c.G_CONNECT_DEFAULT); _ = c.g_signal_connect_data(im_context, "commit", c.G_CALLBACK(>kInputCommit), self, null, c.G_CONNECT_DEFAULT); + _ = c.g_signal_connect_data(gesture_click, "pressed", c.G_CALLBACK(>kMouseDown), self, null, c.G_CONNECT_DEFAULT); + _ = c.g_signal_connect_data(gesture_click, "released", c.G_CALLBACK(>kMouseUp), self, null, c.G_CONNECT_DEFAULT); } fn realize(self: *Surface) !void { @@ -327,6 +341,17 @@ pub const Surface = struct { _ = val; } + pub fn getCursorPos(self: *const Surface) !apprt.CursorPos { + _ = self; + return .{}; + // const unscaled_pos = self.window.getCursorPos(); + // const pos = try self.cursorPosToPixels(unscaled_pos); + // return apprt.CursorPos{ + // .x = @floatCast(f32, pos.xpos), + // .y = @floatCast(f32, pos.ypos), + // }; + } + fn gtkRealize(area: *c.GtkGLArea, ud: ?*anyopaque) callconv(.C) void { log.debug("gl surface realized", .{}); @@ -392,6 +417,44 @@ pub const Surface = struct { alloc.destroy(self); } + fn gtkMouseDown( + gesture: *c.GtkGestureClick, + _: c.gint, + _: c.gdouble, + _: c.gdouble, + ud: ?*anyopaque, + ) callconv(.C) void { + const button = translateMouseButton(c.gtk_gesture_single_get_current_button(@ptrCast( + *c.GtkGestureSingle, + gesture, + ))); + + const self = userdataSelf(ud.?); + self.core_surface.mouseButtonCallback(.press, button, .{}) catch |err| { + log.err("error in key callback err={}", .{err}); + return 0; + }; + } + + fn gtkMouseUp( + gesture: *c.GtkGestureClick, + _: c.gint, + _: c.gdouble, + _: c.gdouble, + ud: ?*anyopaque, + ) callconv(.C) void { + const button = translateMouseButton(c.gtk_gesture_single_get_current_button(@ptrCast( + *c.GtkGestureSingle, + gesture, + ))); + + const self = userdataSelf(ud.?); + self.core_surface.mouseButtonCallback(.release, button, .{}) catch |err| { + log.err("error in key callback err={}", .{err}); + return 0; + }; + } + fn gtkKeyPressed( _: *c.GtkEventControllerKey, keyval: c.guint, @@ -474,6 +537,23 @@ pub const Surface = struct { } }; +fn translateMouseButton(button: c.guint) input.MouseButton { + return switch (button) { + 1 => .left, + 2 => .middle, + 3 => .right, + 4 => .four, + 5 => .five, + 6 => .six, + 7 => .seven, + 8 => .eight, + 9 => .nine, + 10 => .ten, + 11 => .eleven, + else => .unknown, + }; +} + fn translateMods(state: c.GdkModifierType) input.Mods { var mods: input.Mods = .{}; if (state & c.GDK_SHIFT_MASK != 0) mods.shift = true; diff --git a/src/input/mouse.zig b/src/input/mouse.zig index e1059e4c0..224abef35 100644 --- a/src/input/mouse.zig +++ b/src/input/mouse.zig @@ -32,6 +32,7 @@ pub const MouseButton = enum(c_int) { break :max cur; }; + unknown = 0, left = 1, right = 2, middle = 3, From 916f07d9d3f2193c8330fe265ce763265168e3d1 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 23 Feb 2023 17:18:56 -0800 Subject: [PATCH 29/37] gtk: mouse cursor pos and scroll --- src/apprt/gtk.zig | 61 +++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 51 insertions(+), 10 deletions(-) diff --git a/src/apprt/gtk.zig b/src/apprt/gtk.zig index c18635667..441583443 100644 --- a/src/apprt/gtk.zig +++ b/src/apprt/gtk.zig @@ -196,6 +196,7 @@ pub const Surface = struct { /// Cached metrics about the surface from GTK callbacks. size: apprt.SurfaceSize, + cursor_pos: apprt.CursorPos, pub fn init(self: *Surface, app: *App, opts: Options) !void { const widget = @ptrCast(*c.GtkWidget, opts.gl_area); @@ -233,6 +234,19 @@ pub const Surface = struct { gesture_click, )); + // Mouse movement + const ec_motion = c.gtk_event_controller_motion_new(); + errdefer c.g_object_unref(ec_motion); + c.gtk_widget_add_controller(widget, ec_motion); + + // Scroll events + const ec_scroll = c.gtk_event_controller_scroll_new( + c.GTK_EVENT_CONTROLLER_SCROLL_BOTH_AXES | + c.GTK_EVENT_CONTROLLER_SCROLL_DISCRETE, + ); + errdefer c.g_object_unref(ec_scroll); + c.gtk_widget_add_controller(widget, ec_scroll); + // The GL area has to be focusable so that it can receive events c.gtk_widget_set_focusable(widget, 1); c.gtk_widget_set_focus_on_click(widget, 1); @@ -243,6 +257,7 @@ pub const Surface = struct { .gl_area = opts.gl_area, .core_surface = undefined, .size = .{ .width = 800, .height = 600 }, + .cursor_pos = .{ .x = 0, .y = 0 }, }; errdefer self.* = undefined; @@ -259,6 +274,8 @@ pub const Surface = struct { _ = c.g_signal_connect_data(im_context, "commit", c.G_CALLBACK(>kInputCommit), self, null, c.G_CONNECT_DEFAULT); _ = c.g_signal_connect_data(gesture_click, "pressed", c.G_CALLBACK(>kMouseDown), self, null, c.G_CONNECT_DEFAULT); _ = c.g_signal_connect_data(gesture_click, "released", c.G_CALLBACK(>kMouseUp), self, null, c.G_CONNECT_DEFAULT); + _ = c.g_signal_connect_data(ec_motion, "motion", c.G_CALLBACK(>kMouseMotion), self, null, c.G_CONNECT_DEFAULT); + _ = c.g_signal_connect_data(ec_scroll, "scroll", c.G_CALLBACK(>kMouseScroll), self, null, c.G_CONNECT_DEFAULT); } fn realize(self: *Surface) !void { @@ -342,14 +359,7 @@ pub const Surface = struct { } pub fn getCursorPos(self: *const Surface) !apprt.CursorPos { - _ = self; - return .{}; - // const unscaled_pos = self.window.getCursorPos(); - // const pos = try self.cursorPosToPixels(unscaled_pos); - // return apprt.CursorPos{ - // .x = @floatCast(f32, pos.xpos), - // .y = @floatCast(f32, pos.ypos), - // }; + return self.cursor_pos; } fn gtkRealize(area: *c.GtkGLArea, ud: ?*anyopaque) callconv(.C) void { @@ -432,7 +442,7 @@ pub const Surface = struct { const self = userdataSelf(ud.?); self.core_surface.mouseButtonCallback(.press, button, .{}) catch |err| { log.err("error in key callback err={}", .{err}); - return 0; + return; }; } @@ -451,7 +461,38 @@ pub const Surface = struct { const self = userdataSelf(ud.?); self.core_surface.mouseButtonCallback(.release, button, .{}) catch |err| { log.err("error in key callback err={}", .{err}); - return 0; + return; + }; + } + + fn gtkMouseMotion( + _: *c.GtkEventControllerMotion, + x: c.gdouble, + y: c.gdouble, + ud: ?*anyopaque, + ) callconv(.C) void { + const self = userdataSelf(ud.?); + self.cursor_pos = .{ + .x = @max(0, @floatCast(f32, x)), + .y = @floatCast(f32, y), + }; + + self.core_surface.cursorPosCallback(self.cursor_pos) catch |err| { + log.err("error in cursor pos callback err={}", .{err}); + return; + }; + } + + fn gtkMouseScroll( + _: *c.GtkEventControllerScroll, + x: c.gdouble, + y: c.gdouble, + ud: ?*anyopaque, + ) callconv(.C) void { + const self = userdataSelf(ud.?); + self.core_surface.scrollCallback(x, y * -1) catch |err| { + log.err("error in scroll callback err={}", .{err}); + return; }; } From e4b76b597403b3a94d95ced8e4bcf2cec6523f80 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 23 Feb 2023 17:24:18 -0800 Subject: [PATCH 30/37] gtk: set cursor to ibeam over terminal surface --- src/apprt/gtk.zig | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/apprt/gtk.zig b/src/apprt/gtk.zig index 441583443..e18a0843a 100644 --- a/src/apprt/gtk.zig +++ b/src/apprt/gtk.zig @@ -30,9 +30,13 @@ pub const App = struct { }; core_app: *CoreApp, + app: *c.GtkApplication, ctx: *c.GMainContext, + cursor_default: *c.GdkCursor, + cursor_ibeam: *c.GdkCursor, + pub fn init(core_app: *CoreApp, opts: Options) !App { // This is super weird, but we still use GLFW with GTK only so that // we can tap into their folklore logic to get screen DPI. If we can @@ -81,10 +85,18 @@ pub const App = struct { // https://gitlab.gnome.org/GNOME/glib/-/blob/bd2ccc2f69ecfd78ca3f34ab59e42e2b462bad65/gio/gapplication.c#L2302 c.g_application_activate(gapp); + // Get our cursors + const cursor_default = c.gdk_cursor_new_from_name("default", null).?; + errdefer c.g_object_unref(cursor_default); + const cursor_ibeam = c.gdk_cursor_new_from_name("text", cursor_default).?; + errdefer c.g_object_unref(cursor_ibeam); + return .{ .core_app = core_app, .app = app, .ctx = ctx, + .cursor_default = cursor_default, + .cursor_ibeam = cursor_ibeam, }; } @@ -95,6 +107,10 @@ pub const App = struct { while (c.g_main_context_iteration(self.ctx, 0) != 0) {} c.g_main_context_release(self.ctx); c.g_object_unref(self.app); + + c.g_object_unref(self.cursor_ibeam); + c.g_object_unref(self.cursor_default); + glfw.terminate(); } @@ -251,6 +267,9 @@ pub const Surface = struct { c.gtk_widget_set_focusable(widget, 1); c.gtk_widget_set_focus_on_click(widget, 1); + // When we're over the widget, set the cursor to the ibeam + c.gtk_widget_set_cursor(widget, app.cursor_ibeam); + // Build our result self.* = .{ .app = app, From cb412425b27ea07e03b1c1d4dae34f91ecca489d Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 23 Feb 2023 17:27:53 -0800 Subject: [PATCH 31/37] embedded: fix build --- src/apprt/embedded.zig | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/apprt/embedded.zig b/src/apprt/embedded.zig index 8c1180a87..8db67b0a1 100644 --- a/src/apprt/embedded.zig +++ b/src/apprt/embedded.zig @@ -83,6 +83,12 @@ pub const App = struct { surface.deinit(); self.core_app.alloc.destroy(surface); } + + pub fn redrawSurface(self: *App, surface: *Surface) void { + _ = self; + _ = surface; + // No-op, we use a threaded interface so we're constantly drawing. + } }; pub const Surface = struct { From da64fff110a5e2a31430c205a42c10dc0bc553cd Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 23 Feb 2023 17:47:48 -0800 Subject: [PATCH 32/37] gtk: receive key press for all keys --- src/apprt/gtk.zig | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/apprt/gtk.zig b/src/apprt/gtk.zig index e18a0843a..914a61eb5 100644 --- a/src/apprt/gtk.zig +++ b/src/apprt/gtk.zig @@ -238,6 +238,13 @@ pub const Surface = struct { im_context, ); + // Create a second key controller so we can receive the raw + // key-press events BEFORE the input method gets them. + const ec_key_press = c.gtk_event_controller_key_new(); + errdefer c.g_object_unref(ec_key_press); + c.gtk_widget_add_controller(widget, ec_key_press); + errdefer c.gtk_widget_remove_controller(widget, ec_key_press); + // Clicks const gesture_click = c.gtk_gesture_click_new(); errdefer c.g_object_unref(gesture_click); @@ -286,8 +293,8 @@ pub const Surface = struct { _ = c.g_signal_connect_data(opts.gl_area, "render", c.G_CALLBACK(>kRender), self, null, c.G_CONNECT_DEFAULT); _ = c.g_signal_connect_data(opts.gl_area, "resize", c.G_CALLBACK(>kResize), self, null, c.G_CONNECT_DEFAULT); - _ = c.g_signal_connect_data(ec_key, "key-pressed", c.G_CALLBACK(>kKeyPressed), self, null, c.G_CONNECT_DEFAULT); - _ = c.g_signal_connect_data(ec_key, "key-released", c.G_CALLBACK(>kKeyReleased), self, null, c.G_CONNECT_DEFAULT); + _ = c.g_signal_connect_data(ec_key_press, "key-pressed", c.G_CALLBACK(>kKeyPressed), self, null, c.G_CONNECT_DEFAULT); + _ = c.g_signal_connect_data(ec_key_press, "key-released", c.G_CALLBACK(>kKeyReleased), self, null, c.G_CONNECT_DEFAULT); _ = c.g_signal_connect_data(ec_focus, "enter", c.G_CALLBACK(>kFocusEnter), self, null, c.G_CONNECT_DEFAULT); _ = c.g_signal_connect_data(ec_focus, "leave", c.G_CALLBACK(>kFocusLeave), self, null, c.G_CONNECT_DEFAULT); _ = c.g_signal_connect_data(im_context, "commit", c.G_CALLBACK(>kInputCommit), self, null, c.G_CONNECT_DEFAULT); @@ -527,6 +534,7 @@ pub const Surface = struct { const key = translateKey(keyval); const mods = translateMods(state); const self = userdataSelf(ud.?); + log.debug("key-press key={} mods={}", .{ key, mods }); self.core_surface.keyCallback(.press, key, mods) catch |err| { log.err("error in key callback err={}", .{err}); return 0; From aa49cceb49bf412db09bdbb2fb21356766dcb654 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 23 Feb 2023 18:53:06 -0800 Subject: [PATCH 33/37] opengl: make setting font size thread safe --- src/renderer/OpenGL.zig | 44 ++++++++++++++++++++++++++++------------- 1 file changed, 30 insertions(+), 14 deletions(-) diff --git a/src/renderer/OpenGL.zig b/src/renderer/OpenGL.zig index b041f8d2f..8f8005b00 100644 --- a/src/renderer/OpenGL.zig +++ b/src/renderer/OpenGL.zig @@ -105,6 +105,7 @@ surface_mailbox: apprt.surface.Mailbox, /// Some runtimes (GTK) do not support multi-threading so to keep our logic /// simple we apply all OpenGL context changes in the render() call. deferred_screen_size: ?SetScreenSize = null, +deferred_font_size: ?SetFontSize = null, /// If we're drawing with single threaded operations draw_mutex: DrawMutex = drawMutexZero, @@ -157,6 +158,19 @@ const SetScreenSize = struct { } }; +const SetFontSize = struct { + metrics: font.face.Metrics, + + fn apply(self: SetFontSize, r: *const OpenGL) !void { + try r.program.setUniform( + "cell_size", + @Vector(2, f32){ self.metrics.cell_width, self.metrics.cell_height }, + ); + try r.program.setUniform("strikethrough_position", self.metrics.strikethrough_position); + try r.program.setUniform("strikethrough_thickness", self.metrics.strikethrough_thickness); + } +}; + /// The raw structure that maps directly to the buffer sent to the vertex shader. /// This must be "extern" so that the field order is not reordered by the /// Zig compiler. @@ -239,14 +253,11 @@ pub fn init(alloc: Allocator, options: renderer.Options) !OpenGL { ); // Setup our font metrics uniform - const metrics = try resetFontMetrics(alloc, program, options.font_group); + const metrics = try resetFontMetrics(alloc, options.font_group); // Set our cell dimensions const pbind = try program.use(); defer pbind.unbind(); - try program.setUniform("cell_size", @Vector(2, f32){ metrics.cell_width, metrics.cell_height }); - try program.setUniform("strikethrough_position", metrics.strikethrough_position); - try program.setUniform("strikethrough_thickness", metrics.strikethrough_thickness); // Set all of our texture indexes try program.setUniform("text", 0); @@ -379,6 +390,7 @@ pub fn init(alloc: Allocator, options: renderer.Options) !OpenGL { .focused = true, .padding = options.padding, .surface_mailbox = options.surface_mailbox, + .deferred_font_size = .{ .metrics = metrics }, }; } @@ -571,8 +583,10 @@ pub fn blinkCursor(self: *OpenGL, reset: bool) void { /// /// Must be called on the render thread. pub fn setFontSize(self: *OpenGL, size: font.face.DesiredSize) !void { + if (single_threaded_draw) self.draw_mutex.lock(); + defer if (single_threaded_draw) self.draw_mutex.unlock(); + log.info("set font size={}", .{size}); - if (apprt.runtime == apprt.gtk) @panic("TODO: make thread safe"); // Set our new size, this will also reset our font atlas. try self.font_group.setSize(size); @@ -581,7 +595,10 @@ pub fn setFontSize(self: *OpenGL, size: font.face.DesiredSize) !void { self.resetCellsLRU(); // Reset our GPU uniforms - const metrics = try resetFontMetrics(self.alloc, self.program, self.font_group); + const metrics = try resetFontMetrics(self.alloc, self.font_group); + + // Defer our GPU updates + self.deferred_font_size = .{ .metrics = metrics }; // Recalculate our cell size. If it is the same as before, then we do // nothing since the grid size couldn't have possibly changed. @@ -599,7 +616,6 @@ pub fn setFontSize(self: *OpenGL, size: font.face.DesiredSize) !void { /// down to the GPU. fn resetFontMetrics( alloc: Allocator, - program: gl.Program, font_group: *font.GroupCache, ) !font.face.Metrics { // Get our cell metrics based on a regular font ascii 'M'. Why 'M'? @@ -620,13 +636,6 @@ fn resetFontMetrics( .underline_position = @floatToInt(u32, metrics.underline_position), }; - // Set our uniforms that rely on metrics - const pbind = try program.use(); - defer pbind.unbind(); - try program.setUniform("cell_size", @Vector(2, f32){ metrics.cell_width, metrics.cell_height }); - try program.setUniform("strikethrough_position", metrics.strikethrough_position); - try program.setUniform("strikethrough_thickness", metrics.strikethrough_thickness); - return metrics; } @@ -1173,6 +1182,9 @@ fn gridSize(self: *const OpenGL, screen_size: renderer.ScreenSize) renderer.Grid /// Set the screen size for rendering. This will update the projection /// used for the shader so that the scaling of the grid is correct. pub fn setScreenSize(self: *OpenGL, dim: renderer.ScreenSize) !void { + if (single_threaded_draw) self.draw_mutex.lock(); + defer if (single_threaded_draw) self.draw_mutex.unlock(); + // Recalculate the rows/columns. const grid_size = self.gridSize(dim); @@ -1329,6 +1341,10 @@ pub fn draw(self: *OpenGL) !void { try v.apply(self); self.deferred_screen_size = null; } + if (self.deferred_font_size) |v| { + try v.apply(self); + self.deferred_font_size = null; + } try self.drawCells(binding, self.cells_bg); try self.drawCells(binding, self.cells); From c4393ece4c0a22127588737773fb51e5d66019ff Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 23 Feb 2023 19:14:12 -0800 Subject: [PATCH 34/37] gtk: make clear opengl requirements --- src/apprt/gtk.zig | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/apprt/gtk.zig b/src/apprt/gtk.zig index 914a61eb5..26086c382 100644 --- a/src/apprt/gtk.zig +++ b/src/apprt/gtk.zig @@ -216,6 +216,10 @@ pub const Surface = struct { pub fn init(self: *Surface, app: *App, opts: Options) !void { const widget = @ptrCast(*c.GtkWidget, opts.gl_area); + c.gtk_gl_area_set_required_version(opts.gl_area, 3, 3); + c.gtk_gl_area_set_has_stencil_buffer(opts.gl_area, 0); + c.gtk_gl_area_set_has_depth_buffer(opts.gl_area, 0); + c.gtk_gl_area_set_use_es(opts.gl_area, 0); // Key event controller will tell us about raw keypress events. const ec_key = c.gtk_event_controller_key_new(); From c8e0b0c6f377b18b9d079f89a65bfc9c1d52202d Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 23 Feb 2023 21:34:17 -0800 Subject: [PATCH 35/37] don't log config its too noisy --- src/main.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main.zig b/src/main.zig index 52447f456..7489d69fd 100644 --- a/src/main.zig +++ b/src/main.zig @@ -88,7 +88,7 @@ pub fn main() !void { } } try config.finalize(); - std.log.debug("config={}", .{config}); + //std.log.debug("config={}", .{config}); // Create our app state var app = try App.create(alloc, &config); From 382a32be6cff29bdfdc4172637a17bfd67f2108a Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 24 Feb 2023 12:13:31 -0800 Subject: [PATCH 36/37] gtk: clipboard --- src/apprt/gtk.zig | 36 +++++++++++++++++++++++++++++++----- 1 file changed, 31 insertions(+), 5 deletions(-) diff --git a/src/apprt/gtk.zig b/src/apprt/gtk.zig index 26086c382..af74b020a 100644 --- a/src/apprt/gtk.zig +++ b/src/apprt/gtk.zig @@ -213,6 +213,7 @@ pub const Surface = struct { /// Cached metrics about the surface from GTK callbacks. size: apprt.SurfaceSize, cursor_pos: apprt.CursorPos, + clipboard: c.GValue, pub fn init(self: *Surface, app: *App, opts: Options) !void { const widget = @ptrCast(*c.GtkWidget, opts.gl_area); @@ -288,6 +289,7 @@ pub const Surface = struct { .core_surface = undefined, .size = .{ .width = 800, .height = 600 }, .cursor_pos = .{ .x = 0, .y = 0 }, + .clipboard = std.mem.zeroes(c.GValue), }; errdefer self.* = undefined; @@ -327,6 +329,8 @@ pub const Surface = struct { } pub fn deinit(self: *Surface) void { + c.g_value_unset(&self.clipboard); + // We don't allocate anything if we aren't realized. if (!self.realized) return; @@ -378,14 +382,36 @@ pub const Surface = struct { _ = slice; } - pub fn getClipboardString(self: *const Surface) ![:0]const u8 { - _ = self; - return ""; + pub fn getClipboardString(self: *Surface) ![:0]const u8 { + const clipboard = c.gtk_widget_get_clipboard(@ptrCast( + *c.GtkWidget, + self.gl_area, + )); + + const content = c.gdk_clipboard_get_content(clipboard) orelse { + // On my machine, this NEVER works, so we fallback to glfw's + // implementation... + log.debug("no GTK clipboard contents, falling back to glfw", .{}); + return glfw.getClipboardString() orelse return glfw.mustGetErrorCode(); + }; + + c.g_value_unset(&self.clipboard); + _ = c.g_value_init(&self.clipboard, c.G_TYPE_STRING); + if (c.gdk_content_provider_get_value(content, &self.clipboard, null) == 0) { + return ""; + } + + const ptr = c.g_value_get_string(&self.clipboard); + return std.mem.sliceTo(ptr, 0); } pub fn setClipboardString(self: *const Surface, val: [:0]const u8) !void { - _ = self; - _ = val; + const clipboard = c.gtk_widget_get_clipboard(@ptrCast( + *c.GtkWidget, + self.gl_area, + )); + + c.gdk_clipboard_set_text(clipboard, val.ptr); } pub fn getCursorPos(self: *const Surface) !apprt.CursorPos { From f9457e76ab367ffdae61eee21b76eea1f89d26b1 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 24 Feb 2023 12:17:02 -0800 Subject: [PATCH 37/37] gtk: set title stub --- src/apprt/gtk.zig | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/apprt/gtk.zig b/src/apprt/gtk.zig index af74b020a..530dbbe8c 100644 --- a/src/apprt/gtk.zig +++ b/src/apprt/gtk.zig @@ -378,7 +378,13 @@ pub const Surface = struct { } pub fn setTitle(self: *Surface, slice: [:0]const u8) !void { - _ = self; + const root = c.gtk_widget_get_root(@ptrCast( + *c.GtkWidget, + self.gl_area, + )); + + // TODO: we need a way to check if the type is a window + _ = root; _ = slice; }