From 2e9b7876686f429a0b85df846d7c092f8b8963ca Mon Sep 17 00:00:00 2001 From: Thorsten Ball Date: Sat, 19 Aug 2023 09:29:56 +0200 Subject: [PATCH 1/3] macOS: new window can inherit font size This is the second part of #281 and adds the same mechanism that was added for tabs in #296 for windows. It works exactly the same way. --- include/ghostty.h | 3 ++ macos/Sources/AppDelegate.swift | 2 +- .../Primary Window/PrimaryWindowManager.swift | 46 ++++++++++++++++--- macos/Sources/Ghostty/AppState.swift | 24 ++++++++-- macos/Sources/Ghostty/Package.swift | 6 ++- src/Surface.zig | 14 ++++-- src/apprt/embedded.zig | 23 +++++++++- src/input/Binding.zig | 1 + 8 files changed, 99 insertions(+), 20 deletions(-) diff --git a/include/ghostty.h b/include/ghostty.h index a6fc51d9b..9a627e11d 100644 --- a/include/ghostty.h +++ b/include/ghostty.h @@ -234,6 +234,7 @@ typedef enum { GHOSTTY_BINDING_COPY_TO_CLIPBOARD, GHOSTTY_BINDING_PASTE_FROM_CLIPBOARD, GHOSTTY_BINDING_NEW_TAB, + GHOSTTY_BINDING_NEW_WINDOW, } ghostty_binding_action_e; // Fully defined types. This MUST be kept in sync with equivalent Zig @@ -253,6 +254,7 @@ typedef const char* (*ghostty_runtime_read_clipboard_cb)(void *, ghostty_clipboa typedef void (*ghostty_runtime_write_clipboard_cb)(void *, const char *, ghostty_clipboard_e); typedef void (*ghostty_runtime_new_split_cb)(void *, ghostty_split_direction_e); typedef void (*ghostty_runtime_new_tab_cb)(void *, ghostty_surface_config_s); +typedef void (*ghostty_runtime_new_window_cb)(void *, ghostty_surface_config_s); typedef void (*ghostty_runtime_close_surface_cb)(void *, bool); typedef void (*ghostty_runtime_focus_split_cb)(void *, ghostty_split_focus_direction_e); typedef void (*ghostty_runtime_goto_tab_cb)(void *, int32_t); @@ -268,6 +270,7 @@ typedef struct { ghostty_runtime_write_clipboard_cb write_clipboard_cb; ghostty_runtime_new_split_cb new_split_cb; ghostty_runtime_new_tab_cb new_tab_cb; + ghostty_runtime_new_window_cb new_window_cb; ghostty_runtime_close_surface_cb close_surface_cb; ghostty_runtime_focus_split_cb focus_split_cb; ghostty_runtime_goto_tab_cb goto_tab_cb; diff --git a/macos/Sources/AppDelegate.swift b/macos/Sources/AppDelegate.swift index fa468221d..b13bd0aa9 100644 --- a/macos/Sources/AppDelegate.swift +++ b/macos/Sources/AppDelegate.swift @@ -77,7 +77,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject { } @IBAction func newWindow(_ sender: Any?) { - windowManager.addNewWindow() + windowManager.newWindow() } @IBAction func newTab(_ sender: Any?) { diff --git a/macos/Sources/Features/Primary Window/PrimaryWindowManager.swift b/macos/Sources/Features/Primary Window/PrimaryWindowManager.swift index 987dc86a2..4280877c0 100644 --- a/macos/Sources/Features/Primary Window/PrimaryWindowManager.swift +++ b/macos/Sources/Features/Primary Window/PrimaryWindowManager.swift @@ -46,21 +46,32 @@ class PrimaryWindowManager { init(ghostty: Ghostty.AppState) { self.ghostty = ghostty - // Register self as observer for the NewTab notification that - // is triggered via callback from Zig code. - NotificationCenter.default.addObserver( + // Register self as observer for the NewTab/NewWindow notifications that + // are triggered via callback from Zig code. + let center = NotificationCenter.default; + center.addObserver( self, selector: #selector(onNewTab), name: Ghostty.Notification.ghosttyNewTab, object: nil) + center.addObserver( + self, + selector: #selector(onNewWindow), + name: Ghostty.Notification.ghosttyNewWindow, + object: nil) } deinit { - // Clean up the observer. - NotificationCenter.default.removeObserver( + // Clean up the observers. + let center = NotificationCenter.default; + center.removeObserver( self, name: Ghostty.Notification.ghosttyNewTab, object: nil) + center.removeObserver( + self, + name: Ghostty.Notification.ghosttyNewWindow, + object: nil) } /// Add the initial window for the application. This should only be called once from the AppDelegate. @@ -73,12 +84,33 @@ class PrimaryWindowManager { } } - func addNewWindow() { - guard let controller = createWindowController() else { return } + func newWindow() { + if let window = mainWindow as? PrimaryWindow { + // If we already have a window, we go through Zig core code, which calls back into Swift. + self.triggerNewWindow(withParent: window) + } else { + self.addNewWindow() + } + } + + func triggerNewWindow(withParent window: PrimaryWindow) { + guard let surface = window.focusedSurfaceWrapper.surface else { return } + ghostty.newWindow(surface: surface) + } + + func addNewWindow(withBaseConfig config: ghostty_surface_config_s? = nil) { + guard let controller = createWindowController(withBaseConfig: config) else { return } guard let newWindow = addManagedWindow(windowController: controller)?.window else { return } newWindow.makeKeyAndOrderFront(nil) } + @objc private func onNewWindow(notification: SwiftUI.Notification) { + let configAny = notification.userInfo?[Ghostty.Notification.NewWindowKey] + let config = configAny as? ghostty_surface_config_s + + self.addNewWindow(withBaseConfig: config) + } + // triggerNewTab tells the Zig core code to create a new tab, which then calls // back into Swift code. func triggerNewTab(for window: PrimaryWindow) { diff --git a/macos/Sources/Ghostty/AppState.swift b/macos/Sources/Ghostty/AppState.swift index 1a0a17f4b..51ff9db0a 100644 --- a/macos/Sources/Ghostty/AppState.swift +++ b/macos/Sources/Ghostty/AppState.swift @@ -62,6 +62,7 @@ extension Ghostty { write_clipboard_cb: { userdata, str, loc in AppState.writeClipboard(userdata, string: str, location: loc) }, new_split_cb: { userdata, direction in AppState.newSplit(userdata, direction: direction) }, new_tab_cb: { userdata, surfaceConfig in AppState.newTab(userdata, config: surfaceConfig) }, + new_window_cb: { userdata, surfaceConfig in AppState.newWindow(userdata, config: surfaceConfig) }, close_surface_cb: { userdata, processAlive in AppState.closeSurface(userdata, processAlive: processAlive) }, focus_split_cb: { userdata, direction in AppState.focusSplit(userdata, direction: direction) }, goto_tab_cb: { userdata, n in AppState.gotoTab(userdata, n: n) }, @@ -142,6 +143,10 @@ extension Ghostty { ghostty_surface_binding_action(surface, GHOSTTY_BINDING_NEW_TAB, nil) } + func newWindow(surface: ghostty_surface_t) { + ghostty_surface_binding_action(surface, GHOSTTY_BINDING_NEW_WINDOW, nil) + } + func split(surface: ghostty_surface_t, direction: ghostty_split_direction_e) { ghostty_surface_split(surface, direction) } @@ -266,13 +271,24 @@ extension Ghostty { static func newTab(_ userdata: UnsafeMutableRawPointer?, config: ghostty_surface_config_s) { guard let surface = self.surfaceUserdata(from: userdata) else { return } - var userInfo: [AnyHashable : Any] = [:]; - userInfo[Notification.NewTabKey] = config; - NotificationCenter.default.post( name: Notification.ghosttyNewTab, object: surface, - userInfo: userInfo + userInfo: [ + Notification.NewTabKey: config + ] + ) + } + + static func newWindow(_ userdata: UnsafeMutableRawPointer?, config: ghostty_surface_config_s) { + guard let surface = self.surfaceUserdata(from: userdata) else { return } + + NotificationCenter.default.post( + name: Notification.ghosttyNewWindow, + object: surface, + userInfo: [ + Notification.NewWindowKey: config + ] ) } diff --git a/macos/Sources/Ghostty/Package.swift b/macos/Sources/Ghostty/Package.swift index 006433c72..b0ca3d560 100644 --- a/macos/Sources/Ghostty/Package.swift +++ b/macos/Sources/Ghostty/Package.swift @@ -79,9 +79,13 @@ extension Ghostty.Notification { static let ghosttyGotoTab = Notification.Name("com.mitchellh.ghostty.gotoTab") static let GotoTabKey = ghosttyGotoTab.rawValue - /// New tab. Has base surface config requestesd in userinfo. + /// New tab. Has base surface config requested in userinfo. static let ghosttyNewTab = Notification.Name("com.mitchellh.ghostty.newTab") static let NewTabKey = ghosttyNewTab.rawValue + + /// New window. Has base surface config requested in userinfo. + static let ghosttyNewWindow = Notification.Name("com.mitchellh.ghostty.newWindow") + static let NewWindowKey = ghosttyNewWindow.rawValue /// Toggle fullscreen of current window static let ghosttyToggleFullscreen = Notification.Name("com.mitchellh.ghostty.toggleFullscreen") diff --git a/src/Surface.zig b/src/Surface.zig index 582c6cd71..173edea18 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -2119,11 +2119,15 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !void } else log.warn("dev mode was not compiled into this binary", .{}), .new_window => { - _ = self.app_mailbox.push(.{ - .new_window = .{ - .parent = self, - }, - }, .{ .instant = {} }); + if (@hasDecl(apprt.Surface, "newWindow")) { + try self.rt_surface.newWindow(); + } else { + _ = self.app_mailbox.push(.{ + .new_window = .{ + .parent = self, + }, + }, .{ .instant = {} }); + } }, .new_tab => { diff --git a/src/apprt/embedded.zig b/src/apprt/embedded.zig index 777b7870e..adae5b09c 100644 --- a/src/apprt/embedded.zig +++ b/src/apprt/embedded.zig @@ -62,6 +62,9 @@ pub const App = struct { /// New tab with options. new_tab: ?*const fn (SurfaceUD, apprt.Surface.Options) callconv(.C) void = null, + /// New window with options. + new_window: ?*const fn (SurfaceUD, apprt.Surface.Options) callconv(.C) void = null, + /// Close the current surface given by this function. close_surface: ?*const fn (SurfaceUD, bool) callconv(.C) void = null, @@ -615,14 +618,29 @@ pub const Surface = struct { return; }; + const options = self.newSurfaceOptions(); + func(self.opts.userdata, options); + } + + pub fn newWindow(self: *const Surface) !void { + const func = self.app.opts.new_window orelse { + log.info("runtime embedder does not support new_window", .{}); + return; + }; + + const options = self.newSurfaceOptions(); + func(self.opts.userdata, options); + } + + fn newSurfaceOptions(self: *const Surface) apprt.Surface.Options { const font_size: u16 = font_size: { if (!self.app.config.@"window-inherit-font-size") break :font_size 0; break :font_size self.core_surface.font_size.points; }; - func(self.opts.userdata, .{ + return .{ .font_size = font_size, - }); + }; } /// The cursor position from the host directly is in screen coordinates but @@ -855,6 +873,7 @@ pub const CAPI = struct { .copy_to_clipboard => .{ .copy_to_clipboard = {} }, .paste_from_clipboard => .{ .paste_from_clipboard = {} }, .new_tab => .{ .new_tab = {} }, + .new_window => .{ .new_window = {} }, }; ptr.core_surface.performBindingAction(action) catch |err| { diff --git a/src/input/Binding.zig b/src/input/Binding.zig index 400e9695e..8e5e9ec1e 100644 --- a/src/input/Binding.zig +++ b/src/input/Binding.zig @@ -277,6 +277,7 @@ pub const Key = enum(c_int) { copy_to_clipboard, paste_from_clipboard, new_tab, + new_window, }; /// Trigger is the associated key state that can trigger an action. From a76aea4d0a40db4c240ca4de468d6ca7347cd11f Mon Sep 17 00:00:00 2001 From: Thorsten Ball Date: Sat, 19 Aug 2023 09:59:04 +0200 Subject: [PATCH 2/3] macOS: new split can inherit font size --- include/ghostty.h | 2 +- .../Features/Primary Window/PrimaryWindowManager.swift | 4 ++-- macos/Sources/Ghostty/AppState.swift | 9 +++++---- macos/Sources/Ghostty/Ghostty.SplitView.swift | 9 ++++++--- macos/Sources/Ghostty/Package.swift | 5 +++-- src/apprt/embedded.zig | 5 +++-- 6 files changed, 20 insertions(+), 14 deletions(-) diff --git a/include/ghostty.h b/include/ghostty.h index 9a627e11d..bcaba1eac 100644 --- a/include/ghostty.h +++ b/include/ghostty.h @@ -252,7 +252,7 @@ typedef const ghostty_config_t (*ghostty_runtime_reload_config_cb)(void *); typedef void (*ghostty_runtime_set_title_cb)(void *, const char *); typedef const char* (*ghostty_runtime_read_clipboard_cb)(void *, ghostty_clipboard_e); typedef void (*ghostty_runtime_write_clipboard_cb)(void *, const char *, ghostty_clipboard_e); -typedef void (*ghostty_runtime_new_split_cb)(void *, ghostty_split_direction_e); +typedef void (*ghostty_runtime_new_split_cb)(void *, ghostty_split_direction_e, ghostty_surface_config_s); typedef void (*ghostty_runtime_new_tab_cb)(void *, ghostty_surface_config_s); typedef void (*ghostty_runtime_new_window_cb)(void *, ghostty_surface_config_s); typedef void (*ghostty_runtime_close_surface_cb)(void *, bool); diff --git a/macos/Sources/Features/Primary Window/PrimaryWindowManager.swift b/macos/Sources/Features/Primary Window/PrimaryWindowManager.swift index 4280877c0..343a77918 100644 --- a/macos/Sources/Features/Primary Window/PrimaryWindowManager.swift +++ b/macos/Sources/Features/Primary Window/PrimaryWindowManager.swift @@ -105,7 +105,7 @@ class PrimaryWindowManager { } @objc private func onNewWindow(notification: SwiftUI.Notification) { - let configAny = notification.userInfo?[Ghostty.Notification.NewWindowKey] + let configAny = notification.userInfo?[Ghostty.Notification.NewSurfaceConfigKey] let config = configAny as? ghostty_surface_config_s self.addNewWindow(withBaseConfig: config) @@ -130,7 +130,7 @@ class PrimaryWindowManager { guard let surfaceView = notification.object as? Ghostty.SurfaceView else { return } guard let window = surfaceView.window else { return } - let configAny = notification.userInfo?[Ghostty.Notification.NewTabKey] + let configAny = notification.userInfo?[Ghostty.Notification.NewSurfaceConfigKey] let config = configAny as? ghostty_surface_config_s self.addNewTab(to: window, withBaseConfig: config) diff --git a/macos/Sources/Ghostty/AppState.swift b/macos/Sources/Ghostty/AppState.swift index 51ff9db0a..7643b7d0d 100644 --- a/macos/Sources/Ghostty/AppState.swift +++ b/macos/Sources/Ghostty/AppState.swift @@ -60,7 +60,7 @@ extension Ghostty { set_title_cb: { userdata, title in AppState.setTitle(userdata, title: title) }, read_clipboard_cb: { userdata, loc in AppState.readClipboard(userdata, location: loc) }, write_clipboard_cb: { userdata, str, loc in AppState.writeClipboard(userdata, string: str, location: loc) }, - new_split_cb: { userdata, direction in AppState.newSplit(userdata, direction: direction) }, + new_split_cb: { userdata, direction, surfaceConfig in AppState.newSplit(userdata, direction: direction, config: surfaceConfig) }, new_tab_cb: { userdata, surfaceConfig in AppState.newTab(userdata, config: surfaceConfig) }, new_window_cb: { userdata, surfaceConfig in AppState.newWindow(userdata, config: surfaceConfig) }, close_surface_cb: { userdata, processAlive in AppState.closeSurface(userdata, processAlive: processAlive) }, @@ -164,10 +164,11 @@ extension Ghostty { // MARK: Ghostty Callbacks - static func newSplit(_ userdata: UnsafeMutableRawPointer?, direction: ghostty_split_direction_e) { + static func newSplit(_ userdata: UnsafeMutableRawPointer?, direction: ghostty_split_direction_e, config: ghostty_surface_config_s) { guard let surface = self.surfaceUserdata(from: userdata) else { return } NotificationCenter.default.post(name: Notification.ghosttyNewSplit, object: surface, userInfo: [ "direction": direction, + Notification.NewSurfaceConfigKey: config, ]) } @@ -275,7 +276,7 @@ extension Ghostty { name: Notification.ghosttyNewTab, object: surface, userInfo: [ - Notification.NewTabKey: config + Notification.NewSurfaceConfigKey: config ] ) } @@ -287,7 +288,7 @@ extension Ghostty { name: Notification.ghosttyNewWindow, object: surface, userInfo: [ - Notification.NewWindowKey: config + Notification.NewSurfaceConfigKey: config ] ) } diff --git a/macos/Sources/Ghostty/Ghostty.SplitView.swift b/macos/Sources/Ghostty/Ghostty.SplitView.swift index a3e224139..553ede47f 100644 --- a/macos/Sources/Ghostty/Ghostty.SplitView.swift +++ b/macos/Sources/Ghostty/Ghostty.SplitView.swift @@ -82,13 +82,13 @@ extension Ghostty { /// A container is always initialized from some prior leaf because a split has to originate /// from a non-split value. When initializing, we inherit the leaf's surface and then /// initialize a new surface for the new pane. - init(from: Leaf) { + init(from: Leaf, baseConfig: ghostty_surface_config_s? = nil) { self.app = from.app // Initially, both topLeft and bottomRight are in the "nosplit" // state since this is a new split. self.topLeft = .noSplit(from) - self.bottomRight = .noSplit(.init(app, nil)) + self.bottomRight = .noSplit(.init(app, baseConfig)) } } @@ -254,6 +254,9 @@ extension Ghostty { } private func onNewSplit(notification: SwiftUI.Notification) { + let configAny = notification.userInfo?[Ghostty.Notification.NewSurfaceConfigKey] + let config = configAny as? ghostty_surface_config_s + // Determine our desired direction guard let directionAny = notification.userInfo?["direction"] else { return } guard let direction = directionAny as? ghostty_split_direction_e else { return } @@ -270,7 +273,7 @@ extension Ghostty { } // Setup our new container since we are now split - let container = SplitNode.Container(from: leaf) + let container = SplitNode.Container(from: leaf, baseConfig: config) // Depending on the direction, change the parent node. This will trigger // the parent to relayout our views. diff --git a/macos/Sources/Ghostty/Package.swift b/macos/Sources/Ghostty/Package.swift index b0ca3d560..7a09fd872 100644 --- a/macos/Sources/Ghostty/Package.swift +++ b/macos/Sources/Ghostty/Package.swift @@ -64,6 +64,9 @@ extension Ghostty { } extension Ghostty.Notification { + /// Used to pass a configuration along when creating a new tab/window/split. + static let NewSurfaceConfigKey = "com.mitchellh.ghostty.newSurfaceConfig" + /// Posted when a new split is requested. The sending object will be the surface that had focus. The /// userdata has one key "direction" with the direction to split to. static let ghosttyNewSplit = Notification.Name("com.mitchellh.ghostty.newSplit") @@ -81,11 +84,9 @@ extension Ghostty.Notification { /// New tab. Has base surface config requested in userinfo. static let ghosttyNewTab = Notification.Name("com.mitchellh.ghostty.newTab") - static let NewTabKey = ghosttyNewTab.rawValue /// New window. Has base surface config requested in userinfo. static let ghosttyNewWindow = Notification.Name("com.mitchellh.ghostty.newWindow") - static let NewWindowKey = ghosttyNewWindow.rawValue /// Toggle fullscreen of current window static let ghosttyToggleFullscreen = Notification.Name("com.mitchellh.ghostty.toggleFullscreen") diff --git a/src/apprt/embedded.zig b/src/apprt/embedded.zig index adae5b09c..fc1a22ed2 100644 --- a/src/apprt/embedded.zig +++ b/src/apprt/embedded.zig @@ -57,7 +57,7 @@ pub const App = struct { /// Create a new split view. If the embedder doesn't support split /// views then this can be null. - new_split: ?*const fn (SurfaceUD, input.SplitDirection) callconv(.C) void = null, + new_split: ?*const fn (SurfaceUD, input.SplitDirection, apprt.Surface.Options) callconv(.C) void = null, /// New tab with options. new_tab: ?*const fn (SurfaceUD, apprt.Surface.Options) callconv(.C) void = null, @@ -236,7 +236,8 @@ pub const Surface = struct { return; }; - func(self.opts.userdata, direction); + const options = self.newSurfaceOptions(); + func(self.opts.userdata, direction, options); } pub fn close(self: *const Surface, process_alive: bool) void { From fbe42fbac7778d006c0377f75ef4dfa51142f156 Mon Sep 17 00:00:00 2001 From: Thorsten Ball Date: Sat, 19 Aug 2023 19:41:34 +0200 Subject: [PATCH 3/3] macOS: reduce mechanism to create windows back to one --- src/Surface.zig | 14 +++++--------- src/apprt/embedded.zig | 11 +++++++++++ 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/src/Surface.zig b/src/Surface.zig index 173edea18..582c6cd71 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -2119,15 +2119,11 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !void } else log.warn("dev mode was not compiled into this binary", .{}), .new_window => { - if (@hasDecl(apprt.Surface, "newWindow")) { - try self.rt_surface.newWindow(); - } else { - _ = self.app_mailbox.push(.{ - .new_window = .{ - .parent = self, - }, - }, .{ .instant = {} }); - } + _ = self.app_mailbox.push(.{ + .new_window = .{ + .parent = self, + }, + }, .{ .instant = {} }); }, .new_tab => { diff --git a/src/apprt/embedded.zig b/src/apprt/embedded.zig index fc1a22ed2..c298c4b9b 100644 --- a/src/apprt/embedded.zig +++ b/src/apprt/embedded.zig @@ -151,6 +151,17 @@ pub const App = struct { _ = surface; // No-op, we use a threaded interface so we're constantly drawing. } + + pub fn newWindow(self: *App, parent: ?*CoreSurface) !void { + _ = self; + + // Right now we only support creating a new window with a parent + // through this code. + // The other case is handled by the embedding runtime. + if (parent) |surface| { + try surface.rt_surface.newWindow(); + } + } }; pub const Surface = struct {