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_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);
|
||||||
|
@ -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 */,
|
||||||
|
@ -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())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
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);
|
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,
|
||||||
|
@ -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;
|
||||||
|
};
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
Reference in New Issue
Block a user