macos: wire up onDrop

This commit is contained in:
Mitchell Hashimoto
2025-12-27 14:22:46 -08:00
parent 43d87cf9f8
commit 0a80f77766
2 changed files with 39 additions and 22 deletions

View File

@@ -1,15 +1,18 @@
import SwiftUI
import os
struct TerminalSplitTreeView: View {
let tree: SplitTree<Ghostty.SurfaceView>
let onResize: (SplitTree<Ghostty.SurfaceView>.Node, Double) -> Void
let onDrop: (Ghostty.SurfaceView, TerminalSplitLeaf.DropZone) -> Void
var body: some View {
if let node = tree.zoomed ?? tree.root {
TerminalSplitSubtreeView(
node: node,
isRoot: node == tree.root,
onResize: onResize)
onResize: onResize,
onDrop: onDrop)
// This is necessary because we can't rely on SwiftUI's implicit
// structural identity to detect changes to this view. Due to
// the tree structure of splits it could result in bad behaviors.
@@ -25,11 +28,12 @@ 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, TerminalSplitLeaf.DropZone) -> Void
var body: some View {
switch (node) {
case .leaf(let leafView):
TerminalSplitLeaf(surfaceView: leafView, isSplit: !isRoot)
TerminalSplitLeaf(surfaceView: leafView, isSplit: !isRoot, onDrop: onDrop)
case .split(let split):
let splitViewDirection: SplitViewDirection = switch (split.direction) {
@@ -47,10 +51,10 @@ struct TerminalSplitSubtreeView: View {
dividerColor: ghostty.config.splitDividerColor,
resizeIncrements: .init(width: 1, height: 1),
left: {
TerminalSplitSubtreeView(node: split.left, onResize: onResize)
TerminalSplitSubtreeView(node: split.left, onResize: onResize, onDrop: onDrop)
},
right: {
TerminalSplitSubtreeView(node: split.right, onResize: onResize)
TerminalSplitSubtreeView(node: split.right, onResize: onResize, onDrop: onDrop)
},
onEqualize: {
guard let surface = node.leftmostLeaf().surface else { return }
@@ -64,8 +68,9 @@ struct TerminalSplitSubtreeView: View {
struct TerminalSplitLeaf: View {
let surfaceView: Ghostty.SurfaceView
let isSplit: Bool
let onDrop: (Ghostty.SurfaceView, DropZone) -> Void
@State private var dropZone: DropZone = .none
@State private var dropState: DropState = .idle
var body: some View {
Ghostty.InspectableSurface(
@@ -79,15 +84,16 @@ struct TerminalSplitLeaf: View {
GeometryReader { geometry in
Color.clear
.onDrop(of: [.ghosttySurfaceId], delegate: SplitDropDelegate(
dropZone: $dropZone,
viewSize: geometry.size
dropState: $dropState,
viewSize: geometry.size,
onDrop: { zone in onDrop(surfaceView, zone) }
))
}
}
.overlay {
if dropZone != .none {
if case .dropping(let zone) = dropState {
GeometryReader { geometry in
dropZoneOverlay(for: dropZone, in: geometry)
dropZoneOverlay(for: zone, in: geometry)
}
.allowsHitTesting(false)
}
@@ -101,8 +107,6 @@ struct TerminalSplitLeaf: View {
let overlayColor = Color.accentColor.opacity(0.3)
switch zone {
case .none:
EmptyView()
case .top:
VStack(spacing: 0) {
Rectangle()
@@ -134,38 +138,49 @@ struct TerminalSplitLeaf: View {
}
}
enum DropZone: Equatable {
case none
enum DropZone: String, Equatable {
case top
case bottom
case left
case right
}
enum DropState: Equatable {
case idle
case dropping(DropZone)
}
struct SplitDropDelegate: DropDelegate {
@Binding var dropZone: DropZone
@Binding var dropState: DropState
let viewSize: CGSize
let onDrop: (DropZone) -> Void
func validateDrop(info: DropInfo) -> Bool {
info.hasItemsConforming(to: [.ghosttySurfaceId])
}
func dropEntered(info: DropInfo) {
_ = dropUpdated(info: info)
dropState = .dropping(calculateDropZone(at: info.location))
}
func dropUpdated(info: DropInfo) -> DropProposal? {
dropZone = calculateDropZone(at: info.location)
// For some reason dropUpdated is sent after performDrop is called
// and we don't want to reset our drop zone to show it so we have
// to guard on the state here.
guard case .dropping = dropState else { return DropProposal(operation: .forbidden) }
dropState = .dropping(calculateDropZone(at: info.location))
return DropProposal(operation: .move)
}
func dropExited(info: DropInfo) {
dropZone = .none
dropState = .idle
}
func performDrop(info: DropInfo) -> Bool {
dropZone = .none
return false
let zone = calculateDropZone(at: info.location)
dropState = .idle
onDrop(zone)
return true
}
/// Determines which drop zone the cursor is in based on proximity to edges.
@@ -174,8 +189,6 @@ struct TerminalSplitLeaf: View {
/// corner to corner. The drop zone is determined by which edge the cursor
/// is closest to, creating natural triangular hit regions for each side.
private func calculateDropZone(at point: CGPoint) -> DropZone {
guard viewSize.width > 0, viewSize.height > 0 else { return .none }
let relX = point.x / viewSize.width
let relY = point.y / viewSize.height

View File

@@ -1,5 +1,6 @@
import SwiftUI
import GhosttyKit
import os
/// This delegate is notified of actions and property changes regarding the terminal view. This
/// delegate is optional and can be used by a TerminalView caller to react to changes such as
@@ -81,7 +82,10 @@ struct TerminalView<ViewModel: TerminalViewModel>: View {
TerminalSplitTreeView(
tree: viewModel.surfaceTree,
onResize: { delegate?.splitDidResize(node: $0, to: $1) })
onResize: { delegate?.splitDidResize(node: $0, to: $1) },
onDrop: { surface, zone in
Ghostty.logger.info("Drop on surface \(surface) in zone \(zone.rawValue)")
})
.environmentObject(ghostty)
.focused($focused)
.onAppear { self.focused = true }