diff --git a/src/Window.zig b/src/Window.zig index e4b7422af..ea9f53e92 100644 --- a/src/Window.zig +++ b/src/Window.zig @@ -141,44 +141,19 @@ pub fn create(alloc: Allocator, app: *App, config: *const Config) !*Window { // Create the windowing system var winsys = try WindowingSystem.init(app); - winsys.deinit(); + errdefer winsys.deinit(); - // Create our window - const window = try glfw.Window.create(640, 480, "ghostty", null, null, Renderer.windowHints()); - errdefer window.destroy(); - try Renderer.windowInit(window); - - // 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(window).?); - - // 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 window.setCursor(cursor); - } + // Initialize our renderer with our initialized windowing system. + try Renderer.windowInit(winsys); // Determine our DPI configurations so we can properly configure // font points to pixels and handle other high-DPI scaling factors. - const content_scale = try window.getContentScale(); - const x_dpi = content_scale.x_scale * font.face.default_dpi; - const y_dpi = content_scale.y_scale * font.face.default_dpi; + const content_scale = try winsys.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_scale, - content_scale.y_scale, + content_scale.x, + content_scale.y, x_dpi, y_dpi, }); @@ -330,7 +305,7 @@ pub fn create(alloc: Allocator, app: *App, config: *const Config) !*Window { errdefer renderer_impl.deinit(); // Calculate our grid size based on known dimensions. - const window_size = try window.getSize(); + const window_size = try winsys.getSize(); const screen_size: renderer.ScreenSize = .{ .width = window_size.width, .height = window_size.height, @@ -348,7 +323,7 @@ pub fn create(alloc: Allocator, app: *App, config: *const Config) !*Window { // Create the renderer thread var render_thread = try renderer.Thread.init( alloc, - window, + winsys, &self.renderer, &self.renderer_state, ); @@ -382,8 +357,8 @@ pub fn create(alloc: Allocator, app: *App, config: *const Config) !*Window { .font_lib = font_lib, .font_group = font_group, .font_size = font_size, - .window = window, - .cursor = cursor, + .window = winsys.window, + .cursor = winsys.cursor, .renderer = renderer_impl, .renderer_thread = render_thread, .renderer_state = .{ @@ -413,21 +388,22 @@ pub fn create(alloc: Allocator, app: *App, config: *const Config) !*Window { // Set a minimum size that is cols=10 h=4. This matches Mac's Terminal.app // but is otherwise somewhat arbitrary. - try window.setSizeLimits(.{ - .width = @floatToInt(u32, cell_size.width * 10), - .height = @floatToInt(u32, cell_size.height * 4), - }, .{ .width = null, .height = null }); + // TODO: + // try window.setSizeLimits(.{ + // .width = @floatToInt(u32, cell_size.width * 10), + // .height = @floatToInt(u32, cell_size.height * 4), + // }, .{ .width = null, .height = null }); // Setup our callbacks and user data - window.setUserPointer(self); - window.setSizeCallback(sizeCallback); - window.setCharCallback(charCallback); - window.setKeyCallback(keyCallback); - window.setFocusCallback(focusCallback); - window.setRefreshCallback(refreshCallback); - window.setScrollCallback(scrollCallback); - window.setCursorPosCallback(cursorPosCallback); - window.setMouseButtonCallback(mouseButtonCallback); + winsys.window.setUserPointer(self); + winsys.window.setSizeCallback(sizeCallback); + winsys.window.setCharCallback(charCallback); + winsys.window.setKeyCallback(keyCallback); + winsys.window.setFocusCallback(focusCallback); + winsys.window.setRefreshCallback(refreshCallback); + winsys.window.setScrollCallback(scrollCallback); + winsys.window.setCursorPosCallback(cursorPosCallback); + winsys.window.setMouseButtonCallback(mouseButtonCallback); // Call our size callback which handles all our retina setup // Note: this shouldn't be necessary and when we clean up the window @@ -435,7 +411,7 @@ pub fn create(alloc: Allocator, app: *App, config: *const Config) !*Window { // sizeCallback does retina-aware stuff we don't do here and don't want // to duplicate. sizeCallback( - window, + winsys.window, @intCast(i32, window_size.width), @intCast(i32, window_size.height), ); @@ -461,12 +437,12 @@ pub fn create(alloc: Allocator, app: *App, config: *const Config) !*Window { DevMode.instance.window = self; // Let our renderer setup - try renderer_impl.initDevMode(window); + try renderer_impl.initDevMode(winsys); } // Give the renderer one more opportunity to finalize any window // setup on the main thread prior to spinning up the rendering thread. - try renderer_impl.finalizeWindowInit(window); + try renderer_impl.finalizeWindowInit(winsys); // Start our renderer thread self.renderer_thr = try std.Thread.spawn( @@ -495,7 +471,7 @@ pub fn destroy(self: *Window) void { self.renderer_thr.join(); // We need to become the active rendering thread again - self.renderer.threadEnter(self.window) catch unreachable; + self.renderer.threadEnter(self.windowing_system) catch unreachable; self.renderer_thread.deinit(); // If we are devmode-owning, clean that up. @@ -525,51 +501,7 @@ pub fn destroy(self: *Window) void { self.io.deinit(); } - 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 => {}, - } - } - - self.window.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"), .{}); - } - } - - // We can destroy the cursor right away. glfw will just revert any - // windows using it to the default. - self.cursor.destroy(); + self.windowing_system.deinit(); self.font_group.deinit(self.alloc); self.font_lib.deinit(); diff --git a/src/renderer/OpenGL.zig b/src/renderer/OpenGL.zig index a179d2c5b..8ac82b026 100644 --- a/src/renderer/OpenGL.zig +++ b/src/renderer/OpenGL.zig @@ -18,6 +18,7 @@ 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); @@ -350,7 +351,7 @@ fn resetCellsLRU(self: *OpenGL) void { } /// Returns the hints that we want for this -pub fn windowHints() glfw.Window.Hints { +pub fn glfwWindowHints() glfw.Window.Hints { return .{ .context_version_major = 3, .context_version_minor = 3, @@ -363,10 +364,10 @@ pub fn windowHints() glfw.Window.Hints { /// This is called early right after window creation to setup our /// window surface as necessary. -pub fn windowInit(window: glfw.Window) !void { +pub fn windowInit(winsys: window.System) !void { // Treat this like a thread entry const self: OpenGL = undefined; - try self.threadEnter(window); + try self.threadEnter(winsys); // Blending for text try gl.enable(gl.c.GL_BLEND); @@ -380,40 +381,23 @@ pub fn windowInit(window: glfw.Window) !void { // log.debug("OpenGL extension available name={s}", .{ext}); // } // } - - 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 = window.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, - }); - } } /// This is called just prior to spinning up the renderer thread for /// final main thread setup requirements. -pub fn finalizeWindowInit(self: *const OpenGL, window: glfw.Window) !void { +pub fn finalizeWindowInit(self: *const OpenGL, winsys: window.System) !void { _ = self; - _ = window; + _ = winsys; } /// This is called if this renderer runs DevMode. -pub fn initDevMode(self: *const OpenGL, window: glfw.Window) !void { +pub fn initDevMode(self: *const OpenGL, winsys: window.System) !void { _ = self; if (DevMode.enabled) { // Initialize for our window assert(imgui.ImplGlfw.initForOpenGL( - @ptrCast(*imgui.ImplGlfw.GLFWWindow, window.handle), + @ptrCast(*imgui.ImplGlfw.GLFWWindow, winsys.window.handle), true, )); assert(imgui.ImplOpenGL3.init("#version 330 core")); @@ -431,7 +415,7 @@ pub fn deinitDevMode(self: *const OpenGL) void { } /// Callback called by renderer.Thread when it begins. -pub fn threadEnter(self: *const OpenGL, window: glfw.Window) !void { +pub fn threadEnter(self: *const OpenGL, winsys: window.System) !void { _ = self; // We need to make the OpenGL context current. OpenGL requires @@ -439,7 +423,7 @@ pub fn threadEnter(self: *const OpenGL, window: glfw.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. - try glfw.makeContextCurrent(window); + try glfw.makeContextCurrent(winsys.window); errdefer glfw.makeContextCurrent(null) catch |err| log.warn("failed to cleanup OpenGL context err={}", .{err}); try glfw.swapInterval(1); @@ -541,7 +525,7 @@ fn resetFontMetrics( /// The primary render callback that is completely thread-safe. pub fn render( self: *OpenGL, - window: glfw.Window, + winsys: window.System, state: *renderer.State, ) !void { // Data we extract out of the critical area. @@ -657,7 +641,7 @@ pub fn render( } // Swap our window buffers - try window.swapBuffers(); + try winsys.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 04846b79a..d4fe8930b 100644 --- a/src/renderer/Thread.zig +++ b/src/renderer/Thread.zig @@ -4,9 +4,9 @@ pub const Thread = @This(); const std = @import("std"); const builtin = @import("builtin"); -const glfw = @import("glfw"); const libuv = @import("libuv"); const renderer = @import("../renderer.zig"); +const window = @import("../window.zig"); const BlockingQueue = @import("../blocking_queue.zig").BlockingQueue; const tracy = @import("tracy"); const trace = tracy.trace; @@ -37,8 +37,8 @@ render_h: libuv.Timer, /// The timer used for cursor blinking cursor_h: libuv.Timer, -/// The windo we're rendering to. -window: glfw.Window, +/// The window we're rendering to. +window: window.System, /// 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, - window: glfw.Window, + win: window.System, renderer_impl: *renderer.Renderer, state: *renderer.State, ) !Thread { @@ -120,7 +120,7 @@ pub fn init( .stop = stop_h, .render_h = render_h, .cursor_h = cursor_timer, - .window = window, + .window = win, .renderer = renderer_impl, .state = state, .mailbox = mailbox, diff --git a/src/window/Glfw.zig b/src/window/Glfw.zig index ad84b1adf..70d1a52db 100644 --- a/src/window/Glfw.zig +++ b/src/window/Glfw.zig @@ -18,6 +18,8 @@ const glfwNative = glfw.Native(.{ .cocoa = builtin.target.isDarwin(), }); +const log = std.log.scoped(.glfw_window); + /// The glfw window handle window: glfw.Window, @@ -26,9 +28,25 @@ 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.windowHints()); + const win = try glfw.Window.create(640, 480, "ghostty", null, null, Renderer.glfwWindowHints()); errdefer win.destroy(); - try Renderer.windowInit(win); + + 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()) { @@ -53,6 +71,7 @@ pub fn init(app: *const App) !Glfw { try win.setCursor(cursor); } + // Build our result return Glfw{ .window = win, .cursor = cursor, @@ -60,8 +79,50 @@ pub fn init(app: *const App) !Glfw { } 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.