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 let ghostty: Ghostty.App
/// The currently focused surface. /// The currently focused surface.
var focusedSurface: Ghostty.SurfaceView? = nil var focusedSurface: Ghostty.SurfaceView? = nil {
didSet {
syncFocusToSurfaceTree()
}
}
/// The surface tree for this window. /// The surface tree for this window.
@Published var surfaceTree: Ghostty.SplitNode? = nil { @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. // in the old tree have closed and then close the window.
if (surfaceTree == nil) { if (surfaceTree == nil) {
oldValue?.close() oldValue?.close()
focusedSurface = nil
lastSurfaceDidClose() 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 //MARK: - NSWindowController
override func windowWillLoad() { override func windowWillLoad() {
@ -348,6 +368,16 @@ class TerminalController: NSWindowController, NSWindowDelegate,
func windowDidBecomeKey(_ notification: Notification) { func windowDidBecomeKey(_ notification: Notification) {
self.relabelTabs() self.relabelTabs()
self.fixTabBar() 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) { 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 /// "container" which has a recursive top/left SplitNode and bottom/right SplitNode. These
/// values can further be split infinitely. /// values can further be split infinitely.
/// ///
enum SplitNode: Equatable, Hashable, Codable { enum SplitNode: Equatable, Hashable, Codable, Sequence {
case leaf(Leaf) case leaf(Leaf)
case split(Container) 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 // MARK: - Equatable
static func == (lhs: SplitNode, rhs: SplitNode) -> Bool { static func == (lhs: SplitNode, rhs: SplitNode) -> Bool {

View File

@ -51,10 +51,6 @@ extension Ghostty {
@EnvironmentObject private var ghostty: Ghostty.App @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 { var body: some View {
let center = NotificationCenter.default let center = NotificationCenter.default
@ -72,7 +68,7 @@ extension Ghostty {
let pubResign = center.publisher(for: NSWindow.didResignKeyNotification) let pubResign = center.publisher(for: NSWindow.didResignKeyNotification)
#endif #endif
Surface(view: surfaceView, hasFocus: hasFocus, size: geo.size) Surface(view: surfaceView, size: geo.size)
.focused($surfaceFocus) .focused($surfaceFocus)
.focusedValue(\.ghosttySurfaceTitle, surfaceView.title) .focusedValue(\.ghosttySurfaceTitle, surfaceView.title)
.focusedValue(\.ghosttySurfaceView, surfaceView) .focusedValue(\.ghosttySurfaceView, surfaceView)
@ -230,11 +226,6 @@ extension Ghostty {
/// The view to render for the terminal surface. /// The view to render for the terminal surface.
let view: SurfaceView 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 /// 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 /// surface. This does not actually SET the size of our frame, this only sets the size
/// of our Metal surface for drawing. /// of our Metal surface for drawing.
@ -253,7 +244,6 @@ extension Ghostty {
} }
func updateOSView(_ view: SurfaceView, context: Context) { func updateOSView(_ view: SurfaceView, context: Context) {
view.focusDidChange(hasFocus)
view.sizeDidChange(size) view.sizeDidChange(size)
} }
} }

View File

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