ghostty/macos/Sources/Features/Primary Window/PrimaryWindowManager.swift
2023-08-18 06:26:15 +02:00

141 lines
5.5 KiB
Swift

import Cocoa
import Combine
import SwiftUI
// PrimaryWindowManager manages the windows and tabs in the primary window
// of the application. It keeps references to windows and cleans them up when
// they're cloned.
//
// If we ever have multiple tabbed window types we can make this generic but
// right now only our primary window is ever duplicated or tabbed so we're not
// doing that.
//
// It is based on the patterns presented in this blog post:
// https://christiantietze.de/posts/2019/07/nswindow-tabbing-multiple-nswindowcontroller/
class PrimaryWindowManager {
struct ManagedWindow {
let windowController: NSWindowController
let window: NSWindow
let closePublisher: AnyCancellable
}
// Keep track of the last point that our window was launched at so that new
// windows "cascade" over each other and don't just launch directly on top
// of each other.
static var lastCascadePoint = NSPoint(x: 0, y: 0)
/// Returns the main window of the managed window stack.
/// Falls back the first element if no window is main. Note that this would
/// likely be an internal inconsistency we gracefully handle here.
var mainWindow: NSWindow? {
let mainManagedWindow = managedWindows
.first { $0.window.isMainWindow }
// In case we run into the inconsistency, let it crash in debug mode so we
// can fix our window management setup to prevent this from happening.
assert(mainManagedWindow != nil || managedWindows.isEmpty)
return (mainManagedWindow ?? managedWindows.first)
.map { $0.window }
}
private var ghostty: Ghostty.AppState
private var managedWindows: [ManagedWindow] = []
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(
self,
selector: #selector(onNewTab),
name: Ghostty.Notification.ghosttyNewTab,
object: nil)
}
deinit {
// Clean up the observer.
NotificationCenter.default.removeObserver(
self,
name: Ghostty.Notification.ghosttyNewTab,
object: nil)
}
/// Add the initial window for the application. This should only be called once from the AppDelegate.
func addInitialWindow() {
guard let controller = createWindowController() else { return }
controller.showWindow(self)
let result = addManagedWindow(windowController: controller)
if result == nil {
preconditionFailure("Failed to create initial window")
}
}
func addNewWindow() {
guard let controller = createWindowController() else { return }
guard let newWindow = addManagedWindow(windowController: controller)?.window else { return }
newWindow.makeKeyAndOrderFront(nil)
}
// triggerNewTab tells the Zig core code to create a new tab, which then calls
// back into Swift code.
func triggerNewTab(for window: PrimaryWindow) {
guard let surface = window.focusedSurfaceWrapper.surface else { return }
ghostty.newTab(surface: surface)
}
func newTab() {
if let window = mainWindow as? PrimaryWindow {
self.triggerNewTab(for: window)
} else {
self.addNewWindow()
}
}
@objc private func onNewTab(notification: SwiftUI.Notification) {
guard let surfaceView = notification.object as? Ghostty.SurfaceView else { return }
guard let window = surfaceView.window else { return }
let fontSizeAny = notification.userInfo?[Ghostty.Notification.NewTabKey]
let fontSize = fontSizeAny as? UInt8
self.addNewTab(to: window, withFontSize: fontSize)
}
private func addNewTab(to window: NSWindow, withFontSize fontSize: UInt8? = nil) {
guard let controller = createWindowController(withFontSize: fontSize) else { return }
guard let newWindow = addManagedWindow(windowController: controller)?.window else { return }
window.addTabbedWindow(newWindow, ordered: .above)
newWindow.makeKeyAndOrderFront(nil)
}
private func createWindowController(withFontSize fontSize: UInt8? = nil) -> PrimaryWindowController? {
guard let appDelegate = NSApplication.shared.delegate as? AppDelegate else { return nil }
let window = PrimaryWindow.create(ghostty: ghostty, appDelegate: appDelegate, fontSize: fontSize)
Self.lastCascadePoint = window.cascadeTopLeft(from: Self.lastCascadePoint)
let controller = PrimaryWindowController(window: window)
controller.windowManager = self
return controller
}
private func addManagedWindow(windowController: PrimaryWindowController) -> ManagedWindow? {
guard let window = windowController.window else { return nil }
let pubClose = NotificationCenter.default.publisher(for: NSWindow.willCloseNotification, object: window)
.sink { notification in
guard let window = notification.object as? NSWindow else { return }
self.removeWindow(window: window)
}
let managed = ManagedWindow(windowController: windowController, window: window, closePublisher: pubClose)
managedWindows.append(managed)
return managed
}
private func removeWindow(window: NSWindow) {
self.managedWindows.removeAll(where: { $0.window === window })
}
}