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

@ -211,6 +211,7 @@ ghostty_surface_t ghostty_surface_new(ghostty_app_t, ghostty_surface_config_s*);
void ghostty_surface_free(ghostty_surface_t); void ghostty_surface_free(ghostty_surface_t);
void ghostty_surface_refresh(ghostty_surface_t); void ghostty_surface_refresh(ghostty_surface_t);
void ghostty_surface_set_content_scale(ghostty_surface_t, double, double); void ghostty_surface_set_content_scale(ghostty_surface_t, double, double);
void ghostty_surface_set_focus(ghostty_surface_t, bool);
void ghostty_surface_set_size(ghostty_surface_t, uint32_t, uint32_t); void ghostty_surface_set_size(ghostty_surface_t, uint32_t, uint32_t);
void ghostty_surface_key(ghostty_surface_t, ghostty_input_action_e, ghostty_input_key_e, ghostty_input_mods_e); void ghostty_surface_key(ghostty_surface_t, ghostty_input_action_e, ghostty_input_key_e, ghostty_input_mods_e);
void ghostty_surface_char(ghostty_surface_t, uint32_t); void ghostty_surface_char(ghostty_surface_t, uint32_t);

View File

@ -8,6 +8,8 @@
/* Begin PBXBuildFile section */ /* Begin PBXBuildFile section */
A507573E299FF33C009D7DC7 /* TerminalSurfaceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A507573D299FF33C009D7DC7 /* TerminalSurfaceView.swift */; }; A507573E299FF33C009D7DC7 /* TerminalSurfaceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A507573D299FF33C009D7DC7 /* TerminalSurfaceView.swift */; };
A518502429A197C700E4CC4F /* TerminalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A518502329A197C700E4CC4F /* TerminalView.swift */; };
A518502629A1A45100E4CC4F /* WindowTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = A518502529A1A45100E4CC4F /* WindowTracker.swift */; };
A535B9DA299C569B0017E2E4 /* ErrorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A535B9D9299C569B0017E2E4 /* ErrorView.swift */; }; A535B9DA299C569B0017E2E4 /* ErrorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A535B9D9299C569B0017E2E4 /* ErrorView.swift */; };
A55685E029A03A9F004303CE /* AppError.swift in Sources */ = {isa = PBXBuildFile; fileRef = A55685DF29A03A9F004303CE /* AppError.swift */; }; A55685E029A03A9F004303CE /* AppError.swift in Sources */ = {isa = PBXBuildFile; fileRef = A55685DF29A03A9F004303CE /* AppError.swift */; };
A5B30535299BEAAA0047F10C /* GhosttyApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5B30534299BEAAA0047F10C /* GhosttyApp.swift */; }; A5B30535299BEAAA0047F10C /* GhosttyApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5B30534299BEAAA0047F10C /* GhosttyApp.swift */; };
@ -17,6 +19,8 @@
/* Begin PBXFileReference section */ /* Begin PBXFileReference section */
A507573D299FF33C009D7DC7 /* TerminalSurfaceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalSurfaceView.swift; sourceTree = "<group>"; }; A507573D299FF33C009D7DC7 /* TerminalSurfaceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalSurfaceView.swift; sourceTree = "<group>"; };
A518502329A197C700E4CC4F /* TerminalView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalView.swift; sourceTree = "<group>"; };
A518502529A1A45100E4CC4F /* WindowTracker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowTracker.swift; sourceTree = "<group>"; };
A535B9D9299C569B0017E2E4 /* ErrorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorView.swift; sourceTree = "<group>"; }; A535B9D9299C569B0017E2E4 /* ErrorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorView.swift; sourceTree = "<group>"; };
A55685DF29A03A9F004303CE /* AppError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppError.swift; sourceTree = "<group>"; }; A55685DF29A03A9F004303CE /* AppError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppError.swift; sourceTree = "<group>"; };
A5B30531299BEAAA0047F10C /* Ghostty.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Ghostty.app; sourceTree = BUILT_PRODUCTS_DIR; }; A5B30531299BEAAA0047F10C /* Ghostty.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Ghostty.app; sourceTree = BUILT_PRODUCTS_DIR; };
@ -44,8 +48,10 @@
A5D495A0299BEC2200DD1313 /* Preview Content */, A5D495A0299BEC2200DD1313 /* Preview Content */,
A5B30534299BEAAA0047F10C /* GhosttyApp.swift */, A5B30534299BEAAA0047F10C /* GhosttyApp.swift */,
A535B9D9299C569B0017E2E4 /* ErrorView.swift */, A535B9D9299C569B0017E2E4 /* ErrorView.swift */,
A507573D299FF33C009D7DC7 /* TerminalSurfaceView.swift */,
A55685DF29A03A9F004303CE /* AppError.swift */, A55685DF29A03A9F004303CE /* AppError.swift */,
A518502329A197C700E4CC4F /* TerminalView.swift */,
A507573D299FF33C009D7DC7 /* TerminalSurfaceView.swift */,
A518502529A1A45100E4CC4F /* WindowTracker.swift */,
); );
path = Sources; path = Sources;
sourceTree = "<group>"; sourceTree = "<group>";
@ -153,6 +159,8 @@
isa = PBXSourcesBuildPhase; isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
A518502629A1A45100E4CC4F /* WindowTracker.swift in Sources */,
A518502429A197C700E4CC4F /* TerminalView.swift in Sources */,
A55685E029A03A9F004303CE /* AppError.swift in Sources */, A55685E029A03A9F004303CE /* AppError.swift in Sources */,
A507573E299FF33C009D7DC7 /* TerminalSurfaceView.swift in Sources */, A507573E299FF33C009D7DC7 /* TerminalSurfaceView.swift in Sources */,
A535B9DA299C569B0017E2E4 /* ErrorView.swift in Sources */, A535B9DA299C569B0017E2E4 /* ErrorView.swift in Sources */,

