macos: handle split resizing

pull/7523/head
Mitchell Hashimoto 2025-06-03 15:36:40 -07:00
parent 1707159441
commit e3bc3422dc
No known key found for this signature in database
GPG Key ID: 523D5DC389D273BC
6 changed files with 86 additions and 53 deletions

View File

@ -93,6 +93,24 @@ extension SplitTree {
return .init(root: newRoot, zoomed: newZoomed)
}
/// Replace a node in the tree with a new node.
func replace(node: Node, with newNode: Node) throws -> Self {
guard let root else { throw SplitError.viewNotFound }
// Get the path to the node we want to replace
guard let path = root.path(to: node) else {
throw SplitError.viewNotFound
}
// Replace the node
let newRoot = try root.replaceNode(at: path, with: newNode)
// Update zoomed if it was the replaced node
let newZoomed = (zoomed == node) ? newNode : zoomed
return .init(root: newRoot, zoomed: newZoomed)
}
}
// MARK: SplitTree.Node
@ -210,7 +228,7 @@ extension SplitTree.Node {
}
/// Helper function to replace a node at the given path from the root
private func replaceNode(at path: Path, with newNode: Self) throws -> Self {
func replaceNode(at path: Path, with newNode: Self) throws -> Self {
// If path is empty, replace the root
if path.isEmpty {
return newNode
@ -293,6 +311,26 @@ extension SplitTree.Node {
))
}
}
/// Resize a split node to the specified ratio.
/// For leaf nodes, this returns the node unchanged.
/// For split nodes, this creates a new split with the updated ratio.
func resize(to ratio: Double) -> Self {
switch self {
case .leaf:
// Leaf nodes don't have a ratio to resize
return self
case .split(let split):
// Create a new split with the updated ratio
return .split(.init(
direction: split.direction,
ratio: ratio,
left: split.left,
right: split.right
))
}
}
}
// MARK: SplitTree.Node Protocols

View File

@ -2,17 +2,21 @@ import SwiftUI
struct TerminalSplitTreeView: View {
let tree: SplitTree
let onResize: (SplitTree.Node, Double) -> Void
var body: some View {
if let node = tree.root {
TerminalSplitSubtreeView(node: node, isRoot: true)
TerminalSplitSubtreeView(node: node, isRoot: true, onResize: onResize)
}
}
}
struct TerminalSplitSubtreeView: View {
@EnvironmentObject var ghostty: Ghostty.App
let node: SplitTree.Node
var isRoot: Bool = false
let onResize: (SplitTree.Node, Double) -> Void
var body: some View {
switch (node) {
@ -23,40 +27,28 @@ struct TerminalSplitSubtreeView: View {
isSplit: !isRoot)
case .split(let split):
TerminalSplitSplitView(split: split)
}
}
}
struct TerminalSplitSplitView: View {
@EnvironmentObject var ghostty: Ghostty.App
let split: SplitTree.Node.Split
private var splitViewDirection: SplitViewDirection {
switch (split.direction) {
case .horizontal: .horizontal
case .vertical: .vertical
}
}
var body: some View {
SplitView(
splitViewDirection,
.init(get: {
CGFloat(split.ratio)
}, set: { _ in
// TODO
}),
dividerColor: ghostty.config.splitDividerColor,
resizeIncrements: .init(width: 1, height: 1),
resizePublisher: .init(),
left: {
TerminalSplitSubtreeView(node: split.left)
},
right: {
TerminalSplitSubtreeView(node: split.right)
let splitViewDirection: SplitViewDirection = switch (split.direction) {
case .horizontal: .horizontal
case .vertical: .vertical
}
)
SplitView(
splitViewDirection,
.init(get: {
CGFloat(split.ratio)
}, set: {
onResize(node, $0)
}),
dividerColor: ghostty.config.splitDividerColor,
resizeIncrements: .init(width: 1, height: 1),
resizePublisher: .init(),
left: {
TerminalSplitSubtreeView(node: split.left, onResize: onResize)
},
right: {
TerminalSplitSubtreeView(node: split.right, onResize: onResize)
}
)
}
}
}

View File

@ -366,11 +366,6 @@ class BaseTerminalController: NSWindowController,
// MARK: TerminalViewDelegate
// Note: this is different from surfaceDidTreeChange(from:,to:) because this is called
// when the currently set value changed in place and the from:to: variant is called
// when the variable was set.
func surfaceTreeDidChange() {}
func focusedSurfaceDidChange(to: Ghostty.SurfaceView?) {
let lastFocusedSurface = focusedSurface
focusedSurface = to
@ -420,6 +415,16 @@ class BaseTerminalController: NSWindowController,
func zoomStateDidChange(to: Bool) {}
func splitDidResize(node: SplitTree.Node, to newRatio: Double) {
let resizedNode = node.resize(to: newRatio)
do {
surfaceTree2 = try surfaceTree2.replace(node: node, with: resizedNode)
} catch {
// TODO: log
return
}
}
func performAction(_ action: String, on surfaceView: Ghostty.SurfaceView) {
guard let surface = surfaceView.surface else { return }
let len = action.utf8CString.count

View File

@ -107,6 +107,10 @@ class TerminalController: BaseTerminalController {
override func surfaceTreeDidChange(from: Ghostty.SplitNode?, to: Ghostty.SplitNode?) {
super.surfaceTreeDidChange(from: from, to: to)
// Whenever our surface tree changes in any way (new split, close split, etc.)
// we want to invalidate our state.
invalidateRestorableState()
// If our surface tree is now nil then we close our window.
if (to == nil) {
@ -696,12 +700,6 @@ class TerminalController: BaseTerminalController {
}
}
override func surfaceTreeDidChange() {
// Whenever our surface tree changes in any way (new split, close split, etc.)
// we want to invalidate our state.
invalidateRestorableState()
}
override func zoomStateDidChange(to: Bool) {
guard let window = window as? TerminalWindow else { return }
window.surfaceIsZoomed = to

View File

@ -14,15 +14,14 @@ protocol TerminalViewDelegate: AnyObject {
/// The cell size changed.
func cellSizeDidChange(to: NSSize)
/// The surface tree did change in some way, i.e. a split was added, removed, etc. This is
/// not called initially.
func surfaceTreeDidChange()
/// This is called when a split is zoomed.
func zoomStateDidChange(to: Bool)
/// 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.Node, to newRatio: Double)
}
/// The view model is a required implementation for TerminalView callers. This contains
@ -81,7 +80,9 @@ struct TerminalView<ViewModel: TerminalViewModel>: View {
DebugBuildWarningView()
}
TerminalSplitTreeView(tree: viewModel.surfaceTree2)
TerminalSplitTreeView(
tree: viewModel.surfaceTree2,
onResize: { delegate?.splitDidResize(node: $0, to: $1) })
.environmentObject(ghostty)
.focused($focused)
.onAppear { self.focused = true }

View File

@ -30,7 +30,6 @@ class TerminalWindow: NSWindow {
observe(\.surfaceIsZoomed, options: [.initial, .new]) { [weak self] window, _ in
guard let tabGroup = self?.tabGroup else { return }
Ghostty.logger.warning("WOW \(window.surfaceIsZoomed)")
self?.resetZoomTabButton.isHidden = !window.surfaceIsZoomed
self?.updateResetZoomTitlebarButtonVisibility()
},