macos: track surface focus state

This commit is contained in:
Mitchell Hashimoto
2023-02-18 16:51:36 -08:00
parent 78754ff3ac
commit 6b450f7c7d
8 changed files with 132 additions and 4 deletions

View File

@@ -20,7 +20,8 @@ struct GhosttyApp: App {
case .error:
ErrorView()
case .ready:
TerminalSurfaceView(app: ghostty.app!)
TerminalView(app: ghostty.app!)
.modifier(WindowObservationModifier())
}
}
}

View File

@@ -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)

View 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)
}
}
}

View 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) {}
}