mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-16 16:56:09 +03:00
Merge pull request #75 from mitchellh/gtk
Rearchitect "app runtime" abstraction, minimal GTK implementation
This commit is contained in:
37
build.zig
37
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,38 @@ 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
|
||||
switch (app_runtime) {
|
||||
.none => {},
|
||||
|
||||
.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);
|
||||
|
||||
// Imgui
|
||||
// Must also link to imgui
|
||||
const imgui_step = try imgui.link(b, step, imgui_opts);
|
||||
try glfw.link(b, imgui_step, glfw_opts);
|
||||
},
|
||||
|
||||
.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");
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return static_libs;
|
||||
|
@ -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;
|
||||
|
@ -22,6 +22,8 @@
|
||||
, expat
|
||||
, fontconfig
|
||||
, freetype
|
||||
, glib
|
||||
, gtk4
|
||||
, harfbuzz
|
||||
, libpng
|
||||
, libGL
|
||||
@ -53,6 +55,9 @@ let
|
||||
libXcursor
|
||||
libXi
|
||||
libXrandr
|
||||
|
||||
gtk4
|
||||
glib
|
||||
];
|
||||
in mkShell rec {
|
||||
name = "ghostty";
|
||||
@ -102,6 +107,10 @@ in mkShell rec {
|
||||
libXi
|
||||
libXinerama
|
||||
libXrandr
|
||||
|
||||
# Only needed for GTK builds
|
||||
gtk4
|
||||
glib
|
||||
];
|
||||
|
||||
# This should be set onto the rpath of the ghostty binary if you want
|
||||
|
428
src/App.zig
428
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,63 +22,31 @@ const DevMode = @import("DevMode.zig");
|
||||
|
||||
const log = std.log.scoped(.app);
|
||||
|
||||
const WindowList = std.ArrayListUnmanaged(*Window);
|
||||
|
||||
/// The type used for sending messages to the app thread.
|
||||
pub const Mailbox = BlockingQueue(Message, 64);
|
||||
const SurfaceList = std.ArrayListUnmanaged(*apprt.Surface);
|
||||
|
||||
/// General purpose allocator
|
||||
alloc: Allocator,
|
||||
|
||||
/// The runtime for this app.
|
||||
runtime: apprt.runtime.App,
|
||||
|
||||
/// The list of windows that are currently open
|
||||
windows: WindowList,
|
||||
/// The list of surfaces that are currently active.
|
||||
surfaces: SurfaceList,
|
||||
|
||||
// The configuration for the app.
|
||||
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,
|
||||
|
||||
/// 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;
|
||||
}
|
||||
};
|
||||
|
||||
/// 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);
|
||||
|
||||
// If we have DevMode on, store the config so we can show it
|
||||
if (DevMode.enabled) DevMode.instance.config = config;
|
||||
|
||||
@ -86,78 +54,36 @@ pub fn create(
|
||||
errdefer alloc.destroy(app);
|
||||
app.* = .{
|
||||
.alloc = alloc,
|
||||
.runtime = app_backend,
|
||||
.windows = .{},
|
||||
.surfaces = .{},
|
||||
.config = config,
|
||||
.mailbox = 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();
|
||||
errdefer app.surfaces.deinit(alloc);
|
||||
|
||||
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);
|
||||
if (Darwin.enabled) self.darwin.deinit();
|
||||
self.mailbox.destroy(self.alloc);
|
||||
// Clean up all our surfaces
|
||||
for (self.surfaces.items) |surface| surface.deinit();
|
||||
self.surfaces.deinit(self.alloc);
|
||||
|
||||
self.alloc.destroy(self);
|
||||
|
||||
// Close our windowing runtime
|
||||
self.runtime.terminate();
|
||||
}
|
||||
|
||||
/// Wake up the app event loop. This should be called after any messages
|
||||
/// are sent to the mailbox.
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
/// 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();
|
||||
|
||||
// If any windows are closing, destroy them
|
||||
/// 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.App) !bool {
|
||||
// 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()) {
|
||||
rt_app.closeSurface(surface);
|
||||
continue;
|
||||
}
|
||||
|
||||
@ -165,34 +91,27 @@ pub fn tick(self: *App) !void {
|
||||
}
|
||||
|
||||
// Drain our mailbox only if we're not quitting.
|
||||
if (!self.quit) try self.drainMailbox();
|
||||
if (!self.quit) try self.drainMailbox(rt_app);
|
||||
|
||||
// We quit if our quit flag is on or if we have closed all 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();
|
||||
|
||||
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;
|
||||
/// 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);
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
/// 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.windows.items.len) {
|
||||
const current = self.windows.items[i];
|
||||
if (window == current) {
|
||||
window.destroy();
|
||||
_ = self.windows.swapRemove(i);
|
||||
return;
|
||||
while (i < self.surfaces.items.len) {
|
||||
if (self.surfaces.items[i] == rt_surface) {
|
||||
_ = self.surfaces.swapRemove(i);
|
||||
continue;
|
||||
}
|
||||
|
||||
i += 1;
|
||||
@ -200,29 +119,51 @@ pub fn closeWindow(self: *App, window: *Window) void {
|
||||
}
|
||||
|
||||
/// Drain the mailbox.
|
||||
fn drainMailbox(self: *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) {
|
||||
.new_window => |msg| _ = try self.newWindow(msg),
|
||||
.new_tab => |msg| try self.newTab(msg),
|
||||
.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(),
|
||||
.window_message => |msg| try self.windowMessage(msg.window, msg.message),
|
||||
.surface_message => |msg| try self.surfaceMessage(msg.surface, msg.message),
|
||||
.redraw_surface => |surface| try self.redrawSurface(rt_app, surface),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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", .{});
|
||||
fn closeSurface(self: *App, rt_app: *apprt.App, surface: *Surface) !void {
|
||||
if (!self.hasSurface(surface)) return;
|
||||
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")) {
|
||||
log.warn("newWindow is not supported by this runtime", .{});
|
||||
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", .{});
|
||||
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
|
||||
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;
|
||||
}
|
||||
|
||||
@ -232,16 +173,12 @@ 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;
|
||||
}
|
||||
|
||||
// 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
|
||||
@ -249,28 +186,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;
|
||||
@ -284,28 +221,55 @@ 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,
|
||||
|
||||
/// Close a surface. This notifies the runtime that a surface
|
||||
/// should close.
|
||||
close: *Surface,
|
||||
|
||||
/// 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,
|
||||
},
|
||||
|
||||
/// 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 {
|
||||
/// Runtime-specific window options.
|
||||
runtime: apprt.runtime.Window.Options = .{},
|
||||
|
||||
/// The parent window, only used for new tabs.
|
||||
parent: ?*Window = null,
|
||||
|
||||
/// The font size to create the window with or null to default to
|
||||
/// the configuration amount.
|
||||
font_size: ?font.face.DesiredSize = null,
|
||||
/// The parent surface
|
||||
parent: ?*Surface = null,
|
||||
};
|
||||
|
||||
const NewTab = struct {
|
||||
/// The parent surface
|
||||
parent: ?*Surface = null,
|
||||
};
|
||||
};
|
||||
|
||||
/// 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.
|
||||
@ -335,147 +299,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.runtime.Window.Options,
|
||||
) ?*Window {
|
||||
return surface_new_(app, opts) catch |err| {
|
||||
log.err("error initializing surface err={}", .{err});
|
||||
return null;
|
||||
};
|
||||
}
|
||||
|
||||
fn surface_new_(
|
||||
app: *App,
|
||||
opts: *const apprt.runtime.Window.Options,
|
||||
) !*Window {
|
||||
const w = try app.newWindow(.{
|
||||
.runtime = opts.*,
|
||||
});
|
||||
return w;
|
||||
}
|
||||
|
||||
export fn ghostty_surface_free(ptr: ?*Window) void {
|
||||
if (ptr) |v| v.app.closeWindow(v);
|
||||
}
|
||||
|
||||
/// Returns the app associated with a surface.
|
||||
export fn ghostty_surface_app(win: *Window) *App {
|
||||
return win.app;
|
||||
}
|
||||
|
||||
/// Tell the surface that it needs to schedule a render
|
||||
export fn ghostty_surface_refresh(win: *Window) 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 {
|
||||
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 {
|
||||
win.window.updateContentScale(x, y);
|
||||
}
|
||||
|
||||
/// Update the focused state of a surface.
|
||||
export fn ghostty_surface_set_focus(win: *Window, focused: bool) void {
|
||||
win.window.focusCallback(focused);
|
||||
}
|
||||
|
||||
/// Tell the surface that it needs to schedule a render
|
||||
export fn ghostty_surface_key(
|
||||
win: *Window,
|
||||
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: *Window, 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,
|
||||
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: *Window, x: f64, y: f64) void {
|
||||
win.window.cursorPosCallback(x, y);
|
||||
}
|
||||
|
||||
export fn ghostty_surface_mouse_scroll(win: *Window, x: f64, y: f64) void {
|
||||
win.window.scrollCallback(x, y);
|
||||
}
|
||||
|
||||
export fn ghostty_surface_ime_point(win: *Window, x: *f64, y: *f64) void {
|
||||
const pos = win.imePoint();
|
||||
x.* = pos.x;
|
||||
y.* = pos.y;
|
||||
}
|
||||
};
|
||||
|
@ -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;
|
||||
|
||||
@ -29,8 +29,8 @@ visible: bool = false,
|
||||
/// Our app config
|
||||
config: ?*const Config = null,
|
||||
|
||||
/// The window we're tracking.
|
||||
window: ?*Window = 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);
|
||||
|
@ -1,15 +1,19 @@
|
||||
//! Window represents a single OS window.
|
||||
//! 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.
|
||||
//!
|
||||
//! 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();
|
||||
//! 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();
|
||||
|
||||
// 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;
|
||||
pub const Mailbox = apprt.surface.Mailbox;
|
||||
pub const Message = apprt.surface.Message;
|
||||
|
||||
const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
@ -18,7 +22,6 @@ 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");
|
||||
@ -31,12 +34,7 @@ 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);
|
||||
const log = std.log.scoped(.surface);
|
||||
|
||||
// The renderer implementation to use.
|
||||
const Renderer = renderer.Renderer;
|
||||
@ -44,11 +42,11 @@ 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 state
|
||||
window: apprt.runtime.Window,
|
||||
/// The windowing system surface
|
||||
rt_surface: *apprt.runtime.Surface,
|
||||
|
||||
/// The font structures
|
||||
font_lib: font.Library,
|
||||
@ -58,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
|
||||
@ -96,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,
|
||||
@ -111,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,
|
||||
|
||||
@ -125,28 +123,22 @@ const Mouse = struct {
|
||||
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(
|
||||
/// 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,
|
||||
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);
|
||||
app_mailbox: App.Mailbox,
|
||||
rt_surface: *apprt.runtime.Surface,
|
||||
) !void {
|
||||
// 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 window.getContentScale();
|
||||
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={}", .{
|
||||
@ -156,17 +148,17 @@ pub fn create(
|
||||
y_dpi,
|
||||
});
|
||||
|
||||
// The font size we desire along with the DPI determiend for the window
|
||||
// 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 window
|
||||
// Find all the fonts for this surface
|
||||
//
|
||||
// Future: we can share the font group amongst all windows to save
|
||||
// some new window init time and some memory. This will require making
|
||||
// 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();
|
||||
@ -290,7 +282,7 @@ pub fn create(
|
||||
.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,
|
||||
@ -298,15 +290,15 @@ pub fn create(
|
||||
.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 window.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),
|
||||
@ -321,9 +313,10 @@ pub fn create(
|
||||
// Create the renderer thread
|
||||
var render_thread = try renderer.Thread.init(
|
||||
alloc,
|
||||
window,
|
||||
rt_surface,
|
||||
&self.renderer,
|
||||
&self.renderer_state,
|
||||
app_mailbox,
|
||||
);
|
||||
errdefer render_thread.deinit();
|
||||
|
||||
@ -335,7 +328,7 @@ pub fn create(
|
||||
.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();
|
||||
|
||||
@ -343,15 +336,15 @@ pub fn create(
|
||||
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,
|
||||
.app = app,
|
||||
.window = window,
|
||||
.app_mailbox = app_mailbox,
|
||||
.rt_surface = rt_surface,
|
||||
.font_lib = font_lib,
|
||||
.font_group = font_group,
|
||||
.font_size = font_size,
|
||||
@ -384,21 +377,21 @@ pub fn create(
|
||||
|
||||
// Set a minimum size that is cols=10 h=4. This matches Mac's Terminal.app
|
||||
// but is otherwise somewhat arbitrary.
|
||||
try window.setSizeLimits(.{
|
||||
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
|
||||
// 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";
|
||||
|
||||
@ -413,16 +406,16 @@ pub fn create(
|
||||
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(window);
|
||||
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.finalizeWindowInit(window);
|
||||
try renderer_impl.finalizeSurfaceInit(rt_surface);
|
||||
|
||||
// Start our renderer thread
|
||||
self.renderer_thr = try std.Thread.spawn(
|
||||
@ -439,11 +432,9 @@ pub fn create(
|
||||
.{&self.io_thread},
|
||||
);
|
||||
self.io_thr.setName("io") catch {};
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
pub fn destroy(self: *Window) void {
|
||||
pub fn deinit(self: *Surface) void {
|
||||
// Stop rendering thread
|
||||
{
|
||||
self.renderer_thread.stop.notify() catch |err|
|
||||
@ -451,15 +442,15 @@ pub fn destroy(self: *Window) void {
|
||||
self.renderer_thr.join();
|
||||
|
||||
// We need to become the active rendering thread again
|
||||
self.renderer.threadEnter(self.window) catch unreachable;
|
||||
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();
|
||||
@ -480,62 +471,23 @@ pub fn destroy(self: *Window) void {
|
||||
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 {
|
||||
/// surface.
|
||||
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.window.setTitle(slice);
|
||||
try self.rt_surface.setTitle(slice);
|
||||
},
|
||||
|
||||
.cell_size => |size| try self.setCellSize(size),
|
||||
@ -555,7 +507,7 @@ pub fn handleMessage(self: *Window, msg: Message) !void {
|
||||
|
||||
/// Returns the x/y coordinate of where the IME (Input Method Editor)
|
||||
/// keyboard should be rendered.
|
||||
pub fn imePoint(self: *const Window) apprt.IMEPos {
|
||||
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();
|
||||
@ -564,7 +516,7 @@ pub fn imePoint(self: *const Window) apprt.IMEPos {
|
||||
// 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 content_scale = self.rt_surface.getContentScale() catch .{ .x = 1, .y = 1 };
|
||||
|
||||
const x: f64 = x: {
|
||||
// Simple x * cell width gives the top-left corner
|
||||
@ -595,13 +547,13 @@ pub fn imePoint(self: *const Window) apprt.IMEPos {
|
||||
return .{ .x = x, .y = y };
|
||||
}
|
||||
|
||||
fn clipboardRead(self: *const Window, kind: u8) !void {
|
||||
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.window.getClipboardString() catch |err| {
|
||||
const data = self.rt_surface.getClipboardString() catch |err| {
|
||||
log.warn("error reading clipboard: {}", .{err});
|
||||
return;
|
||||
};
|
||||
@ -631,7 +583,7 @@ fn clipboardRead(self: *const Window, kind: u8) !void {
|
||||
self.io_thread.wakeup.notify() catch {};
|
||||
}
|
||||
|
||||
fn clipboardWrite(self: *const Window, data: []const u8) !void {
|
||||
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;
|
||||
@ -649,7 +601,7 @@ fn clipboardWrite(self: *const Window, data: []const u8) !void {
|
||||
try dec.decode(buf, data);
|
||||
assert(buf[buf.len] == 0);
|
||||
|
||||
self.window.setClipboardString(buf) catch |err| {
|
||||
self.rt_surface.setClipboardString(buf) catch |err| {
|
||||
log.err("error setting clipboard string err={}", .{err});
|
||||
return;
|
||||
};
|
||||
@ -657,7 +609,7 @@ fn clipboardWrite(self: *const Window, data: []const u8) !void {
|
||||
|
||||
/// 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 {
|
||||
fn setCellSize(self: *Surface, size: renderer.CellSize) !void {
|
||||
// Update our new cell size for future calcs
|
||||
self.cell_size = size;
|
||||
|
||||
@ -681,7 +633,7 @@ fn setCellSize(self: *Window, size: renderer.CellSize) !void {
|
||||
/// Change the font size.
|
||||
///
|
||||
/// This can only be called from the main thread.
|
||||
pub fn setFontSize(self: *Window, size: font.face.DesiredSize) void {
|
||||
pub fn setFontSize(self: *Surface, size: font.face.DesiredSize) void {
|
||||
// Update our font size so future changes work
|
||||
self.font_size = size;
|
||||
|
||||
@ -697,11 +649,11 @@ pub fn setFontSize(self: *Window, size: font.face.DesiredSize) void {
|
||||
/// 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 {
|
||||
fn queueRender(self: *const Surface) !void {
|
||||
try self.renderer_thread.wakeup.notify();
|
||||
}
|
||||
|
||||
pub fn sizeCallback(self: *Window, size: apprt.WindowSize) !void {
|
||||
pub fn sizeCallback(self: *Surface, size: apprt.SurfaceSize) !void {
|
||||
const tracy = trace(@src());
|
||||
defer tracy.end();
|
||||
|
||||
@ -745,7 +697,7 @@ pub fn sizeCallback(self: *Window, size: apprt.WindowSize) !void {
|
||||
try self.io_thread.wakeup.notify();
|
||||
}
|
||||
|
||||
pub fn charCallback(self: *Window, codepoint: u21) !void {
|
||||
pub fn charCallback(self: *Surface, codepoint: u21) !void {
|
||||
const tracy = trace(@src());
|
||||
defer tracy.end();
|
||||
|
||||
@ -796,7 +748,7 @@ pub fn charCallback(self: *Window, codepoint: u21) !void {
|
||||
}
|
||||
|
||||
pub fn keyCallback(
|
||||
self: *Window,
|
||||
self: *Surface,
|
||||
action: input.Action,
|
||||
key: input.Key,
|
||||
mods: input.Mods,
|
||||
@ -879,7 +831,7 @@ pub fn keyCallback(
|
||||
};
|
||||
defer self.alloc.free(buf);
|
||||
|
||||
self.window.setClipboardString(buf) catch |err| {
|
||||
self.rt_surface.setClipboardString(buf) catch |err| {
|
||||
log.err("error setting clipboard string err={}", .{err});
|
||||
return;
|
||||
};
|
||||
@ -887,7 +839,7 @@ pub fn keyCallback(
|
||||
},
|
||||
|
||||
.paste_from_clipboard => {
|
||||
const data = self.window.getClipboardString() catch |err| {
|
||||
const data = self.rt_surface.getClipboardString() catch |err| {
|
||||
log.warn("error reading clipboard: {}", .{err});
|
||||
return;
|
||||
};
|
||||
@ -950,38 +902,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 = .{
|
||||
.font_size = if (self.config.@"window-inherit-font-size")
|
||||
self.font_size
|
||||
else
|
||||
null,
|
||||
.parent = self,
|
||||
},
|
||||
}, .{ .instant = {} });
|
||||
self.app.wakeup();
|
||||
},
|
||||
|
||||
.new_tab => {
|
||||
_ = self.app.mailbox.push(.{
|
||||
_ = 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(),
|
||||
.close_window => {
|
||||
_ = self.app_mailbox.push(.{ .close = self }, .{ .instant = {} });
|
||||
},
|
||||
|
||||
.quit => {
|
||||
_ = self.app.mailbox.push(.{
|
||||
_ = self.app_mailbox.push(.{
|
||||
.quit = {},
|
||||
}, .{ .instant = {} });
|
||||
self.app.wakeup();
|
||||
},
|
||||
}
|
||||
|
||||
@ -1059,7 +1002,7 @@ pub fn keyCallback(
|
||||
}
|
||||
}
|
||||
|
||||
pub fn focusCallback(self: *Window, focused: bool) !void {
|
||||
pub fn focusCallback(self: *Surface, focused: bool) !void {
|
||||
// Notify our render thread of the new state
|
||||
_ = self.renderer_thread.mailbox.push(.{
|
||||
.focus = focused,
|
||||
@ -1069,17 +1012,17 @@ pub fn focusCallback(self: *Window, focused: bool) !void {
|
||||
try self.queueRender();
|
||||
}
|
||||
|
||||
pub fn refreshCallback(self: *Window) !void {
|
||||
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: *Window, xoff: f64, yoff: f64) !void {
|
||||
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();
|
||||
|
||||
@ -1107,7 +1050,7 @@ pub fn scrollCallback(self: *Window, xoff: f64, yoff: f64) !void {
|
||||
// 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();
|
||||
const pos = try self.rt_surface.getCursorPos();
|
||||
try self.mouseReport(if (yoff < 0) .five else .four, .press, self.mouse.mods, pos);
|
||||
}
|
||||
}
|
||||
@ -1119,14 +1062,14 @@ pub fn scrollCallback(self: *Window, xoff: f64, yoff: f64) !void {
|
||||
const MouseReportAction = enum { press, release, motion };
|
||||
|
||||
fn mouseReport(
|
||||
self: *Window,
|
||||
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?
|
||||
// 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) {
|
||||
@ -1314,7 +1257,7 @@ fn mouseReport(
|
||||
}
|
||||
|
||||
pub fn mouseButtonCallback(
|
||||
self: *Window,
|
||||
self: *Surface,
|
||||
action: input.MouseButtonState,
|
||||
button: input.MouseButton,
|
||||
mods: input.Mods,
|
||||
@ -1322,8 +1265,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();
|
||||
|
||||
@ -1342,7 +1285,7 @@ pub fn mouseButtonCallback(
|
||||
|
||||
// Report mouse events if enabled
|
||||
if (self.io.terminal.modes.mouse_event != .none) {
|
||||
const pos = try self.window.getCursorPos();
|
||||
const pos = try self.rt_surface.getCursorPos();
|
||||
|
||||
const report_action: MouseReportAction = switch (action) {
|
||||
.press => .press,
|
||||
@ -1360,7 +1303,7 @@ pub fn mouseButtonCallback(
|
||||
// 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();
|
||||
const pos = try self.rt_surface.getCursorPos();
|
||||
|
||||
// If we move our cursor too much between clicks then we reset
|
||||
// the multi-click state.
|
||||
@ -1433,14 +1376,14 @@ pub fn mouseButtonCallback(
|
||||
}
|
||||
|
||||
pub fn cursorPosCallback(
|
||||
self: *Window,
|
||||
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 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();
|
||||
|
||||
@ -1495,7 +1438,7 @@ pub fn cursorPosCallback(
|
||||
|
||||
/// Double-click dragging moves the selection one "word" at a time.
|
||||
fn dragLeftClickDouble(
|
||||
self: *Window,
|
||||
self: *Surface,
|
||||
screen_point: terminal.point.ScreenPoint,
|
||||
) void {
|
||||
// Get the word under our current point. If there isn't a word, do nothing.
|
||||
@ -1520,7 +1463,7 @@ fn dragLeftClickDouble(
|
||||
|
||||
/// Triple-click dragging moves the selection one "line" at a time.
|
||||
fn dragLeftClickTriple(
|
||||
self: *Window,
|
||||
self: *Surface,
|
||||
screen_point: terminal.point.ScreenPoint,
|
||||
) void {
|
||||
// Get the word under our current point. If there isn't a word, do nothing.
|
||||
@ -1544,7 +1487,7 @@ fn dragLeftClickTriple(
|
||||
}
|
||||
|
||||
fn dragLeftClickSingle(
|
||||
self: *Window,
|
||||
self: *Surface,
|
||||
screen_point: terminal.point.ScreenPoint,
|
||||
xpos: f64,
|
||||
) void {
|
||||
@ -1650,10 +1593,10 @@ fn dragLeftClickSingle(
|
||||
self.io.terminal.selection.?.end = screen_point;
|
||||
}
|
||||
|
||||
fn posToViewport(self: Window, xpos: f64, ypos: f64) terminal.point.Viewport {
|
||||
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
|
@ -8,25 +8,56 @@
|
||||
//! 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");
|
||||
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.
|
||||
/// 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,
|
||||
};
|
||||
|
||||
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
|
||||
/// 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;
|
||||
}
|
||||
};
|
||||
|
||||
test {
|
||||
@import("std").testing.refAllDecls(@This());
|
||||
}
|
||||
|
@ -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);
|
||||
|
||||
@ -46,30 +46,57 @@ 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 {
|
||||
_ = 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 fn redrawSurface(self: *App, surface: *Surface) void {
|
||||
_ = self;
|
||||
_ = surface;
|
||||
// No-op, we use a threaded interface so we're constantly drawing.
|
||||
}
|
||||
};
|
||||
|
||||
pub const Window = struct {
|
||||
pub const Surface = struct {
|
||||
app: *App,
|
||||
nsview: objc.Object,
|
||||
core_win: *CoreWindow,
|
||||
core_surface: CoreSurface,
|
||||
content_scale: apprt.ContentScale,
|
||||
size: apprt.WindowSize,
|
||||
size: apprt.SurfaceSize,
|
||||
cursor_pos: apprt.CursorPos,
|
||||
opts: Options,
|
||||
|
||||
@ -84,11 +111,10 @@ pub const Window = struct {
|
||||
scale_factor: f64 = 1,
|
||||
};
|
||||
|
||||
pub fn init(app: *const CoreApp, core_win: *CoreWindow, opts: Options) !Window {
|
||||
_ = app;
|
||||
|
||||
return .{
|
||||
.core_win = core_win,
|
||||
pub fn init(self: *Surface, app: *App, opts: Options) !void {
|
||||
self.* = .{
|
||||
.app = app,
|
||||
.core_surface = undefined,
|
||||
.nsview = objc.Object.fromId(opts.nsview),
|
||||
.content_scale = .{
|
||||
.x = @floatCast(f32, opts.scale_factor),
|
||||
@ -98,104 +124,122 @@ pub const Window = struct {
|
||||
.cursor_pos = .{ .x = 0, .y = 0 },
|
||||
.opts = opts,
|
||||
};
|
||||
|
||||
// Add ourselves to the list of surfaces on the app.
|
||||
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.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: *Window) void {
|
||||
_ = self;
|
||||
pub fn deinit(self: *Surface) void {
|
||||
// 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 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 {
|
||||
self.core_win.app.runtime.opts.set_title(
|
||||
pub fn setTitle(self: *Surface, slice: [:0]const u8) !void {
|
||||
self.app.opts.set_title(
|
||||
self.opts.userdata,
|
||||
slice.ptr,
|
||||
);
|
||||
}
|
||||
|
||||
pub fn getClipboardString(self: *const Window) ![:0]const u8 {
|
||||
const ptr = self.core_win.app.runtime.opts.read_clipboard(self.opts.userdata);
|
||||
pub fn getClipboardString(self: *const Surface) ![:0]const u8 {
|
||||
const ptr = self.app.opts.read_clipboard(self.opts.userdata);
|
||||
return std.mem.sliceTo(ptr, 0);
|
||||
}
|
||||
|
||||
pub fn setClipboardString(self: *const Window, val: [:0]const u8) !void {
|
||||
self.core_win.app.runtime.opts.write_clipboard(self.opts.userdata, val.ptr);
|
||||
pub fn setClipboardString(self: *const Surface, val: [:0]const u8) !void {
|
||||
self.app.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 {
|
||||
self.core_win.refreshCallback() catch |err| {
|
||||
pub fn refresh(self: *Surface) void {
|
||||
self.core_surface.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,
|
||||
};
|
||||
|
||||
// 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;
|
||||
};
|
||||
}
|
||||
|
||||
pub fn mouseButtonCallback(
|
||||
self: *const Window,
|
||||
self: *Surface,
|
||||
action: input.MouseButtonState,
|
||||
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 Window, xoff: f64, yoff: f64) void {
|
||||
self.core_win.scrollCallback(xoff, yoff) catch |err| {
|
||||
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;
|
||||
};
|
||||
}
|
||||
|
||||
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(.{
|
||||
self.cursor_pos = self.cursorPosToPixels(.{
|
||||
.x = @floatCast(f32, x),
|
||||
.y = @floatCast(f32, y),
|
||||
}) catch |err| {
|
||||
@ -206,35 +250,35 @@ pub const Window = 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;
|
||||
};
|
||||
}
|
||||
|
||||
pub fn keyCallback(
|
||||
self: *const Window,
|
||||
self: *Surface,
|
||||
action: input.Action,
|
||||
key: input.Key,
|
||||
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;
|
||||
};
|
||||
}
|
||||
|
||||
pub fn charCallback(self: *const Window, cp_: u32) void {
|
||||
pub fn charCallback(self: *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 Window, focused: bool) void {
|
||||
self.core_win.focusCallback(focused) catch |err| {
|
||||
pub fn focusCallback(self: *Surface, focused: bool) void {
|
||||
self.core_surface.focusCallback(focused) catch |err| {
|
||||
log.err("error in focus callback err={}", .{err});
|
||||
return;
|
||||
};
|
||||
@ -242,8 +286,157 @@ 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 };
|
||||
}
|
||||
};
|
||||
|
||||
// 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;
|
||||
}
|
||||
};
|
||||
|
@ -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");
|
||||
@ -16,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(.{
|
||||
@ -26,11 +28,25 @@ 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();
|
||||
|
||||
return .{
|
||||
.app = core_app,
|
||||
.darwin = darwin,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn terminate(self: App) void {
|
||||
@ -38,31 +54,170 @@ 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 {
|
||||
pub fn wakeup(self: *const App) void {
|
||||
_ = self;
|
||||
glfw.postEmptyEvent();
|
||||
}
|
||||
|
||||
/// Wait for events in the event loop to process.
|
||||
pub fn wait(self: App) !void {
|
||||
_ = self;
|
||||
glfw.waitEvents();
|
||||
/// Create a new window for the app.
|
||||
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) !void {
|
||||
if (!Darwin.enabled) {
|
||||
log.warn("tabbing is not supported on this platform", .{});
|
||||
return;
|
||||
}
|
||||
|
||||
// Create the new window
|
||||
const window = try self.newSurface(parent);
|
||||
|
||||
// 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;
|
||||
};
|
||||
}
|
||||
|
||||
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.
|
||||
pub fn closeSurface(self: *App, surface: *Surface) void {
|
||||
surface.deinit();
|
||||
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 });
|
||||
|
||||
// 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 {
|
||||
/// 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,
|
||||
|
||||
/// The app we're part of
|
||||
app: *App,
|
||||
|
||||
/// 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,
|
||||
@ -74,9 +229,9 @@ pub const Window = struct {
|
||||
) orelse return glfw.mustGetErrorCode();
|
||||
errdefer win.destroy();
|
||||
|
||||
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
|
||||
if (builtin.mode == .Debug) {
|
||||
const monitor = win.getMonitor() orelse monitor: {
|
||||
log.warn("window had null monitor, getting primary monitor", .{});
|
||||
break :monitor glfw.Monitor.getPrimary().?;
|
||||
@ -91,8 +246,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).?);
|
||||
|
||||
@ -115,7 +270,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);
|
||||
@ -126,15 +281,37 @@ pub const Window = struct {
|
||||
win.setMouseButtonCallback(mouseButtonCallback);
|
||||
|
||||
// Build our result
|
||||
return Window{
|
||||
self.* = .{
|
||||
.app = app,
|
||||
.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.alloc,
|
||||
app.app.config,
|
||||
.{ .rt_app = app, .mailbox = &app.app.mailbox },
|
||||
self,
|
||||
);
|
||||
errdefer self.core_surface.deinit();
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Window) void {
|
||||
var tabgroup_opt: if (builtin.target.isDarwin()) ?objc.Object else void = undefined;
|
||||
if (comptime builtin.target.isDarwin()) {
|
||||
pub fn deinit(self: *Surface) void {
|
||||
// Remove ourselves from the list of known surfaces in the app.
|
||||
self.app.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;
|
||||
if (App.Darwin.enabled) {
|
||||
const nswindow = objc.Object.fromId(glfwNative.getCocoaWindow(self.window).?);
|
||||
const tabgroup = nswindow.getProperty(objc.Object, "tabGroup");
|
||||
|
||||
@ -171,7 +348,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"), .{});
|
||||
@ -183,7 +360,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.SurfaceSize, max_: ?apprt.SurfaceSize) !void {
|
||||
self.window.setSizeLimits(.{
|
||||
.width = min.width,
|
||||
.height = min.height,
|
||||
@ -197,7 +374,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 };
|
||||
}
|
||||
@ -205,14 +382,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.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
|
||||
/// 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{
|
||||
@ -223,37 +400,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.
|
||||
@ -280,8 +457,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;
|
||||
};
|
||||
@ -297,7 +474,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;
|
||||
@ -449,7 +626,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;
|
||||
@ -460,7 +637,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;
|
||||
@ -471,7 +648,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;
|
||||
@ -482,7 +659,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;
|
||||
@ -497,10 +674,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| {
|
||||
@ -529,7 +706,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);
|
||||
|
801
src/apprt/gtk.zig
Normal file
801
src/apprt/gtk.zig
Normal file
@ -0,0 +1,801 @@
|
||||
//! Application runtime that uses GTK4.
|
||||
|
||||
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 input = @import("../input.zig");
|
||||
const CoreApp = @import("../App.zig");
|
||||
const CoreSurface = @import("../Surface.zig");
|
||||
|
||||
pub const c = @cImport({
|
||||
@cInclude("gtk/gtk.h");
|
||||
});
|
||||
|
||||
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
|
||||
id: [:0]const u8 = "com.mitchellh.ghostty",
|
||||
};
|
||||
|
||||
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
|
||||
// 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,
|
||||
c.G_APPLICATION_DEFAULT_FLAGS,
|
||||
)) orelse return error.GtkInitFailed;
|
||||
errdefer c.g_object_unref(app);
|
||||
_ = 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
|
||||
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;
|
||||
}
|
||||
|
||||
// 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);
|
||||
|
||||
// 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,
|
||||
};
|
||||
}
|
||||
|
||||
// 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) {}
|
||||
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();
|
||||
}
|
||||
|
||||
pub fn wakeup(self: App) void {
|
||||
_ = self;
|
||||
c.g_main_context_wakeup(null);
|
||||
}
|
||||
|
||||
/// 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 (should_quit) return;
|
||||
}
|
||||
}
|
||||
|
||||
/// Close the given surface.
|
||||
pub fn closeSurface(self: *App, surface: *Surface) void {
|
||||
_ = 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 redrawSurface(self: *App, surface: *Surface) void {
|
||||
_ = self;
|
||||
surface.invalidate();
|
||||
}
|
||||
|
||||
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);
|
||||
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);
|
||||
|
||||
// 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);
|
||||
|
||||
// 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 {
|
||||
_ = 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", .{});
|
||||
}
|
||||
};
|
||||
|
||||
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,
|
||||
};
|
||||
|
||||
/// 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,
|
||||
|
||||
/// 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,
|
||||
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);
|
||||
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();
|
||||
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,
|
||||
);
|
||||
|
||||
// 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);
|
||||
c.gtk_gesture_single_set_button(@ptrCast(
|
||||
*c.GtkGestureSingle,
|
||||
gesture_click,
|
||||
), 0);
|
||||
c.gtk_widget_add_controller(widget, @ptrCast(
|
||||
*c.GtkEventController,
|
||||
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);
|
||||
|
||||
// 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,
|
||||
.gl_area = opts.gl_area,
|
||||
.core_surface = undefined,
|
||||
.size = .{ .width = 800, .height = 600 },
|
||||
.cursor_pos = .{ .x = 0, .y = 0 },
|
||||
.clipboard = std.mem.zeroes(c.GValue),
|
||||
};
|
||||
errdefer self.* = undefined;
|
||||
|
||||
// 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_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);
|
||||
_ = 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 {
|
||||
// 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();
|
||||
|
||||
// Note we're realized
|
||||
self.realized = true;
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
// 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();
|
||||
}
|
||||
|
||||
/// 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);
|
||||
}
|
||||
|
||||
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 {
|
||||
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;
|
||||
}
|
||||
|
||||
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 {
|
||||
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 {
|
||||
return self.cursor_pos;
|
||||
}
|
||||
|
||||
fn gtkRealize(area: *c.GtkGLArea, ud: ?*anyopaque) callconv(.C) void {
|
||||
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.?);
|
||||
self.realize() catch |err| {
|
||||
// TODO: we need to destroy the GL area here.
|
||||
log.err("surface failed to realize: {}", .{err});
|
||||
return;
|
||||
};
|
||||
}
|
||||
|
||||
/// render singal
|
||||
fn gtkRender(area: *c.GtkGLArea, ctx: *c.GdkGLContext, ud: ?*anyopaque) callconv(.C) c.gboolean {
|
||||
_ = area;
|
||||
_ = ctx;
|
||||
|
||||
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;
|
||||
log.debug("gl destroy", .{});
|
||||
|
||||
const self = userdataSelf(ud.?);
|
||||
const alloc = self.app.core_app.alloc;
|
||||
self.deinit();
|
||||
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;
|
||||
};
|
||||
}
|
||||
|
||||
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;
|
||||
};
|
||||
}
|
||||
|
||||
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;
|
||||
};
|
||||
}
|
||||
|
||||
fn gtkKeyPressed(
|
||||
_: *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.?);
|
||||
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;
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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| {
|
||||
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));
|
||||
}
|
||||
};
|
||||
|
||||
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;
|
||||
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,
|
||||
};
|
||||
}
|
@ -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,
|
||||
};
|
||||
|
@ -1,15 +1,15 @@
|
||||
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");
|
||||
|
||||
/// 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,26 +25,25 @@ pub const Message = union(enum) {
|
||||
clipboard_write: WriteReq,
|
||||
};
|
||||
|
||||
/// A window mailbox.
|
||||
/// A surface mailbox.
|
||||
pub const Mailbox = struct {
|
||||
window: *Window,
|
||||
app: *App.Mailbox,
|
||||
surface: *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
|
||||
// thread, so we have to rewrap the message with our window
|
||||
/// Send a message to the surface.
|
||||
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(.{
|
||||
.window_message = .{
|
||||
.window = self.window,
|
||||
return self.app.push(.{
|
||||
.surface_message = .{
|
||||
.surface = self.surface,
|
||||
.message = msg,
|
||||
},
|
||||
}, timeout);
|
||||
|
||||
// Wake up our app loop
|
||||
self.window.app.wakeup();
|
||||
|
||||
return result;
|
||||
}
|
||||
};
|
@ -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
|
||||
|
@ -32,6 +32,7 @@ pub const MouseButton = enum(c_int) {
|
||||
break :max cur;
|
||||
};
|
||||
|
||||
unknown = 0,
|
||||
left = 1,
|
||||
right = 2,
|
||||
middle = 3,
|
||||
|
37
src/main.zig
37
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");
|
||||
@ -10,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");
|
||||
@ -86,16 +88,21 @@ pub fn main() !void {
|
||||
}
|
||||
}
|
||||
try config.finalize();
|
||||
std.log.debug("config={}", .{config});
|
||||
//std.log.debug("config={}", .{config});
|
||||
|
||||
// We want to log all our errors
|
||||
glfw.setErrorCallback(glfwErrorCallback);
|
||||
|
||||
// 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();
|
||||
_ = 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(null);
|
||||
|
||||
// Run the GUI event loop
|
||||
try app_runtime.run();
|
||||
}
|
||||
|
||||
// Required by tracy/tracy.zig to enable/disable tracy support.
|
||||
@ -151,20 +158,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.
|
||||
|
@ -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.
|
||||
|
@ -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"), .{});
|
||||
}
|
||||
|
||||
|
@ -18,19 +18,29 @@ 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);
|
||||
|
||||
// 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.
|
||||
pub 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.
|
||||
@ -89,7 +99,77 @@ focused: bool,
|
||||
padding: renderer.Options.Padding,
|
||||
|
||||
/// The mailbox for communicating with the window.
|
||||
window_mailbox: Window.Mailbox,
|
||||
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,
|
||||
deferred_font_size: ?SetFontSize = 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,
|
||||
|
||||
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,
|
||||
),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
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
|
||||
@ -173,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);
|
||||
@ -301,6 +378,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
|
||||
@ -311,7 +389,8 @@ 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,
|
||||
.deferred_font_size = .{ .metrics = metrics },
|
||||
};
|
||||
}
|
||||
|
||||
@ -362,12 +441,26 @@ 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);
|
||||
|
||||
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.
|
||||
@ -388,19 +481,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,15 +511,26 @@ 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) {
|
||||
else => @compileError("unsupported app runtime for OpenGL"),
|
||||
|
||||
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);
|
||||
glfw.makeContextCurrent(surface.window);
|
||||
errdefer glfw.makeContextCurrent(null);
|
||||
glfw.swapInterval(1);
|
||||
|
||||
@ -438,14 +542,27 @@ pub fn threadEnter(self: *const OpenGL, win: apprt.runtime.Window) !void {
|
||||
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;
|
||||
|
||||
switch (apprt.runtime) {
|
||||
else => @compileError("unsupported app runtime for OpenGL"),
|
||||
|
||||
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();
|
||||
glfw.makeContextCurrent(null);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Callback when the focus changes for the terminal this is rendering.
|
||||
@ -466,6 +583,9 @@ 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});
|
||||
|
||||
// Set our new size, this will also reset our font atlas.
|
||||
@ -475,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.
|
||||
@ -484,7 +607,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 = {} });
|
||||
}
|
||||
@ -493,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'?
|
||||
@ -514,20 +636,13 @@ 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;
|
||||
}
|
||||
|
||||
/// 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.
|
||||
@ -568,6 +683,7 @@ pub fn render(
|
||||
|
||||
// Build our devmode draw data
|
||||
const devmode_data = devmode_data: {
|
||||
if (DevMode.enabled) {
|
||||
if (state.devmode) |dm| {
|
||||
if (dm.visible) {
|
||||
imgui.ImplOpenGL3.newFrame();
|
||||
@ -576,6 +692,7 @@ pub fn render(
|
||||
break :devmode_data try dm.render();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
break :devmode_data null;
|
||||
};
|
||||
@ -613,6 +730,14 @@ pub fn render(
|
||||
};
|
||||
defer critical.screen.deinit();
|
||||
|
||||
// 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();
|
||||
|
||||
// Set our draw data
|
||||
self.draw_background = critical.gl_bg;
|
||||
|
||||
// Build our GPU cells
|
||||
try self.rebuildCells(
|
||||
critical.active_screen,
|
||||
@ -620,30 +745,27 @@ pub fn render(
|
||||
&critical.screen,
|
||||
critical.draw_cursor,
|
||||
);
|
||||
}
|
||||
|
||||
// Try to flush our atlas, this will only do something if there
|
||||
// are changes to the atlas.
|
||||
try self.flushAtlas();
|
||||
// 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;
|
||||
|
||||
// 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);
|
||||
|
||||
// We're out of the critical path now. Let's first render our terminal.
|
||||
try self.draw();
|
||||
|
||||
// If we have devmode, then render that
|
||||
if (DevMode.enabled) {
|
||||
if (critical.devmode_data) |data| {
|
||||
imgui.ImplOpenGL3.renderDrawData(data);
|
||||
}
|
||||
}
|
||||
|
||||
// Swap our window buffers
|
||||
win.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
|
||||
@ -1050,7 +1172,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,
|
||||
@ -1060,17 +1182,13 @@ fn gridSize(self: *OpenGL, screen_size: renderer.ScreenSize) renderer.GridSize {
|
||||
/// 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);
|
||||
|
||||
// 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,
|
||||
@ -1092,31 +1210,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.
|
||||
@ -1196,9 +1291,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;
|
||||
@ -1224,6 +1336,16 @@ 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;
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
@ -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.
|
||||
@ -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: Window.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,
|
||||
|
||||
|
@ -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);
|
||||
@ -47,8 +48,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,
|
||||
@ -60,14 +61,18 @@ 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.
|
||||
pub fn init(
|
||||
alloc: Allocator,
|
||||
win: apprt.runtime.Window,
|
||||
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(.{});
|
||||
@ -100,10 +105,11 @@ 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,
|
||||
.app_mailbox = app_mailbox,
|
||||
};
|
||||
}
|
||||
|
||||
@ -135,7 +141,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,8 +311,17 @@ 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});
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
|
@ -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{};
|
||||
}
|
||||
|
||||
|
@ -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 };
|
||||
}
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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),
|
||||
};
|
||||
|
@ -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);
|
||||
@ -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: Window.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: Window.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,15 +995,15 @@ 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(.{
|
||||
.clipboard_write = try Window.Message.WriteReq.init(
|
||||
_ = self.surface_mailbox.push(.{
|
||||
.clipboard_write = try apprt.surface.Message.WriteReq.init(
|
||||
self.alloc,
|
||||
data,
|
||||
),
|
||||
|
@ -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,
|
||||
@ -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: Window.Mailbox,
|
||||
/// The mailbox for sending the surface messages.
|
||||
surface_mailbox: apprt.surface.Mailbox,
|
||||
|
Reference in New Issue
Block a user