From 6eb5a0238af638450929b7d891e9fadc7a8ff20c Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 29 Dec 2022 14:37:18 -0800 Subject: [PATCH 01/14] window: start abstracting a window implementation --- src/Window.zig | 43 +++++++++++++---------- src/window.zig | 18 ++++++++++ src/window/Glfw.zig | 77 ++++++++++++++++++++++++++++++++++++++++++ src/window/Web.zig | 2 ++ src/window/Window.zig | 10 ++++++ src/window/structs.zig | 13 +++++++ 6 files changed, 146 insertions(+), 17 deletions(-) create mode 100644 src/window.zig create mode 100644 src/window/Glfw.zig create mode 100644 src/window/Web.zig create mode 100644 src/window/Window.zig create mode 100644 src/window/structs.zig diff --git a/src/Window.zig b/src/Window.zig index 074d04f26..e4b7422af 100644 --- a/src/Window.zig +++ b/src/Window.zig @@ -30,6 +30,7 @@ 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(.{ @@ -47,6 +48,9 @@ alloc: Allocator, /// The app that this window is a part of. app: *App, +/// The windowing system state +windowing_system: WindowingSystem, + /// The font structures font_lib: font.Library, font_group: *font.GroupCache, @@ -135,6 +139,10 @@ pub fn create(alloc: Allocator, app: *App, config: *const Config) !*Window { var self = try alloc.create(Window); errdefer alloc.destroy(self); + // Create the windowing system + var winsys = try WindowingSystem.init(app); + winsys.deinit(); + // Create our window const window = try glfw.Window.create(640, 480, "ghostty", null, null, Renderer.windowHints()); errdefer window.destroy(); @@ -153,6 +161,16 @@ pub fn create(alloc: Allocator, app: *App, config: *const Config) !*Window { 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); + } + // 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(); @@ -322,23 +340,6 @@ pub fn create(alloc: Allocator, app: *App, config: *const Config) !*Window { cell_size, ); - // 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 }); - - // 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); - } - // The mutex used to protect our renderer state. var mutex = try alloc.create(std.Thread.Mutex); mutex.* = .{}; @@ -377,6 +378,7 @@ pub fn create(alloc: Allocator, app: *App, config: *const Config) !*Window { self.* = .{ .alloc = alloc, .app = app, + .windowing_system = winsys, .font_lib = font_lib, .font_group = font_group, .font_size = font_size, @@ -409,6 +411,13 @@ pub fn create(alloc: Allocator, app: *App, config: *const Config) !*Window { }; 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 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); diff --git a/src/window.zig b/src/window.zig new file mode 100644 index 000000000..6132b90a9 --- /dev/null +++ b/src/window.zig @@ -0,0 +1,18 @@ +//! 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 new file mode 100644 index 000000000..ad84b1adf --- /dev/null +++ b/src/window/Glfw.zig @@ -0,0 +1,77 @@ +//! 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(), +}); + +/// 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.windowHints()); + errdefer win.destroy(); + try Renderer.windowInit(win); + + // 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); + } + + return Glfw{ + .window = win, + .cursor = cursor, + }; +} + +pub fn deinit(self: *Glfw) void { + self.window.destroy(); + self.cursor.destroy(); +} + +/// 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 new file mode 100644 index 000000000..091ffc216 --- /dev/null +++ b/src/window/Web.zig @@ -0,0 +1,2 @@ +//! Window implementation for the web (browser) via WebAssembly. +pub const Window = @This(); diff --git a/src/window/Window.zig b/src/window/Window.zig new file mode 100644 index 000000000..6fa563adc --- /dev/null +++ b/src/window/Window.zig @@ -0,0 +1,10 @@ +//! 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(); diff --git a/src/window/structs.zig b/src/window/structs.zig new file mode 100644 index 000000000..bb5590267 --- /dev/null +++ b/src/window/structs.zig @@ -0,0 +1,13 @@ +/// ContentScale is the ratio between the current DPI and the platform's +/// default DPI. This is used to determine how much certain rendered elements +/// need to be scaled up or down. +pub const ContentScale = struct { + x: f32, + y: f32, +}; + +/// The size of the window in screen coordinates. +pub const Size = struct { + width: u32, + height: u32, +}; From e1cd6502450ec72fe540a9a1e6c0f31f87c42521 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 29 Dec 2022 14:51:56 -0800 Subject: [PATCH 02/14] window: abstract more, it starts --- src/Window.zig | 130 ++++++++++------------------------------ src/renderer/OpenGL.zig | 40 ++++--------- src/renderer/Thread.zig | 10 ++-- src/window/Glfw.zig | 65 +++++++++++++++++++- 4 files changed, 111 insertions(+), 134 deletions(-) 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. From 11a3577ef1e993af19cfd71e4b79c41940ac77fb Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 29 Dec 2022 15:11:03 -0800 Subject: [PATCH 03/14] rename window package to apprt --- src/Window.zig | 11 +- src/apprt.zig | 27 ++++ src/{window/message.zig => apprt/Window.zig} | 0 src/apprt/glfw.zig | 149 +++++++++++++++++++ src/{window => apprt}/structs.zig | 2 +- src/renderer/OpenGL.zig | 22 +-- src/renderer/Thread.zig | 6 +- src/window.zig | 18 --- src/window/Glfw.zig | 138 ----------------- src/window/Web.zig | 2 - src/window/Window.zig | 10 -- 11 files changed, 196 insertions(+), 189 deletions(-) create mode 100644 src/apprt.zig rename src/{window/message.zig => apprt/Window.zig} (100%) create mode 100644 src/apprt/glfw.zig rename src/{window => apprt}/structs.zig (91%) delete mode 100644 src/window.zig delete mode 100644 src/window/Glfw.zig delete mode 100644 src/window/Web.zig delete mode 100644 src/window/Window.zig 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(); From 41399b6871990de7d8361ceeb12ab83c25067569 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 29 Dec 2022 15:14:59 -0800 Subject: [PATCH 04/14] renderer: make Metal apprt-aware --- src/renderer/Metal.zig | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index 6fe468a84..6ee656e6f 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -10,6 +10,7 @@ const glfw = @import("glfw"); const objc = @import("objc"); const macos = @import("macos"); const imgui = @import("imgui"); +const apprt = @import("../apprt.zig"); const font = @import("../font/main.zig"); const terminal = @import("../terminal/main.zig"); const renderer = @import("../renderer.zig"); @@ -124,7 +125,7 @@ const GPUCellMode = enum(u8) { }; /// Returns the hints that we want for this -pub fn windowHints() glfw.Window.Hints { +pub fn glfwWindowHints() glfw.Window.Hints { return .{ .client_api = .no_api, // .cocoa_graphics_switching = builtin.os.tag == .macos, @@ -134,8 +135,8 @@ 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 { - _ = window; +pub fn windowInit(win: apprt.runtime.Window) !void { + _ = win; // We don't do anything else here because we want to set everything // else up during actual initialization. @@ -303,9 +304,12 @@ pub fn deinit(self: *Metal) void { /// This is called just prior to spinning up the renderer thread for /// final main thread setup requirements. -pub fn finalizeWindowInit(self: *const Metal, window: glfw.Window) !void { +pub fn finalizeWindowInit(self: *const Metal, win: apprt.runtime.Window) !void { // Set our window backing layer to be our swapchain - const nswindow = objc.Object.fromId(glfwNative.getCocoaWindow(window).?); + const nswindow = switch (apprt.runtime) { + apprt.glfw => objc.Object.fromId(glfwNative.getCocoaWindow(win.window).?), + else => @compileError("unsupported apprt for metal"), + }; const contentView = objc.Object.fromId(nswindow.getProperty(?*anyopaque, "contentView").?); contentView.setProperty("layer", self.swapchain.value); contentView.setProperty("wantsLayer", true); @@ -319,11 +323,11 @@ pub fn finalizeWindowInit(self: *const Metal, window: glfw.Window) !void { } /// This is called if this renderer runs DevMode. -pub fn initDevMode(self: *const Metal, window: glfw.Window) !void { +pub fn initDevMode(self: *const Metal, win: apprt.runtime.Window) !void { if (DevMode.enabled) { // Initialize for our window assert(imgui.ImplGlfw.initForOther( - @ptrCast(*imgui.ImplGlfw.GLFWWindow, window.handle), + @ptrCast(*imgui.ImplGlfw.GLFWWindow, win.window.handle), true, )); assert(imgui.ImplMetal.init(self.device.value)); @@ -341,9 +345,9 @@ pub fn deinitDevMode(self: *const Metal) void { } /// Callback called by renderer.Thread when it begins. -pub fn threadEnter(self: *const Metal, window: glfw.Window) !void { +pub fn threadEnter(self: *const Metal, win: apprt.runtime.Window) !void { _ = self; - _ = window; + _ = win; // Metal requires no per-thread state. } @@ -425,10 +429,10 @@ pub fn setFontSize(self: *Metal, size: font.face.DesiredSize) !void { /// The primary render callback that is completely thread-safe. pub fn render( self: *Metal, - window: glfw.Window, + win: apprt.runtime.Window, state: *renderer.State, ) !void { - _ = window; + _ = win; // Data we extract out of the critical area. const Critical = struct { From ff1f1d8925ce5442250fc90b9b4dfca42dd546ef Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 30 Dec 2022 13:33:19 -0800 Subject: [PATCH 05/14] move the size callback into apprt --- src/Window.zig | 63 +++++++++++++------------------------------ src/apprt/glfw.zig | 37 ++++++++++++++++++++++--- src/apprt/structs.zig | 2 +- 3 files changed, 54 insertions(+), 48 deletions(-) diff --git a/src/Window.zig b/src/Window.zig index b968de581..bbbcbf6f7 100644 --- a/src/Window.zig +++ b/src/Window.zig @@ -139,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 apprt.runtime.Window.init(app); + var winsys = try apprt.runtime.Window.init(app, self); errdefer winsys.deinit(); // Initialize our renderer with our initialized windowing system. @@ -394,8 +394,6 @@ pub fn create(alloc: Allocator, app: *App, config: *const Config) !*Window { // }, .{ .width = null, .height = null }); // Setup our callbacks and user data - winsys.window.setUserPointer(self); - winsys.window.setSizeCallback(sizeCallback); winsys.window.setCharCallback(charCallback); winsys.window.setKeyCallback(keyCallback); winsys.window.setFocusCallback(focusCallback); @@ -409,11 +407,7 @@ pub fn create(alloc: Allocator, app: *App, config: *const Config) !*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. - sizeCallback( - winsys.window, - @intCast(i32, window_size.width), - @intCast(i32, window_size.height), - ); + 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. @@ -682,67 +676,48 @@ fn cursorPosToPixels(self: Window, pos: glfw.Window.CursorPos) glfw.Window.Curso }; } -fn sizeCallback(window: glfw.Window, width: i32, height: i32) void { +pub fn sizeCallback(self: *Window, size: apprt.WindowSize) !void { const tracy = trace(@src()); defer tracy.end(); - // glfw gives us signed integers, but negative width/height is n - // non-sensical so we use unsigned throughout, so assert. - assert(width >= 0); - assert(height >= 0); - - // Get our framebuffer size since this will give us the size in pixels - // whereas width/height in this callback is in screen coordinates. For - // Retina displays (or any other displays that have a scale factor), - // these will not match. - const px_size = window.getFramebufferSize() catch |err| err: { - log.err("error querying window size in pixels, will use screen size err={}", .{err}); - break :err glfw.Window.Size{ - .width = @intCast(u32, width), - .height = @intCast(u32, height), - }; - }; - - const win = window.getUserPointer(Window) orelse return; - // TODO: if our screen size didn't change, then we should avoid the // overhead of inter-thread communication // Save our screen size - win.screen_size = .{ - .width = px_size.width, - .height = px_size.height, + self.screen_size = .{ + .width = size.width, + .height = size.height, }; // Recalculate our grid size - win.grid_size = renderer.GridSize.init( - win.screen_size.subPadding(win.padding), - win.cell_size, + self.grid_size = renderer.GridSize.init( + self.screen_size.subPadding(self.padding), + self.cell_size, ); - if (win.grid_size.columns < 5 and (win.padding.left > 0 or win.padding.right > 0)) { + 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 (win.grid_size.rows < 2 and (win.padding.top > 0 or win.padding.bottom > 0)) { + 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 - _ = win.renderer_thread.mailbox.push(.{ - .screen_size = win.screen_size, + _ = self.renderer_thread.mailbox.push(.{ + .screen_size = self.screen_size, }, .{ .forever = {} }); - win.queueRender() catch unreachable; + try self.queueRender(); // Mail the IO thread - _ = win.io_thread.mailbox.push(.{ + _ = self.io_thread.mailbox.push(.{ .resize = .{ - .grid_size = win.grid_size, - .screen_size = win.screen_size, - .padding = win.padding, + .grid_size = self.grid_size, + .screen_size = self.screen_size, + .padding = self.padding, }, }, .{ .forever = {} }); - win.io_thread.wakeup.send() catch {}; + try self.io_thread.wakeup.send(); } fn charCallback(window: glfw.Window, codepoint: u21) void { diff --git a/src/apprt/glfw.zig b/src/apprt/glfw.zig index 0862a439d..5865f3e95 100644 --- a/src/apprt/glfw.zig +++ b/src/apprt/glfw.zig @@ -14,6 +14,7 @@ const internal_os = @import("../os/main.zig"); const renderer = @import("../renderer.zig"); const Renderer = renderer.Renderer; const apprt = @import("../apprt.zig"); +const CoreWindow = @import("../Window.zig"); // Get native API access on certain platforms so we can do more customization. const glfwNative = glfw.Native(.{ @@ -29,7 +30,7 @@ pub const Window = struct { /// The glfw mouse cursor handle. cursor: glfw.Cursor, - pub fn init(app: *const App) !Window { + pub fn init(app: *const App, core_win: *CoreWindow) !Window { // Create our window const win = try glfw.Window.create( 640, @@ -81,6 +82,10 @@ pub const Window = struct { try win.setCursor(cursor); } + // Set our callbacks + win.setUserPointer(core_win); + win.setSizeCallback(sizeCallback); + // Build our result return Window{ .window = win, @@ -141,9 +146,35 @@ pub const Window = struct { return apprt.ContentScale{ .x = scale.x_scale, .y = scale.y_scale }; } - /// Returns the size of the window in screen coordinates. + /// 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 { - const size = try self.window.getSize(); + const size = self.window.getFramebufferSize() catch |err| err: { + log.err("error querying window size in pixels, will use screen size err={}", .{err}); + break :err try self.window.getSize(); + }; + return apprt.WindowSize{ .width = size.width, .height = size.height }; } + + fn sizeCallback(window: glfw.Window, width: i32, height: i32) void { + _ = width; + _ = height; + + // 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.windowing_system.getSize() catch |err| { + log.err("error querying window size for size callback err={}", .{err}); + return; + }; + + // Call the primary callback. + core_win.sizeCallback(size) catch |err| { + log.err("error in size callback err={}", .{err}); + return; + }; + } }; diff --git a/src/apprt/structs.zig b/src/apprt/structs.zig index a78121da9..cd0aac678 100644 --- a/src/apprt/structs.zig +++ b/src/apprt/structs.zig @@ -6,7 +6,7 @@ pub const ContentScale = struct { y: f32, }; -/// The size of the window in screen coordinates. +/// The size of the window in pixels. pub const WindowSize = struct { width: u32, height: u32, From 946383eb770491d4ccf950c411881adf3652c33c Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 30 Dec 2022 14:45:03 -0800 Subject: [PATCH 06/14] apprt: key/charCallback, input supports all glfw keys --- src/Window.zig | 214 +++++++++++++++------------------------------ src/apprt/glfw.zig | 173 ++++++++++++++++++++++++++++++++++++ src/input/key.zig | 71 ++++++++++++++- 3 files changed, 312 insertions(+), 146 deletions(-) diff --git a/src/Window.zig b/src/Window.zig index bbbcbf6f7..f76079476 100644 --- a/src/Window.zig +++ b/src/Window.zig @@ -394,8 +394,6 @@ pub fn create(alloc: Allocator, app: *App, config: *const Config) !*Window { // }, .{ .width = null, .height = null }); // Setup our callbacks and user data - winsys.window.setCharCallback(charCallback); - winsys.window.setKeyCallback(keyCallback); winsys.window.setFocusCallback(focusCallback); winsys.window.setRefreshCallback(refreshCallback); winsys.window.setScrollCallback(scrollCallback); @@ -720,51 +718,46 @@ pub fn sizeCallback(self: *Window, size: apprt.WindowSize) !void { try self.io_thread.wakeup.send(); } -fn charCallback(window: glfw.Window, codepoint: u21) void { +pub fn charCallback(self: *Window, codepoint: u21) !void { const tracy = trace(@src()); defer tracy.end(); - const win = window.getUserPointer(Window) orelse return; - // 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) { - win.queueRender() catch |err| - log.err("error scheduling render timer err={}", .{err}); + try self.queueRender(); } } else |_| {} } // Ignore if requested. See field docs for more information. - if (win.ignore_char) { - win.ignore_char = false; + if (self.ignore_char) { + self.ignore_char = false; return; } // Critical area { - win.renderer_state.mutex.lock(); - defer win.renderer_state.mutex.unlock(); + self.renderer_state.mutex.lock(); + defer self.renderer_state.mutex.unlock(); // Clear the selction if we have one. - if (win.io.terminal.selection != null) { - win.io.terminal.selection = null; - win.queueRender() catch |err| - log.err("error scheduling render in charCallback err={}", .{err}); + 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. - win.io.terminal.scrollViewport(.{ .bottom = {} }) catch |err| - log.err("error scrolling viewport err={}", .{err}); + try self.io.terminal.scrollViewport(.{ .bottom = {} }); } // Ask our IO thread to write the data var data: termio.Message.WriteReq.Small.Array = undefined; data[0] = @intCast(u8, codepoint); - _ = win.io_thread.mailbox.push(.{ + _ = self.io_thread.mailbox.push(.{ .write_small = .{ .data = data, .len = 1, @@ -772,38 +765,31 @@ fn charCallback(window: glfw.Window, codepoint: u21) void { }, .{ .forever = {} }); // After sending all our messages we have to notify our IO thread - win.io_thread.wakeup.send() catch {}; + try self.io_thread.wakeup.send(); } -fn keyCallback( - window: glfw.Window, - key: glfw.Key, - scancode: i32, - action: glfw.Action, - mods: glfw.Mods, -) void { +pub fn keyCallback( + self: *Window, + action: input.Action, + key: input.Key, + mods: input.Mods, +) !void { const tracy = trace(@src()); defer tracy.end(); - const win = window.getUserPointer(Window) orelse return; - // 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) { - win.queueRender() catch |err| - log.err("error scheduling render timer err={}", .{err}); + 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. - win.ignore_char = false; - - //log.info("KEY {} {} {} {}", .{ key, scancode, mods, action }); - _ = scancode; + self.ignore_char = false; if (action == .press or action == .repeat) { // Convert our glfw input into a platform agnostic trigger. When we @@ -811,74 +797,12 @@ fn keyCallback( // into a function. For now, this is the only place we do it so we just // put it right here. const trigger: input.Binding.Trigger = .{ - .mods = @bitCast(input.Mods, mods), - .key = switch (key) { - .a => .a, - .b => .b, - .c => .c, - .d => .d, - .e => .e, - .f => .f, - .g => .g, - .h => .h, - .i => .i, - .j => .j, - .k => .k, - .l => .l, - .m => .m, - .n => .n, - .o => .o, - .p => .p, - .q => .q, - .r => .r, - .s => .s, - .t => .t, - .u => .u, - .v => .v, - .w => .w, - .x => .x, - .y => .y, - .z => .z, - .zero => .zero, - .one => .one, - .two => .three, - .three => .four, - .four => .four, - .five => .five, - .six => .six, - .seven => .seven, - .eight => .eight, - .nine => .nine, - .up => .up, - .down => .down, - .right => .right, - .left => .left, - .home => .home, - .end => .end, - .page_up => .page_up, - .page_down => .page_down, - .escape => .escape, - .F1 => .f1, - .F2 => .f2, - .F3 => .f3, - .F4 => .f4, - .F5 => .f5, - .F6 => .f6, - .F7 => .f7, - .F8 => .f8, - .F9 => .f9, - .F10 => .f10, - .F11 => .f11, - .F12 => .f12, - .grave_accent => .grave_accent, - .minus => .minus, - .equal => .equal, - else => .invalid, - }, + .mods = mods, + .key = key, }; //log.warn("BINDING TRIGGER={}", .{trigger}); - if (win.config.keybind.set.get(trigger)) |binding_action| { + if (self.config.keybind.set.get(trigger)) |binding_action| { //log.warn("BINDING ACTION={}", .{binding_action}); switch (binding_action) { @@ -886,13 +810,13 @@ fn keyCallback( .ignore => {}, .csi => |data| { - _ = win.io_thread.mailbox.push(.{ + _ = self.io_thread.mailbox.push(.{ .write_stable = "\x1B[", }, .{ .forever = {} }); - _ = win.io_thread.mailbox.push(.{ + _ = self.io_thread.mailbox.push(.{ .write_stable = data, }, .{ .forever = {} }); - win.io_thread.wakeup.send() catch {}; + try self.io_thread.wakeup.send(); }, .cursor_key => |ck| { @@ -900,37 +824,37 @@ fn keyCallback( // in cursor keys mode. We're in "normal" mode if cursor // keys mdoe is NOT set. const normal = normal: { - win.renderer_state.mutex.lock(); - defer win.renderer_state.mutex.unlock(); - break :normal !win.io.terminal.modes.cursor_keys; + self.renderer_state.mutex.lock(); + defer self.renderer_state.mutex.unlock(); + break :normal !self.io.terminal.modes.cursor_keys; }; if (normal) { - _ = win.io_thread.mailbox.push(.{ + _ = self.io_thread.mailbox.push(.{ .write_stable = ck.normal, }, .{ .forever = {} }); } else { - _ = win.io_thread.mailbox.push(.{ + _ = self.io_thread.mailbox.push(.{ .write_stable = ck.application, }, .{ .forever = {} }); } - win.io_thread.wakeup.send() catch {}; + try self.io_thread.wakeup.send(); }, .copy_to_clipboard => { // We can read from the renderer state without holding // the lock because only we will write to this field. - if (win.io.terminal.selection) |sel| { - var buf = win.io.terminal.screen.selectionString( - win.alloc, + if (self.io.terminal.selection) |sel| { + var buf = self.io.terminal.screen.selectionString( + self.alloc, sel, - win.config.@"clipboard-trim-trailing-spaces", + self.config.@"clipboard-trim-trailing-spaces", ) catch |err| { log.err("error reading selection string err={}", .{err}); return; }; - defer win.alloc.free(buf); + defer self.alloc.free(buf); glfw.setClipboardString(buf) catch |err| { log.err("error setting clipboard string err={}", .{err}); @@ -947,99 +871,99 @@ fn keyCallback( if (data.len > 0) { const bracketed = bracketed: { - win.renderer_state.mutex.lock(); - defer win.renderer_state.mutex.unlock(); - break :bracketed win.io.terminal.modes.bracketed_paste; + self.renderer_state.mutex.lock(); + defer self.renderer_state.mutex.unlock(); + break :bracketed self.io.terminal.modes.bracketed_paste; }; if (bracketed) { - _ = win.io_thread.mailbox.push(.{ + _ = self.io_thread.mailbox.push(.{ .write_stable = "\x1B[200~", }, .{ .forever = {} }); } - _ = win.io_thread.mailbox.push(termio.Message.writeReq( - win.alloc, + _ = self.io_thread.mailbox.push(try termio.Message.writeReq( + self.alloc, data, - ) catch unreachable, .{ .forever = {} }); + ), .{ .forever = {} }); if (bracketed) { - _ = win.io_thread.mailbox.push(.{ + _ = self.io_thread.mailbox.push(.{ .write_stable = "\x1B[201~", }, .{ .forever = {} }); } - win.io_thread.wakeup.send() catch {}; + try self.io_thread.wakeup.send(); } }, .increase_font_size => |delta| { log.debug("increase font size={}", .{delta}); - var size = win.font_size; + var size = self.font_size; size.points +|= delta; - win.setFontSize(size); + self.setFontSize(size); }, .decrease_font_size => |delta| { log.debug("decrease font size={}", .{delta}); - var size = win.font_size; + var size = self.font_size; size.points = @max(1, size.points -| delta); - win.setFontSize(size); + self.setFontSize(size); }, .reset_font_size => { log.debug("reset font size", .{}); - var size = win.font_size; - size.points = win.config.@"font-size"; - win.setFontSize(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; - win.queueRender() catch unreachable; + try self.queueRender(); } else log.warn("dev mode was not compiled into this binary", .{}), .new_window => { - _ = win.app.mailbox.push(.{ + _ = self.app.mailbox.push(.{ .new_window = .{ - .font_size = if (win.config.@"window-inherit-font-size") - win.font_size + .font_size = if (self.config.@"window-inherit-font-size") + self.font_size else null, }, }, .{ .instant = {} }); - win.app.wakeup(); + self.app.wakeup(); }, .new_tab => { - _ = win.app.mailbox.push(.{ + _ = self.app.mailbox.push(.{ .new_tab = .{ - .parent = win, + .parent = self, - .font_size = if (win.config.@"window-inherit-font-size") - win.font_size + .font_size = if (self.config.@"window-inherit-font-size") + self.font_size else null, }, }, .{ .instant = {} }); - win.app.wakeup(); + self.app.wakeup(); }, - .close_window => win.window.setShouldClose(true), + .close_window => self.windowing_system.setShouldClose(), .quit => { - _ = win.app.mailbox.push(.{ + _ = self.app.mailbox.push(.{ .quit = {}, }, .{ .instant = {} }); - win.app.wakeup(); + self.app.wakeup(); }, } // Bindings always result in us ignoring the char if printable - win.ignore_char = true; + self.ignore_char = true; // No matter what, if there is a binding then we are done. return; @@ -1099,7 +1023,7 @@ fn keyCallback( // Ask our IO thread to write the data var data: termio.Message.WriteReq.Small.Array = undefined; data[0] = @intCast(u8, char); - _ = win.io_thread.mailbox.push(.{ + _ = self.io_thread.mailbox.push(.{ .write_small = .{ .data = data, .len = 1, @@ -1107,7 +1031,7 @@ fn keyCallback( }, .{ .forever = {} }); // After sending all our messages we have to notify our IO thread - win.io_thread.wakeup.send() catch {}; + try self.io_thread.wakeup.send(); } } } diff --git a/src/apprt/glfw.zig b/src/apprt/glfw.zig index 5865f3e95..f5e485fc8 100644 --- a/src/apprt/glfw.zig +++ b/src/apprt/glfw.zig @@ -7,9 +7,11 @@ const std = @import("std"); const builtin = @import("builtin"); const assert = std.debug.assert; const Allocator = std.mem.Allocator; +const trace = @import("tracy").trace; const glfw = @import("glfw"); const objc = @import("objc"); const App = @import("../App.zig"); +const input = @import("../input.zig"); const internal_os = @import("../os/main.zig"); const renderer = @import("../renderer.zig"); const Renderer = renderer.Renderer; @@ -85,6 +87,8 @@ pub const Window = struct { // Set our callbacks win.setUserPointer(core_win); win.setSizeCallback(sizeCallback); + win.setCharCallback(charCallback); + win.setKeyCallback(keyCallback); // Build our result return Window{ @@ -158,6 +162,12 @@ pub const Window = struct { return apprt.WindowSize{ .width = size.width, .height = size.height }; } + /// Set the flag that notes this window should be closed for the next + /// iteration of the event loop. + pub fn setShouldClose(self: *Window) void { + self.window.setShouldClose(true); + } + fn sizeCallback(window: glfw.Window, width: i32, height: i32) void { _ = width; _ = height; @@ -177,4 +187,167 @@ pub const Window = struct { return; }; } + + fn charCallback(window: glfw.Window, codepoint: u21) void { + const tracy = trace(@src()); + defer tracy.end(); + + const core_win = window.getUserPointer(CoreWindow) orelse return; + core_win.charCallback(codepoint) catch |err| { + log.err("error in char callback err={}", .{err}); + return; + }; + } + + fn keyCallback( + window: glfw.Window, + glfw_key: glfw.Key, + scancode: i32, + glfw_action: glfw.Action, + glfw_mods: glfw.Mods, + ) void { + _ = scancode; + + const tracy = trace(@src()); + defer tracy.end(); + + // Convert our glfw types into our input types + const mods = @bitCast(input.Mods, glfw_mods); + const action: input.Action = switch (glfw_action) { + .release => .release, + .press => .press, + .repeat => .repeat, + }; + const key: input.Key = switch (glfw_key) { + .a => .a, + .b => .b, + .c => .c, + .d => .d, + .e => .e, + .f => .f, + .g => .g, + .h => .h, + .i => .i, + .j => .j, + .k => .k, + .l => .l, + .m => .m, + .n => .n, + .o => .o, + .p => .p, + .q => .q, + .r => .r, + .s => .s, + .t => .t, + .u => .u, + .v => .v, + .w => .w, + .x => .x, + .y => .y, + .z => .z, + .zero => .zero, + .one => .one, + .two => .three, + .three => .four, + .four => .four, + .five => .five, + .six => .six, + .seven => .seven, + .eight => .eight, + .nine => .nine, + .up => .up, + .down => .down, + .right => .right, + .left => .left, + .home => .home, + .end => .end, + .page_up => .page_up, + .page_down => .page_down, + .escape => .escape, + .F1 => .f1, + .F2 => .f2, + .F3 => .f3, + .F4 => .f4, + .F5 => .f5, + .F6 => .f6, + .F7 => .f7, + .F8 => .f8, + .F9 => .f9, + .F10 => .f10, + .F11 => .f11, + .F12 => .f12, + .F13 => .f13, + .F14 => .f14, + .F15 => .f15, + .F16 => .f16, + .F17 => .f17, + .F18 => .f18, + .F19 => .f19, + .F20 => .f20, + .F21 => .f21, + .F22 => .f22, + .F23 => .f23, + .F24 => .f24, + .F25 => .f25, + .kp_0 => .kp_0, + .kp_1 => .kp_1, + .kp_2 => .kp_2, + .kp_3 => .kp_3, + .kp_4 => .kp_4, + .kp_5 => .kp_5, + .kp_6 => .kp_6, + .kp_7 => .kp_7, + .kp_8 => .kp_8, + .kp_9 => .kp_9, + .kp_decimal => .kp_decimal, + .kp_divide => .kp_divide, + .kp_multiply => .kp_multiply, + .kp_subtract => .kp_subtract, + .kp_add => .kp_add, + .kp_enter => .kp_enter, + .kp_equal => .kp_equal, + .grave_accent => .grave_accent, + .minus => .minus, + .equal => .equal, + .space => .space, + .semicolon => .semicolon, + .apostrophe => .apostrophe, + .comma => .comma, + .period => .period, + .slash => .slash, + .left_bracket => .left_bracket, + .right_bracket => .right_bracket, + .backslash => .backslash, + .enter => .enter, + .tab => .tab, + .backspace => .backspace, + .insert => .insert, + .delete => .delete, + .caps_lock => .caps_lock, + .scroll_lock => .scroll_lock, + .num_lock => .num_lock, + .print_screen => .print_screen, + .pause => .pause, + .left_shift => .left_shift, + .left_control => .left_control, + .left_alt => .left_alt, + .left_super => .left_super, + .right_shift => .right_shift, + .right_control => .right_control, + .right_alt => .right_alt, + .right_super => .right_super, + + .menu, + .world_1, + .world_2, + .unknown, + => .invalid, + }; + + const core_win = window.getUserPointer(CoreWindow) orelse return; + core_win.keyCallback(action, key, mods) catch |err| { + log.err("error in key callback err={}", .{err}); + return; + }; + } }; diff --git a/src/input/key.zig b/src/input/key.zig index da640ccf1..78b5175da 100644 --- a/src/input/key.zig +++ b/src/input/key.zig @@ -13,6 +13,13 @@ pub const Mods = packed struct { _padding: u2 = 0, }; +/// The action associated with an input event. +pub const Action = enum { + release, + press, + repeat, +}; + /// The set of keys that can map to keybindings. These have no fixed enum /// values because we map platform-specific keys to this set. Note that /// this only needs to accomodate what maps to a key. If a key is not bound @@ -61,10 +68,19 @@ pub const Key = enum { eight, nine, - // other + // puncuation + semicolon, + space, + apostrophe, + comma, grave_accent, // ` + period, + slash, minus, equal, + left_bracket, // [ + right_bracket, // ] + backslash, // / // control up, @@ -73,10 +89,21 @@ pub const Key = enum { left, home, end, + insert, + delete, + caps_lock, + scroll_lock, + num_lock, page_up, page_down, escape, + enter, + tab, + backspace, + print_screen, + pause, + // function keys f1, f2, f3, @@ -89,6 +116,48 @@ pub const Key = enum { f10, f11, f12, + f13, + f14, + f15, + f16, + f17, + f18, + f19, + f20, + f21, + f22, + f23, + f24, + f25, + + // keypad + kp_0, + kp_1, + kp_2, + kp_3, + kp_4, + kp_5, + kp_6, + kp_7, + kp_8, + kp_9, + kp_decimal, + kp_divide, + kp_multiply, + kp_subtract, + kp_add, + kp_enter, + kp_equal, + + // modifiers + left_shift, + left_control, + left_alt, + left_super, + right_shift, + right_control, + right_alt, + right_super, // To support more keys (there are obviously more!) add them here // and ensure the mapping is up to date in the Window key handler. From 8196481ddaeb8d590822ffc766b1fc111d64a33b Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 30 Dec 2022 14:47:31 -0800 Subject: [PATCH 07/14] apprt: couple more easy callbacks --- src/Window.zig | 22 +++++----------------- src/apprt/glfw.zig | 24 ++++++++++++++++++++++++ 2 files changed, 29 insertions(+), 17 deletions(-) diff --git a/src/Window.zig b/src/Window.zig index f76079476..85a16703a 100644 --- a/src/Window.zig +++ b/src/Window.zig @@ -394,8 +394,6 @@ pub fn create(alloc: Allocator, app: *App, config: *const Config) !*Window { // }, .{ .width = null, .height = null }); // Setup our callbacks and user data - winsys.window.setFocusCallback(focusCallback); - winsys.window.setRefreshCallback(refreshCallback); winsys.window.setScrollCallback(scrollCallback); winsys.window.setCursorPosCallback(cursorPosCallback); winsys.window.setMouseButtonCallback(mouseButtonCallback); @@ -1036,29 +1034,19 @@ pub fn keyCallback( } } -fn focusCallback(window: glfw.Window, focused: bool) void { - const tracy = trace(@src()); - defer tracy.end(); - - const win = window.getUserPointer(Window) orelse return; - +pub fn focusCallback(self: *Window, focused: bool) !void { // Notify our render thread of the new state - _ = win.renderer_thread.mailbox.push(.{ + _ = self.renderer_thread.mailbox.push(.{ .focus = focused, }, .{ .forever = {} }); // Schedule render which also drains our mailbox - win.queueRender() catch unreachable; + try self.queueRender(); } -fn refreshCallback(window: glfw.Window) void { - const tracy = trace(@src()); - defer tracy.end(); - - const win = window.getUserPointer(Window) orelse return; - +pub fn refreshCallback(self: *Window) !void { // The point of this callback is to schedule a render, so do that. - win.queueRender() catch unreachable; + try self.queueRender(); } fn scrollCallback(window: glfw.Window, xoff: f64, yoff: f64) void { diff --git a/src/apprt/glfw.zig b/src/apprt/glfw.zig index f5e485fc8..e33346fb5 100644 --- a/src/apprt/glfw.zig +++ b/src/apprt/glfw.zig @@ -89,6 +89,8 @@ pub const Window = struct { win.setSizeCallback(sizeCallback); win.setCharCallback(charCallback); win.setKeyCallback(keyCallback); + win.setFocusCallback(focusCallback); + win.setRefreshCallback(refreshCallback); // Build our result return Window{ @@ -350,4 +352,26 @@ pub const Window = struct { return; }; } + + fn focusCallback(window: glfw.Window, focused: bool) void { + const tracy = trace(@src()); + defer tracy.end(); + + const core_win = window.getUserPointer(CoreWindow) orelse return; + core_win.focusCallback(focused) catch |err| { + log.err("error in focus callback err={}", .{err}); + return; + }; + } + + fn refreshCallback(window: glfw.Window) void { + const tracy = trace(@src()); + defer tracy.end(); + + const core_win = window.getUserPointer(CoreWindow) orelse return; + core_win.refreshCallback() catch |err| { + log.err("error in refresh callback err={}", .{err}); + return; + }; + } }; From fe84686a1d478d04f6aa77336ba9228d30ba6e62 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 30 Dec 2022 15:15:35 -0800 Subject: [PATCH 08/14] apprt: all mouse callbacks --- src/Window.zig | 237 ++++++++++++++---------------------------- src/apprt/glfw.zig | 113 ++++++++++++++++++++ src/apprt/structs.zig | 6 ++ 3 files changed, 196 insertions(+), 160 deletions(-) diff --git a/src/Window.zig b/src/Window.zig index 85a16703a..eb5f19e58 100644 --- a/src/Window.zig +++ b/src/Window.zig @@ -393,11 +393,6 @@ pub fn create(alloc: Allocator, app: *App, config: *const Config) !*Window { // .height = @floatToInt(u32, cell_size.height * 4), // }, .{ .width = null, .height = null }); - // Setup our callbacks and user data - 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 // init stuff we should get rid of this. But this is required because @@ -650,28 +645,6 @@ fn queueRender(self: *const Window) !void { try self.renderer_thread.wakeup.send(); } -/// The cursor position from glfw directly is in screen coordinates but -/// all our internal state works in pixels. -fn cursorPosToPixels(self: Window, 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. - const size = self.window.getSize() catch unreachable; - const fb_size = self.window.getFramebufferSize() catch unreachable; - - // If our framebuffer and screen are the same, then there is no scaling - // happening and we can short-circuit by returning the pos as-is. - if (fb_size.width == size.width and fb_size.height == size.height) - return pos; - - const x_scale = @intToFloat(f64, fb_size.width) / @intToFloat(f64, size.width); - const y_scale = @intToFloat(f64, fb_size.height) / @intToFloat(f64, size.height); - return .{ - .xpos = pos.xpos * x_scale, - .ypos = pos.ypos * y_scale, - }; -} - pub fn sizeCallback(self: *Window, size: apprt.WindowSize) !void { const tracy = trace(@src()); defer tracy.end(); @@ -1049,17 +1022,14 @@ pub fn refreshCallback(self: *Window) !void { try self.queueRender(); } -fn scrollCallback(window: glfw.Window, xoff: f64, yoff: f64) void { +pub fn scrollCallback(self: *Window, xoff: f64, yoff: f64) !void { const tracy = trace(@src()); defer tracy.end(); - const win = window.getUserPointer(Window) orelse return; - // 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) { - win.queueRender() catch |err| - log.err("error scheduling render timer err={}", .{err}); + try self.queueRender(); // If the mouse event was handled by imgui, ignore it. if (imgui.IO.get()) |io| { @@ -1072,33 +1042,25 @@ fn scrollCallback(window: glfw.Window, xoff: f64, yoff: f64) void { // Positive is up const sign: isize = if (yoff > 0) -1 else 1; - const delta: isize = sign * @max(@divFloor(win.grid_size.rows, 15), 1); + const delta: isize = sign * @max(@divFloor(self.grid_size.rows, 15), 1); log.info("scroll: delta={}", .{delta}); { - win.renderer_state.mutex.lock(); - defer win.renderer_state.mutex.unlock(); + self.renderer_state.mutex.lock(); + defer self.renderer_state.mutex.unlock(); // Modify our viewport, this requires a lock since it affects rendering - win.io.terminal.scrollViewport(.{ .delta = delta }) catch |err| - log.err("error scrolling viewport err={}", .{err}); + 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 = window.getCursorPos() catch |err| { - log.err("error reading cursor position: {}", .{err}); - return; - }; - - win.mouseReport(if (yoff < 0) .five else .four, .press, win.mouse.mods, pos) catch |err| { - log.err("error reporting mouse event: {}", .{err}); - return; - }; + const pos = try self.windowing_system.getCursorPos(); + try self.mouseReport(if (yoff < 0) .five else .four, .press, self.mouse.mods, pos); } } - win.queueRender() catch unreachable; + try self.queueRender(); } /// The type of action to report for a mouse event. @@ -1109,7 +1071,7 @@ fn mouseReport( button: ?input.MouseButton, action: MouseReportAction, mods: input.Mods, - unscaled_pos: glfw.Window.CursorPos, + 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? @@ -1137,8 +1099,7 @@ fn mouseReport( } // This format reports X/Y - const pos = self.cursorPosToPixels(unscaled_pos); - const viewport_point = self.posToViewport(pos.xpos, pos.ypos); + const viewport_point = self.posToViewport(pos.x, pos.y); // Record our new point self.mouse.event_point = viewport_point; @@ -1281,8 +1242,8 @@ fn mouseReport( var data: termio.Message.WriteReq.Small.Array = undefined; const resp = try std.fmt.bufPrint(&data, "\x1B[<{d};{d};{d}{c}", .{ button_code, - pos.xpos, - pos.ypos, + pos.x, + pos.y, final, }); @@ -1300,22 +1261,19 @@ fn mouseReport( try self.io_thread.wakeup.send(); } -fn mouseButtonCallback( - window: glfw.Window, - glfw_button: glfw.MouseButton, - glfw_action: glfw.Action, - mods: glfw.Mods, -) void { +pub fn mouseButtonCallback( + self: *Window, + action: input.MouseButtonState, + button: input.MouseButton, + mods: input.Mods, +) !void { const tracy = trace(@src()); defer tracy.end(); - const win = window.getUserPointer(Window) orelse return; - // 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) { - win.queueRender() catch |err| - log.err("error scheduling render timer in cursorPosCallback err={}", .{err}); + try self.queueRender(); // If the mouse event was handled by imgui, ignore it. if (imgui.IO.get()) |io| { @@ -1323,125 +1281,96 @@ fn mouseButtonCallback( } else |_| {} } - // Convert glfw button to input button - const button: input.MouseButton = switch (glfw_button) { - .left => .left, - .right => .right, - .middle => .middle, - .four => .four, - .five => .five, - .six => .six, - .seven => .seven, - .eight => .eight, - }; - const action: input.MouseButtonState = switch (glfw_action) { - .press => .press, - .release => .release, - else => unreachable, - }; - // Always record our latest mouse state - win.mouse.click_state[@enumToInt(button)] = action; - win.mouse.mods = @bitCast(input.Mods, mods); + self.mouse.click_state[@enumToInt(button)] = action; + self.mouse.mods = @bitCast(input.Mods, mods); - win.renderer_state.mutex.lock(); - defer win.renderer_state.mutex.unlock(); + self.renderer_state.mutex.lock(); + defer self.renderer_state.mutex.unlock(); // Report mouse events if enabled - if (win.io.terminal.modes.mouse_event != .none) { - const pos = window.getCursorPos() catch |err| { - log.err("error reading cursor position: {}", .{err}); - return; - }; + if (self.io.terminal.modes.mouse_event != .none) { + const pos = try self.windowing_system.getCursorPos(); const report_action: MouseReportAction = switch (action) { .press => .press, .release => .release, }; - win.mouseReport( + try self.mouseReport( button, report_action, - win.mouse.mods, + self.mouse.mods, pos, - ) catch |err| { - log.err("error reporting mouse event: {}", .{err}); - return; - }; + ); } // For left button clicks we always record some information for // selection/highlighting purposes. if (button == .left and action == .press) { - const pos = win.cursorPosToPixels(window.getCursorPos() catch |err| { - log.err("error reading cursor position: {}", .{err}); - return; - }); + const pos = try self.windowing_system.getCursorPos(); // If we move our cursor too much between clicks then we reset // the multi-click state. - if (win.mouse.left_click_count > 0) { - const max_distance = win.cell_size.width; + if (self.mouse.left_click_count > 0) { + const max_distance = self.cell_size.width; const distance = @sqrt( - std.math.pow(f64, pos.xpos - win.mouse.left_click_xpos, 2) + - std.math.pow(f64, pos.ypos - win.mouse.left_click_ypos, 2), + 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) win.mouse.left_click_count = 0; + if (distance > max_distance) self.mouse.left_click_count = 0; } // Store it - const point = win.posToViewport(pos.xpos, pos.ypos); - win.mouse.left_click_point = point.toScreen(&win.io.terminal.screen); - win.mouse.left_click_xpos = pos.xpos; - win.mouse.left_click_ypos = pos.ypos; + 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 (win.mouse.left_click_count > 0) { - const since = now.since(win.mouse.left_click_time); - if (since > win.mouse_interval) { - win.mouse.left_click_count = 0; + 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; } } - win.mouse.left_click_time = now; - win.mouse.left_click_count += 1; + self.mouse.left_click_time = now; + self.mouse.left_click_count += 1; // We only support up to triple-clicks. - if (win.mouse.left_click_count > 3) win.mouse.left_click_count = 1; + if (self.mouse.left_click_count > 3) self.mouse.left_click_count = 1; } else |err| { - win.mouse.left_click_count = 1; + self.mouse.left_click_count = 1; log.err("error reading time, mouse multi-click won't work err={}", .{err}); } - switch (win.mouse.left_click_count) { + switch (self.mouse.left_click_count) { // First mouse click, clear selection - 1 => if (win.io.terminal.selection != null) { - win.io.terminal.selection = null; - win.queueRender() catch |err| - log.err("error scheduling render in mouseButtinCallback err={}", .{err}); + 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_ = win.io.terminal.screen.selectWord(win.mouse.left_click_point); + const sel_ = self.io.terminal.screen.selectWord(self.mouse.left_click_point); if (sel_) |sel| { - win.io.terminal.selection = sel; - win.queueRender() catch |err| - log.err("error scheduling render in mouseButtinCallback err={}", .{err}); + self.io.terminal.selection = sel; + try self.queueRender(); } }, // Triple click, select the line under our mouse 3 => { - const sel_ = win.io.terminal.screen.selectLine(win.mouse.left_click_point); + const sel_ = self.io.terminal.screen.selectLine(self.mouse.left_click_point); if (sel_) |sel| { - win.io.terminal.selection = sel; - win.queueRender() catch |err| - log.err("error scheduling render in mouseButtinCallback err={}", .{err}); + self.io.terminal.selection = sel; + try self.queueRender(); } }, @@ -1451,21 +1380,17 @@ fn mouseButtonCallback( } } -fn cursorPosCallback( - window: glfw.Window, - unscaled_xpos: f64, - unscaled_ypos: f64, -) void { +pub fn cursorPosCallback( + self: *Window, + pos: apprt.CursorPos, +) !void { const tracy = trace(@src()); defer tracy.end(); - const win = window.getUserPointer(Window) orelse return; - // 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) { - win.queueRender() catch |err| - log.err("error scheduling render timer in cursorPosCallback err={}", .{err}); + try self.queueRender(); // If the mouse event was handled by imgui, ignore it. if (imgui.IO.get()) |io| { @@ -1474,25 +1399,19 @@ fn cursorPosCallback( } // We are reading/writing state for the remainder - win.renderer_state.mutex.lock(); - defer win.renderer_state.mutex.unlock(); + self.renderer_state.mutex.lock(); + defer self.renderer_state.mutex.unlock(); // Do a mouse report - if (win.io.terminal.modes.mouse_event != .none) { + 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 (win.mouse.click_state) |state, i| { + const button: ?input.MouseButton = button: for (self.mouse.click_state) |state, i| { if (state == .press) break :button @intToEnum(input.MouseButton, i); } else null; - win.mouseReport(button, .motion, win.mouse.mods, .{ - .xpos = unscaled_xpos, - .ypos = unscaled_ypos, - }) catch |err| { - log.err("error reporting mouse event: {}", .{err}); - return; - }; + try self.mouseReport(button, .motion, self.mouse.mods, pos); // If we're doing mouse motion tracking, we do not support text // selection. @@ -1500,26 +1419,24 @@ fn cursorPosCallback( } // If the cursor isn't clicked currently, it doesn't matter - if (win.mouse.click_state[@enumToInt(input.MouseButton.left)] != .press) return; + if (self.mouse.click_state[@enumToInt(input.MouseButton.left)] != .press) return; // All roads lead to requiring a re-render at this pont. - win.queueRender() catch |err| - log.err("error scheduling render timer in cursorPosCallback err={}", .{err}); + try self.queueRender(); // Convert to pixels from screen coords - const pos = win.cursorPosToPixels(.{ .xpos = unscaled_xpos, .ypos = unscaled_ypos }); - const xpos = pos.xpos; - const ypos = pos.ypos; + const xpos = pos.x; + const ypos = pos.y; // Convert to points - const viewport_point = win.posToViewport(xpos, ypos); - const screen_point = viewport_point.toScreen(&win.io.terminal.screen); + const viewport_point = self.posToViewport(xpos, ypos); + const screen_point = viewport_point.toScreen(&self.io.terminal.screen); // Handle dragging depending on click count - switch (win.mouse.left_click_count) { - 1 => win.dragLeftClickSingle(screen_point, xpos), - 2 => win.dragLeftClickDouble(screen_point), - 3 => win.dragLeftClickTriple(screen_point), + switch (self.mouse.left_click_count) { + 1 => self.dragLeftClickSingle(screen_point, xpos), + 2 => self.dragLeftClickDouble(screen_point), + 3 => self.dragLeftClickTriple(screen_point), else => unreachable, } } diff --git a/src/apprt/glfw.zig b/src/apprt/glfw.zig index e33346fb5..4ca564df0 100644 --- a/src/apprt/glfw.zig +++ b/src/apprt/glfw.zig @@ -91,6 +91,9 @@ pub const Window = struct { win.setKeyCallback(keyCallback); win.setFocusCallback(focusCallback); win.setRefreshCallback(refreshCallback); + win.setScrollCallback(scrollCallback); + win.setCursorPosCallback(cursorPosCallback); + win.setMouseButtonCallback(mouseButtonCallback); // Build our result return Window{ @@ -164,12 +167,45 @@ pub const Window = struct { 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 { + const unscaled_pos = try self.window.getCursorPos(); + const pos = try self.cursorPosToPixels(unscaled_pos); + return apprt.CursorPos{ + .x = @floatCast(f32, pos.xpos), + .y = @floatCast(f32, pos.ypos), + }; + } + /// Set the flag that notes this window should be closed for the next /// iteration of the event loop. pub fn setShouldClose(self: *Window) void { self.window.setShouldClose(true); } + /// 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 { + // 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. + const size = try self.window.getSize(); + const fb_size = try self.window.getFramebufferSize(); + + // If our framebuffer and screen are the same, then there is no scaling + // happening and we can short-circuit by returning the pos as-is. + if (fb_size.width == size.width and fb_size.height == size.height) + return pos; + + const x_scale = @intToFloat(f64, fb_size.width) / @intToFloat(f64, size.width); + const y_scale = @intToFloat(f64, fb_size.height) / @intToFloat(f64, size.height); + return .{ + .xpos = pos.xpos * x_scale, + .ypos = pos.ypos * y_scale, + }; + } + fn sizeCallback(window: glfw.Window, width: i32, height: i32) void { _ = width; _ = height; @@ -374,4 +410,81 @@ pub const Window = struct { return; }; } + + fn scrollCallback(window: glfw.Window, xoff: f64, yoff: f64) void { + const tracy = trace(@src()); + defer tracy.end(); + + const core_win = window.getUserPointer(CoreWindow) orelse return; + core_win.scrollCallback(xoff, yoff) catch |err| { + log.err("error in scroll callback err={}", .{err}); + return; + }; + } + + fn cursorPosCallback( + window: glfw.Window, + unscaled_xpos: f64, + unscaled_ypos: f64, + ) void { + const tracy = trace(@src()); + defer tracy.end(); + + const core_win = window.getUserPointer(CoreWindow) orelse return; + + // Convert our unscaled x/y to scaled. + const pos = core_win.windowing_system.cursorPosToPixels(.{ + .xpos = unscaled_xpos, + .ypos = unscaled_ypos, + }) catch |err| { + log.err( + "error converting cursor pos to scaled pixels in cursor pos callback err={}", + .{err}, + ); + return; + }; + + core_win.cursorPosCallback(.{ + .x = @floatCast(f32, pos.xpos), + .y = @floatCast(f32, pos.ypos), + }) catch |err| { + log.err("error in cursor pos callback err={}", .{err}); + return; + }; + } + + fn mouseButtonCallback( + window: glfw.Window, + glfw_button: glfw.MouseButton, + glfw_action: glfw.Action, + glfw_mods: glfw.Mods, + ) void { + const tracy = trace(@src()); + defer tracy.end(); + + const core_win = window.getUserPointer(CoreWindow) orelse return; + + // Convert glfw button to input button + const mods = @bitCast(input.Mods, glfw_mods); + const button: input.MouseButton = switch (glfw_button) { + .left => .left, + .right => .right, + .middle => .middle, + .four => .four, + .five => .five, + .six => .six, + .seven => .seven, + .eight => .eight, + }; + const action: input.MouseButtonState = switch (glfw_action) { + .press => .press, + .release => .release, + else => unreachable, + }; + + core_win.mouseButtonCallback(action, button, mods) catch |err| { + log.err("error in scroll callback err={}", .{err}); + return; + }; + } }; diff --git a/src/apprt/structs.zig b/src/apprt/structs.zig index cd0aac678..74fa2fcd1 100644 --- a/src/apprt/structs.zig +++ b/src/apprt/structs.zig @@ -11,3 +11,9 @@ pub const WindowSize = struct { width: u32, height: u32, }; + +/// The position of the cursor in pixels. +pub const CursorPos = struct { + x: f32, + y: f32, +}; From ba0cbecd79ea0773189531bfe65862a9736351c8 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 30 Dec 2022 15:18:32 -0800 Subject: [PATCH 09/14] core window doesn't have reference to glfw window anymore! --- src/App.zig | 2 +- src/Window.zig | 12 ++---------- src/apprt/glfw.zig | 9 +++++++++ 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/src/App.zig b/src/App.zig index 514dc813f..b83109f20 100644 --- a/src/App.zig +++ b/src/App.zig @@ -203,7 +203,7 @@ fn setQuit(self: *App) !void { // Mark that all our windows should close for (self.windows.items) |window| { - window.window.setShouldClose(true); + window.windowing_system.setShouldClose(); } } diff --git a/src/Window.zig b/src/Window.zig index eb5f19e58..6559acc31 100644 --- a/src/Window.zig +++ b/src/Window.zig @@ -55,12 +55,6 @@ font_lib: font.Library, font_group: *font.GroupCache, font_size: font.face.DesiredSize, -/// The glfw window handle. -window: glfw.Window, - -/// The glfw mouse cursor handle. -cursor: glfw.Cursor, - /// Imgui context imgui_ctx: if (DevMode.enabled) *imgui.Context else void, @@ -356,8 +350,6 @@ pub fn create(alloc: Allocator, app: *App, config: *const Config) !*Window { .font_lib = font_lib, .font_group = font_group, .font_size = font_size, - .window = winsys.window, - .cursor = winsys.cursor, .renderer = renderer_impl, .renderer_thread = render_thread, .renderer_state = .{ @@ -497,7 +489,7 @@ pub fn destroy(self: *Window) void { } pub fn shouldClose(self: Window) bool { - return self.window.shouldClose(); + return self.windowing_system.shouldClose(); } /// Add a window to the tab group of this window. @@ -521,7 +513,7 @@ pub fn handleMessage(self: *Window, msg: Message) !void { // We know that our title should end in 0. const slice = std.mem.sliceTo(@ptrCast([*:0]const u8, v), 0); log.debug("changing title \"{s}\"", .{slice}); - try self.window.setTitle(slice.ptr); + try self.windowing_system.setTitle(slice); }, .cell_size => |size| try self.setCellSize(size), diff --git a/src/apprt/glfw.zig b/src/apprt/glfw.zig index 4ca564df0..4973ca44f 100644 --- a/src/apprt/glfw.zig +++ b/src/apprt/glfw.zig @@ -184,6 +184,15 @@ pub const Window = struct { self.window.setShouldClose(true); } + /// Returns true if the window is flagged to close. + pub fn shouldClose(self: *const Window) bool { + return self.window.shouldClose(); + } + + pub fn setTitle(self: *Window, slice: [:0]const u8) !void { + try self.window.setTitle(slice.ptr); + } + /// 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 { From 71355ada3104ddc9f166a5d728cc74839cb0a0b4 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 30 Dec 2022 15:20:45 -0800 Subject: [PATCH 10/14] window should reach into glfw for addWindow for now --- src/Window.zig | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/Window.zig b/src/Window.zig index 6559acc31..0d4d9fb59 100644 --- a/src/Window.zig +++ b/src/Window.zig @@ -496,10 +496,16 @@ pub fn shouldClose(self: Window) bool { pub fn addWindow(self: Window, other: *Window) void { assert(builtin.target.isDarwin()); + // This has a hard dependency on GLFW currently. If we want to support + // this in other windowing systems we should abstract this. This is NOT + // the right interface. + const self_win = glfwNative.getCocoaWindow(self.windowing_system.window).?; + const other_win = glfwNative.getCocoaWindow(other.windowing_system.window).?; + const NSWindowOrderingMode = enum(isize) { below = -1, out = 0, above = 1 }; - const nswindow = objc.Object.fromId(glfwNative.getCocoaWindow(self.window).?); + const nswindow = objc.Object.fromId(self_win); nswindow.msgSend(void, objc.sel("addTabbedWindow:ordered:"), .{ - objc.Object.fromId(glfwNative.getCocoaWindow(other.window).?), + objc.Object.fromId(other_win), NSWindowOrderingMode.above, }); } From 8907104e7c692cef713cd39d835da281a96bbf40 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 30 Dec 2022 15:22:18 -0800 Subject: [PATCH 11/14] comments --- src/Window.zig | 6 +----- src/apprt/glfw.zig | 1 + 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/src/Window.zig b/src/Window.zig index 0d4d9fb59..f2348a94e 100644 --- a/src/Window.zig +++ b/src/Window.zig @@ -761,10 +761,6 @@ pub fn keyCallback( self.ignore_char = false; if (action == .press or action == .repeat) { - // Convert our glfw input into a platform agnostic trigger. When we - // extract the platform out of this file, we'll pull a lot of this out - // into a function. For now, this is the only place we do it so we just - // put it right here. const trigger: input.Binding.Trigger = .{ .mods = mods, .key = key, @@ -941,7 +937,7 @@ pub fn keyCallback( // Handle non-printables const char: u8 = char: { const mods_int = @bitCast(u8, mods); - const ctrl_only = @bitCast(u8, glfw.Mods{ .control = true }); + 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. diff --git a/src/apprt/glfw.zig b/src/apprt/glfw.zig index 4973ca44f..1a94d17a7 100644 --- a/src/apprt/glfw.zig +++ b/src/apprt/glfw.zig @@ -189,6 +189,7 @@ pub const Window = struct { return self.window.shouldClose(); } + /// Set the title of the window. pub fn setTitle(self: *Window, slice: [:0]const u8) !void { try self.window.setTitle(slice.ptr); } From 0e73c5eb936c7db51c6dc7d9deb1aa34293a3472 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 30 Dec 2022 15:29:36 -0800 Subject: [PATCH 12/14] apprt: clipboard --- src/Window.zig | 8 ++++---- src/apprt/glfw.zig | 14 ++++++++++++++ 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/src/Window.zig b/src/Window.zig index f2348a94e..e127d8535 100644 --- a/src/Window.zig +++ b/src/Window.zig @@ -543,7 +543,7 @@ fn clipboardRead(self: *const Window, kind: u8) !void { return; } - const data = glfw.getClipboardString() catch |err| { + const data = self.windowing_system.getClipboardString() catch |err| { log.warn("error reading clipboard: {}", .{err}); return; }; @@ -591,7 +591,7 @@ fn clipboardWrite(self: *const Window, data: []const u8) !void { try dec.decode(buf, data); assert(buf[buf.len] == 0); - glfw.setClipboardString(buf) catch |err| { + self.windowing_system.setClipboardString(buf) catch |err| { log.err("error setting clipboard string err={}", .{err}); return; }; @@ -821,7 +821,7 @@ pub fn keyCallback( }; defer self.alloc.free(buf); - glfw.setClipboardString(buf) catch |err| { + self.windowing_system.setClipboardString(buf) catch |err| { log.err("error setting clipboard string err={}", .{err}); return; }; @@ -829,7 +829,7 @@ pub fn keyCallback( }, .paste_from_clipboard => { - const data = glfw.getClipboardString() catch |err| { + const data = self.windowing_system.getClipboardString() catch |err| { log.warn("error reading clipboard: {}", .{err}); return; }; diff --git a/src/apprt/glfw.zig b/src/apprt/glfw.zig index 1a94d17a7..8e8bd58fa 100644 --- a/src/apprt/glfw.zig +++ b/src/apprt/glfw.zig @@ -194,6 +194,20 @@ pub const Window = struct { try 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 { + _ = self; + return try glfw.getClipboardString(); + } + + /// Set the clipboard. + pub fn setClipboardString(self: *const Window, val: [:0]const u8) !void { + _ = self; + try 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 { From d5895f903479890c2fb61fd80dce32a5bbd7e84e Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 30 Dec 2022 15:32:36 -0800 Subject: [PATCH 13/14] rename windowing_system to just window --- src/App.zig | 2 +- src/Window.zig | 48 +++++++++++++++++++++++----------------------- src/apprt/glfw.zig | 4 ++-- 3 files changed, 27 insertions(+), 27 deletions(-) diff --git a/src/App.zig b/src/App.zig index b83109f20..9ebf7d698 100644 --- a/src/App.zig +++ b/src/App.zig @@ -203,7 +203,7 @@ fn setQuit(self: *App) !void { // Mark that all our windows should close for (self.windows.items) |window| { - window.windowing_system.setShouldClose(); + window.window.setShouldClose(); } } diff --git a/src/Window.zig b/src/Window.zig index e127d8535..1729ced23 100644 --- a/src/Window.zig +++ b/src/Window.zig @@ -48,7 +48,7 @@ alloc: Allocator, app: *App, /// The windowing system state -windowing_system: apprt.runtime.Window, +window: apprt.runtime.Window, /// The font structures font_lib: font.Library, @@ -133,15 +133,15 @@ pub fn create(alloc: Allocator, app: *App, config: *const Config) !*Window { errdefer alloc.destroy(self); // Create the windowing system - var winsys = try apprt.runtime.Window.init(app, self); - errdefer winsys.deinit(); + var window = try apprt.runtime.Window.init(app, self); + errdefer window.deinit(); // Initialize our renderer with our initialized windowing system. - try Renderer.windowInit(winsys); + try Renderer.windowInit(window); // Determine our DPI configurations so we can properly configure // font points to pixels and handle other high-DPI scaling factors. - const content_scale = try winsys.getContentScale(); + const content_scale = try window.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={}", .{ @@ -298,7 +298,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 winsys.getSize(); + const window_size = try window.getSize(); const screen_size: renderer.ScreenSize = .{ .width = window_size.width, .height = window_size.height, @@ -316,7 +316,7 @@ pub fn create(alloc: Allocator, app: *App, config: *const Config) !*Window { // Create the renderer thread var render_thread = try renderer.Thread.init( alloc, - winsys, + window, &self.renderer, &self.renderer_state, ); @@ -346,7 +346,7 @@ pub fn create(alloc: Allocator, app: *App, config: *const Config) !*Window { self.* = .{ .alloc = alloc, .app = app, - .windowing_system = winsys, + .window = window, .font_lib = font_lib, .font_group = font_group, .font_size = font_size, @@ -413,12 +413,12 @@ pub fn create(alloc: Allocator, app: *App, config: *const Config) !*Window { DevMode.instance.window = self; // Let our renderer setup - try renderer_impl.initDevMode(winsys); + try renderer_impl.initDevMode(window); } // 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(winsys); + try renderer_impl.finalizeWindowInit(window); // Start our renderer thread self.renderer_thr = try std.Thread.spawn( @@ -447,7 +447,7 @@ pub fn destroy(self: *Window) void { self.renderer_thr.join(); // We need to become the active rendering thread again - self.renderer.threadEnter(self.windowing_system) catch unreachable; + self.renderer.threadEnter(self.window) catch unreachable; self.renderer_thread.deinit(); // If we are devmode-owning, clean that up. @@ -477,7 +477,7 @@ pub fn destroy(self: *Window) void { self.io.deinit(); } - self.windowing_system.deinit(); + self.window.deinit(); self.font_group.deinit(self.alloc); self.font_lib.deinit(); @@ -489,7 +489,7 @@ pub fn destroy(self: *Window) void { } pub fn shouldClose(self: Window) bool { - return self.windowing_system.shouldClose(); + return self.window.shouldClose(); } /// Add a window to the tab group of this window. @@ -499,8 +499,8 @@ pub fn addWindow(self: Window, other: *Window) void { // This has a hard dependency on GLFW currently. If we want to support // this in other windowing systems we should abstract this. This is NOT // the right interface. - const self_win = glfwNative.getCocoaWindow(self.windowing_system.window).?; - const other_win = glfwNative.getCocoaWindow(other.windowing_system.window).?; + const self_win = glfwNative.getCocoaWindow(self.window.window).?; + const other_win = glfwNative.getCocoaWindow(other.window.window).?; const NSWindowOrderingMode = enum(isize) { below = -1, out = 0, above = 1 }; const nswindow = objc.Object.fromId(self_win); @@ -519,7 +519,7 @@ pub fn handleMessage(self: *Window, msg: Message) !void { // 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.windowing_system.setTitle(slice); + try self.window.setTitle(slice); }, .cell_size => |size| try self.setCellSize(size), @@ -543,7 +543,7 @@ fn clipboardRead(self: *const Window, kind: u8) !void { return; } - const data = self.windowing_system.getClipboardString() catch |err| { + const data = self.window.getClipboardString() catch |err| { log.warn("error reading clipboard: {}", .{err}); return; }; @@ -591,7 +591,7 @@ fn clipboardWrite(self: *const Window, data: []const u8) !void { try dec.decode(buf, data); assert(buf[buf.len] == 0); - self.windowing_system.setClipboardString(buf) catch |err| { + self.window.setClipboardString(buf) catch |err| { log.err("error setting clipboard string err={}", .{err}); return; }; @@ -821,7 +821,7 @@ pub fn keyCallback( }; defer self.alloc.free(buf); - self.windowing_system.setClipboardString(buf) catch |err| { + self.window.setClipboardString(buf) catch |err| { log.err("error setting clipboard string err={}", .{err}); return; }; @@ -829,7 +829,7 @@ pub fn keyCallback( }, .paste_from_clipboard => { - const data = self.windowing_system.getClipboardString() catch |err| { + const data = self.window.getClipboardString() catch |err| { log.warn("error reading clipboard: {}", .{err}); return; }; @@ -917,7 +917,7 @@ pub fn keyCallback( self.app.wakeup(); }, - .close_window => self.windowing_system.setShouldClose(), + .close_window => self.window.setShouldClose(), .quit => { _ = self.app.mailbox.push(.{ @@ -1049,7 +1049,7 @@ pub fn scrollCallback(self: *Window, xoff: f64, yoff: f64) !void { // If we're scrolling up or down, then send a mouse event. This requires // a lock since we read terminal state. if (yoff != 0) { - const pos = try self.windowing_system.getCursorPos(); + const pos = try self.window.getCursorPos(); try self.mouseReport(if (yoff < 0) .five else .four, .press, self.mouse.mods, pos); } } @@ -1284,7 +1284,7 @@ pub fn mouseButtonCallback( // Report mouse events if enabled if (self.io.terminal.modes.mouse_event != .none) { - const pos = try self.windowing_system.getCursorPos(); + const pos = try self.window.getCursorPos(); const report_action: MouseReportAction = switch (action) { .press => .press, @@ -1302,7 +1302,7 @@ pub fn mouseButtonCallback( // For left button clicks we always record some information for // selection/highlighting purposes. if (button == .left and action == .press) { - const pos = try self.windowing_system.getCursorPos(); + const pos = try self.window.getCursorPos(); // If we move our cursor too much between clicks then we reset // the multi-click state. diff --git a/src/apprt/glfw.zig b/src/apprt/glfw.zig index 8e8bd58fa..53c28a4a9 100644 --- a/src/apprt/glfw.zig +++ b/src/apprt/glfw.zig @@ -238,7 +238,7 @@ pub const Window = struct { // 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.windowing_system.getSize() catch |err| { + const size = core_win.window.getSize() catch |err| { log.err("error querying window size for size callback err={}", .{err}); return; }; @@ -457,7 +457,7 @@ pub const Window = struct { const core_win = window.getUserPointer(CoreWindow) orelse return; // Convert our unscaled x/y to scaled. - const pos = core_win.windowing_system.cursorPosToPixels(.{ + const pos = core_win.window.cursorPosToPixels(.{ .xpos = unscaled_xpos, .ypos = unscaled_ypos, }) catch |err| { From b502d5aa7dc76dff33df17b3077b612ba0b4da5e Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 30 Dec 2022 15:36:25 -0800 Subject: [PATCH 14/14] apprt: window size limits --- src/Window.zig | 9 ++++----- src/apprt/glfw.zig | 17 +++++++++++++++++ 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/src/Window.zig b/src/Window.zig index 1729ced23..4e88fb77e 100644 --- a/src/Window.zig +++ b/src/Window.zig @@ -379,11 +379,10 @@ 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. - // TODO: - // try window.setSizeLimits(.{ - // .width = @floatToInt(u32, cell_size.width * 10), - // .height = @floatToInt(u32, cell_size.height * 4), - // }, .{ .width = null, .height = null }); + try window.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 diff --git a/src/apprt/glfw.zig b/src/apprt/glfw.zig index 53c28a4a9..d48939191 100644 --- a/src/apprt/glfw.zig +++ b/src/apprt/glfw.zig @@ -149,6 +149,23 @@ pub const Window = struct { } } + /// Set the size limits of the window. + /// 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 { + try self.window.setSizeLimits(.{ + .width = min.width, + .height = min.height, + }, if (max_) |max| .{ + .width = max.width, + .height = max.height, + } else .{ + .width = null, + .height = null, + }); + } + /// Returns the content scale for the created window. pub fn getContentScale(self: *const Window) !apprt.ContentScale { const scale = try self.window.getContentScale();