diff --git a/macos/Sources/Features/Terminal/BaseTerminalController.swift b/macos/Sources/Features/Terminal/BaseTerminalController.swift index ce2d5a5f2..529b1d18a 100644 --- a/macos/Sources/Features/Terminal/BaseTerminalController.swift +++ b/macos/Sources/Features/Terminal/BaseTerminalController.swift @@ -734,6 +734,9 @@ class BaseTerminalController: NSWindowController, // it is already a single split. guard surfaceTree.isSplit else { return } + // Extract the drop position from the notification + let dropPoint = notification.userInfo?[Notification.Name.ghosttySurfaceDragEndedNoTargetPointKey] as? NSPoint + // Remove the surface from our tree let removedTree = surfaceTree.remove(targetNode) @@ -748,7 +751,7 @@ class BaseTerminalController: NSWindowController, } replaceSurfaceTree(removedTree, moveFocusFrom: focusedSurface) - _ = TerminalController.newWindow(ghostty, tree: newTree) + _ = TerminalController.newWindow(ghostty, tree: newTree, position: dropPoint) } // MARK: Local Events diff --git a/macos/Sources/Features/Terminal/TerminalController.swift b/macos/Sources/Features/Terminal/TerminalController.swift index bd2c99a22..021b9f394 100644 --- a/macos/Sources/Features/Terminal/TerminalController.swift +++ b/macos/Sources/Features/Terminal/TerminalController.swift @@ -277,9 +277,15 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr /// Create a new window with an existing split tree. /// The window will be sized to match the tree's current view bounds if available. + /// - Parameters: + /// - ghostty: The Ghostty app instance. + /// - tree: The split tree to use for the new window. + /// - position: Optional screen position (top-left corner) for the new window. + /// If nil, the window will cascade from the last cascade point. static func newWindow( _ ghostty: Ghostty.App, - tree: SplitTree + tree: SplitTree, + position: NSPoint? = nil ) -> TerminalController { let c = TerminalController.init(ghostty, withSurfaceTree: tree) @@ -295,7 +301,12 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr } if !window.styleMask.contains(.fullScreen) { - Self.lastCascadePoint = window.cascadeTopLeft(from: Self.lastCascadePoint) + if let position { + window.setFrameTopLeftPoint(position) + window.constrainToScreen() + } else { + Self.lastCascadePoint = window.cascadeTopLeft(from: Self.lastCascadePoint) + } } } diff --git a/macos/Sources/Ghostty/Surface View/SurfaceDragSource.swift b/macos/Sources/Ghostty/Surface View/SurfaceDragSource.swift index 34184e46e..21416ac75 100644 --- a/macos/Sources/Ghostty/Surface View/SurfaceDragSource.swift +++ b/macos/Sources/Ghostty/Surface View/SurfaceDragSource.swift @@ -177,7 +177,11 @@ extension Ghostty { } onDragStateChanged?(true) - beginDraggingSession(with: [item], event: event, source: self) + let session = beginDraggingSession(with: [item], event: event, source: self) + + // We need to disable this so that endedAt happens immediately for our + // drags outside of any targets. + session.animatesToStartingPositionsOnCancelOrFail = false } // MARK: NSDraggingSource @@ -229,7 +233,8 @@ extension Ghostty { if !endsInWindow { NotificationCenter.default.post( name: .ghosttySurfaceDragEndedNoTarget, - object: surfaceView + object: surfaceView, + userInfo: [Foundation.Notification.Name.ghosttySurfaceDragEndedNoTargetPointKey: screenPoint] ) } } @@ -245,4 +250,7 @@ extension Notification.Name { /// released outside a valid drop target) and was not cancelled by the user /// pressing escape. The notification's object is the SurfaceView that was dragged. static let ghosttySurfaceDragEndedNoTarget = Notification.Name("ghosttySurfaceDragEndedNoTarget") + + /// Key for the screen point where the drag ended in the userInfo dictionary. + static let ghosttySurfaceDragEndedNoTargetPointKey = "endedAtPoint" }