macos: hook up onDrop to move splits

This commit is contained in:
Mitchell Hashimoto
2025-12-27 14:47:09 -08:00
parent d92fe44d0d
commit 8f8b5846c6
3 changed files with 64 additions and 8 deletions

View File

@@ -4,7 +4,7 @@ import os
struct TerminalSplitTreeView: View {
let tree: SplitTree<Ghostty.SurfaceView>
let onResize: (SplitTree<Ghostty.SurfaceView>.Node, Double) -> Void
let onDrop: (Ghostty.SurfaceView, TerminalSplitDropZone) -> Void
let onDrop: (_ source: Ghostty.SurfaceView, _ destination: Ghostty.SurfaceView, TerminalSplitDropZone) -> Void
var body: some View {
if let node = tree.zoomed ?? tree.root {
@@ -28,7 +28,7 @@ struct TerminalSplitSubtreeView: View {
let node: SplitTree<Ghostty.SurfaceView>.Node
var isRoot: Bool = false
let onResize: (SplitTree<Ghostty.SurfaceView>.Node, Double) -> Void
let onDrop: (Ghostty.SurfaceView, TerminalSplitDropZone) -> Void
let onDrop: (_ source: Ghostty.SurfaceView, _ destination: Ghostty.SurfaceView, TerminalSplitDropZone) -> Void
var body: some View {
switch (node) {
@@ -68,7 +68,7 @@ struct TerminalSplitSubtreeView: View {
struct TerminalSplitLeaf: View {
let surfaceView: Ghostty.SurfaceView
let isSplit: Bool
let onDrop: (Ghostty.SurfaceView, TerminalSplitDropZone) -> Void
let onDrop: (_ source: Ghostty.SurfaceView, _ destination: Ghostty.SurfaceView, TerminalSplitDropZone) -> Void
@State private var dropState: DropState = .idle
@@ -86,7 +86,8 @@ struct TerminalSplitLeaf: View {
.onDrop(of: [.ghosttySurfaceId], delegate: SplitDropDelegate(
dropState: $dropState,
viewSize: geometry.size,
onDrop: { zone in onDrop(surfaceView, zone) }
destinationSurface: surfaceView,
onDrop: onDrop
))
}
}
@@ -110,7 +111,8 @@ struct TerminalSplitLeaf: View {
private struct SplitDropDelegate: DropDelegate {
@Binding var dropState: DropState
let viewSize: CGSize
let onDrop: (TerminalSplitDropZone) -> Void
let destinationSurface: Ghostty.SurfaceView
let onDrop: (_ source: Ghostty.SurfaceView, _ destination: Ghostty.SurfaceView, TerminalSplitDropZone) -> Void
func validateDrop(info: DropInfo) -> Bool {
info.hasItemsConforming(to: [.ghosttySurfaceId])
@@ -134,8 +136,27 @@ struct TerminalSplitLeaf: View {
}
func performDrop(info: DropInfo) -> Bool {
let zone = TerminalSplitDropZone.calculate(at: info.location, in: viewSize)
dropState = .idle
onDrop(.calculate(at: info.location, in: viewSize))
// Load the dropped surface asynchronously using Transferable
let providers = info.itemProviders(for: [.ghosttySurfaceId])
guard let provider = providers.first else { return false }
_ = provider.loadTransferable(type: Ghostty.SurfaceView.self) { [weak destinationSurface] result in
switch result {
case .success(let sourceSurface):
DispatchQueue.main.async {
// Don't allow dropping on self
guard let destinationSurface else { return }
guard sourceSurface !== destinationSurface else { return }
onDrop(sourceSurface, destinationSurface, zone)
}
case .failure:
break
}
}
return true
}
}

View File

@@ -827,6 +827,38 @@ class BaseTerminalController: NSWindowController,
}
}
func splitDidDrop(source: Ghostty.SurfaceView, destination: Ghostty.SurfaceView, zone: TerminalSplitDropZone) {
// Find the source node in the tree
guard let sourceNode = surfaceTree.root?.node(view: source) else {
Ghostty.logger.warning("source surface not found in tree during drop")
return
}
// Map drop zone to split direction
let direction: SplitTree<Ghostty.SurfaceView>.NewDirection = switch zone {
case .top: .up
case .bottom: .down
case .left: .left
case .right: .right
}
// Remove source from its current position first
let treeWithoutSource = surfaceTree.remove(sourceNode)
// Insert source at destination in the appropriate direction
do {
let newTree = try treeWithoutSource.insert(view: source, at: destination, direction: direction)
replaceSurfaceTree(
newTree,
moveFocusTo: source,
moveFocusFrom: focusedSurface,
undoAction: "Move Split")
} catch {
Ghostty.logger.warning("failed to insert surface during drop: \(error)")
return
}
}
func performAction(_ action: String, on surfaceView: Ghostty.SurfaceView) {
guard let surface = surfaceView.surface else { return }
let len = action.utf8CString.count

View File

@@ -20,6 +20,9 @@ protocol TerminalViewDelegate: AnyObject {
/// A split is resizing to a given value.
func splitDidResize(node: SplitTree<Ghostty.SurfaceView>.Node, to newRatio: Double)
/// A surface was dropped onto another surface to create a split.
func splitDidDrop(source: Ghostty.SurfaceView, destination: Ghostty.SurfaceView, zone: TerminalSplitDropZone)
}
/// The view model is a required implementation for TerminalView callers. This contains
@@ -83,8 +86,8 @@ struct TerminalView<ViewModel: TerminalViewModel>: View {
TerminalSplitTreeView(
tree: viewModel.surfaceTree,
onResize: { delegate?.splitDidResize(node: $0, to: $1) },
onDrop: { surface, zone in
Ghostty.logger.info("Drop on surface \(surface) in zone \(zone.rawValue)")
onDrop: { source, destination, zone in
delegate?.splitDidDrop(source: source, destination: destination, zone: zone)
})
.environmentObject(ghostty)
.focused($focused)