mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-09-05 19:08:17 +00:00
macos: focus split previous/next
This commit is contained in:
@@ -61,6 +61,7 @@
|
||||
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 */; };
|
||||
A586366B2DF0A98C00E04A10 /* Array+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A586366A2DF0A98900E04A10 /* Array+Extension.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 */; };
|
||||
@@ -168,6 +169,7 @@
|
||||
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>"; };
|
||||
A586366A2DF0A98900E04A10 /* Array+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array+Extension.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>"; };
|
||||
@@ -292,6 +294,7 @@
|
||||
A534263D2A7DCBB000EBB7A2 /* Helpers */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
A58636692DF0A98100E04A10 /* Extensions */,
|
||||
A5874D9B2DAD781100E83852 /* Private */,
|
||||
A5AEB1642D5BE7BF00513529 /* LastWindowPosition.swift */,
|
||||
A5A6F7292CC41B8700B232A5 /* Xcode.swift */,
|
||||
@@ -442,6 +445,14 @@
|
||||
path = Splits;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
A58636692DF0A98100E04A10 /* Extensions */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
A586366A2DF0A98900E04A10 /* Array+Extension.swift */,
|
||||
);
|
||||
path = Extensions;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
A5874D9B2DAD781100E83852 /* Private */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@@ -721,6 +732,7 @@
|
||||
A55B7BBC29B6FC330055DE60 /* SurfaceView.swift in Sources */,
|
||||
A5333E1C2B5A1CE3008AEFF7 /* CrossKit.swift in Sources */,
|
||||
A5874D992DAD751B00E83852 /* CGS.swift in Sources */,
|
||||
A586366B2DF0A98C00E04A10 /* Array+Extension.swift in Sources */,
|
||||
A59444F729A2ED5200725BBA /* SettingsView.swift in Sources */,
|
||||
A56D58862ACDDB4100508D2C /* Ghostty.Shell.swift in Sources */,
|
||||
A5985CD72C320C4500C57AD3 /* String+Extension.swift in Sources */,
|
||||
|
@@ -50,6 +50,20 @@ struct SplitTree<ViewType: NSView & Codable>: Codable {
|
||||
case down
|
||||
case up
|
||||
}
|
||||
|
||||
/// The direction that focus can move from a node.
|
||||
enum FocusDirection {
|
||||
// Follow a consistent tree-like structure.
|
||||
case previous
|
||||
case next
|
||||
|
||||
// Geospatially-aware navigation targets. These take into account the
|
||||
// dimensions of the view to find the correct node to go to.
|
||||
case up
|
||||
case down
|
||||
case left
|
||||
case right
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: SplitTree
|
||||
@@ -111,6 +125,44 @@ extension SplitTree {
|
||||
|
||||
return .init(root: newRoot, zoomed: newZoomed)
|
||||
}
|
||||
|
||||
/// Find the next view to focus based on the current focused node and direction
|
||||
func focusTarget(for direction: FocusDirection, from currentNode: Node) -> ViewType? {
|
||||
guard let root else { return nil }
|
||||
|
||||
switch direction {
|
||||
case .previous:
|
||||
// For previous, we traverse in order and find the previous leaf from our leftmost
|
||||
let allLeaves = root.leaves()
|
||||
let currentView = currentNode.leftmostLeaf()
|
||||
guard let currentIndex = allLeaves.firstIndex(where: { $0 === currentView }) else {
|
||||
// Shouldn't be possible leftmostLeaf can't return something that doesn't exist!
|
||||
return nil
|
||||
}
|
||||
let index = allLeaves.indexWrapping(before: currentIndex)
|
||||
return allLeaves[index]
|
||||
|
||||
case .next:
|
||||
// For previous, we traverse in order and find the next leaf from our rightmost
|
||||
let allLeaves = root.leaves()
|
||||
let currentView = currentNode.rightmostLeaf()
|
||||
guard let currentIndex = allLeaves.firstIndex(where: { $0 === currentView }) else {
|
||||
return nil
|
||||
}
|
||||
let index = allLeaves.indexWrapping(after: currentIndex)
|
||||
return allLeaves[index]
|
||||
|
||||
case .up, .down, .left, .right:
|
||||
// For directional movement, we need to traverse the tree structure
|
||||
return directionalTarget(for: direction, from: currentNode)
|
||||
}
|
||||
}
|
||||
|
||||
/// Find focus target in a specific direction by traversing split boundaries
|
||||
private func directionalTarget(for direction: FocusDirection, from currentNode: Node) -> ViewType? {
|
||||
// TODO
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: SplitTree.Node
|
||||
@@ -331,6 +383,26 @@ extension SplitTree.Node {
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the leftmost leaf in this subtree
|
||||
func leftmostLeaf() -> ViewType {
|
||||
switch self {
|
||||
case .leaf(let view):
|
||||
return view
|
||||
case .split(let split):
|
||||
return split.left.leftmostLeaf()
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the rightmost leaf in this subtree
|
||||
func rightmostLeaf() -> ViewType {
|
||||
switch self {
|
||||
case .leaf(let view):
|
||||
return view
|
||||
case .split(let split):
|
||||
return split.right.rightmostLeaf()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: SplitTree.Node Protocols
|
||||
|
@@ -145,6 +145,11 @@ class BaseTerminalController: NSWindowController,
|
||||
selector: #selector(ghosttyDidEqualizeSplits(_:)),
|
||||
name: Ghostty.Notification.didEqualizeSplits,
|
||||
object: nil)
|
||||
center.addObserver(
|
||||
self,
|
||||
selector: #selector(ghosttyDidFocusSplit(_:)),
|
||||
name: Ghostty.Notification.ghosttyFocusSplit,
|
||||
object: nil)
|
||||
|
||||
// Listen for local events that we need to know of outside of
|
||||
// single surface handlers.
|
||||
@@ -386,6 +391,38 @@ class BaseTerminalController: NSWindowController,
|
||||
_ = container.equalize()
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func ghosttyDidFocusSplit(_ notification: Notification) {
|
||||
// The target must be within our tree
|
||||
guard let target = notification.object as? Ghostty.SurfaceView else { return }
|
||||
guard surfaceTree2.root?.node(view: target) != nil else { return }
|
||||
|
||||
// Get the direction from the notification
|
||||
guard let directionAny = notification.userInfo?[Ghostty.Notification.SplitDirectionKey] else { return }
|
||||
guard let direction = directionAny as? Ghostty.SplitFocusDirection else { return }
|
||||
|
||||
// Convert Ghostty.SplitFocusDirection to our SplitTree.FocusDirection
|
||||
let focusDirection: SplitTree<Ghostty.SurfaceView>.FocusDirection
|
||||
switch direction {
|
||||
case .previous: focusDirection = .previous
|
||||
case .next: focusDirection = .next
|
||||
case .up: focusDirection = .up
|
||||
case .down: focusDirection = .down
|
||||
case .left: focusDirection = .left
|
||||
case .right: focusDirection = .right
|
||||
}
|
||||
|
||||
// Find the node for the target surface
|
||||
guard let targetNode = surfaceTree2.root?.node(view: target) else { return }
|
||||
|
||||
// Find the next surface to focus
|
||||
guard let nextSurface = surfaceTree2.focusTarget(for: focusDirection, from: targetNode) else {
|
||||
return
|
||||
}
|
||||
|
||||
// Move focus to the next surface
|
||||
Ghostty.moveFocus(to: nextSurface, from: target)
|
||||
}
|
||||
|
||||
// MARK: Local Events
|
||||
|
||||
|
@@ -921,7 +921,8 @@ extension Ghostty {
|
||||
// we should only be returning true if we actually performed the action,
|
||||
// but this handles the most common case of caring about goto_split performability
|
||||
// which is the no-split case.
|
||||
guard controller.surfaceTree?.isSplit ?? false else { return false }
|
||||
// TODO: fix this
|
||||
//guard controller.surfaceTree?.isSplit ?? false else { return false }
|
||||
|
||||
NotificationCenter.default.post(
|
||||
name: Notification.ghosttyFocusSplit,
|
||||
|
19
macos/Sources/Helpers/Extensions/Array+Extension.swift
Normal file
19
macos/Sources/Helpers/Extensions/Array+Extension.swift
Normal file
@@ -0,0 +1,19 @@
|
||||
extension Array {
|
||||
/// Returns the index before i, with wraparound. Assumes i is a valid index.
|
||||
func indexWrapping(before i: Int) -> Int {
|
||||
if i == 0 {
|
||||
return count - 1
|
||||
}
|
||||
|
||||
return i - 1
|
||||
}
|
||||
|
||||
/// Returns the index after i, with wraparound. Assumes i is a valid index.
|
||||
func indexWrapping(after i: Int) -> Int {
|
||||
if i == count - 1 {
|
||||
return 0
|
||||
}
|
||||
|
||||
return i + 1
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user