mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-16 16:56:09 +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
|
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) {
|
||||||
|
@ -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 {
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user