mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-17 01:06:08 +03:00
new SplitTree
This commit is contained in:
@ -59,6 +59,8 @@
|
||||
A571AB1D2A206FCF00248498 /* GhosttyKit.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = A5D495A1299BEC7E00DD1313 /* GhosttyKit.xcframework */; };
|
||||
A57D79272C9C879B001D522E /* SecureInput.swift in Sources */ = {isa = PBXBuildFile; fileRef = A57D79262C9C8798001D522E /* SecureInput.swift */; };
|
||||
A586167C2B7703CC009BDB1D /* fish in Resources */ = {isa = PBXBuildFile; fileRef = A586167B2B7703CC009BDB1D /* fish */; };
|
||||
A586365F2DEE6C2300E04A10 /* SplitTree.swift in Sources */ = {isa = PBXBuildFile; fileRef = A586365E2DEE6C2100E04A10 /* SplitTree.swift */; };
|
||||
A58636662DEF964100E04A10 /* TerminalSplitTreeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A58636652DEF963F00E04A10 /* TerminalSplitTreeView.swift */; };
|
||||
A5874D992DAD751B00E83852 /* CGS.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5874D982DAD751A00E83852 /* CGS.swift */; };
|
||||
A5874D9D2DAD786100E83852 /* NSWindow+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5874D9C2DAD785F00E83852 /* NSWindow+Extension.swift */; };
|
||||
A59444F729A2ED5200725BBA /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A59444F629A2ED5200725BBA /* SettingsView.swift */; };
|
||||
@ -164,6 +166,8 @@
|
||||
A571AB1C2A206FC600248498 /* Ghostty-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = "Ghostty-Info.plist"; sourceTree = "<group>"; };
|
||||
A57D79262C9C8798001D522E /* SecureInput.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureInput.swift; sourceTree = "<group>"; };
|
||||
A586167B2B7703CC009BDB1D /* fish */ = {isa = PBXFileReference; lastKnownFileType = folder; name = fish; path = "../zig-out/share/fish"; sourceTree = "<group>"; };
|
||||
A586365E2DEE6C2100E04A10 /* SplitTree.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplitTree.swift; sourceTree = "<group>"; };
|
||||
A58636652DEF963F00E04A10 /* TerminalSplitTreeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalSplitTreeView.swift; sourceTree = "<group>"; };
|
||||
A5874D982DAD751A00E83852 /* CGS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CGS.swift; sourceTree = "<group>"; };
|
||||
A5874D9C2DAD785F00E83852 /* NSWindow+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSWindow+Extension.swift"; sourceTree = "<group>"; };
|
||||
A59444F629A2ED5200725BBA /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = "<group>"; };
|
||||
@ -275,6 +279,7 @@
|
||||
A5CBD05A2CA0C5910017A1AE /* QuickTerminal */,
|
||||
A5E112912AF73E4D00C6E0C2 /* ClipboardConfirmation */,
|
||||
A57D79252C9C8782001D522E /* Secure Input */,
|
||||
A58636622DEF955100E04A10 /* Splits */,
|
||||
A53A29742DB2E04900B6E02C /* Command Palette */,
|
||||
A534263E2A7DCC5800EBB7A2 /* Settings */,
|
||||
A51BFC1C2B2FB5AB00E92F16 /* About */,
|
||||
@ -428,6 +433,15 @@
|
||||
path = "Secure Input";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
A58636622DEF955100E04A10 /* Splits */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
A586365E2DEE6C2100E04A10 /* SplitTree.swift */,
|
||||
A58636652DEF963F00E04A10 /* TerminalSplitTreeView.swift */,
|
||||
);
|
||||
path = Splits;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
A5874D9B2DAD781100E83852 /* Private */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@ -685,6 +699,7 @@
|
||||
A5CBD05E2CA0C5EC0017A1AE /* QuickTerminalController.swift in Sources */,
|
||||
A5CF66D72D29DDB500139794 /* Ghostty.Event.swift in Sources */,
|
||||
A5A2A3CA2D4445E30033CF96 /* Dock.swift in Sources */,
|
||||
A586365F2DEE6C2300E04A10 /* SplitTree.swift in Sources */,
|
||||
A51BFC222B2FB6B400E92F16 /* AboutView.swift in Sources */,
|
||||
A5278A9B2AA05B2600CD3039 /* Ghostty.Input.swift in Sources */,
|
||||
A53A29812DB44A6100B6E02C /* KeyboardShortcut+Extension.swift in Sources */,
|
||||
@ -734,6 +749,7 @@
|
||||
A5CEAFFF29C2410700646FDA /* Backport.swift in Sources */,
|
||||
A5E112952AF73E8A00C6E0C2 /* ClipboardConfirmationController.swift in Sources */,
|
||||
A596309E2AEE1D6C00D64628 /* TerminalView.swift in Sources */,
|
||||
A58636662DEF964100E04A10 /* TerminalSplitTreeView.swift in Sources */,
|
||||
A52FFF592CAA4FF3000C6A5B /* Fullscreen.swift in Sources */,
|
||||
AEF9CE242B6AD07A0017E195 /* TerminalToolbar.swift in Sources */,
|
||||
C159E81D2B66A06B00FDFE9C /* OSColor+Extension.swift in Sources */,
|
||||
|
314
macos/Sources/Features/Splits/SplitTree.swift
Normal file
314
macos/Sources/Features/Splits/SplitTree.swift
Normal file
@ -0,0 +1,314 @@
|
||||
import AppKit
|
||||
|
||||
/// SplitTree represents a tree of views that can be divided.
|
||||
struct SplitTree {
|
||||
/// The root of the tree. This can be nil to indicate the tree is empty.
|
||||
let root: Node?
|
||||
|
||||
/// The node that is currently zoomed. A zoomed split is expected to take up the full
|
||||
/// size of the view area where the splits are shown.
|
||||
let zoomed: Node?
|
||||
|
||||
/// A single node in the tree is either a leaf node (a view) or a split (has a
|
||||
/// left/right or top/bottom).
|
||||
indirect enum Node {
|
||||
case leaf(view: NSView)
|
||||
case split(Split)
|
||||
|
||||
struct Split: Equatable {
|
||||
let direction: Direction
|
||||
let ratio: Double
|
||||
let left: Node
|
||||
let right: Node
|
||||
}
|
||||
}
|
||||
|
||||
enum Direction {
|
||||
case horizontal // Splits are laid out left and right
|
||||
case vertical // Splits are laid out top and bottom
|
||||
}
|
||||
|
||||
/// The path to a specific node in the tree.
|
||||
struct Path {
|
||||
let path: [Component]
|
||||
|
||||
var isEmpty: Bool { path.isEmpty }
|
||||
|
||||
enum Component {
|
||||
case left
|
||||
case right
|
||||
}
|
||||
}
|
||||
|
||||
enum SplitError: Error {
|
||||
case viewNotFound
|
||||
}
|
||||
|
||||
enum NewDirection {
|
||||
case left
|
||||
case right
|
||||
case down
|
||||
case up
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: SplitTree
|
||||
|
||||
extension SplitTree {
|
||||
var isEmpty: Bool {
|
||||
root == nil
|
||||
}
|
||||
|
||||
init() {
|
||||
self.init(root: nil, zoomed: nil)
|
||||
}
|
||||
|
||||
init(view: NSView) {
|
||||
self.init(root: .leaf(view: view), zoomed: nil)
|
||||
}
|
||||
|
||||
/// Insert a new view at the given view point by creating a split in the given direction.
|
||||
func insert(view: NSView, at: NSView, direction: NewDirection) throws -> Self {
|
||||
guard let root else { throw SplitError.viewNotFound }
|
||||
return .init(
|
||||
root: try root.insert(view: view, at: at, direction: direction),
|
||||
zoomed: zoomed)
|
||||
}
|
||||
|
||||
/// Remove a node from the tree. If the node being removed is part of a split,
|
||||
/// the sibling node takes the place of the parent split.
|
||||
func remove(_ target: Node) -> Self {
|
||||
guard let root else { return self }
|
||||
|
||||
// If we're removing the root itself, return an empty tree
|
||||
if root == target {
|
||||
return .init(root: nil, zoomed: nil)
|
||||
}
|
||||
|
||||
// Otherwise, try to remove from the tree
|
||||
let newRoot = root.remove(target)
|
||||
|
||||
// Update zoomed if it was the removed node
|
||||
let newZoomed = (zoomed == target) ? nil : zoomed
|
||||
|
||||
return .init(root: newRoot, zoomed: newZoomed)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: SplitTree.Node
|
||||
|
||||
extension SplitTree.Node {
|
||||
typealias Node = SplitTree.Node
|
||||
typealias NewDirection = SplitTree.NewDirection
|
||||
typealias SplitError = SplitTree.SplitError
|
||||
typealias Path = SplitTree.Path
|
||||
|
||||
/// Returns the node in the tree that contains the given view.
|
||||
func node(view: NSView) -> Node? {
|
||||
switch (self) {
|
||||
case .leaf(view):
|
||||
return self
|
||||
|
||||
case .split(let split):
|
||||
if let result = split.left.node(view: view) {
|
||||
return result
|
||||
} else if let result = split.right.node(view: view) {
|
||||
return result
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the path to a given node in the tree. If the returned value is nil then the
|
||||
/// node doesn't exist.
|
||||
func path(to node: Self) -> Path? {
|
||||
var components: [Path.Component] = []
|
||||
func search(_ current: Self) -> Bool {
|
||||
if current == node {
|
||||
return true
|
||||
}
|
||||
|
||||
switch current {
|
||||
case .leaf:
|
||||
return false
|
||||
|
||||
case .split(let split):
|
||||
// Try left branch
|
||||
components.append(.left)
|
||||
if search(split.left) {
|
||||
return true
|
||||
}
|
||||
components.removeLast()
|
||||
|
||||
// Try right branch
|
||||
components.append(.right)
|
||||
if search(split.right) {
|
||||
return true
|
||||
}
|
||||
components.removeLast()
|
||||
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return search(self) ? Path(path: components) : nil
|
||||
}
|
||||
|
||||
/// Inserts a new view into the split tree by creating a split at the location of an existing view.
|
||||
///
|
||||
/// This method creates a new split node containing both the existing view and the new view,
|
||||
/// The position of the new view relative to the existing view is determined by the direction parameter.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - view: The new view to insert into the tree
|
||||
/// - at: The existing view at whose location the split should be created
|
||||
/// - direction: The direction relative to the existing view where the new view should be placed
|
||||
///
|
||||
/// - Note: If the existing view (`at`) is not found in the tree, this method does nothing. We should
|
||||
/// maybe throw instead but at the moment we just do nothing.
|
||||
func insert(view: NSView, at: NSView, direction: NewDirection) throws -> Self {
|
||||
// Get the path to our insertion point. If it doesn't exist we do
|
||||
// nothing.
|
||||
guard let path = path(to: .leaf(view: at)) else {
|
||||
throw SplitError.viewNotFound
|
||||
}
|
||||
|
||||
// Determine split direction and which side the new view goes on
|
||||
let splitDirection: SplitTree.Direction
|
||||
let newViewOnLeft: Bool
|
||||
switch direction {
|
||||
case .left:
|
||||
splitDirection = .horizontal
|
||||
newViewOnLeft = true
|
||||
case .right:
|
||||
splitDirection = .horizontal
|
||||
newViewOnLeft = false
|
||||
case .up:
|
||||
splitDirection = .vertical
|
||||
newViewOnLeft = true
|
||||
case .down:
|
||||
splitDirection = .vertical
|
||||
newViewOnLeft = false
|
||||
}
|
||||
|
||||
// Create the new split node
|
||||
let newNode: Node = .leaf(view: view)
|
||||
let existingNode: Node = .leaf(view: at)
|
||||
let newSplit: Node = .split(.init(
|
||||
direction: splitDirection,
|
||||
ratio: 0.5,
|
||||
left: newViewOnLeft ? newNode : existingNode,
|
||||
right: newViewOnLeft ? existingNode : newNode
|
||||
))
|
||||
|
||||
// Replace the node at the path with the new split
|
||||
return try replaceNode(at: path, with: newSplit)
|
||||
}
|
||||
|
||||
/// Helper function to replace a node at the given path from the root
|
||||
private func replaceNode(at path: Path, with newNode: Self) throws -> Self {
|
||||
// If path is empty, replace the root
|
||||
if path.isEmpty {
|
||||
return newNode
|
||||
}
|
||||
|
||||
// Otherwise, we need to replace the proper left/right all along
|
||||
// the way since Node is a value type (enum). To do that, we need
|
||||
// recursion. We can't use a simple iterative approach because we
|
||||
// can't update in-place.
|
||||
func replaceInner(current: Node, pathOffset: Int) throws -> Node {
|
||||
// Base case: if we've consumed the entire path, replace this node
|
||||
if pathOffset >= path.path.count {
|
||||
return newNode
|
||||
}
|
||||
|
||||
// We need to go deeper, so current must be a split for the path
|
||||
// to be valid. Otherwise, the path is invalid.
|
||||
guard case .split(let split) = current else {
|
||||
throw SplitError.viewNotFound
|
||||
}
|
||||
|
||||
let component = path.path[pathOffset]
|
||||
switch component {
|
||||
case .left:
|
||||
return .split(.init(
|
||||
direction: split.direction,
|
||||
ratio: split.ratio,
|
||||
left: try replaceInner(current: split.left, pathOffset: pathOffset + 1),
|
||||
right: split.right
|
||||
))
|
||||
case .right:
|
||||
return .split(.init(
|
||||
direction: split.direction,
|
||||
ratio: split.ratio,
|
||||
left: split.left,
|
||||
right: try replaceInner(current: split.right, pathOffset: pathOffset + 1)
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
return try replaceInner(current: self, pathOffset: 0)
|
||||
}
|
||||
|
||||
/// Remove a node from the tree. Returns the modified tree, or nil if removing
|
||||
/// the node results in an empty tree.
|
||||
func remove(_ target: Node) -> Node? {
|
||||
// If we're removing ourselves, return nil
|
||||
if self == target {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch self {
|
||||
case .leaf:
|
||||
// A leaf that isn't the target stays as is
|
||||
return self
|
||||
|
||||
case .split(let split):
|
||||
// Neither child is directly the target, so we need to recursively
|
||||
// try to remove from both children
|
||||
let newLeft = split.left.remove(target)
|
||||
let newRight = split.right.remove(target)
|
||||
|
||||
// If both are nil then we remove everything. This shouldn't ever
|
||||
// happen because duplicate nodes shouldn't exist, but we want to
|
||||
// be robust against it.
|
||||
if newLeft == nil && newRight == nil {
|
||||
return nil
|
||||
} else if newLeft == nil {
|
||||
return newRight
|
||||
} else if newRight == nil {
|
||||
return newLeft
|
||||
}
|
||||
|
||||
// Both children still exist after removal
|
||||
return .split(.init(
|
||||
direction: split.direction,
|
||||
ratio: split.ratio,
|
||||
left: newLeft!,
|
||||
right: newRight!
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: SplitTree.Node Protocols
|
||||
|
||||
extension SplitTree.Node: Equatable {
|
||||
static func == (lhs: Self, rhs: Self) -> Bool {
|
||||
switch (lhs, rhs) {
|
||||
case let (.leaf(leftView), .leaf(rightView)):
|
||||
// Compare NSView instances by object identity
|
||||
return leftView === rightView
|
||||
|
||||
case let (.split(split1), .split(split2)):
|
||||
return split1 == split2
|
||||
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
62
macos/Sources/Features/Splits/TerminalSplitTreeView.swift
Normal file
62
macos/Sources/Features/Splits/TerminalSplitTreeView.swift
Normal file
@ -0,0 +1,62 @@
|
||||
import SwiftUI
|
||||
|
||||
struct TerminalSplitTreeView: View {
|
||||
let tree: SplitTree
|
||||
|
||||
var body: some View {
|
||||
if let node = tree.root {
|
||||
TerminalSplitSubtreeView(node: node, isRoot: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct TerminalSplitSubtreeView: View {
|
||||
let node: SplitTree.Node
|
||||
var isRoot: Bool = false
|
||||
|
||||
var body: some View {
|
||||
switch (node) {
|
||||
case .leaf(let leafView):
|
||||
// TODO: Fix the as!
|
||||
Ghostty.InspectableSurface(
|
||||
surfaceView: leafView as! Ghostty.SurfaceView,
|
||||
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)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
@ -46,6 +46,8 @@ class BaseTerminalController: NSWindowController,
|
||||
didSet { surfaceTreeDidChange(from: oldValue, to: surfaceTree) }
|
||||
}
|
||||
|
||||
@Published var surfaceTree2: SplitTree = .init()
|
||||
|
||||
/// This can be set to show/hide the command palette.
|
||||
@Published var commandPaletteIsShowing: Bool = false
|
||||
|
||||
@ -97,6 +99,9 @@ class BaseTerminalController: NSWindowController,
|
||||
guard let ghostty_app = ghostty.app else { preconditionFailure("app must be loaded") }
|
||||
self.surfaceTree = tree ?? .leaf(.init(ghostty_app, baseConfig: base))
|
||||
|
||||
let firstView = Ghostty.SurfaceView(ghostty_app, baseConfig: base)
|
||||
self.surfaceTree2 = .init(view: firstView)
|
||||
|
||||
// Setup our notifications for behaviors
|
||||
let center = NotificationCenter.default
|
||||
center.addObserver(
|
||||
@ -124,6 +129,18 @@ class BaseTerminalController: NSWindowController,
|
||||
selector: #selector(ghosttyMaximizeDidToggle(_:)),
|
||||
name: .ghosttyMaximizeDidToggle,
|
||||
object: nil)
|
||||
|
||||
// Splits
|
||||
center.addObserver(
|
||||
self,
|
||||
selector: #selector(ghosttyDidCloseSurface(_:)),
|
||||
name: Ghostty.Notification.ghosttyCloseSurface,
|
||||
object: nil)
|
||||
center.addObserver(
|
||||
self,
|
||||
selector: #selector(ghosttyDidNewSplit(_:)),
|
||||
name: Ghostty.Notification.ghosttyNewSplit,
|
||||
object: nil)
|
||||
center.addObserver(
|
||||
self,
|
||||
selector: #selector(ghosttyDidEqualizeSplits(_:)),
|
||||
@ -252,7 +269,58 @@ class BaseTerminalController: NSWindowController,
|
||||
guard surfaceTree?.contains(view: surfaceView) ?? false else { return }
|
||||
window.zoom(nil)
|
||||
}
|
||||
|
||||
|
||||
@objc private func ghosttyDidCloseSurface(_ notification: Notification) {
|
||||
// The target must be within our tree
|
||||
guard let oldView = notification.object as? Ghostty.SurfaceView else { return }
|
||||
guard let node = surfaceTree2.root?.node(view: oldView) else { return }
|
||||
|
||||
// Remove it
|
||||
surfaceTree2 = surfaceTree2.remove(node)
|
||||
|
||||
// TODO: fix focus
|
||||
}
|
||||
|
||||
@objc private func ghosttyDidNewSplit(_ notification: Notification) {
|
||||
// The target must be within our tree
|
||||
guard let oldView = notification.object as? Ghostty.SurfaceView else { return }
|
||||
guard surfaceTree2.root?.node(view: oldView) != nil else { return }
|
||||
|
||||
// Notification must contain our base config
|
||||
let configAny = notification.userInfo?[Ghostty.Notification.NewSurfaceConfigKey]
|
||||
let config = configAny as? Ghostty.SurfaceConfiguration
|
||||
|
||||
// Determine our desired direction
|
||||
guard let directionAny = notification.userInfo?["direction"] else { return }
|
||||
guard let direction = directionAny as? ghostty_action_split_direction_e else { return }
|
||||
let splitDirection: SplitTree.NewDirection
|
||||
switch (direction) {
|
||||
case GHOSTTY_SPLIT_DIRECTION_RIGHT: splitDirection = .right
|
||||
case GHOSTTY_SPLIT_DIRECTION_LEFT: splitDirection = .left
|
||||
case GHOSTTY_SPLIT_DIRECTION_DOWN: splitDirection = .down
|
||||
case GHOSTTY_SPLIT_DIRECTION_UP: splitDirection = .up
|
||||
default: return
|
||||
}
|
||||
|
||||
// Create a new surface view
|
||||
guard let ghostty_app = ghostty.app else { return }
|
||||
let newView = Ghostty.SurfaceView(ghostty_app, baseConfig: config)
|
||||
|
||||
// Do the split
|
||||
do {
|
||||
surfaceTree2 = try surfaceTree2.insert(view: newView, at: oldView, direction: splitDirection)
|
||||
} catch {
|
||||
// If splitting fails for any reason (it should not), then we just log
|
||||
// and return. The new view we created will be deinitialized and its
|
||||
// no big deal.
|
||||
// TODO: log
|
||||
return
|
||||
}
|
||||
|
||||
// Once we've split, we need to move focus to the new split
|
||||
Ghostty.moveFocus(to: newView, from: oldView)
|
||||
}
|
||||
|
||||
@objc private func ghosttyDidEqualizeSplits(_ notification: Notification) {
|
||||
guard let target = notification.object as? Ghostty.SurfaceView else { return }
|
||||
|
||||
|
@ -228,6 +228,9 @@ class TerminalController: BaseTerminalController {
|
||||
// Update our window light/darkness based on our updated background color
|
||||
window.isLightTheme = OSColor(surfaceConfig.backgroundColor).isLightColor
|
||||
|
||||
// Sync our zoom state for splits
|
||||
window.surfaceIsZoomed = surfaceTree2.zoomed != nil
|
||||
|
||||
// If our window is not visible, then we do nothing. Some things such as blurring
|
||||
// have no effect if the window is not visible. Ultimately, we'll have this called
|
||||
// at some point when a surface becomes focused.
|
||||
|
@ -31,7 +31,7 @@ protocol TerminalViewDelegate: AnyObject {
|
||||
protocol TerminalViewModel: ObservableObject {
|
||||
/// The tree of terminal surfaces (splits) within the view. This is mutated by TerminalView
|
||||
/// and children. This should be @Published.
|
||||
var surfaceTree: Ghostty.SplitNode? { get set }
|
||||
var surfaceTree2: SplitTree { get set }
|
||||
|
||||
/// The command palette state.
|
||||
var commandPaletteIsShowing: Bool { get set }
|
||||
@ -81,7 +81,7 @@ struct TerminalView<ViewModel: TerminalViewModel>: View {
|
||||
DebugBuildWarningView()
|
||||
}
|
||||
|
||||
Ghostty.TerminalSplit(node: $viewModel.surfaceTree)
|
||||
TerminalSplitTreeView(tree: viewModel.surfaceTree2)
|
||||
.environmentObject(ghostty)
|
||||
.focused($focused)
|
||||
.onAppear { self.focused = true }
|
||||
@ -100,12 +100,6 @@ struct TerminalView<ViewModel: TerminalViewModel>: View {
|
||||
guard let size = newValue else { return }
|
||||
self.delegate?.cellSizeDidChange(to: size)
|
||||
}
|
||||
.onChange(of: viewModel.surfaceTree?.hashValue) { _ in
|
||||
// This is funky, but its the best way I could think of to detect
|
||||
// ANY CHANGE within the deeply nested surface tree -- detecting a change
|
||||
// in the hash value.
|
||||
self.delegate?.surfaceTreeDidChange()
|
||||
}
|
||||
.onChange(of: zoomedSplit) { newValue in
|
||||
self.delegate?.zoomStateDidChange(to: newValue ?? false)
|
||||
}
|
||||
|
@ -30,6 +30,7 @@ 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()
|
||||
},
|
||||
@ -375,6 +376,7 @@ class TerminalWindow: NSWindow {
|
||||
if !titlebarAccessoryViewControllers.contains(resetZoomTitlebarAccessoryViewController) {
|
||||
addTitlebarAccessoryViewController(resetZoomTitlebarAccessoryViewController)
|
||||
}
|
||||
|
||||
resetZoomTitlebarAccessoryViewController.view.isHidden = tabGroup.isTabBarVisible ? true : !surfaceIsZoomed
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user