diff --git a/macos/Sources/Features/Splits/SplitTree.swift b/macos/Sources/Features/Splits/SplitTree.swift index 6f98dfefc..6829a742e 100644 --- a/macos/Sources/Features/Splits/SplitTree.swift +++ b/macos/Sources/Features/Splits/SplitTree.swift @@ -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 diff --git a/macos/Sources/Features/Splits/TerminalSplitTreeView.swift b/macos/Sources/Features/Splits/TerminalSplitTreeView.swift index c55192e44..8f78dcbf8 100644 --- a/macos/Sources/Features/Splits/TerminalSplitTreeView.swift +++ b/macos/Sources/Features/Splits/TerminalSplitTreeView.swift @@ -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) + } + ) + } } } diff --git a/macos/Sources/Features/Terminal/BaseTerminalController.swift b/macos/Sources/Features/Terminal/BaseTerminalController.swift index 628c0acbf..cb5a15f1b 100644 --- a/macos/Sources/Features/Terminal/BaseTerminalController.swift +++ b/macos/Sources/Features/Terminal/BaseTerminalController.swift @@ -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 diff --git a/macos/Sources/Features/Terminal/TerminalController.swift b/macos/Sources/Features/Terminal/TerminalController.swift index d8f42bb1a..82491e76d 100644 --- a/macos/Sources/Features/Terminal/TerminalController.swift +++ b/macos/Sources/Features/Terminal/TerminalController.swift @@ -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 diff --git a/macos/Sources/Features/Terminal/TerminalView.swift b/macos/Sources/Features/Terminal/TerminalView.swift index d2f4d8bdb..2970f19c6 100644 --- a/macos/Sources/Features/Terminal/TerminalView.swift +++ b/macos/Sources/Features/Terminal/TerminalView.swift @@ -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: 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 } diff --git a/macos/Sources/Features/Terminal/TerminalWindow.swift b/macos/Sources/Features/Terminal/TerminalWindow.swift index f244b95ee..1c440be33 100644 --- a/macos/Sources/Features/Terminal/TerminalWindow.swift +++ b/macos/Sources/Features/Terminal/TerminalWindow.swift @@ -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() },