diff --git a/src/Window.zig b/src/Window.zig index ea9f53e92..b968de581 100644 --- a/src/Window.zig +++ b/src/Window.zig @@ -7,9 +7,9 @@ const Window = @This(); // TODO: eventually, I want to extract Window.zig into the "window" package // so we can also have alternate implementations (i.e. not glfw). -const message = @import("window/message.zig"); -pub const Mailbox = message.Mailbox; -pub const Message = message.Message; +const apprt = @import("apprt.zig"); +pub const Mailbox = apprt.Window.Mailbox; +pub const Message = apprt.Window.Message; const std = @import("std"); const builtin = @import("builtin"); @@ -30,7 +30,6 @@ const input = @import("input.zig"); const DevMode = @import("DevMode.zig"); const App = @import("App.zig"); const internal_os = @import("os/main.zig"); -const WindowingSystem = @import("window.zig").System; // Get native API access on certain platforms so we can do more customization. const glfwNative = glfw.Native(.{ @@ -49,7 +48,7 @@ alloc: Allocator, app: *App, /// The windowing system state -windowing_system: WindowingSystem, +windowing_system: apprt.runtime.Window, /// The font structures font_lib: font.Library, @@ -140,7 +139,7 @@ pub fn create(alloc: Allocator, app: *App, config: *const Config) !*Window { errdefer alloc.destroy(self); // Create the windowing system - var winsys = try WindowingSystem.init(app); + var winsys = try apprt.runtime.Window.init(app); errdefer winsys.deinit(); // Initialize our renderer with our initialized windowing system. diff --git a/src/apprt.zig b/src/apprt.zig new file mode 100644 index 000000000..35efc0d85 --- /dev/null +++ b/src/apprt.zig @@ -0,0 +1,27 @@ +//! "apprt" is the "application runtime" package. This abstracts the +//! application runtime and lifecycle management such as creating windows, +//! getting user input (mouse/keyboard), etc. +//! +//! This enables compile-time interfaces to be built to swap out the underlying +//! application runtime. For example: glfw, pure macOS Cocoa, GTK+, browser, etc. +//! +//! The goal is to have different implementations share as much of the core +//! logic as possible, and to only reach out to platform-specific implementation +//! code when absolutely necessary. +const builtin = @import("builtin"); + +pub usingnamespace @import("apprt/structs.zig"); +pub const glfw = @import("apprt/glfw.zig"); +pub const Window = @import("apprt/Window.zig"); + +/// The implementation to use for the app runtime. This is comptime chosen +/// so that every build has exactly one application runtime implementation. +/// Note: it is very rare to use Runtime directly; most usage will use +/// Window or something. +pub const runtime = switch (builtin.os.tag) { + else => glfw, +}; + +test { + @import("std").testing.refAllDecls(@This()); +} diff --git a/src/window/message.zig b/src/apprt/Window.zig similarity index 100% rename from src/window/message.zig rename to src/apprt/Window.zig diff --git a/src/apprt/glfw.zig b/src/apprt/glfw.zig new file mode 100644 index 000000000..0862a439d --- /dev/null +++ b/src/apprt/glfw.zig @@ -0,0 +1,149 @@ +//! Application runtime implementation that uses GLFW (https://www.glfw.org/). +//! +//! This works on macOS and Linux with OpenGL and Metal. +//! (The above sentence may be out of date). + +const std = @import("std"); +const builtin = @import("builtin"); +const assert = std.debug.assert; +const Allocator = std.mem.Allocator; +const glfw = @import("glfw"); +const objc = @import("objc"); +const App = @import("../App.zig"); +const internal_os = @import("../os/main.zig"); +const renderer = @import("../renderer.zig"); +const Renderer = renderer.Renderer; +const apprt = @import("../apprt.zig"); + +// Get native API access on certain platforms so we can do more customization. +const glfwNative = glfw.Native(.{ + .cocoa = builtin.target.isDarwin(), +}); + +const log = std.log.scoped(.glfw); + +pub const Window = struct { + /// The glfw window handle + window: glfw.Window, + + /// The glfw mouse cursor handle. + cursor: glfw.Cursor, + + pub fn init(app: *const App) !Window { + // Create our window + const win = try glfw.Window.create( + 640, + 480, + "ghostty", + null, + null, + Renderer.glfwWindowHints(), + ); + errdefer win.destroy(); + + if (builtin.mode == .Debug) { + // Get our physical DPI - debug only because we don't have a use for + // this but the logging of it may be useful + const monitor = win.getMonitor() orelse monitor: { + log.warn("window had null monitor, getting primary monitor", .{}); + break :monitor glfw.Monitor.getPrimary().?; + }; + const physical_size = monitor.getPhysicalSize(); + const video_mode = try monitor.getVideoMode(); + const physical_x_dpi = @intToFloat(f32, video_mode.getWidth()) / (@intToFloat(f32, physical_size.width_mm) / 25.4); + const physical_y_dpi = @intToFloat(f32, video_mode.getHeight()) / (@intToFloat(f32, physical_size.height_mm) / 25.4); + log.debug("physical dpi x={} y={}", .{ + physical_x_dpi, + physical_y_dpi, + }); + } + + // On Mac, enable tabbing + if (comptime builtin.target.isDarwin()) { + const NSWindowTabbingMode = enum(usize) { automatic = 0, preferred = 1, disallowed = 2 }; + const nswindow = objc.Object.fromId(glfwNative.getCocoaWindow(win).?); + + // Tabbing mode enables tabbing at all + nswindow.setProperty("tabbingMode", NSWindowTabbingMode.automatic); + + // All windows within a tab bar must have a matching tabbing ID. + // The app sets this up for us. + nswindow.setProperty("tabbingIdentifier", app.darwin.tabbing_id); + } + + // Create the cursor + const cursor = try glfw.Cursor.createStandard(.ibeam); + errdefer cursor.destroy(); + if ((comptime !builtin.target.isDarwin()) or internal_os.macosVersionAtLeast(13, 0, 0)) { + // We only set our cursor if we're NOT on Mac, or if we are then the + // macOS version is >= 13 (Ventura). On prior versions, glfw crashes + // since we use a tab group. + try win.setCursor(cursor); + } + + // Build our result + return Window{ + .window = win, + .cursor = cursor, + }; + } + + pub fn deinit(self: *Window) 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).?); + const tabgroup = nswindow.getProperty(objc.Object, "tabGroup"); + + // On macOS versions prior to Ventura, we lose window focus on tab close + // for some reason. We manually fix this by keeping track of the tab + // group and just selecting the next window. + if (internal_os.macosVersionAtLeast(13, 0, 0)) + tabgroup_opt = null + else + tabgroup_opt = tabgroup; + + const windows = tabgroup.getProperty(objc.Object, "windows"); + switch (windows.getProperty(usize, "count")) { + // If we're going down to one window our tab bar is going to be + // destroyed so unset it so that the later logic doesn't try to + // use it. + 1 => tabgroup_opt = null, + + // If our tab bar is visible and we are going down to 1 window, + // hide the tab bar. The check is "2" because our current window + // is still present. + 2 => if (tabgroup.getProperty(bool, "tabBarVisible")) { + nswindow.msgSend(void, objc.sel("toggleTabBar:"), .{nswindow.value}); + }, + + else => {}, + } + } + + // We can now safely destroy our windows. We have to do this BEFORE + // setting up the new focused window below. + self.window.destroy(); + self.cursor.destroy(); + + // 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 (tabgroup_opt) |tabgroup| { + const selected = tabgroup.getProperty(objc.Object, "selectedWindow"); + selected.msgSend(void, objc.sel("makeKeyWindow"), .{}); + } + } + } + + /// Returns the content scale for the created window. + pub fn getContentScale(self: *const Window) !apprt.ContentScale { + const scale = try self.window.getContentScale(); + return apprt.ContentScale{ .x = scale.x_scale, .y = scale.y_scale }; + } + + /// Returns the size of the window in screen coordinates. + pub fn getSize(self: *const Window) !apprt.WindowSize { + const size = try self.window.getSize(); + return apprt.WindowSize{ .width = size.width, .height = size.height }; + } +}; diff --git a/src/window/structs.zig b/src/apprt/structs.zig similarity index 91% rename from src/window/structs.zig rename to src/apprt/structs.zig index bb5590267..a78121da9 100644 --- a/src/window/structs.zig +++ b/src/apprt/structs.zig @@ -7,7 +7,7 @@ pub const ContentScale = struct { }; /// The size of the window in screen coordinates. -pub const Size = struct { +pub const WindowSize = struct { width: u32, height: u32, }; diff --git a/src/renderer/OpenGL.zig b/src/renderer/OpenGL.zig index 8ac82b026..7e0f40842 100644 --- a/src/renderer/OpenGL.zig +++ b/src/renderer/OpenGL.zig @@ -7,6 +7,7 @@ const glfw = @import("glfw"); const assert = std.debug.assert; const testing = std.testing; const Allocator = std.mem.Allocator; +const apprt = @import("../apprt.zig"); const font = @import("../font/main.zig"); const imgui = @import("imgui"); const renderer = @import("../renderer.zig"); @@ -18,7 +19,6 @@ const math = @import("../math.zig"); const lru = @import("../lru.zig"); const DevMode = @import("../DevMode.zig"); const Window = @import("../Window.zig"); -const window = @import("../window.zig"); const log = std.log.scoped(.grid); @@ -364,10 +364,10 @@ pub fn glfwWindowHints() glfw.Window.Hints { /// This is called early right after window creation to setup our /// window surface as necessary. -pub fn windowInit(winsys: window.System) !void { +pub fn windowInit(win: apprt.runtime.Window) !void { // Treat this like a thread entry const self: OpenGL = undefined; - try self.threadEnter(winsys); + try self.threadEnter(win); // Blending for text try gl.enable(gl.c.GL_BLEND); @@ -385,19 +385,19 @@ pub fn windowInit(winsys: window.System) !void { /// This is called just prior to spinning up the renderer thread for /// final main thread setup requirements. -pub fn finalizeWindowInit(self: *const OpenGL, winsys: window.System) !void { +pub fn finalizeWindowInit(self: *const OpenGL, win: apprt.runtime.Window) !void { _ = self; - _ = winsys; + _ = win; } /// This is called if this renderer runs DevMode. -pub fn initDevMode(self: *const OpenGL, winsys: window.System) !void { +pub fn initDevMode(self: *const OpenGL, win: apprt.runtime.Window) !void { _ = self; if (DevMode.enabled) { // Initialize for our window assert(imgui.ImplGlfw.initForOpenGL( - @ptrCast(*imgui.ImplGlfw.GLFWWindow, winsys.window.handle), + @ptrCast(*imgui.ImplGlfw.GLFWWindow, win.window.handle), true, )); assert(imgui.ImplOpenGL3.init("#version 330 core")); @@ -415,7 +415,7 @@ pub fn deinitDevMode(self: *const OpenGL) void { } /// Callback called by renderer.Thread when it begins. -pub fn threadEnter(self: *const OpenGL, winsys: window.System) !void { +pub fn threadEnter(self: *const OpenGL, win: apprt.runtime.Window) !void { _ = self; // We need to make the OpenGL context current. OpenGL requires @@ -423,7 +423,7 @@ pub fn threadEnter(self: *const OpenGL, winsys: window.System) !void { // ensures that the context switches over to our thread. Important: // the prior thread MUST have detached the context prior to calling // this entrypoint. - try glfw.makeContextCurrent(winsys.window); + try glfw.makeContextCurrent(win.window); errdefer glfw.makeContextCurrent(null) catch |err| log.warn("failed to cleanup OpenGL context err={}", .{err}); try glfw.swapInterval(1); @@ -525,7 +525,7 @@ fn resetFontMetrics( /// The primary render callback that is completely thread-safe. pub fn render( self: *OpenGL, - winsys: window.System, + win: apprt.runtime.Window, state: *renderer.State, ) !void { // Data we extract out of the critical area. @@ -641,7 +641,7 @@ pub fn render( } // Swap our window buffers - try winsys.window.swapBuffers(); + try win.window.swapBuffers(); } /// rebuildCells rebuilds all the GPU cells from our CPU state. This is a diff --git a/src/renderer/Thread.zig b/src/renderer/Thread.zig index d4fe8930b..5028096ae 100644 --- a/src/renderer/Thread.zig +++ b/src/renderer/Thread.zig @@ -6,7 +6,7 @@ const std = @import("std"); const builtin = @import("builtin"); const libuv = @import("libuv"); const renderer = @import("../renderer.zig"); -const window = @import("../window.zig"); +const apprt = @import("../apprt.zig"); const BlockingQueue = @import("../blocking_queue.zig").BlockingQueue; const tracy = @import("tracy"); const trace = tracy.trace; @@ -38,7 +38,7 @@ render_h: libuv.Timer, cursor_h: libuv.Timer, /// The window we're rendering to. -window: window.System, +window: apprt.runtime.Window, /// The underlying renderer implementation. renderer: *renderer.Renderer, @@ -55,7 +55,7 @@ mailbox: *Mailbox, /// is up to the caller to start the thread with the threadMain entrypoint. pub fn init( alloc: Allocator, - win: window.System, + win: apprt.runtime.Window, renderer_impl: *renderer.Renderer, state: *renderer.State, ) !Thread { diff --git a/src/window.zig b/src/window.zig deleted file mode 100644 index 6132b90a9..000000000 --- a/src/window.zig +++ /dev/null @@ -1,18 +0,0 @@ -//! Window implementation and utilities. The window subsystem is responsible -//! for maintaining a "window" or "surface" abstraction around a terminal, -//! effectively being the primary interface to the terminal. - -const builtin = @import("builtin"); - -pub usingnamespace @import("window/structs.zig"); -pub const Glfw = @import("window/Glfw.zig"); - -/// The implementation to use for the windowing system. This is comptime chosen -/// so that every build has exactly one windowing implementation. -pub const System = switch (builtin.os.tag) { - else => Glfw, -}; - -test { - @import("std").testing.refAllDecls(@This()); -} diff --git a/src/window/Glfw.zig b/src/window/Glfw.zig deleted file mode 100644 index 70d1a52db..000000000 --- a/src/window/Glfw.zig +++ /dev/null @@ -1,138 +0,0 @@ -//! Window implementation that uses GLFW (https://www.glfw.org/). -pub const Glfw = @This(); - -const std = @import("std"); -const builtin = @import("builtin"); -const assert = std.debug.assert; -const Allocator = std.mem.Allocator; -const glfw = @import("glfw"); -const objc = @import("objc"); -const App = @import("../App.zig"); -const internal_os = @import("../os/main.zig"); -const renderer = @import("../renderer.zig"); -const Renderer = renderer.Renderer; -const window = @import("../window.zig"); - -// Get native API access on certain platforms so we can do more customization. -const glfwNative = glfw.Native(.{ - .cocoa = builtin.target.isDarwin(), -}); - -const log = std.log.scoped(.glfw_window); - -/// The glfw window handle -window: glfw.Window, - -/// The glfw mouse cursor handle. -cursor: glfw.Cursor, - -pub fn init(app: *const App) !Glfw { - // Create our window - const win = try glfw.Window.create(640, 480, "ghostty", null, null, Renderer.glfwWindowHints()); - errdefer win.destroy(); - - if (builtin.mode == .Debug) { - // Get our physical DPI - debug only because we don't have a use for - // this but the logging of it may be useful - const monitor = win.getMonitor() orelse monitor: { - log.warn("window had null monitor, getting primary monitor", .{}); - break :monitor glfw.Monitor.getPrimary().?; - }; - const physical_size = monitor.getPhysicalSize(); - const video_mode = try monitor.getVideoMode(); - const physical_x_dpi = @intToFloat(f32, video_mode.getWidth()) / (@intToFloat(f32, physical_size.width_mm) / 25.4); - const physical_y_dpi = @intToFloat(f32, video_mode.getHeight()) / (@intToFloat(f32, physical_size.height_mm) / 25.4); - log.debug("physical dpi x={} y={}", .{ - physical_x_dpi, - physical_y_dpi, - }); - } - - // On Mac, enable tabbing - if (comptime builtin.target.isDarwin()) { - const NSWindowTabbingMode = enum(usize) { automatic = 0, preferred = 1, disallowed = 2 }; - const nswindow = objc.Object.fromId(glfwNative.getCocoaWindow(win).?); - - // Tabbing mode enables tabbing at all - nswindow.setProperty("tabbingMode", NSWindowTabbingMode.automatic); - - // All windows within a tab bar must have a matching tabbing ID. - // The app sets this up for us. - nswindow.setProperty("tabbingIdentifier", app.darwin.tabbing_id); - } - - // Create the cursor - const cursor = try glfw.Cursor.createStandard(.ibeam); - errdefer cursor.destroy(); - if ((comptime !builtin.target.isDarwin()) or internal_os.macosVersionAtLeast(13, 0, 0)) { - // We only set our cursor if we're NOT on Mac, or if we are then the - // macOS version is >= 13 (Ventura). On prior versions, glfw crashes - // since we use a tab group. - try win.setCursor(cursor); - } - - // Build our result - return Glfw{ - .window = win, - .cursor = cursor, - }; -} - -pub fn deinit(self: *Glfw) 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).?); - const tabgroup = nswindow.getProperty(objc.Object, "tabGroup"); - - // On macOS versions prior to Ventura, we lose window focus on tab close - // for some reason. We manually fix this by keeping track of the tab - // group and just selecting the next window. - if (internal_os.macosVersionAtLeast(13, 0, 0)) - tabgroup_opt = null - else - tabgroup_opt = tabgroup; - - const windows = tabgroup.getProperty(objc.Object, "windows"); - switch (windows.getProperty(usize, "count")) { - // If we're going down to one window our tab bar is going to be - // destroyed so unset it so that the later logic doesn't try to - // use it. - 1 => tabgroup_opt = null, - - // If our tab bar is visible and we are going down to 1 window, - // hide the tab bar. The check is "2" because our current window - // is still present. - 2 => if (tabgroup.getProperty(bool, "tabBarVisible")) { - nswindow.msgSend(void, objc.sel("toggleTabBar:"), .{nswindow.value}); - }, - - else => {}, - } - } - - // We can now safely destroy our windows. We have to do this BEFORE - // setting up the new focused window below. - self.window.destroy(); - self.cursor.destroy(); - - // 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 (tabgroup_opt) |tabgroup| { - const selected = tabgroup.getProperty(objc.Object, "selectedWindow"); - selected.msgSend(void, objc.sel("makeKeyWindow"), .{}); - } - } -} - -/// Returns the content scale for the created window. -pub fn getContentScale(self: *const Glfw) !window.ContentScale { - const scale = try self.window.getContentScale(); - return window.ContentScale{ .x = scale.x_scale, .y = scale.y_scale }; -} - -/// Returns the size of the window in screen coordinates. -pub fn getSize(self: *const Glfw) !window.Size { - const size = try self.window.getSize(); - return window.Size{ .width = size.width, .height = size.height }; -} diff --git a/src/window/Web.zig b/src/window/Web.zig deleted file mode 100644 index 091ffc216..000000000 --- a/src/window/Web.zig +++ /dev/null @@ -1,2 +0,0 @@ -//! Window implementation for the web (browser) via WebAssembly. -pub const Window = @This(); diff --git a/src/window/Window.zig b/src/window/Window.zig deleted file mode 100644 index 6fa563adc..000000000 --- a/src/window/Window.zig +++ /dev/null @@ -1,10 +0,0 @@ -//! Window represents a single terminal window. A terminal window is -//! a single drawable terminal surface. -//! -//! This Window is the abstract window logic that applies to all platforms. -//! Platforms are expected to implement a compile-time "interface" to -//! implement platform-specific logic. -//! -//! Note(mitchellh): We current conflate a "window" and a "surface". If -//! we implement splits, we probably will need to separate these concepts. -pub const Window = @This();