mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-08-02 14:57:31 +03:00
macos: track surface focus state
This commit is contained in:
@ -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_refresh(ghostty_surface_t);
|
||||
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_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);
|
||||
|
@ -8,6 +8,8 @@
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
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 */; };
|
||||
A55685E029A03A9F004303CE /* AppError.swift in Sources */ = {isa = PBXBuildFile; fileRef = A55685DF29A03A9F004303CE /* AppError.swift */; };
|
||||
A5B30535299BEAAA0047F10C /* GhosttyApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5B30534299BEAAA0047F10C /* GhosttyApp.swift */; };
|
||||
@ -17,6 +19,8 @@
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
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>"; };
|
||||
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; };
|
||||
@ -44,8 +48,10 @@
|
||||
A5D495A0299BEC2200DD1313 /* Preview Content */,
|
||||
A5B30534299BEAAA0047F10C /* GhosttyApp.swift */,
|
||||
A535B9D9299C569B0017E2E4 /* ErrorView.swift */,
|
||||
A507573D299FF33C009D7DC7 /* TerminalSurfaceView.swift */,
|
||||
A55685DF29A03A9F004303CE /* AppError.swift */,
|
||||
A518502329A197C700E4CC4F /* TerminalView.swift */,
|
||||
A507573D299FF33C009D7DC7 /* TerminalSurfaceView.swift */,
|
||||
A518502529A1A45100E4CC4F /* WindowTracker.swift */,
|
||||
);
|
||||
path = Sources;
|
||||
sourceTree = "<group>";
|
||||
@ -153,6 +159,8 @@
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
A518502629A1A45100E4CC4F /* WindowTracker.swift in Sources */,
|
||||
A518502429A197C700E4CC4F /* TerminalView.swift in Sources */,
|
||||
A55685E029A03A9F004303CE /* AppError.swift in Sources */,
|
||||
A507573E299FF33C009D7DC7 /* TerminalSurfaceView.swift in Sources */,
|
||||
A535B9DA299C569B0017E2E4 /* ErrorView.swift in Sources */,
|
||||
|
@ -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) {}
|
||||
}
|
@ -414,6 +414,11 @@ pub const CAPI = struct {
|
||||
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
|
||||
export fn ghostty_surface_key(
|
||||
win: *Window,
|
||||
|
@ -167,4 +167,11 @@ pub const Window = struct {
|
||||
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;
|
||||
};
|
||||
}
|
||||
};
|
||||
|
Reference in New Issue
Block a user