macos: setup drop UI on our split views

This commit is contained in:
Mitchell Hashimoto
2025-12-27 13:41:09 -08:00
parent ddfd4fe7c2
commit 43d87cf9f8

View File

@@ -29,17 +29,7 @@ struct TerminalSplitSubtreeView: View {
var body: some View {
switch (node) {
case .leaf(let leafView):
Ghostty.InspectableSurface(
surfaceView: leafView,
isSplit: !isRoot)
.dropDestination(for: Ghostty.SurfaceView.self) { views, point in
Ghostty.logger.warning("BABY WHAT!")
return false
} isTargeted: { targeted in
Ghostty.logger.warning("BABY TARGETED=\(targeted)")
}
.accessibilityElement(children: .contain)
.accessibilityLabel("Terminal pane")
TerminalSplitLeaf(surfaceView: leafView, isSplit: !isRoot)
case .split(let split):
let splitViewDirection: SplitViewDirection = switch (split.direction) {
@@ -70,3 +60,136 @@ struct TerminalSplitSubtreeView: View {
}
}
}
struct TerminalSplitLeaf: View {
let surfaceView: Ghostty.SurfaceView
let isSplit: Bool
@State private var dropZone: DropZone = .none
var body: some View {
Ghostty.InspectableSurface(
surfaceView: surfaceView,
isSplit: isSplit)
.background {
// We use background for the drop delegate and overlay for the visual indicator
// so that we don't block mouse events from reaching the surface view. The
// background receives drop events while the overlay (with allowsHitTesting
// disabled) only provides visual feedback.
GeometryReader { geometry in
Color.clear
.onDrop(of: [.ghosttySurfaceId], delegate: SplitDropDelegate(
dropZone: $dropZone,
viewSize: geometry.size
))
}
}
.overlay {
if dropZone != .none {
GeometryReader { geometry in
dropZoneOverlay(for: dropZone, in: geometry)
}
.allowsHitTesting(false)
}
}
.accessibilityElement(children: .contain)
.accessibilityLabel("Terminal pane")
}
@ViewBuilder
private func dropZoneOverlay(for zone: DropZone, in geometry: GeometryProxy) -> some View {
let overlayColor = Color.accentColor.opacity(0.3)
switch zone {
case .none:
EmptyView()
case .top:
VStack(spacing: 0) {
Rectangle()
.fill(overlayColor)
.frame(height: geometry.size.height / 2)
Spacer()
}
case .bottom:
VStack(spacing: 0) {
Spacer()
Rectangle()
.fill(overlayColor)
.frame(height: geometry.size.height / 2)
}
case .left:
HStack(spacing: 0) {
Rectangle()
.fill(overlayColor)
.frame(width: geometry.size.width / 2)
Spacer()
}
case .right:
HStack(spacing: 0) {
Spacer()
Rectangle()
.fill(overlayColor)
.frame(width: geometry.size.width / 2)
}
}
}
enum DropZone: Equatable {
case none
case top
case bottom
case left
case right
}
struct SplitDropDelegate: DropDelegate {
@Binding var dropZone: DropZone
let viewSize: CGSize
func validateDrop(info: DropInfo) -> Bool {
info.hasItemsConforming(to: [.ghosttySurfaceId])
}
func dropEntered(info: DropInfo) {
_ = dropUpdated(info: info)
}
func dropUpdated(info: DropInfo) -> DropProposal? {
dropZone = calculateDropZone(at: info.location)
return DropProposal(operation: .move)
}
func dropExited(info: DropInfo) {
dropZone = .none
}
func performDrop(info: DropInfo) -> Bool {
dropZone = .none
return false
}
/// Determines which drop zone the cursor is in based on proximity to edges.
///
/// Divides the view into four triangular regions by drawing diagonals from
/// 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
let distToLeft = relX
let distToRight = 1 - relX
let distToTop = relY
let distToBottom = 1 - relY
let minDist = min(distToLeft, distToRight, distToTop, distToBottom)
if minDist == distToLeft { return .left }
if minDist == distToRight { return .right }
if minDist == distToTop { return .top }
return .bottom
}
}
}