mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-22 11:46:11 +03:00
430 lines
12 KiB
Zig
430 lines
12 KiB
Zig
//! App is the primary GUI application for ghostty. This builds the window,
|
|
//! sets up the renderer, etc. The primary run loop is started by calling
|
|
//! the "run" function.
|
|
const App = @This();
|
|
|
|
const std = @import("std");
|
|
const builtin = @import("builtin");
|
|
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 tracy = @import("tracy");
|
|
const input = @import("input.zig");
|
|
const Config = @import("config.zig").Config;
|
|
const BlockingQueue = @import("./blocking_queue.zig").BlockingQueue;
|
|
const renderer = @import("renderer.zig");
|
|
const font = @import("font/main.zig");
|
|
const macos = @import("macos");
|
|
const objc = @import("objc");
|
|
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);
|
|
|
|
/// General purpose allocator
|
|
alloc: Allocator,
|
|
|
|
/// The list of windows that are currently open
|
|
windows: WindowList,
|
|
|
|
// 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,
|
|
|
|
/// Set to true once we're quitting. This never goes false again.
|
|
quit: bool,
|
|
|
|
/// App will call this when tick should be called.
|
|
wakeup_cb: ?*const fn () void = null,
|
|
|
|
/// Initialize the main app instance. This creates the main window, sets
|
|
/// up the renderer state, compiles the shaders, etc. This is the primary
|
|
/// "startup" logic.
|
|
pub fn create(
|
|
alloc: Allocator,
|
|
config: *const Config,
|
|
) !*App {
|
|
// The mailbox for messaging this thread
|
|
var mailbox = try Mailbox.create(alloc);
|
|
errdefer mailbox.destroy(alloc);
|
|
|
|
// If we have DevMode on, store the config so we can show it
|
|
if (DevMode.enabled) DevMode.instance.config = config;
|
|
|
|
var app = try alloc.create(App);
|
|
errdefer alloc.destroy(app);
|
|
app.* = .{
|
|
.alloc = alloc,
|
|
.windows = .{},
|
|
.config = config,
|
|
.mailbox = mailbox,
|
|
.quit = false,
|
|
};
|
|
errdefer app.windows.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);
|
|
self.mailbox.destroy(self.alloc);
|
|
|
|
self.alloc.destroy(self);
|
|
}
|
|
|
|
/// Request the app runtime to process app events via tick.
|
|
pub fn wakeup(self: App) void {
|
|
if (self.wakeup_cb) |cb| cb();
|
|
}
|
|
|
|
/// Tick ticks the app loop. This will drain our mailbox and process those
|
|
/// events. This should be called by the application runtime on every loop
|
|
/// tick.
|
|
///
|
|
/// This returns whether the app should quit or not.
|
|
pub fn tick(self: *App, rt_app: *apprt.runtime.App) !bool {
|
|
// If any windows are closing, destroy them
|
|
var i: usize = 0;
|
|
while (i < self.windows.items.len) {
|
|
const window = self.windows.items[i];
|
|
if (window.shouldClose()) {
|
|
window.destroy();
|
|
_ = self.windows.swapRemove(i);
|
|
continue;
|
|
}
|
|
|
|
i += 1;
|
|
}
|
|
|
|
// Drain our mailbox only if we're not quitting.
|
|
if (!self.quit) try self.drainMailbox(rt_app);
|
|
|
|
// We quit if our quit flag is on or if we have closed all windows.
|
|
return self.quit or self.windows.items.len == 0;
|
|
}
|
|
|
|
/// Create a new window. This can be called only on the main thread. This
|
|
/// 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;
|
|
}
|
|
|
|
/// Close a window and free all resources associated with it. This can
|
|
/// only be called from the main thread.
|
|
pub fn closeWindow(self: *App, window: *Window) void {
|
|
var i: usize = 0;
|
|
while (i < self.windows.items.len) {
|
|
const current = self.windows.items[i];
|
|
if (window == current) {
|
|
window.destroy();
|
|
_ = self.windows.swapRemove(i);
|
|
return;
|
|
}
|
|
|
|
i += 1;
|
|
}
|
|
}
|
|
|
|
/// Drain the mailbox.
|
|
fn drainMailbox(self: *App, rt_app: *apprt.runtime.App) !void {
|
|
_ = rt_app;
|
|
|
|
while (self.mailbox.pop()) |message| {
|
|
log.debug("mailbox message={s}", .{@tagName(message)});
|
|
switch (message) {
|
|
.new_window => |msg| _ = try self.newWindow(msg),
|
|
.new_tab => |msg| try self.newTab(msg),
|
|
.quit => try self.setQuit(),
|
|
.window_message => |msg| try self.windowMessage(msg.window, msg.message),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Create a new tab in the parent window
|
|
fn newTab(self: *App, msg: Message.NewWindow) !void {
|
|
if (comptime !builtin.target.isDarwin()) {
|
|
log.warn("tabbing is not supported on this platform", .{});
|
|
return;
|
|
}
|
|
|
|
// In embedded mode, it is up to the embedder to implement tabbing
|
|
// on their own.
|
|
if (comptime build_config.artifact != .exe) {
|
|
log.warn("tabbing is not supported in embedded mode", .{});
|
|
return;
|
|
}
|
|
|
|
const parent = msg.parent orelse {
|
|
log.warn("parent must be set in new_tab message", .{});
|
|
return;
|
|
};
|
|
|
|
// If the parent was closed prior to us handling the message, we do nothing.
|
|
if (!self.hasWindow(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);
|
|
}
|
|
|
|
/// Start quitting
|
|
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();
|
|
}
|
|
}
|
|
|
|
/// Handle a window message
|
|
fn windowMessage(self: *App, win: *Window, msg: Window.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);
|
|
}
|
|
|
|
// 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;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/// The message types that can be sent to the app thread.
|
|
pub const Message = union(enum) {
|
|
/// Create a new terminal window.
|
|
new_window: NewWindow,
|
|
|
|
/// 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,
|
|
|
|
/// Quit
|
|
quit: void,
|
|
|
|
/// A message for a specific window
|
|
window_message: struct {
|
|
window: *Window,
|
|
message: Window.Message,
|
|
},
|
|
|
|
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,
|
|
};
|
|
};
|
|
|
|
// Wasm API.
|
|
pub const Wasm = if (!builtin.target.isWasm()) struct {} else struct {
|
|
const wasm = @import("os/wasm.zig");
|
|
const alloc = wasm.alloc;
|
|
|
|
// export fn app_new(config: *Config) ?*App {
|
|
// return app_new_(config) catch |err| { log.err("error initializing app err={}", .{err});
|
|
// return null;
|
|
// };
|
|
// }
|
|
//
|
|
// fn app_new_(config: *Config) !*App {
|
|
// const app = try App.create(alloc, config);
|
|
// errdefer app.destroy();
|
|
//
|
|
// const result = try alloc.create(App);
|
|
// result.* = app;
|
|
// return result;
|
|
// }
|
|
//
|
|
// export fn app_free(ptr: ?*App) void {
|
|
// if (ptr) |v| {
|
|
// v.destroy();
|
|
// alloc.destroy(v);
|
|
// }
|
|
// }
|
|
};
|
|
|
|
// 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;
|
|
}
|
|
};
|