mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-08-29 10:31:40 +00:00
macos: track surface focus state
This commit is contained in:
@@ -20,7 +20,8 @@ struct GhosttyApp: App {
|
||||
case .error:
|
||||
ErrorView()
|
||||
case .ready:
|
||||
TerminalSurfaceView(app: ghostty.app!)
|
||||
TerminalView(app: ghostty.app!)
|
||||
.modifier(WindowObservationModifier())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,10 +10,12 @@ import GhosttyKit
|
||||
/// since that is what the Metal renderer in Ghostty expects. In the future, it may make more sense to
|
||||
/// wrap an MTKView and use that, but for legacy reasons we didn't do that to begin with.
|
||||
struct TerminalSurfaceView: NSViewRepresentable {
|
||||
var hasFocus: Bool
|
||||
@StateObject private var state: TerminalSurfaceView_Real
|
||||
|
||||
init(app: ghostty_app_t) {
|
||||
init(app: ghostty_app_t, hasFocus: Bool) {
|
||||
self._state = StateObject(wrappedValue: TerminalSurfaceView_Real(app))
|
||||
self.hasFocus = hasFocus
|
||||
}
|
||||
|
||||
func makeNSView(context: Context) -> TerminalSurfaceView_Real {
|
||||
@@ -24,7 +26,7 @@ struct TerminalSurfaceView: NSViewRepresentable {
|
||||
}
|
||||
|
||||
func updateNSView(_ view: TerminalSurfaceView_Real, context: Context) {
|
||||
// Nothing we need to do here.
|
||||
state.focusDidChange(hasFocus)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -187,6 +189,11 @@ class TerminalSurfaceView_Real: NSView, NSTextInputClient, ObservableObject {
|
||||
ghostty_surface_free(surface)
|
||||
}
|
||||
|
||||
func focusDidChange(_ focused: Bool) {
|
||||
guard let surface = self.surface else { return }
|
||||
ghostty_surface_set_focus(surface, focused ? 1 : 0)
|
||||
}
|
||||
|
||||
override func resize(withOldSuperviewSize oldSize: NSSize) {
|
||||
super.resize(withOldSuperviewSize: oldSize)
|
||||
|
||||
|
||||
19
macos/Sources/TerminalView.swift
Normal file
19
macos/Sources/TerminalView.swift
Normal file
@@ -0,0 +1,19 @@
|
||||
import SwiftUI
|
||||
import GhosttyKit
|
||||
|
||||
struct TerminalView: View {
|
||||
let app: ghostty_app_t
|
||||
@FocusState private var surfaceFocus: Bool
|
||||
@Environment(\.isKeyWindow) private var isKeyWindow: Bool
|
||||
|
||||
// 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 && isKeyWindow }
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
TerminalSurfaceView(app: app, hasFocus: hasFocus)
|
||||
.focused($surfaceFocus)
|
||||
}
|
||||
}
|
||||
}
|
||||
80
macos/Sources/WindowTracker.swift
Normal file
80
macos/Sources/WindowTracker.swift
Normal file
@@ -0,0 +1,80 @@
|
||||
import SwiftUI
|
||||
|
||||
/// This modifier tracks whether the window is the key window in the isKeyWindow environment value.
|
||||
struct WindowObservationModifier: ViewModifier {
|
||||
@StateObject var windowObserver: WindowObserver = WindowObserver()
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
content.background(
|
||||
HostingWindowFinder { [weak windowObserver] window in
|
||||
windowObserver?.window = window
|
||||
}
|
||||
).environment(\.isKeyWindow, windowObserver.isKeyWindow)
|
||||
}
|
||||
}
|
||||
|
||||
extension EnvironmentValues {
|
||||
struct IsKeyWindowKey: EnvironmentKey {
|
||||
static var defaultValue: Bool = false
|
||||
typealias Value = Bool
|
||||
}
|
||||
|
||||
fileprivate(set) var isKeyWindow: Bool {
|
||||
get {
|
||||
self[IsKeyWindowKey.self]
|
||||
}
|
||||
set {
|
||||
self[IsKeyWindowKey.self] = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class WindowObserver: ObservableObject {
|
||||
@Published public private(set) var isKeyWindow: Bool = false
|
||||
|
||||
private var becomeKeyobserver: NSObjectProtocol?
|
||||
private var resignKeyobserver: NSObjectProtocol?
|
||||
|
||||
weak var window: NSWindow? {
|
||||
didSet {
|
||||
self.isKeyWindow = window?.isKeyWindow ?? false
|
||||
guard let window = window else {
|
||||
self.becomeKeyobserver = nil
|
||||
self.resignKeyobserver = nil
|
||||
return
|
||||
}
|
||||
|
||||
self.becomeKeyobserver = NotificationCenter.default.addObserver(
|
||||
forName: NSWindow.didBecomeKeyNotification,
|
||||
object: window,
|
||||
queue: .main
|
||||
) { (n) in
|
||||
self.isKeyWindow = true
|
||||
}
|
||||
|
||||
self.resignKeyobserver = NotificationCenter.default.addObserver(
|
||||
forName: NSWindow.didResignKeyNotification,
|
||||
object: window,
|
||||
queue: .main
|
||||
) { (n) in
|
||||
self.isKeyWindow = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// This view calls the callback with the window value that hosts the view.
|
||||
struct HostingWindowFinder: NSViewRepresentable {
|
||||
var callback: (NSWindow?) -> ()
|
||||
|
||||
func makeNSView(context: Self.Context) -> NSView {
|
||||
let view = NSView()
|
||||
view.translatesAutoresizingMaskIntoConstraints = false
|
||||
DispatchQueue.main.async { [weak view] in
|
||||
self.callback(view?.window)
|
||||
}
|
||||
return view
|
||||
}
|
||||
|
||||
func updateNSView(_ nsView: NSView, context: Context) {}
|
||||
}
|
||||
Reference in New Issue
Block a user