mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-16 00:36:07 +03:00
Merge pull request #1504 from mitchellh/macos-focus
macos: more robust surface focus state detection
This commit is contained in:
@ -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) {
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
Reference in New Issue
Block a user