macos: focus split previous/next
parent
b84b715ddb
commit
0fb58298a7
|
|
@ -61,6 +61,7 @@
|
||||||
A586167C2B7703CC009BDB1D /* fish in Resources */ = {isa = PBXBuildFile; fileRef = A586167B2B7703CC009BDB1D /* fish */; };
|
A586167C2B7703CC009BDB1D /* fish in Resources */ = {isa = PBXBuildFile; fileRef = A586167B2B7703CC009BDB1D /* fish */; };
|
||||||
A586365F2DEE6C2300E04A10 /* SplitTree.swift in Sources */ = {isa = PBXBuildFile; fileRef = A586365E2DEE6C2100E04A10 /* SplitTree.swift */; };
|
A586365F2DEE6C2300E04A10 /* SplitTree.swift in Sources */ = {isa = PBXBuildFile; fileRef = A586365E2DEE6C2100E04A10 /* SplitTree.swift */; };
|
||||||
A58636662DEF964100E04A10 /* TerminalSplitTreeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A58636652DEF963F00E04A10 /* TerminalSplitTreeView.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 */; };
|
A5874D992DAD751B00E83852 /* CGS.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5874D982DAD751A00E83852 /* CGS.swift */; };
|
||||||
A5874D9D2DAD786100E83852 /* NSWindow+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5874D9C2DAD785F00E83852 /* NSWindow+Extension.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 */; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
A59444F629A2ED5200725BBA /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = "<group>"; };
|
||||||
|
|
@ -292,6 +294,7 @@
|
||||||
A534263D2A7DCBB000EBB7A2 /* Helpers */ = {
|
A534263D2A7DCBB000EBB7A2 /* Helpers */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
A58636692DF0A98100E04A10 /* Extensions */,
|
||||||
A5874D9B2DAD781100E83852 /* Private */,
|
A5874D9B2DAD781100E83852 /* Private */,
|
||||||
A5AEB1642D5BE7BF00513529 /* LastWindowPosition.swift */,
|
A5AEB1642D5BE7BF00513529 /* LastWindowPosition.swift */,
|
||||||
A5A6F7292CC41B8700B232A5 /* Xcode.swift */,
|
A5A6F7292CC41B8700B232A5 /* Xcode.swift */,
|
||||||
|
|
@ -442,6 +445,14 @@
|
||||||
path = Splits;
|
path = Splits;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
A58636692DF0A98100E04A10 /* Extensions */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
A586366A2DF0A98900E04A10 /* Array+Extension.swift */,
|
||||||
|
);
|
||||||
|
path = Extensions;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
A5874D9B2DAD781100E83852 /* Private */ = {
|
A5874D9B2DAD781100E83852 /* Private */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
|
@ -721,6 +732,7 @@
|
||||||
A55B7BBC29B6FC330055DE60 /* SurfaceView.swift in Sources */,
|
A55B7BBC29B6FC330055DE60 /* SurfaceView.swift in Sources */,
|
||||||
A5333E1C2B5A1CE3008AEFF7 /* CrossKit.swift in Sources */,
|
A5333E1C2B5A1CE3008AEFF7 /* CrossKit.swift in Sources */,
|
||||||
A5874D992DAD751B00E83852 /* CGS.swift in Sources */,
|
A5874D992DAD751B00E83852 /* CGS.swift in Sources */,
|
||||||
|
A586366B2DF0A98C00E04A10 /* Array+Extension.swift in Sources */,
|
||||||
A59444F729A2ED5200725BBA /* SettingsView.swift in Sources */,
|
A59444F729A2ED5200725BBA /* SettingsView.swift in Sources */,
|
||||||
A56D58862ACDDB4100508D2C /* Ghostty.Shell.swift in Sources */,
|
A56D58862ACDDB4100508D2C /* Ghostty.Shell.swift in Sources */,
|
||||||
A5985CD72C320C4500C57AD3 /* String+Extension.swift in Sources */,
|
A5985CD72C320C4500C57AD3 /* String+Extension.swift in Sources */,
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,20 @@ struct SplitTree<ViewType: NSView & Codable>: Codable {
|
||||||
case down
|
case down
|
||||||
case up
|
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
|
// MARK: SplitTree
|
||||||
|
|
@ -111,6 +125,44 @@ extension SplitTree {
|
||||||
|
|
||||||
return .init(root: newRoot, zoomed: newZoomed)
|
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
|
// 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
|
// MARK: SplitTree.Node Protocols
|
||||||
|
|
|
||||||
|
|
@ -145,6 +145,11 @@ class BaseTerminalController: NSWindowController,
|
||||||
selector: #selector(ghosttyDidEqualizeSplits(_:)),
|
selector: #selector(ghosttyDidEqualizeSplits(_:)),
|
||||||
name: Ghostty.Notification.didEqualizeSplits,
|
name: Ghostty.Notification.didEqualizeSplits,
|
||||||
object: nil)
|
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
|
// Listen for local events that we need to know of outside of
|
||||||
// single surface handlers.
|
// single surface handlers.
|
||||||
|
|
@ -386,6 +391,38 @@ class BaseTerminalController: NSWindowController,
|
||||||
_ = container.equalize()
|
_ = 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
|
// MARK: Local Events
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -921,7 +921,8 @@ extension Ghostty {
|
||||||
// we should only be returning true if we actually performed the action,
|
// 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
|
// but this handles the most common case of caring about goto_split performability
|
||||||
// which is the no-split case.
|
// 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(
|
NotificationCenter.default.post(
|
||||||
name: Notification.ghosttyFocusSplit,
|
name: Notification.ghosttyFocusSplit,
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue