macos: unify split callbacks into a single tagged enum

This commit is contained in:
Mitchell Hashimoto
2025-12-28 07:07:44 -08:00
parent 60bc5a9ae7
commit 9724541a33
3 changed files with 51 additions and 29 deletions

View File

@@ -1,18 +1,37 @@
import SwiftUI
import os
enum TerminalSplitOperation {
case resize(Resize)
case drop(Drop)
struct Resize {
let node: SplitTree<Ghostty.SurfaceView>.Node
let ratio: Double
}
struct Drop {
/// The surface being dragged.
let payload: Ghostty.SurfaceView
/// The surface it was dragged onto
let destination: Ghostty.SurfaceView
/// The zone it was dropped to determine how to split the destination.
let zone: TerminalSplitDropZone
}
}
struct TerminalSplitTreeView: View {
let tree: SplitTree<Ghostty.SurfaceView>
let onResize: (SplitTree<Ghostty.SurfaceView>.Node, Double) -> Void
let onDrop: (_ source: Ghostty.SurfaceView, _ destination: Ghostty.SurfaceView, TerminalSplitDropZone) -> Void
let action: (TerminalSplitOperation) -> Void
var body: some View {
if let node = tree.zoomed ?? tree.root {
TerminalSplitSubtreeView(
node: node,
isRoot: node == tree.root,
onResize: onResize,
onDrop: onDrop)
action: action)
// 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.
@@ -27,13 +46,12 @@ struct TerminalSplitSubtreeView: View {
let node: SplitTree<Ghostty.SurfaceView>.Node
var isRoot: Bool = false
let onResize: (SplitTree<Ghostty.SurfaceView>.Node, Double) -> Void
let onDrop: (_ source: Ghostty.SurfaceView, _ destination: Ghostty.SurfaceView, TerminalSplitDropZone) -> Void
let action: (TerminalSplitOperation) -> Void
var body: some View {
switch (node) {
case .leaf(let leafView):
TerminalSplitLeaf(surfaceView: leafView, isSplit: !isRoot, onDrop: onDrop)
TerminalSplitLeaf(surfaceView: leafView, isSplit: !isRoot, action: action)
case .split(let split):
let splitViewDirection: SplitViewDirection = switch (split.direction) {
@@ -46,15 +64,15 @@ struct TerminalSplitSubtreeView: View {
.init(get: {
CGFloat(split.ratio)
}, set: {
onResize(node, $0)
action(.resize(.init(node: node, ratio: $0)))
}),
dividerColor: ghostty.config.splitDividerColor,
resizeIncrements: .init(width: 1, height: 1),
left: {
TerminalSplitSubtreeView(node: split.left, onResize: onResize, onDrop: onDrop)
TerminalSplitSubtreeView(node: split.left, action: action)
},
right: {
TerminalSplitSubtreeView(node: split.right, onResize: onResize, onDrop: onDrop)
TerminalSplitSubtreeView(node: split.right, action: action)
},
onEqualize: {
guard let surface = node.leftmostLeaf().surface else { return }
@@ -68,7 +86,7 @@ struct TerminalSplitSubtreeView: View {
struct TerminalSplitLeaf: View {
let surfaceView: Ghostty.SurfaceView
let isSplit: Bool
let onDrop: (_ source: Ghostty.SurfaceView, _ destination: Ghostty.SurfaceView, TerminalSplitDropZone) -> Void
let action: (TerminalSplitOperation) -> Void
@State private var dropState: DropState = .idle
@@ -81,7 +99,7 @@ struct TerminalSplitLeaf: View {
dropState: $dropState,
viewSize: geometry.size,
destinationSurface: surfaceView,
onDrop: onDrop
action: action
))
.overlay {
if case .dropping(let zone) = dropState {
@@ -103,7 +121,7 @@ struct TerminalSplitLeaf: View {
@Binding var dropState: DropState
let viewSize: CGSize
let destinationSurface: Ghostty.SurfaceView
let onDrop: (_ source: Ghostty.SurfaceView, _ destination: Ghostty.SurfaceView, TerminalSplitDropZone) -> Void
let action: (TerminalSplitOperation) -> Void
func validateDrop(info: DropInfo) -> Bool {
info.hasItemsConforming(to: [.ghosttySurfaceId])
@@ -133,6 +151,8 @@ struct TerminalSplitLeaf: View {
// Load the dropped surface asynchronously using Transferable
let providers = info.itemProviders(for: [.ghosttySurfaceId])
guard let provider = providers.first else { return false }
// Capture action before the async closure
_ = provider.loadTransferable(type: Ghostty.SurfaceView.self) { [weak destinationSurface] result in
switch result {
case .success(let sourceSurface):
@@ -140,7 +160,7 @@ struct TerminalSplitLeaf: View {
// Don't allow dropping on self
guard let destinationSurface else { return }
guard sourceSurface !== destinationSurface else { return }
onDrop(sourceSurface, destinationSurface, zone)
action(.drop(.init(payload: sourceSurface, destination: destinationSurface, zone: zone)))
}
case .failure:

View File

@@ -817,17 +817,25 @@ class BaseTerminalController: NSWindowController,
self.window?.contentResizeIncrements = to
}
func splitDidResize(node: SplitTree<Ghostty.SurfaceView>.Node, to newRatio: Double) {
func performSplitAction(_ action: TerminalSplitOperation) {
switch action {
case .resize(let resize):
splitDidResize(node: resize.node, to: resize.ratio)
case .drop(let drop):
splitDidDrop(source: drop.payload, destination: drop.destination, zone: drop.zone)
}
}
private func splitDidResize(node: SplitTree<Ghostty.SurfaceView>.Node, to newRatio: Double) {
let resizedNode = node.resize(to: newRatio)
do {
surfaceTree = try surfaceTree.replace(node: node, with: resizedNode)
} catch {
Ghostty.logger.warning("failed to replace node during split resize: \(error)")
return
}
}
func splitDidDrop(source: Ghostty.SurfaceView, destination: Ghostty.SurfaceView, zone: TerminalSplitDropZone) {
private func splitDidDrop(source: Ghostty.SurfaceView, destination: Ghostty.SurfaceView, zone: TerminalSplitDropZone) {
// Map drop zone to split direction
let direction: SplitTree<Ghostty.SurfaceView>.NewDirection = switch zone {
case .top: .up
@@ -851,7 +859,7 @@ class BaseTerminalController: NSWindowController,
} catch {
Ghostty.logger.warning("failed to insert surface during drop: \(error)")
}
return
}
@@ -872,7 +880,7 @@ class BaseTerminalController: NSWindowController,
Ghostty.logger.warning("source surface not found in any window during drop")
return
}
// TODO: Undo for cross window move.
// Remove from source controller's tree

View File

@@ -17,12 +17,9 @@ protocol TerminalViewDelegate: AnyObject {
/// Perform an action. At the time of writing this is only triggered by the command palette.
func performAction(_ action: String, on: Ghostty.SurfaceView)
/// 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)
/// A split tree operation
func performSplitAction(_ action: TerminalSplitOperation)
}
/// The view model is a required implementation for TerminalView callers. This contains
@@ -85,10 +82,7 @@ struct TerminalView<ViewModel: TerminalViewModel>: View {
TerminalSplitTreeView(
tree: viewModel.surfaceTree,
onResize: { delegate?.splitDidResize(node: $0, to: $1) },
onDrop: { source, destination, zone in
delegate?.splitDidDrop(source: source, destination: destination, zone: zone)
})
action: { delegate?.performSplitAction($0) })
.environmentObject(ghostty)
.focused($focused)
.onAppear { self.focused = true }