Merge pull request #1504 from mitchellh/macos-focus

macos: more robust surface focus state detection
This commit is contained in:
Mitchell Hashimoto
2024-02-11 09:28:35 -08:00
committed by GitHub
4 changed files with 55 additions and 18 deletions

View File

@ -14,7 +14,11 @@ class TerminalController: NSWindowController, NSWindowDelegate,
let ghostty: Ghostty.App
/// The currently focused surface.
var focusedSurface: Ghostty.SurfaceView? = nil
var focusedSurface: Ghostty.SurfaceView? = nil {
didSet {
syncFocusToSurfaceTree()
}
}
/// The surface tree for this window.
@Published var surfaceTree: Ghostty.SplitNode? = nil {
@ -23,6 +27,7 @@ class TerminalController: NSWindowController, NSWindowDelegate,
// in the old tree have closed and then close the window.
if (surfaceTree == nil) {
oldValue?.close()
focusedSurface = nil
lastSurfaceDidClose()
}
}
@ -181,6 +186,21 @@ class TerminalController: NSWindowController, NSWindowDelegate,
}
}
/// Update all surfaces with the focus state. This ensures that libghostty has an accurate view about
/// what surface is focused. This must be called whenever a surface OR window changes focus.
private func syncFocusToSurfaceTree() {
guard let tree = self.surfaceTree else { return }
for leaf in tree {
// Our focus state requires that this window is key and our currently
// focused surface is the surface in this leaf.
let focused: Bool = (window?.isKeyWindow ?? false) &&
focusedSurface != nil &&
leaf.surface == focusedSurface!
leaf.surface.focusDidChange(focused)
}
}
//MARK: - NSWindowController
override func windowWillLoad() {
@ -348,6 +368,16 @@ class TerminalController: NSWindowController, NSWindowDelegate,
func windowDidBecomeKey(_ notification: Notification) {
self.relabelTabs()
self.fixTabBar()
// Becoming/losing key means we have to notify our surface(s) that we have focus
// so things like cursors blink, pty events are sent, etc.
self.syncFocusToSurfaceTree()
}
func windowDidResignKey(_ notification: Notification) {
// Becoming/losing key means we have to notify our surface(s) that we have focus
// so things like cursors blink, pty events are sent, etc.
self.syncFocusToSurfaceTree()
}
func windowDidMove(_ notification: Notification) {

View File

@ -11,7 +11,7 @@ extension Ghostty {
/// "container" which has a recursive top/left SplitNode and bottom/right SplitNode. These
/// values can further be split infinitely.
///
enum SplitNode: Equatable, Hashable, Codable {
enum SplitNode: Equatable, Hashable, Codable, Sequence {
case leaf(Leaf)
case split(Container)
@ -136,6 +136,24 @@ extension Ghostty {
}
}
// MARK: - Sequence
func makeIterator() -> IndexingIterator<[Leaf]> {
return leaves().makeIterator()
}
/// Return all the leaves in this split node. This isn't very efficient but our split trees are never super
/// deep so its not an issue.
private func leaves() -> [Leaf] {
switch (self) {
case .leaf(let leaf):
return [leaf]
case .split(let container):
return container.topLeft.leaves() + container.bottomRight.leaves()
}
}
// MARK: - Equatable
static func == (lhs: SplitNode, rhs: SplitNode) -> Bool {

View File

@ -51,10 +51,6 @@ extension Ghostty {
@EnvironmentObject private var ghostty: Ghostty.App
// This is true if the terminal is considered "focused". The terminal is focused if
// it is both individually focused and the containing window is key.
private var hasFocus: Bool { surfaceFocus && windowFocus }
var body: some View {
let center = NotificationCenter.default
@ -72,7 +68,7 @@ extension Ghostty {
let pubResign = center.publisher(for: NSWindow.didResignKeyNotification)
#endif
Surface(view: surfaceView, hasFocus: hasFocus, size: geo.size)
Surface(view: surfaceView, size: geo.size)
.focused($surfaceFocus)
.focusedValue(\.ghosttySurfaceTitle, surfaceView.title)
.focusedValue(\.ghosttySurfaceView, surfaceView)
@ -230,11 +226,6 @@ extension Ghostty {
/// The view to render for the terminal surface.
let view: SurfaceView
/// This should be set to true when the surface has focus. This is up to the parent because
/// focus is also defined by window focus. It is important this is set correctly since if it is
/// false then the surface will idle at almost 0% CPU.
let hasFocus: Bool
/// The size of the frame containing this view. We use this to update the the underlying
/// surface. This does not actually SET the size of our frame, this only sets the size
/// of our Metal surface for drawing.
@ -253,7 +244,6 @@ extension Ghostty {
}
func updateOSView(_ view: SurfaceView, context: Context) {
view.focusDidChange(hasFocus)
view.sizeDidChange(size)
}
}

View File

@ -188,6 +188,8 @@ extension Ghostty {
func focusDidChange(_ focused: Bool) {
guard let surface = self.surface else { return }
guard self.focused != focused else { return }
self.focused = focused
ghostty_surface_set_focus(surface, focused)
}
@ -358,7 +360,7 @@ extension Ghostty {
override func becomeFirstResponder() -> Bool {
let result = super.becomeFirstResponder()
if (result) { focused = true }
if (result) { focusDidChange(true) }
return result
}
@ -367,10 +369,7 @@ extension Ghostty {
// We sometimes call this manually (see SplitView) as a way to force us to
// yield our focus state.
if (result) {
focusDidChange(false)
focused = false
}
if (result) { focusDidChange(false) }
return result
}