mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-05-05 07:48:38 +03:00

This fixes two things: 1. Issue #294: cascade point for new windows is set when creating new tabs 2. Cascade point was *not* reset when closing windows, which lead to a big "gap" appearing when, say, opening 5 windows, closing 4, opening a new window.
185 lines
7.2 KiB
Swift
185 lines
7.2 KiB
Swift
import Cocoa
|
|
import Combine
|
|
import GhosttyKit
|
|
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/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 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.
|
|
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 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.NewSurfaceConfigKey]
|
|
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) {
|
|
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 configAny = notification.userInfo?[Ghostty.Notification.NewSurfaceConfigKey]
|
|
let config = configAny as? ghostty_surface_config_s
|
|
|
|
self.addNewTab(to: window, withBaseConfig: config)
|
|
}
|
|
|
|
private func addNewTab(to window: NSWindow, withBaseConfig config: ghostty_surface_config_s? = nil) {
|
|
guard let controller = createWindowController(withBaseConfig: config, cascade: false) else { return }
|
|
guard let newWindow = addManagedWindow(windowController: controller)?.window else { return }
|
|
window.addTabbedWindow(newWindow, ordered: .above)
|
|
newWindow.makeKeyAndOrderFront(nil)
|
|
}
|
|
|
|
private func createWindowController(withBaseConfig config: ghostty_surface_config_s? = nil, cascade: Bool = true) -> PrimaryWindowController? {
|
|
guard let appDelegate = NSApplication.shared.delegate as? AppDelegate else { return nil }
|
|
|
|
let window = PrimaryWindow.create(ghostty: ghostty, appDelegate: appDelegate, baseConfig: config)
|
|
if (cascade) {
|
|
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 })
|
|
|
|
// If we remove a window, we reset the cascade point to the key window so that
|
|
// the next window cascade's from that one.
|
|
if let focusedWindow = NSApplication.shared.keyWindow {
|
|
let frame = focusedWindow.frame
|
|
Self.lastCascadePoint = NSPoint(x: frame.minX, y: frame.maxY)
|
|
}
|
|
}
|
|
}
|