From fbe35c226bf354d7c256d2bb5970f434d567889a Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 22 Feb 2023 14:37:37 -0800 Subject: [PATCH] Integrating new surface --- src/App.zig | 169 +-- src/DevMode.zig | 4 +- src/Surface.zig | 1635 +++++++++++++++++++++++++ src/apprt.zig | 3 +- src/apprt/{Window.zig => Surface.zig} | 10 +- src/apprt/glfw.zig | 100 +- src/main.zig | 1 + src/renderer/OpenGL.zig | 27 +- src/renderer/Options.zig | 4 +- src/renderer/Thread.zig | 12 +- src/termio/Exec.zig | 8 +- src/termio/Options.zig | 4 +- 12 files changed, 1831 insertions(+), 146 deletions(-) create mode 100644 src/Surface.zig rename src/apprt/{Window.zig => Surface.zig} (86%) diff --git a/src/App.zig b/src/App.zig index 4f8fdc85a..6819d5059 100644 --- a/src/App.zig +++ b/src/App.zig @@ -9,7 +9,7 @@ const assert = std.debug.assert; const Allocator = std.mem.Allocator; const build_config = @import("build_config.zig"); const apprt = @import("apprt.zig"); -const Window = @import("Window.zig"); +const Surface = @import("Surface.zig"); const tracy = @import("tracy"); const input = @import("input.zig"); const Config = @import("config.zig").Config; @@ -22,7 +22,8 @@ const DevMode = @import("DevMode.zig"); const log = std.log.scoped(.app); -const WindowList = std.ArrayListUnmanaged(*Window); +const SurfaceList = std.ArrayListUnmanaged(*apprt.Surface); +const SurfacePool = std.heap.MemoryPool(apprt.Surface); /// The type used for sending messages to the app thread. pub const Mailbox = BlockingQueue(Message, 64); @@ -30,8 +31,14 @@ pub const Mailbox = BlockingQueue(Message, 64); /// General purpose allocator alloc: Allocator, -/// The list of windows that are currently open -windows: WindowList, +/// The list of surfaces that are currently active. +surfaces: SurfaceList, + +/// The memory pool to request surfaces. We use a memory pool because surfaces +/// typically require stable pointers due to runtime GUI callbacks. Centralizing +/// all the allocations in this pool makes it so that all our pools remain +/// close in memory. +surface_pool: SurfacePool, // The configuration for the app. config: *const Config, @@ -64,20 +71,23 @@ pub fn create( errdefer alloc.destroy(app); app.* = .{ .alloc = alloc, - .windows = .{}, + .surfaces = .{}, + .surface_pool = try SurfacePool.initPreheated(alloc, 2), .config = config, .mailbox = mailbox, .quit = false, }; - errdefer app.windows.deinit(alloc); + errdefer app.surfaces.deinit(alloc); + errdefer app.surface_pool.deinit(); return app; } pub fn destroy(self: *App) void { - // Clean up all our windows - for (self.windows.items) |window| window.destroy(); - self.windows.deinit(self.alloc); + // Clean up all our surfaces + for (self.surfaces.items) |surface| surface.deinit(); + self.surfaces.deinit(self.alloc); + self.surface_pool.deinit(); self.mailbox.destroy(self.alloc); self.alloc.destroy(self); @@ -94,13 +104,14 @@ pub fn wakeup(self: App) void { /// /// This returns whether the app should quit or not. pub fn tick(self: *App, rt_app: *apprt.runtime.App) !bool { - // If any windows are closing, destroy them + // If any surfaces are closing, destroy them var i: usize = 0; - while (i < self.windows.items.len) { - const window = self.windows.items[i]; - if (window.shouldClose()) { - window.destroy(); - _ = self.windows.swapRemove(i); + while (i < self.surfaces.items.len) { + const surface = self.surfaces.items[i]; + if (surface.shouldClose()) { + surface.deinit(); + _ = self.surfaces.swapRemove(i); + self.surface_pool.destroy(surface); continue; } @@ -110,52 +121,56 @@ pub fn tick(self: *App, rt_app: *apprt.runtime.App) !bool { // Drain our mailbox only if we're not quitting. if (!self.quit) try self.drainMailbox(rt_app); - // We quit if our quit flag is on or if we have closed all windows. - return self.quit or self.windows.items.len == 0; + // We quit if our quit flag is on or if we have closed all surfaces. + return self.quit or self.surfaces.items.len == 0; } -/// Create a new window. This can be called only on the main thread. This -/// can be called prior to ever running the app loop. -pub fn newWindow(self: *App, msg: Message.NewWindow) !*Window { - var window = try Window.create(self.alloc, self, self.config, msg.runtime); - errdefer window.destroy(); +/// Add an initialized surface. This is really only for the runtime +/// implementations to call and should NOT be called by general app users. +/// The surface must be from the pool. +pub fn addSurface(self: *App, rt_surface: *apprt.Surface) !void { + try self.surfaces.append(self.alloc, rt_surface); +} - try self.windows.append(self.alloc, window); - errdefer _ = self.windows.pop(); - - // Set initial font size if given - if (msg.font_size) |size| window.setFontSize(size); - - return window; +/// Delete the surface from the known surface list. This will NOT call the +/// destructor or free the memory. +pub fn deleteSurface(self: *App, rt_surface: *apprt.Surface) void { + var i: usize = 0; + while (i < self.surfaces.items.len) { + if (self.surfaces.items[i] == rt_surface) { + _ = self.surfaces.swapRemove(i); + } + } } /// Close a window and free all resources associated with it. This can /// only be called from the main thread. -pub fn closeWindow(self: *App, window: *Window) void { - var i: usize = 0; - while (i < self.windows.items.len) { - const current = self.windows.items[i]; - if (window == current) { - window.destroy(); - _ = self.windows.swapRemove(i); - return; - } - - i += 1; - } -} +// pub fn closeWindow(self: *App, window: *Window) void { +// var i: usize = 0; +// while (i < self.surfaces.items.len) { +// const current = self.surfaces.items[i]; +// if (window == current) { +// window.destroy(); +// _ = self.surfaces.swapRemove(i); +// return; +// } +// +// i += 1; +// } +// } /// Drain the mailbox. fn drainMailbox(self: *App, rt_app: *apprt.runtime.App) !void { - _ = rt_app; - while (self.mailbox.pop()) |message| { log.debug("mailbox message={s}", .{@tagName(message)}); switch (message) { - .new_window => |msg| _ = try self.newWindow(msg), + .new_window => |msg| { + _ = msg; // TODO + try rt_app.newWindow(); + }, .new_tab => |msg| try self.newTab(msg), .quit => try self.setQuit(), - .window_message => |msg| try self.windowMessage(msg.window, msg.message), + .surface_message => |msg| try self.surfaceMessage(msg.surface, msg.message), } } } @@ -180,7 +195,7 @@ fn newTab(self: *App, msg: Message.NewWindow) !void { }; // If the parent was closed prior to us handling the message, we do nothing. - if (!self.hasWindow(parent)) { + if (!self.hasSurface(parent)) { log.warn("new_tab parent is gone, not launching a new tab", .{}); return; } @@ -197,28 +212,28 @@ fn setQuit(self: *App) !void { if (self.quit) return; self.quit = true; - // Mark that all our windows should close - for (self.windows.items) |window| { - window.window.setShouldClose(); + // Mark that all our surfaces should close + for (self.surfaces.items) |surface| { + surface.setShouldClose(); } } /// Handle a window message -fn windowMessage(self: *App, win: *Window, msg: Window.Message) !void { +fn surfaceMessage(self: *App, surface: *Surface, msg: apprt.surface.Message) !void { // We want to ensure our window is still active. Window messages // are quite rare and we normally don't have many windows so we do // a simple linear search here. - if (self.hasWindow(win)) { - try win.handleMessage(msg); + if (self.hasSurface(surface)) { + try surface.handleMessage(msg); } // Window was not found, it probably quit before we handled the message. // Not a problem. } -fn hasWindow(self: *App, win: *Window) bool { - for (self.windows.items) |window| { - if (window == win) return true; +fn hasSurface(self: *App, surface: *Surface) bool { + for (self.surfaces.items) |v| { + if (&v.core_surface == surface) return true; } return false; @@ -237,18 +252,18 @@ pub const Message = union(enum) { /// Quit quit: void, - /// A message for a specific window - window_message: struct { - window: *Window, - message: Window.Message, + /// A message for a specific surface. + surface_message: struct { + surface: *Surface, + message: apprt.surface.Message, }, const NewWindow = struct { /// Runtime-specific window options. - runtime: apprt.runtime.Window.Options = .{}, + runtime: apprt.runtime.Surface.Options = .{}, - /// The parent window, only used for new tabs. - parent: ?*Window = null, + /// The parent surface, only used for new tabs. + parent: ?*Surface = null, /// The font size to create the window with or null to default to /// the configuration amount. @@ -332,7 +347,7 @@ pub const CAPI = struct { export fn ghostty_surface_new( app: *App, opts: *const apprt.runtime.Window.Options, - ) ?*Window { + ) ?*Surface { return surface_new_(app, opts) catch |err| { log.err("error initializing surface err={}", .{err}); return null; @@ -342,46 +357,46 @@ pub const CAPI = struct { fn surface_new_( app: *App, opts: *const apprt.runtime.Window.Options, - ) !*Window { + ) !*Surface { const w = try app.newWindow(.{ .runtime = opts.*, }); return w; } - export fn ghostty_surface_free(ptr: ?*Window) void { + export fn ghostty_surface_free(ptr: ?*Surface) void { if (ptr) |v| v.app.closeWindow(v); } /// Returns the app associated with a surface. - export fn ghostty_surface_app(win: *Window) *App { + export fn ghostty_surface_app(win: *Surface) *App { return win.app; } /// Tell the surface that it needs to schedule a render - export fn ghostty_surface_refresh(win: *Window) void { + export fn ghostty_surface_refresh(win: *Surface) void { win.window.refresh(); } /// Update the size of a surface. This will trigger resize notifications /// to the pty and the renderer. - export fn ghostty_surface_set_size(win: *Window, w: u32, h: u32) void { + export fn ghostty_surface_set_size(win: *Surface, w: u32, h: u32) void { win.window.updateSize(w, h); } /// Update the content scale of the surface. - export fn ghostty_surface_set_content_scale(win: *Window, x: f64, y: f64) void { + export fn ghostty_surface_set_content_scale(win: *Surface, x: f64, y: f64) void { win.window.updateContentScale(x, y); } /// Update the focused state of a surface. - export fn ghostty_surface_set_focus(win: *Window, focused: bool) void { + export fn ghostty_surface_set_focus(win: *Surface, focused: bool) void { win.window.focusCallback(focused); } /// Tell the surface that it needs to schedule a render export fn ghostty_surface_key( - win: *Window, + win: *Surface, action: input.Action, key: input.Key, mods: c_int, @@ -394,13 +409,13 @@ pub const CAPI = struct { } /// Tell the surface that it needs to schedule a render - export fn ghostty_surface_char(win: *Window, codepoint: u32) void { + export fn ghostty_surface_char(win: *Surface, codepoint: u32) void { win.window.charCallback(codepoint); } /// Tell the surface that it needs to schedule a render export fn ghostty_surface_mouse_button( - win: *Window, + win: *Surface, action: input.MouseButtonState, button: input.MouseButton, mods: c_int, @@ -413,15 +428,15 @@ pub const CAPI = struct { } /// Update the mouse position within the view. - export fn ghostty_surface_mouse_pos(win: *Window, x: f64, y: f64) void { + export fn ghostty_surface_mouse_pos(win: *Surface, x: f64, y: f64) void { win.window.cursorPosCallback(x, y); } - export fn ghostty_surface_mouse_scroll(win: *Window, x: f64, y: f64) void { + export fn ghostty_surface_mouse_scroll(win: *Surface, x: f64, y: f64) void { win.window.scrollCallback(x, y); } - export fn ghostty_surface_ime_point(win: *Window, x: *f64, y: *f64) void { + export fn ghostty_surface_ime_point(win: *Surface, x: *f64, y: *f64) void { const pos = win.imePoint(); x.* = pos.x; y.* = pos.y; diff --git a/src/DevMode.zig b/src/DevMode.zig index 2b037ce2a..931c04dea 100644 --- a/src/DevMode.zig +++ b/src/DevMode.zig @@ -10,7 +10,7 @@ const Allocator = std.mem.Allocator; const assert = std.debug.assert; const font = @import("font/main.zig"); -const Window = @import("Window.zig"); +const Surface = @import("Surface.zig"); const renderer = @import("renderer.zig"); const Config = @import("config.zig").Config; @@ -30,7 +30,7 @@ visible: bool = false, config: ?*const Config = null, /// The window we're tracking. -window: ?*Window = null, +window: ?*Surface = null, /// Update the state associated with the dev mode. This should generally /// only be called paired with a render since it otherwise wastes CPU diff --git a/src/Surface.zig b/src/Surface.zig new file mode 100644 index 000000000..04da5d0a8 --- /dev/null +++ b/src/Surface.zig @@ -0,0 +1,1635 @@ +//! Surface represents a single terminal "surface". A terminal surface is +//! a minimal "widget" where the terminal is drawn and responds to events +//! such as keyboard and mouse. Each surface also creates and owns its pty +//! session. +//! +//! The word "surface" is used because it is left to the higher level +//! application runtime to determine if the surface is a window, a tab, +//! a split, a preview pane in a larger window, etc. This struct doesn't care: +//! it just draws and responds to events. The events come from the application +//! runtime so the runtime can determine when and how those are delivered +//! (i.e. with focus, without focus, and so on). +const Surface = @This(); + +const apprt = @import("apprt.zig"); +pub const Mailbox = apprt.surface.Mailbox; +pub const Message = apprt.surface.Message; + +const std = @import("std"); +const builtin = @import("builtin"); +const assert = std.debug.assert; +const Allocator = std.mem.Allocator; +const renderer = @import("renderer.zig"); +const termio = @import("termio.zig"); +const objc = @import("objc"); +const imgui = @import("imgui"); +const Pty = @import("Pty.zig"); +const font = @import("font/main.zig"); +const Command = @import("Command.zig"); +const trace = @import("tracy").trace; +const terminal = @import("terminal/main.zig"); +const Config = @import("config.zig").Config; +const input = @import("input.zig"); +const DevMode = @import("DevMode.zig"); +const App = @import("App.zig"); +const internal_os = @import("os/main.zig"); + +const log = std.log.scoped(.window); + +// The renderer implementation to use. +const Renderer = renderer.Renderer; + +/// Allocator +alloc: Allocator, + +/// The app that this window is a part of. +app: *App, + +/// The windowing system surface +rt_surface: *apprt.runtime.Surface, + +/// The font structures +font_lib: font.Library, +font_group: *font.GroupCache, +font_size: font.face.DesiredSize, + +/// Imgui context +imgui_ctx: if (DevMode.enabled) *imgui.Context else void, + +/// The renderer for this window. +renderer: Renderer, + +/// The render state +renderer_state: renderer.State, + +/// The renderer thread manager +renderer_thread: renderer.Thread, + +/// The actual thread +renderer_thr: std.Thread, + +/// Mouse state. +mouse: Mouse, +mouse_interval: u64, + +/// The terminal IO handler. +io: termio.Impl, +io_thread: termio.Thread, +io_thr: std.Thread, + +/// All the cached sizes since we need them at various times. +screen_size: renderer.ScreenSize, +grid_size: renderer.GridSize, +cell_size: renderer.CellSize, + +/// Explicit padding due to configuration +padding: renderer.Padding, + +/// The app configuration +config: *const Config, + +/// Set to true for a single GLFW key/char callback cycle to cause the +/// char callback to ignore. GLFW seems to always do key followed by char +/// callbacks so we abuse that here. This is to solve an issue where commands +/// like such as "control-v" will write a "v" even if they're intercepted. +ignore_char: bool = false, + +/// Mouse state for the window. +const Mouse = struct { + /// The last tracked mouse button state by button. + click_state: [input.MouseButton.max]input.MouseButtonState = .{.release} ** input.MouseButton.max, + + /// The last mods state when the last mouse button (whatever it was) was + /// pressed or release. + mods: input.Mods = .{}, + + /// The point at which the left mouse click happened. This is in screen + /// coordinates so that scrolling preserves the location. + left_click_point: terminal.point.ScreenPoint = .{}, + + /// The starting xpos/ypos of the left click. Note that if scrolling occurs, + /// these will point to different "cells", but the xpos/ypos will stay + /// stable during scrolling relative to the window. + left_click_xpos: f64 = 0, + left_click_ypos: f64 = 0, + + /// The count of clicks to count double and triple clicks and so on. + /// The left click time was the last time the left click was done. This + /// is always set on the first left click. + left_click_count: u8 = 0, + left_click_time: std.time.Instant = undefined, + + /// The last x/y sent for mouse reports. + event_point: terminal.point.Viewport = .{}, +}; + +/// Create a new surface. This must be called from the main thread. The +/// pointer to the memory for the surface must be provided and must be +/// stable due to interfacing with various callbacks. +pub fn init( + self: *Surface, + app: *App, + config: *const Config, + rt_surface: *apprt.runtime.Surface, +) !void { + const alloc = app.alloc; + + // Initialize our renderer with our initialized surface. + try Renderer.surfaceInit(rt_surface); + + // Determine our DPI configurations so we can properly configure + // font points to pixels and handle other high-DPI scaling factors. + const content_scale = try rt_surface.getContentScale(); + const x_dpi = content_scale.x * font.face.default_dpi; + const y_dpi = content_scale.y * font.face.default_dpi; + log.debug("xscale={} yscale={} xdpi={} ydpi={}", .{ + content_scale.x, + content_scale.y, + x_dpi, + y_dpi, + }); + + // The font size we desire along with the DPI determined for the surface + const font_size: font.face.DesiredSize = .{ + .points = config.@"font-size", + .xdpi = @floatToInt(u16, x_dpi), + .ydpi = @floatToInt(u16, y_dpi), + }; + + // Find all the fonts for this surface + // + // Future: we can share the font group amongst all surfaces to save + // some new surface init time and some memory. This will require making + // thread-safe changes to font structs. + var font_lib = try font.Library.init(); + errdefer font_lib.deinit(); + var font_group = try alloc.create(font.GroupCache); + errdefer alloc.destroy(font_group); + font_group.* = try font.GroupCache.init(alloc, group: { + var group = try font.Group.init(alloc, font_lib, font_size); + errdefer group.deinit(); + + // Search for fonts + if (font.Discover != void) { + var disco = font.Discover.init(); + group.discover = disco; + + if (config.@"font-family") |family| { + var disco_it = try disco.discover(.{ + .family = family, + .size = font_size.points, + }); + defer disco_it.deinit(); + if (try disco_it.next()) |face| { + log.info("font regular: {s}", .{try face.name()}); + try group.addFace(alloc, .regular, face); + } + } + if (config.@"font-family-bold") |family| { + var disco_it = try disco.discover(.{ + .family = family, + .size = font_size.points, + .bold = true, + }); + defer disco_it.deinit(); + if (try disco_it.next()) |face| { + log.info("font bold: {s}", .{try face.name()}); + try group.addFace(alloc, .bold, face); + } + } + if (config.@"font-family-italic") |family| { + var disco_it = try disco.discover(.{ + .family = family, + .size = font_size.points, + .italic = true, + }); + defer disco_it.deinit(); + if (try disco_it.next()) |face| { + log.info("font italic: {s}", .{try face.name()}); + try group.addFace(alloc, .italic, face); + } + } + if (config.@"font-family-bold-italic") |family| { + var disco_it = try disco.discover(.{ + .family = family, + .size = font_size.points, + .bold = true, + .italic = true, + }); + defer disco_it.deinit(); + if (try disco_it.next()) |face| { + log.info("font bold+italic: {s}", .{try face.name()}); + try group.addFace(alloc, .bold_italic, face); + } + } + } + + // Our built-in font will be used as a backup + try group.addFace( + alloc, + .regular, + font.DeferredFace.initLoaded(try font.Face.init(font_lib, face_ttf, font_size)), + ); + try group.addFace( + alloc, + .bold, + font.DeferredFace.initLoaded(try font.Face.init(font_lib, face_bold_ttf, font_size)), + ); + + // Emoji fallback. We don't include this on Mac since Mac is expected + // to always have the Apple Emoji available. + if (builtin.os.tag != .macos or font.Discover == void) { + try group.addFace( + alloc, + .regular, + font.DeferredFace.initLoaded(try font.Face.init(font_lib, face_emoji_ttf, font_size)), + ); + try group.addFace( + alloc, + .regular, + font.DeferredFace.initLoaded(try font.Face.init(font_lib, face_emoji_text_ttf, font_size)), + ); + } + + // If we're on Mac, then we try to use the Apple Emoji font for Emoji. + if (builtin.os.tag == .macos and font.Discover != void) { + var disco = font.Discover.init(); + defer disco.deinit(); + var disco_it = try disco.discover(.{ + .family = "Apple Color Emoji", + .size = font_size.points, + }); + defer disco_it.deinit(); + if (try disco_it.next()) |face| { + log.debug("font emoji: {s}", .{try face.name()}); + try group.addFace(alloc, .regular, face); + } + } + + break :group group; + }); + errdefer font_group.deinit(alloc); + + // Pre-calculate our initial cell size ourselves. + const cell_size = try renderer.CellSize.init(alloc, font_group); + + // Convert our padding from points to pixels + const padding_x = (@intToFloat(f32, config.@"window-padding-x") * x_dpi) / 72; + const padding_y = (@intToFloat(f32, config.@"window-padding-y") * y_dpi) / 72; + const padding: renderer.Padding = .{ + .top = padding_y, + .bottom = padding_y, + .right = padding_x, + .left = padding_x, + }; + + // Create our terminal grid with the initial window size + var renderer_impl = try Renderer.init(alloc, .{ + .config = config, + .font_group = font_group, + .padding = .{ + .explicit = padding, + .balance = config.@"window-padding-balance", + }, + .window_mailbox = .{ .window = self, .app = app.mailbox }, + }); + errdefer renderer_impl.deinit(); + + // Calculate our grid size based on known dimensions. + const window_size = try rt_surface.getSize(); + const screen_size: renderer.ScreenSize = .{ + .width = window_size.width, + .height = window_size.height, + }; + const grid_size = renderer.GridSize.init( + screen_size.subPadding(padding), + cell_size, + ); + + // The mutex used to protect our renderer state. + var mutex = try alloc.create(std.Thread.Mutex); + mutex.* = .{}; + errdefer alloc.destroy(mutex); + + // Create the renderer thread + var render_thread = try renderer.Thread.init( + alloc, + rt_surface, + &self.renderer, + &self.renderer_state, + ); + errdefer render_thread.deinit(); + + // Start our IO implementation + var io = try termio.Impl.init(alloc, .{ + .grid_size = grid_size, + .screen_size = screen_size, + .config = config, + .renderer_state = &self.renderer_state, + .renderer_wakeup = render_thread.wakeup, + .renderer_mailbox = render_thread.mailbox, + .window_mailbox = .{ .window = self, .app = app.mailbox }, + }); + errdefer io.deinit(); + + // Create the IO thread + var io_thread = try termio.Thread.init(alloc, &self.io); + errdefer io_thread.deinit(); + + // True if this window is hosting devmode. We only host devmode on + // the first window since imgui is not threadsafe. We need to do some + // work to make DevMode work with multiple threads. + const host_devmode = DevMode.enabled and DevMode.instance.window == null; + + self.* = .{ + .alloc = alloc, + .app = app, + .rt_surface = rt_surface, + .font_lib = font_lib, + .font_group = font_group, + .font_size = font_size, + .renderer = renderer_impl, + .renderer_thread = render_thread, + .renderer_state = .{ + .mutex = mutex, + .cursor = .{ + .style = .blinking_block, + .visible = true, + }, + .terminal = &self.io.terminal, + .devmode = if (!host_devmode) null else &DevMode.instance, + }, + .renderer_thr = undefined, + .mouse = .{}, + .mouse_interval = config.@"click-repeat-interval" * 1_000_000, // 500ms + .io = io, + .io_thread = io_thread, + .io_thr = undefined, + .screen_size = screen_size, + .grid_size = grid_size, + .cell_size = cell_size, + .padding = padding, + .config = config, + + .imgui_ctx = if (!DevMode.enabled) {} else try imgui.Context.create(), + }; + errdefer if (DevMode.enabled) self.imgui_ctx.destroy(); + + // Set a minimum size that is cols=10 h=4. This matches Mac's Terminal.app + // but is otherwise somewhat arbitrary. + try rt_surface.setSizeLimits(.{ + .width = @floatToInt(u32, cell_size.width * 10), + .height = @floatToInt(u32, cell_size.height * 4), + }, null); + + // Call our size callback which handles all our retina setup + // Note: this shouldn't be necessary and when we clean up the window + // init stuff we should get rid of this. But this is required because + // sizeCallback does retina-aware stuff we don't do here and don't want + // to duplicate. + try self.sizeCallback(window_size); + + // Load imgui. This must be done LAST because it has to be done after + // all our GLFW setup is complete. + if (DevMode.enabled and DevMode.instance.window == null) { + const dev_io = try imgui.IO.get(); + dev_io.cval().IniFilename = "ghostty_dev_mode.ini"; + + // Add our built-in fonts so it looks slightly better + const dev_atlas = @ptrCast(*imgui.FontAtlas, dev_io.cval().Fonts); + dev_atlas.addFontFromMemoryTTF( + face_ttf, + @intToFloat(f32, font_size.pixels()), + ); + + // Default dark style + const style = try imgui.Style.get(); + style.colorsDark(); + + // Add our window to the instance if it isn't set. + DevMode.instance.window = self; + + // Let our renderer setup + try renderer_impl.initDevMode(rt_surface); + } + + // Give the renderer one more opportunity to finalize any window + // setup on the main thread prior to spinning up the rendering thread. + try renderer_impl.finalizeSurfaceInit(rt_surface); + + // Start our renderer thread + self.renderer_thr = try std.Thread.spawn( + .{}, + renderer.Thread.threadMain, + .{&self.renderer_thread}, + ); + self.renderer_thr.setName("renderer") catch {}; + + // Start our IO thread + self.io_thr = try std.Thread.spawn( + .{}, + termio.Thread.threadMain, + .{&self.io_thread}, + ); + self.io_thr.setName("io") catch {}; +} + +pub fn destroy(self: *Surface) void { + // Stop rendering thread + { + self.renderer_thread.stop.notify() catch |err| + log.err("error notifying renderer thread to stop, may stall err={}", .{err}); + self.renderer_thr.join(); + + // We need to become the active rendering thread again + self.renderer.threadEnter(self.rt_surface) catch unreachable; + + // If we are devmode-owning, clean that up. + if (DevMode.enabled and DevMode.instance.window == self) { + // Let our renderer clean up + self.renderer.deinitDevMode(); + + // Clear the window + DevMode.instance.window = null; + + // Uninitialize imgui + self.imgui_ctx.destroy(); + } + } + + // Stop our IO thread + { + self.io_thread.stop.notify() catch |err| + log.err("error notifying io thread to stop, may stall err={}", .{err}); + self.io_thr.join(); + } + + // We need to deinit AFTER everything is stopped, since there are + // shared values between the two threads. + self.renderer_thread.deinit(); + self.renderer.deinit(); + self.io_thread.deinit(); + self.io.deinit(); + + self.rt_surface.deinit(); + + self.font_group.deinit(self.alloc); + self.font_lib.deinit(); + self.alloc.destroy(self.font_group); + + self.alloc.destroy(self.renderer_state.mutex); + + self.alloc.destroy(self); +} + +/// Called from the app thread to handle mailbox messages to our specific +/// window. +pub fn handleMessage(self: *Surface, msg: Message) !void { + switch (msg) { + .set_title => |*v| { + // The ptrCast just gets sliceTo to return the proper type. + // We know that our title should end in 0. + const slice = std.mem.sliceTo(@ptrCast([*:0]const u8, v), 0); + log.debug("changing title \"{s}\"", .{slice}); + try self.rt_surface.setTitle(slice); + }, + + .cell_size => |size| try self.setCellSize(size), + + .clipboard_read => |kind| try self.clipboardRead(kind), + + .clipboard_write => |req| switch (req) { + .small => |v| try self.clipboardWrite(v.data[0..v.len]), + .stable => |v| try self.clipboardWrite(v), + .alloc => |v| { + defer v.alloc.free(v.data); + try self.clipboardWrite(v.data); + }, + }, + } +} + +/// Returns the x/y coordinate of where the IME (Input Method Editor) +/// keyboard should be rendered. +pub fn imePoint(self: *const Surface) apprt.IMEPos { + self.renderer_state.mutex.lock(); + const cursor = self.renderer_state.terminal.screen.cursor; + self.renderer_state.mutex.unlock(); + + // TODO: need to handle when scrolling and the cursor is not + // in the visible portion of the screen. + + // Our sizes are all scaled so we need to send the unscaled values back. + const content_scale = self.rt_surface.getContentScale() catch .{ .x = 1, .y = 1 }; + + const x: f64 = x: { + // Simple x * cell width gives the top-left corner + var x: f64 = @floatCast(f64, @intToFloat(f32, cursor.x) * self.cell_size.width); + + // We want the midpoint + x += self.cell_size.width / 2; + + // And scale it + x /= content_scale.x; + + break :x x; + }; + + const y: f64 = y: { + // Simple x * cell width gives the top-left corner + var y: f64 = @floatCast(f64, @intToFloat(f32, cursor.y) * self.cell_size.height); + + // We want the bottom + y += self.cell_size.height; + + // And scale it + y /= content_scale.y; + + break :y y; + }; + + return .{ .x = x, .y = y }; +} + +fn clipboardRead(self: *const Surface, kind: u8) !void { + if (!self.config.@"clipboard-read") { + log.info("application attempted to read clipboard, but 'clipboard-read' setting is off", .{}); + return; + } + + const data = self.rt_surface.getClipboardString() catch |err| { + log.warn("error reading clipboard: {}", .{err}); + return; + }; + + // Even if the clipboard data is empty we reply, since presumably + // the client app is expecting a reply. We first allocate our buffer. + // This must hold the base64 encoded data PLUS the OSC code surrounding it. + const enc = std.base64.standard.Encoder; + const size = enc.calcSize(data.len); + var buf = try self.alloc.alloc(u8, size + 9); // const for OSC + defer self.alloc.free(buf); + + // Wrap our data with the OSC code + const prefix = try std.fmt.bufPrint(buf, "\x1b]52;{c};", .{kind}); + assert(prefix.len == 7); + buf[buf.len - 2] = '\x1b'; + buf[buf.len - 1] = '\\'; + + // Do the base64 encoding + const encoded = enc.encode(buf[prefix.len..], data); + assert(encoded.len == size); + + _ = self.io_thread.mailbox.push(try termio.Message.writeReq( + self.alloc, + buf, + ), .{ .forever = {} }); + self.io_thread.wakeup.notify() catch {}; +} + +fn clipboardWrite(self: *const Surface, data: []const u8) !void { + if (!self.config.@"clipboard-write") { + log.info("application attempted to write clipboard, but 'clipboard-write' setting is off", .{}); + return; + } + + const dec = std.base64.standard.Decoder; + + // Build buffer + const size = try dec.calcSizeForSlice(data); + var buf = try self.alloc.allocSentinel(u8, size, 0); + defer self.alloc.free(buf); + buf[buf.len] = 0; + + // Decode + try dec.decode(buf, data); + assert(buf[buf.len] == 0); + + self.rt_surface.setClipboardString(buf) catch |err| { + log.err("error setting clipboard string err={}", .{err}); + return; + }; +} + +/// Change the cell size for the terminal grid. This can happen as +/// a result of changing the font size at runtime. +fn setCellSize(self: *Surface, size: renderer.CellSize) !void { + // Update our new cell size for future calcs + self.cell_size = size; + + // Update our grid_size + self.grid_size = renderer.GridSize.init( + self.screen_size.subPadding(self.padding), + self.cell_size, + ); + + // Notify the terminal + _ = self.io_thread.mailbox.push(.{ + .resize = .{ + .grid_size = self.grid_size, + .screen_size = self.screen_size, + .padding = self.padding, + }, + }, .{ .forever = {} }); + self.io_thread.wakeup.notify() catch {}; +} + +/// Change the font size. +/// +/// This can only be called from the main thread. +pub fn setFontSize(self: *Surface, size: font.face.DesiredSize) void { + // Update our font size so future changes work + self.font_size = size; + + // Notify our render thread of the font size. This triggers everything else. + _ = self.renderer_thread.mailbox.push(.{ + .font_size = size, + }, .{ .forever = {} }); + + // Schedule render which also drains our mailbox + self.queueRender() catch unreachable; +} + +/// This queues a render operation with the renderer thread. The render +/// isn't guaranteed to happen immediately but it will happen as soon as +/// practical. +fn queueRender(self: *const Surface) !void { + try self.renderer_thread.wakeup.notify(); +} + +pub fn sizeCallback(self: *Surface, size: apprt.WindowSize) !void { + const tracy = trace(@src()); + defer tracy.end(); + + // TODO: if our screen size didn't change, then we should avoid the + // overhead of inter-thread communication + + // Save our screen size + self.screen_size = .{ + .width = size.width, + .height = size.height, + }; + + // Recalculate our grid size + self.grid_size = renderer.GridSize.init( + self.screen_size.subPadding(self.padding), + self.cell_size, + ); + if (self.grid_size.columns < 5 and (self.padding.left > 0 or self.padding.right > 0)) { + log.warn("WARNING: very small terminal grid detected with padding " ++ + "set. Is your padding reasonable?", .{}); + } + if (self.grid_size.rows < 2 and (self.padding.top > 0 or self.padding.bottom > 0)) { + log.warn("WARNING: very small terminal grid detected with padding " ++ + "set. Is your padding reasonable?", .{}); + } + + // Mail the renderer + _ = self.renderer_thread.mailbox.push(.{ + .screen_size = self.screen_size, + }, .{ .forever = {} }); + try self.queueRender(); + + // Mail the IO thread + _ = self.io_thread.mailbox.push(.{ + .resize = .{ + .grid_size = self.grid_size, + .screen_size = self.screen_size, + .padding = self.padding, + }, + }, .{ .forever = {} }); + try self.io_thread.wakeup.notify(); +} + +pub fn charCallback(self: *Surface, codepoint: u21) !void { + const tracy = trace(@src()); + defer tracy.end(); + + // Dev Mode + if (DevMode.enabled and DevMode.instance.visible) { + // If the event was handled by imgui, ignore it. + if (imgui.IO.get()) |io| { + if (io.cval().WantCaptureKeyboard) { + try self.queueRender(); + } + } else |_| {} + } + + // Ignore if requested. See field docs for more information. + if (self.ignore_char) { + self.ignore_char = false; + return; + } + + // Critical area + { + self.renderer_state.mutex.lock(); + defer self.renderer_state.mutex.unlock(); + + // Clear the selction if we have one. + if (self.io.terminal.selection != null) { + self.io.terminal.selection = null; + try self.queueRender(); + } + + // We want to scroll to the bottom + // TODO: detect if we're at the bottom to avoid the render call here. + try self.io.terminal.scrollViewport(.{ .bottom = {} }); + } + + // Ask our IO thread to write the data + var data: termio.Message.WriteReq.Small.Array = undefined; + const len = try std.unicode.utf8Encode(codepoint, &data); + _ = self.io_thread.mailbox.push(.{ + .write_small = .{ + .data = data, + .len = len, + }, + }, .{ .forever = {} }); + + // After sending all our messages we have to notify our IO thread + try self.io_thread.wakeup.notify(); +} + +pub fn keyCallback( + self: *Surface, + action: input.Action, + key: input.Key, + mods: input.Mods, +) !void { + const tracy = trace(@src()); + defer tracy.end(); + + // Dev Mode + if (DevMode.enabled and DevMode.instance.visible) { + // If the event was handled by imgui, ignore it. + if (imgui.IO.get()) |io| { + if (io.cval().WantCaptureKeyboard) { + try self.queueRender(); + } + } else |_| {} + } + + // Reset the ignore char setting. If we didn't handle the char + // by here, we aren't going to get it so we just reset this. + self.ignore_char = false; + + if (action == .press or action == .repeat) { + const trigger: input.Binding.Trigger = .{ + .mods = mods, + .key = key, + }; + + //log.warn("BINDING TRIGGER={}", .{trigger}); + if (self.config.keybind.set.get(trigger)) |binding_action| { + //log.warn("BINDING ACTION={}", .{binding_action}); + + switch (binding_action) { + .unbind => unreachable, + .ignore => {}, + + .csi => |data| { + _ = self.io_thread.mailbox.push(.{ + .write_stable = "\x1B[", + }, .{ .forever = {} }); + _ = self.io_thread.mailbox.push(.{ + .write_stable = data, + }, .{ .forever = {} }); + try self.io_thread.wakeup.notify(); + }, + + .cursor_key => |ck| { + // We send a different sequence depending on if we're + // in cursor keys mode. We're in "normal" mode if cursor + // keys mdoe is NOT set. + const normal = normal: { + self.renderer_state.mutex.lock(); + defer self.renderer_state.mutex.unlock(); + break :normal !self.io.terminal.modes.cursor_keys; + }; + + if (normal) { + _ = self.io_thread.mailbox.push(.{ + .write_stable = ck.normal, + }, .{ .forever = {} }); + } else { + _ = self.io_thread.mailbox.push(.{ + .write_stable = ck.application, + }, .{ .forever = {} }); + } + + try self.io_thread.wakeup.notify(); + }, + + .copy_to_clipboard => { + // We can read from the renderer state without holding + // the lock because only we will write to this field. + if (self.io.terminal.selection) |sel| { + var buf = self.io.terminal.screen.selectionString( + self.alloc, + sel, + self.config.@"clipboard-trim-trailing-spaces", + ) catch |err| { + log.err("error reading selection string err={}", .{err}); + return; + }; + defer self.alloc.free(buf); + + self.rt_surface.setClipboardString(buf) catch |err| { + log.err("error setting clipboard string err={}", .{err}); + return; + }; + } + }, + + .paste_from_clipboard => { + const data = self.rt_surface.getClipboardString() catch |err| { + log.warn("error reading clipboard: {}", .{err}); + return; + }; + + if (data.len > 0) { + const bracketed = bracketed: { + self.renderer_state.mutex.lock(); + defer self.renderer_state.mutex.unlock(); + break :bracketed self.io.terminal.modes.bracketed_paste; + }; + + if (bracketed) { + _ = self.io_thread.mailbox.push(.{ + .write_stable = "\x1B[200~", + }, .{ .forever = {} }); + } + + _ = self.io_thread.mailbox.push(try termio.Message.writeReq( + self.alloc, + data, + ), .{ .forever = {} }); + + if (bracketed) { + _ = self.io_thread.mailbox.push(.{ + .write_stable = "\x1B[201~", + }, .{ .forever = {} }); + } + + try self.io_thread.wakeup.notify(); + } + }, + + .increase_font_size => |delta| { + log.debug("increase font size={}", .{delta}); + + var size = self.font_size; + size.points +|= delta; + self.setFontSize(size); + }, + + .decrease_font_size => |delta| { + log.debug("decrease font size={}", .{delta}); + + var size = self.font_size; + size.points = @max(1, size.points -| delta); + self.setFontSize(size); + }, + + .reset_font_size => { + log.debug("reset font size", .{}); + + var size = self.font_size; + size.points = self.config.@"font-size"; + self.setFontSize(size); + }, + + .toggle_dev_mode => if (DevMode.enabled) { + DevMode.instance.visible = !DevMode.instance.visible; + try self.queueRender(); + } else log.warn("dev mode was not compiled into this binary", .{}), + + .new_window => { + _ = self.app.mailbox.push(.{ + .new_window = .{ + .font_size = if (self.config.@"window-inherit-font-size") + self.font_size + else + null, + }, + }, .{ .instant = {} }); + self.app.wakeup(); + }, + + .new_tab => { + _ = self.app.mailbox.push(.{ + .new_tab = .{ + .parent = self, + + .font_size = if (self.config.@"window-inherit-font-size") + self.font_size + else + null, + }, + }, .{ .instant = {} }); + self.app.wakeup(); + }, + + .close_window => self.rt_surface.setShouldClose(), + + .quit => { + _ = self.app.mailbox.push(.{ + .quit = {}, + }, .{ .instant = {} }); + self.app.wakeup(); + }, + } + + // Bindings always result in us ignoring the char if printable + self.ignore_char = true; + + // No matter what, if there is a binding then we are done. + return; + } + + // Handle non-printables + const char: u8 = char: { + const mods_int = @bitCast(u8, mods); + const ctrl_only = @bitCast(u8, input.Mods{ .ctrl = true }); + + // If we're only pressing control, check if this is a character + // we convert to a non-printable. + if (mods_int == ctrl_only) { + const val: u8 = switch (key) { + .a => 0x01, + .b => 0x02, + .c => 0x03, + .d => 0x04, + .e => 0x05, + .f => 0x06, + .g => 0x07, + .h => 0x08, + .i => 0x09, + .j => 0x0A, + .k => 0x0B, + .l => 0x0C, + .m => 0x0D, + .n => 0x0E, + .o => 0x0F, + .p => 0x10, + .q => 0x11, + .r => 0x12, + .s => 0x13, + .t => 0x14, + .u => 0x15, + .v => 0x16, + .w => 0x17, + .x => 0x18, + .y => 0x19, + .z => 0x1A, + else => 0, + }; + + if (val > 0) break :char val; + } + + // Otherwise, we don't care what modifiers we press we do this. + break :char @as(u8, switch (key) { + .backspace => 0x7F, + .enter => '\r', + .tab => '\t', + .escape => 0x1B, + else => 0, + }); + }; + if (char > 0) { + // Ask our IO thread to write the data + var data: termio.Message.WriteReq.Small.Array = undefined; + data[0] = @intCast(u8, char); + _ = self.io_thread.mailbox.push(.{ + .write_small = .{ + .data = data, + .len = 1, + }, + }, .{ .forever = {} }); + + // After sending all our messages we have to notify our IO thread + try self.io_thread.wakeup.notify(); + } + } +} + +pub fn focusCallback(self: *Surface, focused: bool) !void { + // Notify our render thread of the new state + _ = self.renderer_thread.mailbox.push(.{ + .focus = focused, + }, .{ .forever = {} }); + + // Schedule render which also drains our mailbox + try self.queueRender(); +} + +pub fn refreshCallback(self: *Surface) !void { + // The point of this callback is to schedule a render, so do that. + try self.queueRender(); +} + +pub fn scrollCallback(self: *Surface, xoff: f64, yoff: f64) !void { + const tracy = trace(@src()); + defer tracy.end(); + + // If our dev mode window is visible then we always schedule a render on + // cursor move because the cursor might touch our windows. + if (DevMode.enabled and DevMode.instance.visible) { + try self.queueRender(); + + // If the mouse event was handled by imgui, ignore it. + if (imgui.IO.get()) |io| { + if (io.cval().WantCaptureMouse) return; + } else |_| {} + } + + //log.info("SCROLL: {} {}", .{ xoff, yoff }); + _ = xoff; + + // Positive is up + const sign: isize = if (yoff > 0) -1 else 1; + const delta: isize = sign * @max(@divFloor(self.grid_size.rows, 15), 1); + log.info("scroll: delta={}", .{delta}); + + { + self.renderer_state.mutex.lock(); + defer self.renderer_state.mutex.unlock(); + + // Modify our viewport, this requires a lock since it affects rendering + try self.io.terminal.scrollViewport(.{ .delta = delta }); + + // If we're scrolling up or down, then send a mouse event. This requires + // a lock since we read terminal state. + if (yoff != 0) { + const pos = try self.rt_surface.getCursorPos(); + try self.mouseReport(if (yoff < 0) .five else .four, .press, self.mouse.mods, pos); + } + } + + try self.queueRender(); +} + +/// The type of action to report for a mouse event. +const MouseReportAction = enum { press, release, motion }; + +fn mouseReport( + self: *Surface, + button: ?input.MouseButton, + action: MouseReportAction, + mods: input.Mods, + pos: apprt.CursorPos, +) !void { + // TODO: posToViewport currently clamps to the window boundary, + // do we want to not report mouse events at all outside the window? + + // Depending on the event, we may do nothing at all. + switch (self.io.terminal.modes.mouse_event) { + .none => return, + + // X10 only reports clicks with mouse button 1, 2, 3. We verify + // the button later. + .x10 => if (action != .press or + button == null or + !(button.? == .left or + button.? == .right or + button.? == .middle)) return, + + // Doesn't report motion + .normal => if (action == .motion) return, + + // Button must be pressed + .button => if (button == null) return, + + // Everything + .any => {}, + } + + // This format reports X/Y + const viewport_point = self.posToViewport(pos.x, pos.y); + + // Record our new point + self.mouse.event_point = viewport_point; + + // Get the code we'll actually write + const button_code: u8 = code: { + var acc: u8 = 0; + + // Determine our initial button value + if (button == null) { + // Null button means motion without a button pressed + acc = 3; + } else if (action == .release and self.io.terminal.modes.mouse_format != .sgr) { + // Release is 3. It is NOT 3 in SGR mode because SGR can tell + // the application what button was released. + acc = 3; + } else { + acc = switch (button.?) { + .left => 0, + .right => 1, + .middle => 2, + .four => 64, + .five => 65, + else => return, // unsupported + }; + } + + // X10 doesn't have modifiers + if (self.io.terminal.modes.mouse_event != .x10) { + if (mods.shift) acc += 4; + if (mods.super) acc += 8; + if (mods.ctrl) acc += 16; + } + + // Motion adds another bit + if (action == .motion) acc += 32; + + break :code acc; + }; + + switch (self.io.terminal.modes.mouse_format) { + .x10 => { + if (viewport_point.x > 222 or viewport_point.y > 222) { + log.info("X10 mouse format can only encode X/Y up to 223", .{}); + return; + } + + // + 1 below is because our x/y is 0-indexed and proto wants 1 + var data: termio.Message.WriteReq.Small.Array = undefined; + assert(data.len >= 5); + data[0] = '\x1b'; + data[1] = '['; + data[2] = 'M'; + data[3] = 32 + button_code; + data[4] = 32 + @intCast(u8, viewport_point.x) + 1; + data[5] = 32 + @intCast(u8, viewport_point.y) + 1; + + // Ask our IO thread to write the data + _ = self.io_thread.mailbox.push(.{ + .write_small = .{ + .data = data, + .len = 5, + }, + }, .{ .forever = {} }); + }, + + .utf8 => { + // Maximum of 12 because at most we have 2 fully UTF-8 encoded chars + var data: termio.Message.WriteReq.Small.Array = undefined; + assert(data.len >= 12); + data[0] = '\x1b'; + data[1] = '['; + data[2] = 'M'; + + // The button code will always fit in a single u8 + data[3] = 32 + button_code; + + // UTF-8 encode the x/y + var i: usize = 4; + i += try std.unicode.utf8Encode(@intCast(u21, 32 + viewport_point.x + 1), data[i..]); + i += try std.unicode.utf8Encode(@intCast(u21, 32 + viewport_point.y + 1), data[i..]); + + // Ask our IO thread to write the data + _ = self.io_thread.mailbox.push(.{ + .write_small = .{ + .data = data, + .len = @intCast(u8, i), + }, + }, .{ .forever = {} }); + }, + + .sgr => { + // Final character to send in the CSI + const final: u8 = if (action == .release) 'm' else 'M'; + + // Response always is at least 4 chars, so this leaves the + // remainder for numbers which are very large... + var data: termio.Message.WriteReq.Small.Array = undefined; + const resp = try std.fmt.bufPrint(&data, "\x1B[<{d};{d};{d}{c}", .{ + button_code, + viewport_point.x + 1, + viewport_point.y + 1, + final, + }); + + // Ask our IO thread to write the data + _ = self.io_thread.mailbox.push(.{ + .write_small = .{ + .data = data, + .len = @intCast(u8, resp.len), + }, + }, .{ .forever = {} }); + }, + + .urxvt => { + // Response always is at least 4 chars, so this leaves the + // remainder for numbers which are very large... + var data: termio.Message.WriteReq.Small.Array = undefined; + const resp = try std.fmt.bufPrint(&data, "\x1B[{d};{d};{d}M", .{ + 32 + button_code, + viewport_point.x + 1, + viewport_point.y + 1, + }); + + // Ask our IO thread to write the data + _ = self.io_thread.mailbox.push(.{ + .write_small = .{ + .data = data, + .len = @intCast(u8, resp.len), + }, + }, .{ .forever = {} }); + }, + + .sgr_pixels => { + // Final character to send in the CSI + const final: u8 = if (action == .release) 'm' else 'M'; + + // Response always is at least 4 chars, so this leaves the + // remainder for numbers which are very large... + var data: termio.Message.WriteReq.Small.Array = undefined; + const resp = try std.fmt.bufPrint(&data, "\x1B[<{d};{d};{d}{c}", .{ + button_code, + pos.x, + pos.y, + final, + }); + + // Ask our IO thread to write the data + _ = self.io_thread.mailbox.push(.{ + .write_small = .{ + .data = data, + .len = @intCast(u8, resp.len), + }, + }, .{ .forever = {} }); + }, + } + + // After sending all our messages we have to notify our IO thread + try self.io_thread.wakeup.notify(); +} + +pub fn mouseButtonCallback( + self: *Surface, + action: input.MouseButtonState, + button: input.MouseButton, + mods: input.Mods, +) !void { + const tracy = trace(@src()); + defer tracy.end(); + + // If our dev mode window is visible then we always schedule a render on + // cursor move because the cursor might touch our windows. + if (DevMode.enabled and DevMode.instance.visible) { + try self.queueRender(); + + // If the mouse event was handled by imgui, ignore it. + if (imgui.IO.get()) |io| { + if (io.cval().WantCaptureMouse) return; + } else |_| {} + } + + // Always record our latest mouse state + self.mouse.click_state[@intCast(usize, @enumToInt(button))] = action; + self.mouse.mods = @bitCast(input.Mods, mods); + + self.renderer_state.mutex.lock(); + defer self.renderer_state.mutex.unlock(); + + // Report mouse events if enabled + if (self.io.terminal.modes.mouse_event != .none) { + const pos = try self.rt_surface.getCursorPos(); + + const report_action: MouseReportAction = switch (action) { + .press => .press, + .release => .release, + }; + + try self.mouseReport( + button, + report_action, + self.mouse.mods, + pos, + ); + } + + // For left button clicks we always record some information for + // selection/highlighting purposes. + if (button == .left and action == .press) { + const pos = try self.rt_surface.getCursorPos(); + + // If we move our cursor too much between clicks then we reset + // the multi-click state. + if (self.mouse.left_click_count > 0) { + const max_distance = self.cell_size.width; + const distance = @sqrt( + std.math.pow(f64, pos.x - self.mouse.left_click_xpos, 2) + + std.math.pow(f64, pos.y - self.mouse.left_click_ypos, 2), + ); + + if (distance > max_distance) self.mouse.left_click_count = 0; + } + + // Store it + const point = self.posToViewport(pos.x, pos.y); + self.mouse.left_click_point = point.toScreen(&self.io.terminal.screen); + self.mouse.left_click_xpos = pos.x; + self.mouse.left_click_ypos = pos.y; + + // Setup our click counter and timer + if (std.time.Instant.now()) |now| { + // If we have mouse clicks, then we check if the time elapsed + // is less than and our interval and if so, increase the count. + if (self.mouse.left_click_count > 0) { + const since = now.since(self.mouse.left_click_time); + if (since > self.mouse_interval) { + self.mouse.left_click_count = 0; + } + } + + self.mouse.left_click_time = now; + self.mouse.left_click_count += 1; + + // We only support up to triple-clicks. + if (self.mouse.left_click_count > 3) self.mouse.left_click_count = 1; + } else |err| { + self.mouse.left_click_count = 1; + log.err("error reading time, mouse multi-click won't work err={}", .{err}); + } + + switch (self.mouse.left_click_count) { + // First mouse click, clear selection + 1 => if (self.io.terminal.selection != null) { + self.io.terminal.selection = null; + try self.queueRender(); + }, + + // Double click, select the word under our mouse + 2 => { + const sel_ = self.io.terminal.screen.selectWord(self.mouse.left_click_point); + if (sel_) |sel| { + self.io.terminal.selection = sel; + try self.queueRender(); + } + }, + + // Triple click, select the line under our mouse + 3 => { + const sel_ = self.io.terminal.screen.selectLine(self.mouse.left_click_point); + if (sel_) |sel| { + self.io.terminal.selection = sel; + try self.queueRender(); + } + }, + + // We should be bounded by 1 to 3 + else => unreachable, + } + } +} + +pub fn cursorPosCallback( + self: *Surface, + pos: apprt.CursorPos, +) !void { + const tracy = trace(@src()); + defer tracy.end(); + + // If our dev mode window is visible then we always schedule a render on + // cursor move because the cursor might touch our windows. + if (DevMode.enabled and DevMode.instance.visible) { + try self.queueRender(); + + // If the mouse event was handled by imgui, ignore it. + if (imgui.IO.get()) |io| { + if (io.cval().WantCaptureMouse) return; + } else |_| {} + } + + // We are reading/writing state for the remainder + self.renderer_state.mutex.lock(); + defer self.renderer_state.mutex.unlock(); + + // Do a mouse report + if (self.io.terminal.modes.mouse_event != .none) { + // We use the first mouse button we find pressed in order to report + // since the spec (afaict) does not say... + const button: ?input.MouseButton = button: for (self.mouse.click_state) |state, i| { + if (state == .press) + break :button @intToEnum(input.MouseButton, i); + } else null; + + try self.mouseReport(button, .motion, self.mouse.mods, pos); + + // If we're doing mouse motion tracking, we do not support text + // selection. + return; + } + + // If the cursor isn't clicked currently, it doesn't matter + if (self.mouse.click_state[@enumToInt(input.MouseButton.left)] != .press) return; + + // All roads lead to requiring a re-render at this pont. + try self.queueRender(); + + // Convert to pixels from screen coords + const xpos = pos.x; + const ypos = pos.y; + + // Convert to points + const viewport_point = self.posToViewport(xpos, ypos); + const screen_point = viewport_point.toScreen(&self.io.terminal.screen); + + // Handle dragging depending on click count + switch (self.mouse.left_click_count) { + 1 => self.dragLeftClickSingle(screen_point, xpos), + 2 => self.dragLeftClickDouble(screen_point), + 3 => self.dragLeftClickTriple(screen_point), + else => unreachable, + } +} + +/// Double-click dragging moves the selection one "word" at a time. +fn dragLeftClickDouble( + self: *Surface, + screen_point: terminal.point.ScreenPoint, +) void { + // Get the word under our current point. If there isn't a word, do nothing. + const word = self.io.terminal.screen.selectWord(screen_point) orelse return; + + // Get our selection to grow it. If we don't have a selection, start it now. + // We may not have a selection if we started our dbl-click in an area + // that had no data, then we dragged our mouse into an area with data. + var sel = self.io.terminal.screen.selectWord(self.mouse.left_click_point) orelse { + self.io.terminal.selection = word; + return; + }; + + // Grow our selection + if (screen_point.before(self.mouse.left_click_point)) { + sel.start = word.start; + } else { + sel.end = word.end; + } + self.io.terminal.selection = sel; +} + +/// Triple-click dragging moves the selection one "line" at a time. +fn dragLeftClickTriple( + self: *Surface, + screen_point: terminal.point.ScreenPoint, +) void { + // Get the word under our current point. If there isn't a word, do nothing. + const word = self.io.terminal.screen.selectLine(screen_point) orelse return; + + // Get our selection to grow it. If we don't have a selection, start it now. + // We may not have a selection if we started our dbl-click in an area + // that had no data, then we dragged our mouse into an area with data. + var sel = self.io.terminal.screen.selectLine(self.mouse.left_click_point) orelse { + self.io.terminal.selection = word; + return; + }; + + // Grow our selection + if (screen_point.before(self.mouse.left_click_point)) { + sel.start = word.start; + } else { + sel.end = word.end; + } + self.io.terminal.selection = sel; +} + +fn dragLeftClickSingle( + self: *Surface, + screen_point: terminal.point.ScreenPoint, + xpos: f64, +) void { + // NOTE(mitchellh): This logic super sucks. There has to be an easier way + // to calculate this, but this is good for a v1. Selection isn't THAT + // common so its not like this performance heavy code is running that + // often. + // TODO: unit test this, this logic sucks + + // If we were selecting, and we switched directions, then we restart + // calculations because it forces us to reconsider if the first cell is + // selected. + if (self.io.terminal.selection) |sel| { + const reset: bool = if (sel.end.before(sel.start)) + sel.start.before(screen_point) + else + screen_point.before(sel.start); + + if (reset) self.io.terminal.selection = null; + } + + // Our logic for determing if the starting cell is selected: + // + // - The "xboundary" is 60% the width of a cell from the left. We choose + // 60% somewhat arbitrarily based on feeling. + // - If we started our click left of xboundary, backwards selections + // can NEVER select the current char. + // - If we started our click right of xboundary, backwards selections + // ALWAYS selected the current char, but we must move the cursor + // left of the xboundary. + // - Inverted logic for forwards selections. + // + + // the boundary point at which we consider selection or non-selection + const cell_xboundary = self.cell_size.width * 0.6; + + // first xpos of the clicked cell + const cell_xstart = @intToFloat(f32, self.mouse.left_click_point.x) * self.cell_size.width; + const cell_start_xpos = self.mouse.left_click_xpos - cell_xstart; + + // If this is the same cell, then we only start the selection if weve + // moved past the boundary point the opposite direction from where we + // started. + if (std.meta.eql(screen_point, self.mouse.left_click_point)) { + const cell_xpos = xpos - cell_xstart; + const selected: bool = if (cell_start_xpos < cell_xboundary) + cell_xpos >= cell_xboundary + else + cell_xpos < cell_xboundary; + + self.io.terminal.selection = if (selected) .{ + .start = screen_point, + .end = screen_point, + } else null; + + return; + } + + // If this is a different cell and we haven't started selection, + // we determine the starting cell first. + if (self.io.terminal.selection == null) { + // - If we're moving to a point before the start, then we select + // the starting cell if we started after the boundary, else + // we start selection of the prior cell. + // - Inverse logic for a point after the start. + const click_point = self.mouse.left_click_point; + const start: terminal.point.ScreenPoint = if (screen_point.before(click_point)) start: { + if (self.mouse.left_click_xpos > cell_xboundary) { + break :start click_point; + } else { + break :start if (click_point.x > 0) terminal.point.ScreenPoint{ + .y = click_point.y, + .x = click_point.x - 1, + } else terminal.point.ScreenPoint{ + .x = self.io.terminal.screen.cols - 1, + .y = click_point.y -| 1, + }; + } + } else start: { + if (self.mouse.left_click_xpos < cell_xboundary) { + break :start click_point; + } else { + break :start if (click_point.x < self.io.terminal.screen.cols - 1) terminal.point.ScreenPoint{ + .y = click_point.y, + .x = click_point.x + 1, + } else terminal.point.ScreenPoint{ + .y = click_point.y + 1, + .x = 0, + }; + } + }; + + self.io.terminal.selection = .{ .start = start, .end = screen_point }; + return; + } + + // TODO: detect if selection point is passed the point where we've + // actually written data before and disallow it. + + // We moved! Set the selection end point. The start point should be + // set earlier. + assert(self.io.terminal.selection != null); + self.io.terminal.selection.?.end = screen_point; +} + +fn posToViewport(self: Surface, xpos: f64, ypos: f64) terminal.point.Viewport { + // xpos and ypos can be negative if while dragging, the user moves the + // mouse off the window. Likewise, they can be larger than our window + // width if the user drags out of the window positively. + return .{ + .x = if (xpos < 0) 0 else x: { + // Our cell is the mouse divided by cell width + const cell_width = @floatCast(f64, self.cell_size.width); + const x = @floatToInt(usize, xpos / cell_width); + + // Can be off the screen if the user drags it out, so max + // it out on our available columns + break :x @min(x, self.grid_size.columns - 1); + }, + + .y = if (ypos < 0) 0 else y: { + const cell_height = @floatCast(f64, self.cell_size.height); + const y = @floatToInt(usize, ypos / cell_height); + break :y @min(y, self.grid_size.rows - 1); + }, + }; +} + +const face_ttf = @embedFile("font/res/FiraCode-Regular.ttf"); +const face_bold_ttf = @embedFile("font/res/FiraCode-Bold.ttf"); +const face_emoji_ttf = @embedFile("font/res/NotoColorEmoji.ttf"); +const face_emoji_text_ttf = @embedFile("font/res/NotoEmoji-Regular.ttf"); diff --git a/src/apprt.zig b/src/apprt.zig index b2f4c93b1..24b85b00b 100644 --- a/src/apprt.zig +++ b/src/apprt.zig @@ -17,7 +17,7 @@ pub const glfw = @import("apprt/glfw.zig"); pub const gtk = @import("apprt/gtk.zig"); pub const browser = @import("apprt/browser.zig"); pub const embedded = @import("apprt/embedded.zig"); -pub const Window = @import("apprt/Window.zig"); +pub const surface = @import("apprt/Surface.zig"); /// The implementation to use for the app runtime. This is comptime chosen /// so that every build has exactly one application runtime implementation. @@ -34,6 +34,7 @@ pub const runtime = switch (build_config.artifact) { }; pub const App = runtime.App; +pub const Surface = runtime.Surface; /// Runtime is the runtime to use for Ghostty. All runtimes do not provide /// equivalent feature sets. For example, GTK offers tabbing and more features diff --git a/src/apprt/Window.zig b/src/apprt/Surface.zig similarity index 86% rename from src/apprt/Window.zig rename to src/apprt/Surface.zig index 159b6e92a..fff956504 100644 --- a/src/apprt/Window.zig +++ b/src/apprt/Surface.zig @@ -1,5 +1,5 @@ const App = @import("../App.zig"); -const Window = @import("../Window.zig"); +const Surface = @import("../Surface.zig"); const renderer = @import("../renderer.zig"); const termio = @import("../termio.zig"); @@ -27,17 +27,17 @@ pub const Message = union(enum) { /// A window mailbox. pub const Mailbox = struct { - window: *Window, + window: *Surface, app: *App.Mailbox, /// Send a message to the window. pub fn push(self: Mailbox, msg: Message, timeout: App.Mailbox.Timeout) App.Mailbox.Size { - // Window message sending is actually implemented on the app + // Surface message sending is actually implemented on the app // thread, so we have to rewrap the message with our window // pointer and send it to the app thread. const result = self.app.push(.{ - .window_message = .{ - .window = self.window, + .surface_message = .{ + .surface = self.window, .message = msg, }, }, timeout); diff --git a/src/apprt/glfw.zig b/src/apprt/glfw.zig index 5473e13d4..2150711d5 100644 --- a/src/apprt/glfw.zig +++ b/src/apprt/glfw.zig @@ -18,7 +18,7 @@ const renderer = @import("../renderer.zig"); const Renderer = renderer.Renderer; const apprt = @import("../apprt.zig"); const CoreApp = @import("../App.zig"); -const CoreWindow = @import("../Window.zig"); +const CoreSurface = @import("../Surface.zig"); // Get native API access on certain platforms so we can do more customization. const glfwNative = glfw.Native(.{ @@ -75,6 +75,16 @@ pub const App = struct { glfw.postEmptyEvent(); } + /// Create a new window for the app. + pub fn newWindow(self: *App) !void { + // Grab a surface allocation because we're going to need it. + const surface = try self.app.surface_pool.create(); + errdefer self.app.surface_pool.destroy(surface); + + // Create the surface -- because windows are surfaces for glfw. + try surface.init(self); + } + fn glfwErrorCallback(code: glfw.ErrorCode, desc: [:0]const u8) void { std.log.warn("glfw error={} message={s}", .{ code, desc }); @@ -120,18 +130,32 @@ pub const App = struct { }; }; -pub const Window = struct { +/// Surface represents the drawable surface for glfw. In glfw, a surface +/// is always a window because that is the only abstraction that glfw exposes. +/// +/// This means that there is no way for the glfw runtime to support tabs, +/// splits, etc. without considerable effort. In fact, on Darwin, we do +/// support tabs because the minimal tabbing interface is a window abstraction, +/// but this is a bit of a hack. The native Swift runtime should be used instead +/// which uses real native tabbing. +/// +/// Other runtimes a surface usually represents the equivalent of a "view" +/// or "widget" level granularity. +pub const Surface = struct { /// The glfw window handle window: glfw.Window, /// The glfw mouse cursor handle. cursor: glfw.Cursor, + /// A core surface + core_surface: CoreSurface, + pub const Options = struct {}; - pub fn init(app: *const CoreApp, core_win: *CoreWindow, opts: Options) !Window { - _ = opts; - + /// Initialize the surface into the given self pointer. This gives a + /// stable pointer to the destination that can be used for callbacks. + pub fn init(self: *Surface, app: *App) !void { // Create our window const win = glfw.Window.create( 640, @@ -143,9 +167,9 @@ pub const Window = struct { ) orelse return glfw.mustGetErrorCode(); errdefer win.destroy(); + // Get our physical DPI - debug only because we don't have a use for + // this but the logging of it may be useful if (builtin.mode == .Debug) { - // Get our physical DPI - debug only because we don't have a use for - // this but the logging of it may be useful const monitor = win.getMonitor() orelse monitor: { log.warn("window had null monitor, getting primary monitor", .{}); break :monitor glfw.Monitor.getPrimary().?; @@ -160,8 +184,8 @@ pub const Window = struct { }); } - // On Mac, enable tabbing - if (comptime builtin.target.isDarwin()) { + // On Mac, enable window tabbing + if (App.Darwin.enabled) { const NSWindowTabbingMode = enum(usize) { automatic = 0, preferred = 1, disallowed = 2 }; const nswindow = objc.Object.fromId(glfwNative.getCocoaWindow(win).?); @@ -184,7 +208,7 @@ pub const Window = struct { } // Set our callbacks - win.setUserPointer(core_win); + win.setUserPointer(&self.core_surface); win.setSizeCallback(sizeCallback); win.setCharCallback(charCallback); win.setKeyCallback(keyCallback); @@ -195,13 +219,23 @@ pub const Window = struct { win.setMouseButtonCallback(mouseButtonCallback); // Build our result - return Window{ + self.* = .{ .window = win, .cursor = cursor, + .core_surface = undefined, }; + errdefer self.* = undefined; + + // Add ourselves to the list of surfaces on the app. + try app.app.addSurface(self); + errdefer app.app.deleteSurface(self); + + // Initialize our surface now that we have the stable pointer. + try self.core_surface.init(app.app, app.app.config, self); + errdefer self.core_surface.destroy(); } - pub fn deinit(self: *Window) void { + pub fn deinit(self: *Surface) void { var tabgroup_opt: if (builtin.target.isDarwin()) ?objc.Object else void = undefined; if (comptime builtin.target.isDarwin()) { const nswindow = objc.Object.fromId(glfwNative.getCocoaWindow(self.window).?); @@ -240,7 +274,7 @@ pub const Window = struct { // If we have a tabgroup set, we want to manually focus the next window. // We should NOT have to do this usually, see the comments above. - if (comptime builtin.target.isDarwin()) { + if (App.Darwin.enabled) { if (tabgroup_opt) |tabgroup| { const selected = tabgroup.getProperty(objc.Object, "selectedWindow"); selected.msgSend(void, objc.sel("makeKeyWindow"), .{}); @@ -252,7 +286,7 @@ pub const Window = struct { /// Note: this interface is not good, we should redo it if we plan /// to use this more. i.e. you can't set max width but no max height, /// or no mins. - pub fn setSizeLimits(self: *Window, min: apprt.WindowSize, max_: ?apprt.WindowSize) !void { + pub fn setSizeLimits(self: *Surface, min: apprt.WindowSize, max_: ?apprt.WindowSize) !void { self.window.setSizeLimits(.{ .width = min.width, .height = min.height, @@ -266,7 +300,7 @@ pub const Window = struct { } /// Returns the content scale for the created window. - pub fn getContentScale(self: *const Window) !apprt.ContentScale { + pub fn getContentScale(self: *const Surface) !apprt.ContentScale { const scale = self.window.getContentScale(); return apprt.ContentScale{ .x = scale.x_scale, .y = scale.y_scale }; } @@ -274,14 +308,14 @@ pub const Window = struct { /// Returns the size of the window in pixels. The pixel size may /// not match screen coordinate size but we should be able to convert /// back and forth using getContentScale. - pub fn getSize(self: *const Window) !apprt.WindowSize { + pub fn getSize(self: *const Surface) !apprt.WindowSize { const size = self.window.getFramebufferSize(); return apprt.WindowSize{ .width = size.width, .height = size.height }; } /// Returns the cursor position in scaled pixels relative to the /// upper-left of the window. - pub fn getCursorPos(self: *const Window) !apprt.CursorPos { + pub fn getCursorPos(self: *const Surface) !apprt.CursorPos { const unscaled_pos = self.window.getCursorPos(); const pos = try self.cursorPosToPixels(unscaled_pos); return apprt.CursorPos{ @@ -292,37 +326,37 @@ pub const Window = struct { /// Set the flag that notes this window should be closed for the next /// iteration of the event loop. - pub fn setShouldClose(self: *Window) void { + pub fn setShouldClose(self: *Surface) void { self.window.setShouldClose(true); } /// Returns true if the window is flagged to close. - pub fn shouldClose(self: *const Window) bool { + pub fn shouldClose(self: *const Surface) bool { return self.window.shouldClose(); } /// Set the title of the window. - pub fn setTitle(self: *Window, slice: [:0]const u8) !void { + pub fn setTitle(self: *Surface, slice: [:0]const u8) !void { self.window.setTitle(slice.ptr); } /// Read the clipboard. The windowing system is responsible for allocating /// a buffer as necessary. This should be a stable pointer until the next /// time getClipboardString is called. - pub fn getClipboardString(self: *const Window) ![:0]const u8 { + pub fn getClipboardString(self: *const Surface) ![:0]const u8 { _ = self; return glfw.getClipboardString() orelse return glfw.mustGetErrorCode(); } /// Set the clipboard. - pub fn setClipboardString(self: *const Window, val: [:0]const u8) !void { + pub fn setClipboardString(self: *const Surface, val: [:0]const u8) !void { _ = self; glfw.setClipboardString(val); } /// The cursor position from glfw directly is in screen coordinates but /// all our interface works in pixels. - fn cursorPosToPixels(self: *const Window, pos: glfw.Window.CursorPos) !glfw.Window.CursorPos { + fn cursorPosToPixels(self: *const Surface, pos: glfw.Window.CursorPos) !glfw.Window.CursorPos { // The cursor position is in screen coordinates but we // want it in pixels. we need to get both the size of the // window in both to get the ratio to make the conversion. @@ -349,8 +383,8 @@ pub const Window = struct { // Get the size. We are given a width/height but this is in screen // coordinates and we want raw pixels. The core window uses the content // scale to scale appropriately. - const core_win = window.getUserPointer(CoreWindow) orelse return; - const size = core_win.window.getSize() catch |err| { + const core_win = window.getUserPointer(CoreSurface) orelse return; + const size = core_win.rt_surface.getSize() catch |err| { log.err("error querying window size for size callback err={}", .{err}); return; }; @@ -366,7 +400,7 @@ pub const Window = struct { const tracy = trace(@src()); defer tracy.end(); - const core_win = window.getUserPointer(CoreWindow) orelse return; + const core_win = window.getUserPointer(CoreSurface) orelse return; core_win.charCallback(codepoint) catch |err| { log.err("error in char callback err={}", .{err}); return; @@ -518,7 +552,7 @@ pub const Window = struct { => .invalid, }; - const core_win = window.getUserPointer(CoreWindow) orelse return; + const core_win = window.getUserPointer(CoreSurface) orelse return; core_win.keyCallback(action, key, mods) catch |err| { log.err("error in key callback err={}", .{err}); return; @@ -529,7 +563,7 @@ pub const Window = struct { const tracy = trace(@src()); defer tracy.end(); - const core_win = window.getUserPointer(CoreWindow) orelse return; + const core_win = window.getUserPointer(CoreSurface) orelse return; core_win.focusCallback(focused) catch |err| { log.err("error in focus callback err={}", .{err}); return; @@ -540,7 +574,7 @@ pub const Window = struct { const tracy = trace(@src()); defer tracy.end(); - const core_win = window.getUserPointer(CoreWindow) orelse return; + const core_win = window.getUserPointer(CoreSurface) orelse return; core_win.refreshCallback() catch |err| { log.err("error in refresh callback err={}", .{err}); return; @@ -551,7 +585,7 @@ pub const Window = struct { const tracy = trace(@src()); defer tracy.end(); - const core_win = window.getUserPointer(CoreWindow) orelse return; + const core_win = window.getUserPointer(CoreSurface) orelse return; core_win.scrollCallback(xoff, yoff) catch |err| { log.err("error in scroll callback err={}", .{err}); return; @@ -566,10 +600,10 @@ pub const Window = struct { const tracy = trace(@src()); defer tracy.end(); - const core_win = window.getUserPointer(CoreWindow) orelse return; + const core_win = window.getUserPointer(CoreSurface) orelse return; // Convert our unscaled x/y to scaled. - const pos = core_win.window.cursorPosToPixels(.{ + const pos = core_win.rt_surface.cursorPosToPixels(.{ .xpos = unscaled_xpos, .ypos = unscaled_ypos, }) catch |err| { @@ -598,7 +632,7 @@ pub const Window = struct { const tracy = trace(@src()); defer tracy.end(); - const core_win = window.getUserPointer(CoreWindow) orelse return; + const core_win = window.getUserPointer(CoreSurface) orelse return; // Convert glfw button to input button const mods = @bitCast(input.Mods, glfw_mods); diff --git a/src/main.zig b/src/main.zig index 35c24ccb5..8d50d3045 100644 --- a/src/main.zig +++ b/src/main.zig @@ -100,6 +100,7 @@ pub fn main() !void { defer app_runtime.terminate(); // Create an initial window + try app_runtime.newWindow(); // Run the GUI event loop try app_runtime.run(); diff --git a/src/renderer/OpenGL.zig b/src/renderer/OpenGL.zig index f631ec81e..951835998 100644 --- a/src/renderer/OpenGL.zig +++ b/src/renderer/OpenGL.zig @@ -18,7 +18,7 @@ const trace = @import("tracy").trace; const math = @import("../math.zig"); const lru = @import("../lru.zig"); const DevMode = @import("../DevMode.zig"); -const Window = @import("../Window.zig"); +const Surface = @import("../Surface.zig"); const log = std.log.scoped(.grid); @@ -89,7 +89,7 @@ focused: bool, padding: renderer.Options.Padding, /// The mailbox for communicating with the window. -window_mailbox: Window.Mailbox, +window_mailbox: apprt.surface.Mailbox, /// The raw structure that maps directly to the buffer sent to the vertex shader. /// This must be "extern" so that the field order is not reordered by the @@ -362,12 +362,11 @@ pub fn glfwWindowHints() glfw.Window.Hints { }; } -/// This is called early right after window creation to setup our -/// window surface as necessary. -pub fn windowInit(win: apprt.runtime.Window) !void { +/// This is called early right after surface creation. +pub fn surfaceInit(surface: *apprt.Surface) !void { // Treat this like a thread entry const self: OpenGL = undefined; - try self.threadEnter(win); + try self.threadEnter(surface); // Blending for text. We use GL_ONE here because we should be using // premultiplied alpha for all our colors in our fragment shaders. @@ -388,19 +387,19 @@ pub fn windowInit(win: apprt.runtime.Window) !void { /// This is called just prior to spinning up the renderer thread for /// final main thread setup requirements. -pub fn finalizeWindowInit(self: *const OpenGL, win: apprt.runtime.Window) !void { +pub fn finalizeSurfaceInit(self: *const OpenGL, surface: *apprt.Surface) !void { _ = self; - _ = win; + _ = surface; } /// This is called if this renderer runs DevMode. -pub fn initDevMode(self: *const OpenGL, win: apprt.runtime.Window) !void { +pub fn initDevMode(self: *const OpenGL, surface: *apprt.Surface) !void { _ = self; if (DevMode.enabled) { // Initialize for our window assert(imgui.ImplGlfw.initForOpenGL( - @ptrCast(*imgui.ImplGlfw.GLFWWindow, win.window.handle), + @ptrCast(*imgui.ImplGlfw.GLFWWindow, surface.window.handle), true, )); assert(imgui.ImplOpenGL3.init("#version 330 core")); @@ -418,7 +417,7 @@ pub fn deinitDevMode(self: *const OpenGL) void { } /// Callback called by renderer.Thread when it begins. -pub fn threadEnter(self: *const OpenGL, win: apprt.runtime.Window) !void { +pub fn threadEnter(self: *const OpenGL, surface: *apprt.Surface) !void { _ = self; switch (apprt.runtime) { @@ -437,7 +436,7 @@ pub fn threadEnter(self: *const OpenGL, win: apprt.runtime.Window) !void { // ensures that the context switches over to our thread. Important: // the prior thread MUST have detached the context prior to calling // this entrypoint. - glfw.makeContextCurrent(win.window); + glfw.makeContextCurrent(surface.window); errdefer glfw.makeContextCurrent(null); glfw.swapInterval(1); @@ -548,7 +547,7 @@ fn resetFontMetrics( /// The primary render callback that is completely thread-safe. pub fn render( self: *OpenGL, - win: apprt.runtime.Window, + surface: *apprt.Surface, state: *renderer.State, ) !void { // Data we extract out of the critical area. @@ -669,7 +668,7 @@ pub fn render( // Swap our window buffers if (apprt.runtime == apprt.gtk) @panic("TODO"); - win.window.swapBuffers(); + surface.window.swapBuffers(); } /// rebuildCells rebuilds all the GPU cells from our CPU state. This is a diff --git a/src/renderer/Options.zig b/src/renderer/Options.zig index c456c287d..0687d4e99 100644 --- a/src/renderer/Options.zig +++ b/src/renderer/Options.zig @@ -1,8 +1,8 @@ //! The options that are used to configure a renderer. +const apprt = @import("../apprt.zig"); const font = @import("../font/main.zig"); const renderer = @import("../renderer.zig"); -const Window = @import("../Window.zig"); const Config = @import("../config.zig").Config; /// The app configuration. @@ -16,7 +16,7 @@ padding: Padding, /// The mailbox for sending the window messages. This is only valid /// once the thread has started and should not be used outside of the thread. -window_mailbox: Window.Mailbox, +window_mailbox: apprt.surface.Mailbox, pub const Padding = struct { // Explicit padding options, in pixels. The windowing thread is diff --git a/src/renderer/Thread.zig b/src/renderer/Thread.zig index 534561fa1..2ffd0a1df 100644 --- a/src/renderer/Thread.zig +++ b/src/renderer/Thread.zig @@ -47,8 +47,8 @@ cursor_h: xev.Timer, cursor_c: xev.Completion = .{}, cursor_c_cancel: xev.Completion = .{}, -/// The window we're rendering to. -window: apprt.runtime.Window, +/// The surface we're rendering to. +surface: *apprt.Surface, /// The underlying renderer implementation. renderer: *renderer.Renderer, @@ -65,7 +65,7 @@ mailbox: *Mailbox, /// is up to the caller to start the thread with the threadMain entrypoint. pub fn init( alloc: Allocator, - win: apprt.runtime.Window, + surface: *apprt.Surface, renderer_impl: *renderer.Renderer, state: *renderer.State, ) !Thread { @@ -100,7 +100,7 @@ pub fn init( .stop = stop_h, .render_h = render_h, .cursor_h = cursor_timer, - .window = win, + .surface = surface, .renderer = renderer_impl, .state = state, .mailbox = mailbox, @@ -135,7 +135,7 @@ fn threadMain_(self: *Thread) !void { // Run our thread start/end callbacks. This is important because some // renderers have to do per-thread setup. For example, OpenGL has to set // some thread-local state since that is how it works. - try self.renderer.threadEnter(self.window); + try self.renderer.threadEnter(self.surface); defer self.renderer.threadExit(); // Start the async handlers @@ -305,7 +305,7 @@ fn renderCallback( return .disarm; }; - t.renderer.render(t.window, t.state) catch |err| + t.renderer.render(t.surface, t.state) catch |err| log.warn("error rendering err={}", .{err}); return .disarm; } diff --git a/src/termio/Exec.zig b/src/termio/Exec.zig index 01e0d300a..a6a56e90b 100644 --- a/src/termio/Exec.zig +++ b/src/termio/Exec.zig @@ -8,7 +8,6 @@ const assert = std.debug.assert; const Allocator = std.mem.Allocator; const termio = @import("../termio.zig"); const Command = @import("../Command.zig"); -const Window = @import("../Window.zig"); const Pty = @import("../Pty.zig"); const SegmentedPool = @import("../segmented_pool.zig").SegmentedPool; const terminal = @import("../terminal/main.zig"); @@ -16,6 +15,7 @@ const xev = @import("xev"); const renderer = @import("../renderer.zig"); const tracy = @import("tracy"); const trace = tracy.trace; +const apprt = @import("../apprt.zig"); const fastmem = @import("../fastmem.zig"); const log = std.log.scoped(.io_exec); @@ -52,7 +52,7 @@ renderer_wakeup: xev.Async, renderer_mailbox: *renderer.Thread.Mailbox, /// The mailbox for communicating with the window. -window_mailbox: Window.Mailbox, +window_mailbox: apprt.surface.Mailbox, /// The cached grid size whenever a resize is called. grid_size: renderer.GridSize, @@ -638,7 +638,7 @@ const StreamHandler = struct { alloc: Allocator, grid_size: *renderer.GridSize, terminal: *terminal.Terminal, - window_mailbox: Window.Mailbox, + window_mailbox: apprt.surface.Mailbox, /// This is set to true when a message was written to the writer /// mailbox. This can be used by callers to determine if they need @@ -1003,7 +1003,7 @@ const StreamHandler = struct { // Write clipboard contents _ = self.window_mailbox.push(.{ - .clipboard_write = try Window.Message.WriteReq.init( + .clipboard_write = try apprt.surface.Message.WriteReq.init( self.alloc, data, ), diff --git a/src/termio/Options.zig b/src/termio/Options.zig index d571d0fac..df90a8369 100644 --- a/src/termio/Options.zig +++ b/src/termio/Options.zig @@ -1,9 +1,9 @@ //! The options that are used to configure a terminal IO implementation. const xev = @import("xev"); +const apprt = @import("../apprt.zig"); const renderer = @import("../renderer.zig"); const Config = @import("../config.zig").Config; -const Window = @import("../Window.zig"); /// The size of the terminal grid. grid_size: renderer.GridSize, @@ -28,4 +28,4 @@ renderer_wakeup: xev.Async, renderer_mailbox: *renderer.Thread.Mailbox, /// The mailbox for sending the window messages. -window_mailbox: Window.Mailbox, +window_mailbox: apprt.surface.Mailbox,