View File

@ -20,7 +20,8 @@ struct GhosttyApp: App {
case .error: case .error:
ErrorView() ErrorView()
case .ready: 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 /// 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. /// wrap an MTKView and use that, but for legacy reasons we didn't do that to begin with.
struct TerminalSurfaceView: NSViewRepresentable { struct TerminalSurfaceView: NSViewRepresentable {
var hasFocus: Bool
@StateObject private var state: TerminalSurfaceView_Real @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._state = StateObject(wrappedValue: TerminalSurfaceView_Real(app))
self.hasFocus = hasFocus
} }
func makeNSView(context: Context) -> TerminalSurfaceView_Real { func makeNSView(context: Context) -> TerminalSurfaceView_Real {
@ -24,7 +26,7 @@ struct TerminalSurfaceView: NSViewRepresentable {
} }
func updateNSView(_ view: TerminalSurfaceView_Real, context: Context) { 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) 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) { override func resize(withOldSuperviewSize oldSize: NSSize) {
super.resize(withOldSuperviewSize: oldSize) 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) {}
}

View File

@ -414,6 +414,11 @@ pub const CAPI = struct {
win.window.updateContentScale(x, y); win.window.updateContentScale(x, y);
} }
/// Update the focused state of a surface.
export fn ghostty_surface_set_focus(win: *Window, focused: bool) void {
win.window.focusCallback(focused);
}
/// Tell the surface that it needs to schedule a render /// Tell the surface that it needs to schedule a render
export fn ghostty_surface_key( export fn ghostty_surface_key(
win: *Window, win: *Window,

View File

@ -167,4 +167,11 @@ pub const Window = struct {
return; return;
}; };
} }
pub fn focusCallback(self: *const Window, focused: bool) void {
self.core_win.focusCallback(focused) catch |err| {
log.err("error in focus callback err={}", .{err});
return;
};
}
}; };