mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-12-29 17:44:49 +00:00
macos: setup drop UI on our split views
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user