mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-15 00:06:09 +03:00
Merge pull request #1327 from mitchellh/ios-adventures
More apple things cross-platform: Metal renderer, SurfaceView, etc.
This commit is contained in:
@ -33,6 +33,7 @@ typedef void *ghostty_inspector_t;
|
|||||||
typedef enum {
|
typedef enum {
|
||||||
GHOSTTY_PLATFORM_INVALID,
|
GHOSTTY_PLATFORM_INVALID,
|
||||||
GHOSTTY_PLATFORM_MACOS,
|
GHOSTTY_PLATFORM_MACOS,
|
||||||
|
GHOSTTY_PLATFORM_IOS,
|
||||||
} ghostty_platform_e;
|
} ghostty_platform_e;
|
||||||
|
|
||||||
typedef enum {
|
typedef enum {
|
||||||
@ -358,8 +359,13 @@ typedef struct {
|
|||||||
void *nsview;
|
void *nsview;
|
||||||
} ghostty_platform_macos_s;
|
} ghostty_platform_macos_s;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
void *uiview;
|
||||||
|
} ghostty_platform_ios_s;
|
||||||
|
|
||||||
typedef union {
|
typedef union {
|
||||||
ghostty_platform_macos_s macos;
|
ghostty_platform_macos_s macos;
|
||||||
|
ghostty_platform_ios_s ios;
|
||||||
} ghostty_platform_u;
|
} ghostty_platform_u;
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
|
@ -21,6 +21,12 @@
|
|||||||
A51BFC272B30F1B800E92F16 /* Sparkle in Frameworks */ = {isa = PBXBuildFile; productRef = A51BFC262B30F1B800E92F16 /* Sparkle */; };
|
A51BFC272B30F1B800E92F16 /* Sparkle in Frameworks */ = {isa = PBXBuildFile; productRef = A51BFC262B30F1B800E92F16 /* Sparkle */; };
|
||||||
A51BFC2B2B30F6BE00E92F16 /* UpdateDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51BFC2A2B30F6BE00E92F16 /* UpdateDelegate.swift */; };
|
A51BFC2B2B30F6BE00E92F16 /* UpdateDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51BFC2A2B30F6BE00E92F16 /* UpdateDelegate.swift */; };
|
||||||
A5278A9B2AA05B2600CD3039 /* Ghostty.Input.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5278A9A2AA05B2600CD3039 /* Ghostty.Input.swift */; };
|
A5278A9B2AA05B2600CD3039 /* Ghostty.Input.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5278A9A2AA05B2600CD3039 /* Ghostty.Input.swift */; };
|
||||||
|
A5333E1C2B5A1CE3008AEFF7 /* CrossKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5333E1B2B5A1CE3008AEFF7 /* CrossKit.swift */; };
|
||||||
|
A5333E1D2B5A1CE3008AEFF7 /* CrossKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5333E1B2B5A1CE3008AEFF7 /* CrossKit.swift */; };
|
||||||
|
A5333E202B5A2111008AEFF7 /* SurfaceView_UIKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5333E152B59DE8E008AEFF7 /* SurfaceView_UIKit.swift */; };
|
||||||
|
A5333E222B5A2128008AEFF7 /* SurfaceView_AppKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5333E212B5A2128008AEFF7 /* SurfaceView_AppKit.swift */; };
|
||||||
|
A5333E232B5A219A008AEFF7 /* SurfaceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A55B7BBB29B6FC330055DE60 /* SurfaceView.swift */; };
|
||||||
|
A5333E242B5A22D9008AEFF7 /* Ghostty.Shell.swift in Sources */ = {isa = PBXBuildFile; fileRef = A56D58852ACDDB4100508D2C /* Ghostty.Shell.swift */; };
|
||||||
A53426352A7DA53D00EBB7A2 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = A53426342A7DA53D00EBB7A2 /* AppDelegate.swift */; };
|
A53426352A7DA53D00EBB7A2 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = A53426342A7DA53D00EBB7A2 /* AppDelegate.swift */; };
|
||||||
A535B9DA299C569B0017E2E4 /* ErrorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A535B9D9299C569B0017E2E4 /* ErrorView.swift */; };
|
A535B9DA299C569B0017E2E4 /* ErrorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A535B9D9299C569B0017E2E4 /* ErrorView.swift */; };
|
||||||
A53D0C8E2B53B0EA00305CE6 /* GhosttyKit.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = A5D495A1299BEC7E00DD1313 /* GhosttyKit.xcframework */; };
|
A53D0C8E2B53B0EA00305CE6 /* GhosttyKit.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = A5D495A1299BEC7E00DD1313 /* GhosttyKit.xcframework */; };
|
||||||
@ -75,6 +81,9 @@
|
|||||||
A51BFC282B30F26D00E92F16 /* GhosttyDebug.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = GhosttyDebug.entitlements; sourceTree = "<group>"; };
|
A51BFC282B30F26D00E92F16 /* GhosttyDebug.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = GhosttyDebug.entitlements; sourceTree = "<group>"; };
|
||||||
A51BFC2A2B30F6BE00E92F16 /* UpdateDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateDelegate.swift; sourceTree = "<group>"; };
|
A51BFC2A2B30F6BE00E92F16 /* UpdateDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateDelegate.swift; sourceTree = "<group>"; };
|
||||||
A5278A9A2AA05B2600CD3039 /* Ghostty.Input.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Ghostty.Input.swift; sourceTree = "<group>"; };
|
A5278A9A2AA05B2600CD3039 /* Ghostty.Input.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Ghostty.Input.swift; sourceTree = "<group>"; };
|
||||||
|
A5333E152B59DE8E008AEFF7 /* SurfaceView_UIKit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SurfaceView_UIKit.swift; sourceTree = "<group>"; };
|
||||||
|
A5333E1B2B5A1CE3008AEFF7 /* CrossKit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CrossKit.swift; sourceTree = "<group>"; };
|
||||||
|
A5333E212B5A2128008AEFF7 /* SurfaceView_AppKit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SurfaceView_AppKit.swift; sourceTree = "<group>"; };
|
||||||
A53426342A7DA53D00EBB7A2 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
A53426342A7DA53D00EBB7A2 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.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>"; };
|
||||||
A53D0C932B53B43700305CE6 /* iOSApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = iOSApp.swift; sourceTree = "<group>"; };
|
A53D0C932B53B43700305CE6 /* iOSApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = iOSApp.swift; sourceTree = "<group>"; };
|
||||||
@ -173,6 +182,7 @@
|
|||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
A5CEAFFE29C2410700646FDA /* Backport.swift */,
|
A5CEAFFE29C2410700646FDA /* Backport.swift */,
|
||||||
|
A5333E1B2B5A1CE3008AEFF7 /* CrossKit.swift */,
|
||||||
A5D0AF3C2B37804400D21823 /* CodableBridge.swift */,
|
A5D0AF3C2B37804400D21823 /* CodableBridge.swift */,
|
||||||
8503D7C62A549C66006CFF3D /* FullScreenHandler.swift */,
|
8503D7C62A549C66006CFF3D /* FullScreenHandler.swift */,
|
||||||
A59630962AEE163600D64628 /* HostingWindow.swift */,
|
A59630962AEE163600D64628 /* HostingWindow.swift */,
|
||||||
@ -236,6 +246,8 @@
|
|||||||
children = (
|
children = (
|
||||||
A55B7BB729B6F53A0055DE60 /* Package.swift */,
|
A55B7BB729B6F53A0055DE60 /* Package.swift */,
|
||||||
A55B7BBB29B6FC330055DE60 /* SurfaceView.swift */,
|
A55B7BBB29B6FC330055DE60 /* SurfaceView.swift */,
|
||||||
|
A5333E212B5A2128008AEFF7 /* SurfaceView_AppKit.swift */,
|
||||||
|
A5333E152B59DE8E008AEFF7 /* SurfaceView_UIKit.swift */,
|
||||||
A59FB5CE2AE0DB50009128F3 /* InspectorView.swift */,
|
A59FB5CE2AE0DB50009128F3 /* InspectorView.swift */,
|
||||||
A53D0C992B543F3B00305CE6 /* Ghostty.App.swift */,
|
A53D0C992B543F3B00305CE6 /* Ghostty.App.swift */,
|
||||||
A514C8D52B54A16400493A16 /* Ghostty.Config.swift */,
|
A514C8D52B54A16400493A16 /* Ghostty.Config.swift */,
|
||||||
@ -457,8 +469,10 @@
|
|||||||
A59630A02AEF6AEB00D64628 /* TerminalManager.swift in Sources */,
|
A59630A02AEF6AEB00D64628 /* TerminalManager.swift in Sources */,
|
||||||
A51BFC2B2B30F6BE00E92F16 /* UpdateDelegate.swift in Sources */,
|
A51BFC2B2B30F6BE00E92F16 /* UpdateDelegate.swift in Sources */,
|
||||||
A53426352A7DA53D00EBB7A2 /* AppDelegate.swift in Sources */,
|
A53426352A7DA53D00EBB7A2 /* AppDelegate.swift in Sources */,
|
||||||
|
A5333E222B5A2128008AEFF7 /* SurfaceView_AppKit.swift in Sources */,
|
||||||
A5CDF1952AAFA19600513312 /* ConfigurationErrorsView.swift in Sources */,
|
A5CDF1952AAFA19600513312 /* ConfigurationErrorsView.swift in Sources */,
|
||||||
A55B7BBC29B6FC330055DE60 /* SurfaceView.swift in Sources */,
|
A55B7BBC29B6FC330055DE60 /* SurfaceView.swift in Sources */,
|
||||||
|
A5333E1C2B5A1CE3008AEFF7 /* CrossKit.swift in Sources */,
|
||||||
A59444F729A2ED5200725BBA /* SettingsView.swift in Sources */,
|
A59444F729A2ED5200725BBA /* SettingsView.swift in Sources */,
|
||||||
A56D58862ACDDB4100508D2C /* Ghostty.Shell.swift in Sources */,
|
A56D58862ACDDB4100508D2C /* Ghostty.Shell.swift in Sources */,
|
||||||
A59630A22AF0415000D64628 /* Ghostty.TerminalSplit.swift in Sources */,
|
A59630A22AF0415000D64628 /* Ghostty.TerminalSplit.swift in Sources */,
|
||||||
@ -487,8 +501,12 @@
|
|||||||
files = (
|
files = (
|
||||||
A53D0C942B53B43700305CE6 /* iOSApp.swift in Sources */,
|
A53D0C942B53B43700305CE6 /* iOSApp.swift in Sources */,
|
||||||
A514C8D72B54A16400493A16 /* Ghostty.Config.swift in Sources */,
|
A514C8D72B54A16400493A16 /* Ghostty.Config.swift in Sources */,
|
||||||
|
A5333E232B5A219A008AEFF7 /* SurfaceView.swift in Sources */,
|
||||||
|
A5333E202B5A2111008AEFF7 /* SurfaceView_UIKit.swift in Sources */,
|
||||||
|
A5333E1D2B5A1CE3008AEFF7 /* CrossKit.swift in Sources */,
|
||||||
A53D0C9C2B543F7B00305CE6 /* Package.swift in Sources */,
|
A53D0C9C2B543F7B00305CE6 /* Package.swift in Sources */,
|
||||||
A53D0C9B2B543F3B00305CE6 /* Ghostty.App.swift in Sources */,
|
A53D0C9B2B543F3B00305CE6 /* Ghostty.App.swift in Sources */,
|
||||||
|
A5333E242B5A22D9008AEFF7 /* Ghostty.Shell.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
@ -6,13 +6,26 @@ struct Ghostty_iOSApp: App {
|
|||||||
|
|
||||||
var body: some Scene {
|
var body: some Scene {
|
||||||
WindowGroup {
|
WindowGroup {
|
||||||
iOS_ContentView()
|
iOS_GhosttyTerminal()
|
||||||
.environmentObject(ghostty_app)
|
.environmentObject(ghostty_app)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct iOS_ContentView: View {
|
struct iOS_GhosttyTerminal: View {
|
||||||
|
@EnvironmentObject private var ghostty_app: Ghostty.App
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
ZStack {
|
||||||
|
// Make sure that our background color extends to all parts of the screen
|
||||||
|
Color(ghostty_app.config.backgroundColor).ignoresSafeArea()
|
||||||
|
|
||||||
|
Ghostty.Terminal()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct iOS_GhosttyInitView: View {
|
||||||
@EnvironmentObject private var ghostty_app: Ghostty.App
|
@EnvironmentObject private var ghostty_app: Ghostty.App
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
@ -27,7 +40,3 @@ struct iOS_ContentView: View {
|
|||||||
.padding()
|
.padding()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#Preview {
|
|
||||||
iOS_ContentView()
|
|
||||||
}
|
|
||||||
|
File diff suppressed because it is too large
Load Diff
955
macos/Sources/Ghostty/SurfaceView_AppKit.swift
Normal file
955
macos/Sources/Ghostty/SurfaceView_AppKit.swift
Normal file
@ -0,0 +1,955 @@
|
|||||||
|
import SwiftUI
|
||||||
|
import UserNotifications
|
||||||
|
import GhosttyKit
|
||||||
|
|
||||||
|
extension Ghostty {
|
||||||
|
/// The NSView implementation for a terminal surface.
|
||||||
|
class SurfaceView: OSView, ObservableObject {
|
||||||
|
/// Unique ID per surface
|
||||||
|
let uuid: UUID
|
||||||
|
|
||||||
|
// The current title of the surface as defined by the pty. This can be
|
||||||
|
// changed with escape codes. This is public because the callbacks go
|
||||||
|
// to the app level and it is set from there.
|
||||||
|
@Published var title: String = "👻"
|
||||||
|
|
||||||
|
// The cell size of this surface. This is set by the core when the
|
||||||
|
// surface is first created and any time the cell size changes (i.e.
|
||||||
|
// when the font size changes). This is used to allow windows to be
|
||||||
|
// resized in discrete steps of a single cell.
|
||||||
|
@Published var cellSize: NSSize = .zero
|
||||||
|
|
||||||
|
// The health state of the surface. This currently only reflects the
|
||||||
|
// renderer health. In the future we may want to make this an enum.
|
||||||
|
@Published var healthy: Bool = true
|
||||||
|
|
||||||
|
// Any error while initializing the surface.
|
||||||
|
@Published var error: Error? = nil
|
||||||
|
|
||||||
|
// An initial size to request for a window. This will only affect
|
||||||
|
// then the view is moved to a new window.
|
||||||
|
var initialSize: NSSize? = nil
|
||||||
|
|
||||||
|
// Returns true if quit confirmation is required for this surface to
|
||||||
|
// exit safely.
|
||||||
|
var needsConfirmQuit: Bool {
|
||||||
|
guard let surface = self.surface else { return false }
|
||||||
|
return ghostty_surface_needs_confirm_quit(surface)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the pwd of the surface if it has one.
|
||||||
|
var pwd: String? {
|
||||||
|
guard let surface = self.surface else { return nil }
|
||||||
|
let v = String(unsafeUninitializedCapacity: 1024) {
|
||||||
|
Int(ghostty_surface_pwd(surface, $0.baseAddress, UInt($0.count)))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (v.count == 0) { return nil }
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the inspector instance for this surface, or nil if the
|
||||||
|
// surface has been closed.
|
||||||
|
var inspector: ghostty_inspector_t? {
|
||||||
|
guard let surface = self.surface else { return nil }
|
||||||
|
return ghostty_surface_inspector(surface)
|
||||||
|
}
|
||||||
|
|
||||||
|
// True if the inspector should be visible
|
||||||
|
@Published var inspectorVisible: Bool = false {
|
||||||
|
didSet {
|
||||||
|
if (oldValue && !inspectorVisible) {
|
||||||
|
guard let surface = self.surface else { return }
|
||||||
|
ghostty_inspector_free(surface)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Notification identifiers associated with this surface
|
||||||
|
var notificationIdentifiers: Set<String> = []
|
||||||
|
|
||||||
|
private(set) var surface: ghostty_surface_t?
|
||||||
|
private var markedText: NSMutableAttributedString
|
||||||
|
private var mouseEntered: Bool = false
|
||||||
|
private(set) var focused: Bool = true
|
||||||
|
private var cursor: NSCursor = .iBeam
|
||||||
|
private var cursorVisible: CursorVisibility = .visible
|
||||||
|
|
||||||
|
// This is set to non-null during keyDown to accumulate insertText contents
|
||||||
|
private var keyTextAccumulator: [String]? = nil
|
||||||
|
|
||||||
|
// We need to support being a first responder so that we can get input events
|
||||||
|
override var acceptsFirstResponder: Bool { return true }
|
||||||
|
|
||||||
|
// I don't think we need this but this lets us know we should redraw our layer
|
||||||
|
// so we'll use that to tell ghostty to refresh.
|
||||||
|
override var wantsUpdateLayer: Bool { return true }
|
||||||
|
|
||||||
|
// State machine for mouse cursor visibility because every call to
|
||||||
|
// NSCursor.hide/unhide must be balanced.
|
||||||
|
enum CursorVisibility {
|
||||||
|
case visible
|
||||||
|
case hidden
|
||||||
|
case pendingVisible
|
||||||
|
case pendingHidden
|
||||||
|
}
|
||||||
|
|
||||||
|
init(_ app: ghostty_app_t, baseConfig: SurfaceConfiguration? = nil, uuid: UUID? = nil) {
|
||||||
|
self.markedText = NSMutableAttributedString()
|
||||||
|
self.uuid = uuid ?? .init()
|
||||||
|
|
||||||
|
// Initialize with some default frame size. The important thing is that this
|
||||||
|
// is non-zero so that our layer bounds are non-zero so that our renderer
|
||||||
|
// can do SOMETHING.
|
||||||
|
super.init(frame: NSMakeRect(0, 0, 800, 600))
|
||||||
|
|
||||||
|
// Before we initialize the surface we want to register our notifications
|
||||||
|
// so there is no window where we can't receive them.
|
||||||
|
let center = NotificationCenter.default
|
||||||
|
center.addObserver(
|
||||||
|
self,
|
||||||
|
selector: #selector(onUpdateRendererHealth),
|
||||||
|
name: Ghostty.Notification.didUpdateRendererHealth,
|
||||||
|
object: self)
|
||||||
|
|
||||||
|
// Setup our surface. This will also initialize all the terminal IO.
|
||||||
|
let surface_cfg = baseConfig ?? SurfaceConfiguration()
|
||||||
|
var surface_cfg_c = surface_cfg.ghosttyConfig(view: self)
|
||||||
|
guard let surface = ghostty_surface_new(app, &surface_cfg_c) else {
|
||||||
|
self.error = AppError.surfaceCreateError
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.surface = surface;
|
||||||
|
|
||||||
|
// Setup our tracking area so we get mouse moved events
|
||||||
|
updateTrackingAreas()
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) is not supported for this view")
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
// Remove all of our notificationcenter subscriptions
|
||||||
|
let center = NotificationCenter.default
|
||||||
|
center.removeObserver(self)
|
||||||
|
|
||||||
|
// Whenever the surface is removed, we need to note that our restorable
|
||||||
|
// state is invalid to prevent the surface from being restored.
|
||||||
|
invalidateRestorableState()
|
||||||
|
|
||||||
|
trackingAreas.forEach { removeTrackingArea($0) }
|
||||||
|
|
||||||
|
// mouseExited is not called by AppKit one last time when the view
|
||||||
|
// closes so we do it manually to ensure our NSCursor state remains
|
||||||
|
// accurate.
|
||||||
|
if (mouseEntered) {
|
||||||
|
mouseExited(with: NSEvent())
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let surface = self.surface else { return }
|
||||||
|
ghostty_surface_free(surface)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Close the surface early. This will free the associated Ghostty surface and the view will
|
||||||
|
/// no longer render. The view can never be used again. This is a way for us to free the
|
||||||
|
/// Ghostty resources while references may still be held to this view. I've found that SwiftUI
|
||||||
|
/// tends to hold this view longer than it should so we free the expensive stuff explicitly.
|
||||||
|
func close() {
|
||||||
|
// Remove any notifications associated with this surface
|
||||||
|
let identifiers = Array(self.notificationIdentifiers)
|
||||||
|
UNUserNotificationCenter.current().removeDeliveredNotifications(withIdentifiers: identifiers)
|
||||||
|
|
||||||
|
guard let surface = self.surface else { return }
|
||||||
|
ghostty_surface_free(surface)
|
||||||
|
self.surface = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func focusDidChange(_ focused: Bool) {
|
||||||
|
guard let surface = self.surface else { return }
|
||||||
|
ghostty_surface_set_focus(surface, focused)
|
||||||
|
}
|
||||||
|
|
||||||
|
func sizeDidChange(_ size: CGSize) {
|
||||||
|
guard let surface = self.surface else { return }
|
||||||
|
|
||||||
|
// Ghostty wants to know the actual framebuffer size... It is very important
|
||||||
|
// here that we use "size" and NOT the view frame. If we're in the middle of
|
||||||
|
// an animation (i.e. a fullscreen animation), the frame will not yet be updated.
|
||||||
|
// The size represents our final size we're going for.
|
||||||
|
let scaledSize = self.convertToBacking(size)
|
||||||
|
ghostty_surface_set_size(surface, UInt32(scaledSize.width), UInt32(scaledSize.height))
|
||||||
|
|
||||||
|
// Frame changes do not always call mouseEntered/mouseExited, so we do some
|
||||||
|
// calculations ourself to call those events.
|
||||||
|
if let window = self.window {
|
||||||
|
let mouseScreen = NSEvent.mouseLocation
|
||||||
|
let mouseWindow = window.convertPoint(fromScreen: mouseScreen)
|
||||||
|
let mouseView = self.convert(mouseWindow, from: nil)
|
||||||
|
let isEntered = self.isMousePoint(mouseView, in: bounds)
|
||||||
|
if (isEntered) {
|
||||||
|
mouseEntered(with: NSEvent())
|
||||||
|
} else {
|
||||||
|
mouseExited(with: NSEvent())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// If we don't have a window, then our mouse can NOT be in our view.
|
||||||
|
// When the window comes back, I believe this event fires again so
|
||||||
|
// we'll get a mouseEntered.
|
||||||
|
mouseExited(with: NSEvent())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func setCursorShape(_ shape: ghostty_mouse_shape_e) {
|
||||||
|
switch (shape) {
|
||||||
|
case GHOSTTY_MOUSE_SHAPE_DEFAULT:
|
||||||
|
cursor = .arrow
|
||||||
|
|
||||||
|
case GHOSTTY_MOUSE_SHAPE_CONTEXT_MENU:
|
||||||
|
cursor = .contextualMenu
|
||||||
|
|
||||||
|
case GHOSTTY_MOUSE_SHAPE_TEXT:
|
||||||
|
cursor = .iBeam
|
||||||
|
|
||||||
|
case GHOSTTY_MOUSE_SHAPE_CROSSHAIR:
|
||||||
|
cursor = .crosshair
|
||||||
|
|
||||||
|
case GHOSTTY_MOUSE_SHAPE_GRAB:
|
||||||
|
cursor = .openHand
|
||||||
|
|
||||||
|
case GHOSTTY_MOUSE_SHAPE_GRABBING:
|
||||||
|
cursor = .closedHand
|
||||||
|
|
||||||
|
case GHOSTTY_MOUSE_SHAPE_POINTER:
|
||||||
|
cursor = .pointingHand
|
||||||
|
|
||||||
|
case GHOSTTY_MOUSE_SHAPE_W_RESIZE:
|
||||||
|
cursor = .resizeLeft
|
||||||
|
|
||||||
|
case GHOSTTY_MOUSE_SHAPE_E_RESIZE:
|
||||||
|
cursor = .resizeRight
|
||||||
|
|
||||||
|
case GHOSTTY_MOUSE_SHAPE_N_RESIZE:
|
||||||
|
cursor = .resizeUp
|
||||||
|
|
||||||
|
case GHOSTTY_MOUSE_SHAPE_S_RESIZE:
|
||||||
|
cursor = .resizeDown
|
||||||
|
|
||||||
|
case GHOSTTY_MOUSE_SHAPE_NS_RESIZE:
|
||||||
|
cursor = .resizeUpDown
|
||||||
|
|
||||||
|
case GHOSTTY_MOUSE_SHAPE_EW_RESIZE:
|
||||||
|
cursor = .resizeLeftRight
|
||||||
|
|
||||||
|
case GHOSTTY_MOUSE_SHAPE_VERTICAL_TEXT:
|
||||||
|
cursor = .iBeamCursorForVerticalLayout
|
||||||
|
|
||||||
|
case GHOSTTY_MOUSE_SHAPE_NOT_ALLOWED:
|
||||||
|
cursor = .operationNotAllowed
|
||||||
|
|
||||||
|
default:
|
||||||
|
// We ignore unknown shapes.
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set our cursor immediately if our mouse is over our window
|
||||||
|
if (mouseEntered) { cursorUpdate(with: NSEvent()) }
|
||||||
|
if let window = self.window {
|
||||||
|
window.invalidateCursorRects(for: self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func setCursorVisibility(_ visible: Bool) {
|
||||||
|
switch (cursorVisible) {
|
||||||
|
case .visible:
|
||||||
|
// If we want to be visible, do nothing. If we want to be hidden
|
||||||
|
// enter the pending state.
|
||||||
|
if (visible) { return }
|
||||||
|
cursorVisible = .pendingHidden
|
||||||
|
|
||||||
|
case .hidden:
|
||||||
|
// If we want to be hidden, do nothing. If we want to be visible
|
||||||
|
// enter the pending state.
|
||||||
|
if (!visible) { return }
|
||||||
|
cursorVisible = .pendingVisible
|
||||||
|
|
||||||
|
case .pendingVisible:
|
||||||
|
// If we want to be visible, do nothing because we're already pending.
|
||||||
|
// If we want to be hidden, we're already hidden so reset state.
|
||||||
|
if (visible) { return }
|
||||||
|
cursorVisible = .hidden
|
||||||
|
|
||||||
|
case .pendingHidden:
|
||||||
|
// If we want to be hidden, do nothing because we're pending that switch.
|
||||||
|
// If we want to be visible, we're already visible so reset state.
|
||||||
|
if (!visible) { return }
|
||||||
|
cursorVisible = .visible
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mouseEntered) {
|
||||||
|
cursorUpdate(with: NSEvent())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Notifications
|
||||||
|
|
||||||
|
@objc private func onUpdateRendererHealth(notification: SwiftUI.Notification) {
|
||||||
|
guard let healthAny = notification.userInfo?["health"] else { return }
|
||||||
|
guard let health = healthAny as? ghostty_renderer_health_e else { return }
|
||||||
|
healthy = health == GHOSTTY_RENDERER_HEALTH_OK
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - NSView
|
||||||
|
|
||||||
|
override func viewDidMoveToWindow() {
|
||||||
|
// Set our background blur if requested
|
||||||
|
setWindowBackgroundBlur(window)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This function sets the window background to blur if it is configured on the surface.
|
||||||
|
private func setWindowBackgroundBlur(_ targetWindow: NSWindow?) {
|
||||||
|
// Surface must desire transparency
|
||||||
|
guard let surface = self.surface,
|
||||||
|
ghostty_surface_transparent(surface) else { return }
|
||||||
|
|
||||||
|
// Our target should always be our own view window
|
||||||
|
guard let target = targetWindow,
|
||||||
|
let window = self.window,
|
||||||
|
target == window else { return }
|
||||||
|
|
||||||
|
// If our window is not visible, then delay this. This is possible specifically
|
||||||
|
// during state restoration but probably in other scenarios as well. To delay,
|
||||||
|
// we just loop directly on the dispatch queue.
|
||||||
|
guard window.isVisible else {
|
||||||
|
// Weak window so that if the window changes or is destroyed we aren't holding a ref
|
||||||
|
DispatchQueue.main.async { [weak self, weak window] in self?.setWindowBackgroundBlur(window) }
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the window transparency settings
|
||||||
|
window.isOpaque = false
|
||||||
|
window.hasShadow = false
|
||||||
|
window.backgroundColor = .clear
|
||||||
|
|
||||||
|
// If we have a blur, set the blur
|
||||||
|
ghostty_set_window_background_blur(surface, Unmanaged.passUnretained(window).toOpaque())
|
||||||
|
}
|
||||||
|
|
||||||
|
override func becomeFirstResponder() -> Bool {
|
||||||
|
let result = super.becomeFirstResponder()
|
||||||
|
if (result) { focused = true }
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
override func resignFirstResponder() -> Bool {
|
||||||
|
let result = super.resignFirstResponder()
|
||||||
|
|
||||||
|
// We sometimes call this manually (see SplitView) as a way to force us to
|
||||||
|
// yield our focus state.
|
||||||
|
if (result) {
|
||||||
|
focusDidChange(false)
|
||||||
|
focused = false
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
override func updateTrackingAreas() {
|
||||||
|
// To update our tracking area we just recreate it all.
|
||||||
|
trackingAreas.forEach { removeTrackingArea($0) }
|
||||||
|
|
||||||
|
// This tracking area is across the entire frame to notify us of mouse movements.
|
||||||
|
addTrackingArea(NSTrackingArea(
|
||||||
|
rect: frame,
|
||||||
|
options: [
|
||||||
|
.mouseEnteredAndExited,
|
||||||
|
.mouseMoved,
|
||||||
|
|
||||||
|
// Only send mouse events that happen in our visible (not obscured) rect
|
||||||
|
.inVisibleRect,
|
||||||
|
|
||||||
|
// We want active always because we want to still send mouse reports
|
||||||
|
// even if we're not focused or key.
|
||||||
|
.activeAlways,
|
||||||
|
],
|
||||||
|
owner: self,
|
||||||
|
userInfo: nil))
|
||||||
|
}
|
||||||
|
|
||||||
|
override func resetCursorRects() {
|
||||||
|
discardCursorRects()
|
||||||
|
addCursorRect(frame, cursor: self.cursor)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewDidChangeBackingProperties() {
|
||||||
|
guard let surface = self.surface else { return }
|
||||||
|
|
||||||
|
// Detect our X/Y scale factor so we can update our surface
|
||||||
|
let fbFrame = self.convertToBacking(self.frame)
|
||||||
|
let xScale = fbFrame.size.width / self.frame.size.width
|
||||||
|
let yScale = fbFrame.size.height / self.frame.size.height
|
||||||
|
ghostty_surface_set_content_scale(surface, xScale, yScale)
|
||||||
|
|
||||||
|
// When our scale factor changes, so does our fb size so we send that too
|
||||||
|
ghostty_surface_set_size(surface, UInt32(fbFrame.size.width), UInt32(fbFrame.size.height))
|
||||||
|
}
|
||||||
|
|
||||||
|
override func updateLayer() {
|
||||||
|
guard let surface = self.surface else { return }
|
||||||
|
ghostty_surface_refresh(surface);
|
||||||
|
}
|
||||||
|
|
||||||
|
override func acceptsFirstMouse(for event: NSEvent?) -> Bool {
|
||||||
|
// "Override this method in a subclass to allow instances to respond to
|
||||||
|
// click-through. This allows the user to click on a view in an inactive
|
||||||
|
// window, activating the view with one click, instead of clicking first
|
||||||
|
// to make the window active and then clicking the view."
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override func mouseDown(with event: NSEvent) {
|
||||||
|
guard let surface = self.surface else { return }
|
||||||
|
let mods = Ghostty.ghosttyMods(event.modifierFlags)
|
||||||
|
ghostty_surface_mouse_button(surface, GHOSTTY_MOUSE_PRESS, GHOSTTY_MOUSE_LEFT, mods)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func mouseUp(with event: NSEvent) {
|
||||||
|
guard let surface = self.surface else { return }
|
||||||
|
let mods = Ghostty.ghosttyMods(event.modifierFlags)
|
||||||
|
ghostty_surface_mouse_button(surface, GHOSTTY_MOUSE_RELEASE, GHOSTTY_MOUSE_LEFT, mods)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func otherMouseDown(with event: NSEvent) {
|
||||||
|
guard let surface = self.surface else { return }
|
||||||
|
guard event.buttonNumber == 2 else { return }
|
||||||
|
let mods = Ghostty.ghosttyMods(event.modifierFlags)
|
||||||
|
ghostty_surface_mouse_button(surface, GHOSTTY_MOUSE_PRESS, GHOSTTY_MOUSE_MIDDLE, mods)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func otherMouseUp(with event: NSEvent) {
|
||||||
|
guard let surface = self.surface else { return }
|
||||||
|
guard event.buttonNumber == 2 else { return }
|
||||||
|
let mods = Ghostty.ghosttyMods(event.modifierFlags)
|
||||||
|
ghostty_surface_mouse_button(surface, GHOSTTY_MOUSE_RELEASE, GHOSTTY_MOUSE_MIDDLE, mods)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
override func rightMouseDown(with event: NSEvent) {
|
||||||
|
guard let surface = self.surface else { return }
|
||||||
|
let mods = Ghostty.ghosttyMods(event.modifierFlags)
|
||||||
|
ghostty_surface_mouse_button(surface, GHOSTTY_MOUSE_PRESS, GHOSTTY_MOUSE_RIGHT, mods)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func rightMouseUp(with event: NSEvent) {
|
||||||
|
guard let surface = self.surface else { return }
|
||||||
|
let mods = Ghostty.ghosttyMods(event.modifierFlags)
|
||||||
|
ghostty_surface_mouse_button(surface, GHOSTTY_MOUSE_RELEASE, GHOSTTY_MOUSE_RIGHT, mods)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func mouseMoved(with event: NSEvent) {
|
||||||
|
guard let surface = self.surface else { return }
|
||||||
|
|
||||||
|
// Convert window position to view position. Note (0, 0) is bottom left.
|
||||||
|
let pos = self.convert(event.locationInWindow, from: nil)
|
||||||
|
ghostty_surface_mouse_pos(surface, pos.x, frame.height - pos.y)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
override func mouseDragged(with event: NSEvent) {
|
||||||
|
self.mouseMoved(with: event)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func mouseEntered(with event: NSEvent) {
|
||||||
|
// For reasons unknown (Cocoaaaaaaaaa), mouseEntered is called
|
||||||
|
// multiple times in an unbalanced way with mouseExited when a new
|
||||||
|
// tab is created. In this scenario, we only want to process our
|
||||||
|
// callback once since this is stateful and we expect balancing.
|
||||||
|
if (mouseEntered) { return }
|
||||||
|
|
||||||
|
mouseEntered = true
|
||||||
|
|
||||||
|
// Update our cursor when we enter so we fully process our
|
||||||
|
// cursorVisible state.
|
||||||
|
cursorUpdate(with: NSEvent())
|
||||||
|
}
|
||||||
|
|
||||||
|
override func mouseExited(with event: NSEvent) {
|
||||||
|
// See mouseEntered
|
||||||
|
if (!mouseEntered) { return }
|
||||||
|
|
||||||
|
mouseEntered = false
|
||||||
|
|
||||||
|
// If the mouse is currently hidden, we want to show it when we exit
|
||||||
|
// this view. We go through the cursorVisible dance so that only
|
||||||
|
// cursorUpdate manages cursor state.
|
||||||
|
if (cursorVisible == .hidden) {
|
||||||
|
cursorVisible = .pendingVisible
|
||||||
|
cursorUpdate(with: NSEvent())
|
||||||
|
assert(cursorVisible == .visible)
|
||||||
|
|
||||||
|
// We set the state to pending hidden again for the next time
|
||||||
|
// we enter.
|
||||||
|
cursorVisible = .pendingHidden
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override func scrollWheel(with event: NSEvent) {
|
||||||
|
guard let surface = self.surface else { return }
|
||||||
|
|
||||||
|
// Builds up the "input.ScrollMods" bitmask
|
||||||
|
var mods: Int32 = 0
|
||||||
|
|
||||||
|
var x = event.scrollingDeltaX
|
||||||
|
var y = event.scrollingDeltaY
|
||||||
|
if event.hasPreciseScrollingDeltas {
|
||||||
|
mods = 1
|
||||||
|
|
||||||
|
// We do a 2x speed multiplier. This is subjective, it "feels" better to me.
|
||||||
|
x *= 2;
|
||||||
|
y *= 2;
|
||||||
|
|
||||||
|
// TODO(mitchellh): do we have to scale the x/y here by window scale factor?
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine our momentum value
|
||||||
|
var momentum: ghostty_input_mouse_momentum_e = GHOSTTY_MOUSE_MOMENTUM_NONE
|
||||||
|
switch (event.momentumPhase) {
|
||||||
|
case .began:
|
||||||
|
momentum = GHOSTTY_MOUSE_MOMENTUM_BEGAN
|
||||||
|
case .stationary:
|
||||||
|
momentum = GHOSTTY_MOUSE_MOMENTUM_STATIONARY
|
||||||
|
case .changed:
|
||||||
|
momentum = GHOSTTY_MOUSE_MOMENTUM_CHANGED
|
||||||
|
case .ended:
|
||||||
|
momentum = GHOSTTY_MOUSE_MOMENTUM_ENDED
|
||||||
|
case .cancelled:
|
||||||
|
momentum = GHOSTTY_MOUSE_MOMENTUM_CANCELLED
|
||||||
|
case .mayBegin:
|
||||||
|
momentum = GHOSTTY_MOUSE_MOMENTUM_MAY_BEGIN
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pack our momentum value into the mods bitmask
|
||||||
|
mods |= Int32(momentum.rawValue) << 1
|
||||||
|
|
||||||
|
ghostty_surface_mouse_scroll(surface, x, y, mods)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func cursorUpdate(with event: NSEvent) {
|
||||||
|
switch (cursorVisible) {
|
||||||
|
case .visible, .hidden:
|
||||||
|
// Do nothing, stable state
|
||||||
|
break
|
||||||
|
|
||||||
|
case .pendingHidden:
|
||||||
|
NSCursor.hide()
|
||||||
|
cursorVisible = .hidden
|
||||||
|
|
||||||
|
case .pendingVisible:
|
||||||
|
NSCursor.unhide()
|
||||||
|
cursorVisible = .visible
|
||||||
|
}
|
||||||
|
|
||||||
|
cursor.set()
|
||||||
|
}
|
||||||
|
|
||||||
|
override func keyDown(with event: NSEvent) {
|
||||||
|
guard let surface = self.surface else {
|
||||||
|
self.interpretKeyEvents([event])
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// We need to translate the mods (maybe) to handle configs such as option-as-alt
|
||||||
|
let translationModsGhostty = Ghostty.eventModifierFlags(
|
||||||
|
mods: ghostty_surface_key_translation_mods(
|
||||||
|
surface,
|
||||||
|
Ghostty.ghosttyMods(event.modifierFlags)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
// There are hidden bits set in our event that matter for certain dead keys
|
||||||
|
// so we can't use translationModsGhostty directly. Instead, we just check
|
||||||
|
// for exact states and set them.
|
||||||
|
var translationMods = event.modifierFlags
|
||||||
|
for flag in [NSEvent.ModifierFlags.shift, .control, .option, .command] {
|
||||||
|
if (translationModsGhostty.contains(flag)) {
|
||||||
|
translationMods.insert(flag)
|
||||||
|
} else {
|
||||||
|
translationMods.remove(flag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the translation modifiers are not equal to our original modifiers
|
||||||
|
// then we need to construct a new NSEvent. If they are equal we reuse the
|
||||||
|
// old one. IMPORTANT: we MUST reuse the old event if they're equal because
|
||||||
|
// this keeps things like Korean input working. There must be some object
|
||||||
|
// equality happening in AppKit somewhere because this is required.
|
||||||
|
let translationEvent: NSEvent
|
||||||
|
if (translationMods == event.modifierFlags) {
|
||||||
|
translationEvent = event
|
||||||
|
} else {
|
||||||
|
translationEvent = NSEvent.keyEvent(
|
||||||
|
with: event.type,
|
||||||
|
location: event.locationInWindow,
|
||||||
|
modifierFlags: translationMods,
|
||||||
|
timestamp: event.timestamp,
|
||||||
|
windowNumber: event.windowNumber,
|
||||||
|
context: nil,
|
||||||
|
characters: event.characters(byApplyingModifiers: translationMods) ?? "",
|
||||||
|
charactersIgnoringModifiers: event.charactersIgnoringModifiers ?? "",
|
||||||
|
isARepeat: event.isARepeat,
|
||||||
|
keyCode: event.keyCode
|
||||||
|
) ?? event
|
||||||
|
}
|
||||||
|
|
||||||
|
let action = event.isARepeat ? GHOSTTY_ACTION_REPEAT : GHOSTTY_ACTION_PRESS
|
||||||
|
|
||||||
|
// By setting this to non-nil, we note that we're in a keyDown event. From here,
|
||||||
|
// we call interpretKeyEvents so that we can handle complex input such as Korean
|
||||||
|
// language.
|
||||||
|
keyTextAccumulator = []
|
||||||
|
defer { keyTextAccumulator = nil }
|
||||||
|
|
||||||
|
// We need to know what the length of marked text was before this event to
|
||||||
|
// know if these events cleared it.
|
||||||
|
let markedTextBefore = markedText.length > 0
|
||||||
|
|
||||||
|
self.interpretKeyEvents([translationEvent])
|
||||||
|
|
||||||
|
// If we have text, then we've composed a character, send that down. We do this
|
||||||
|
// first because if we completed a preedit, the text will be available here
|
||||||
|
// AND we'll have a preedit.
|
||||||
|
var handled: Bool = false
|
||||||
|
if let list = keyTextAccumulator, list.count > 0 {
|
||||||
|
handled = true
|
||||||
|
for text in list {
|
||||||
|
keyAction(action, event: event, text: text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we have marked text, we're in a preedit state. Send that down.
|
||||||
|
// If we don't have marked text but we had marked text before, then the preedit
|
||||||
|
// was cleared so we want to send down an empty string to ensure we've cleared
|
||||||
|
// the preedit.
|
||||||
|
if (markedText.length > 0 || markedTextBefore) {
|
||||||
|
handled = true
|
||||||
|
keyAction(action, event: event, preedit: markedText.string)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!handled) {
|
||||||
|
// No text or anything, we want to handle this manually.
|
||||||
|
keyAction(action, event: event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override func keyUp(with event: NSEvent) {
|
||||||
|
keyAction(GHOSTTY_ACTION_RELEASE, event: event)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Special case handling for some control keys
|
||||||
|
override func performKeyEquivalent(with event: NSEvent) -> Bool {
|
||||||
|
// Only process keys when Control is the only modifier
|
||||||
|
if (!event.modifierFlags.contains(.control) ||
|
||||||
|
!event.modifierFlags.isDisjoint(with: [.shift, .command, .option])) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only process key down events
|
||||||
|
if (event.type != .keyDown) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
let equivalent: String
|
||||||
|
switch (event.charactersIgnoringModifiers) {
|
||||||
|
case "/":
|
||||||
|
// Treat C-/ as C-_. We do this because C-/ makes macOS make a beep
|
||||||
|
// sound and we don't like the beep sound.
|
||||||
|
equivalent = "_"
|
||||||
|
|
||||||
|
default:
|
||||||
|
// Ignore other events
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
let newEvent = NSEvent.keyEvent(
|
||||||
|
with: .keyDown,
|
||||||
|
location: event.locationInWindow,
|
||||||
|
modifierFlags: .control,
|
||||||
|
timestamp: event.timestamp,
|
||||||
|
windowNumber: event.windowNumber,
|
||||||
|
context: nil,
|
||||||
|
characters: equivalent,
|
||||||
|
charactersIgnoringModifiers: equivalent,
|
||||||
|
isARepeat: event.isARepeat,
|
||||||
|
keyCode: event.keyCode
|
||||||
|
)
|
||||||
|
|
||||||
|
self.keyDown(with: newEvent!)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override func flagsChanged(with event: NSEvent) {
|
||||||
|
let mod: UInt32;
|
||||||
|
switch (event.keyCode) {
|
||||||
|
case 0x39: mod = GHOSTTY_MODS_CAPS.rawValue
|
||||||
|
case 0x38, 0x3C: mod = GHOSTTY_MODS_SHIFT.rawValue
|
||||||
|
case 0x3B, 0x3E: mod = GHOSTTY_MODS_CTRL.rawValue
|
||||||
|
case 0x3A, 0x3D: mod = GHOSTTY_MODS_ALT.rawValue
|
||||||
|
case 0x37, 0x36: mod = GHOSTTY_MODS_SUPER.rawValue
|
||||||
|
default: return
|
||||||
|
}
|
||||||
|
|
||||||
|
// The keyAction function will do this AGAIN below which sucks to repeat
|
||||||
|
// but this is super cheap and flagsChanged isn't that common.
|
||||||
|
let mods = Ghostty.ghosttyMods(event.modifierFlags)
|
||||||
|
|
||||||
|
// If the key that pressed this is active, its a press, else release.
|
||||||
|
var action = GHOSTTY_ACTION_RELEASE
|
||||||
|
if (mods.rawValue & mod != 0) {
|
||||||
|
// If the key is pressed, its slightly more complicated, because we
|
||||||
|
// want to check if the pressed modifier is the correct side. If the
|
||||||
|
// correct side is pressed then its a press event otherwise its a release
|
||||||
|
// event with the opposite modifier still held.
|
||||||
|
let sidePressed: Bool
|
||||||
|
switch (event.keyCode) {
|
||||||
|
case 0x3C:
|
||||||
|
sidePressed = event.modifierFlags.rawValue & UInt(NX_DEVICERSHIFTKEYMASK) != 0;
|
||||||
|
case 0x3E:
|
||||||
|
sidePressed = event.modifierFlags.rawValue & UInt(NX_DEVICERCTLKEYMASK) != 0;
|
||||||
|
case 0x3D:
|
||||||
|
sidePressed = event.modifierFlags.rawValue & UInt(NX_DEVICERALTKEYMASK) != 0;
|
||||||
|
case 0x36:
|
||||||
|
sidePressed = event.modifierFlags.rawValue & UInt(NX_DEVICERCMDKEYMASK) != 0;
|
||||||
|
default:
|
||||||
|
sidePressed = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sidePressed) {
|
||||||
|
action = GHOSTTY_ACTION_PRESS
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
keyAction(action, event: event)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func keyAction(_ action: ghostty_input_action_e, event: NSEvent) {
|
||||||
|
guard let surface = self.surface else { return }
|
||||||
|
|
||||||
|
var key_ev = ghostty_input_key_s()
|
||||||
|
key_ev.action = action
|
||||||
|
key_ev.mods = Ghostty.ghosttyMods(event.modifierFlags)
|
||||||
|
key_ev.keycode = UInt32(event.keyCode)
|
||||||
|
key_ev.text = nil
|
||||||
|
key_ev.composing = false
|
||||||
|
ghostty_surface_key(surface, key_ev)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func keyAction(_ action: ghostty_input_action_e, event: NSEvent, preedit: String) {
|
||||||
|
guard let surface = self.surface else { return }
|
||||||
|
|
||||||
|
preedit.withCString { ptr in
|
||||||
|
var key_ev = ghostty_input_key_s()
|
||||||
|
key_ev.action = action
|
||||||
|
key_ev.mods = Ghostty.ghosttyMods(event.modifierFlags)
|
||||||
|
key_ev.keycode = UInt32(event.keyCode)
|
||||||
|
key_ev.text = ptr
|
||||||
|
key_ev.composing = true
|
||||||
|
ghostty_surface_key(surface, key_ev)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func keyAction(_ action: ghostty_input_action_e, event: NSEvent, text: String) {
|
||||||
|
guard let surface = self.surface else { return }
|
||||||
|
|
||||||
|
text.withCString { ptr in
|
||||||
|
var key_ev = ghostty_input_key_s()
|
||||||
|
key_ev.action = action
|
||||||
|
key_ev.mods = Ghostty.ghosttyMods(event.modifierFlags)
|
||||||
|
key_ev.keycode = UInt32(event.keyCode)
|
||||||
|
key_ev.text = ptr
|
||||||
|
ghostty_surface_key(surface, key_ev)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: Menu Handlers
|
||||||
|
|
||||||
|
@IBAction func copy(_ sender: Any?) {
|
||||||
|
guard let surface = self.surface else { return }
|
||||||
|
let action = "copy_to_clipboard"
|
||||||
|
if (!ghostty_surface_binding_action(surface, action, UInt(action.count))) {
|
||||||
|
AppDelegate.logger.warning("action failed action=\(action)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@IBAction func paste(_ sender: Any?) {
|
||||||
|
guard let surface = self.surface else { return }
|
||||||
|
let action = "paste_from_clipboard"
|
||||||
|
if (!ghostty_surface_binding_action(surface, action, UInt(action.count))) {
|
||||||
|
AppDelegate.logger.warning("action failed action=\(action)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@IBAction func pasteAsPlainText(_ sender: Any?) {
|
||||||
|
guard let surface = self.surface else { return }
|
||||||
|
let action = "paste_from_clipboard"
|
||||||
|
if (!ghostty_surface_binding_action(surface, action, UInt(action.count))) {
|
||||||
|
AppDelegate.logger.warning("action failed action=\(action)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@IBAction override func selectAll(_ sender: Any?) {
|
||||||
|
guard let surface = self.surface else { return }
|
||||||
|
let action = "select_all"
|
||||||
|
if (!ghostty_surface_binding_action(surface, action, UInt(action.count))) {
|
||||||
|
AppDelegate.logger.warning("action failed action=\(action)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Show a user notification and associate it with this surface
|
||||||
|
func showUserNotification(title: String, body: String) {
|
||||||
|
let content = UNMutableNotificationContent()
|
||||||
|
content.title = title
|
||||||
|
content.subtitle = self.title
|
||||||
|
content.body = body
|
||||||
|
content.sound = UNNotificationSound.default
|
||||||
|
content.categoryIdentifier = Ghostty.userNotificationCategory
|
||||||
|
content.userInfo = ["surface": self.uuid.uuidString]
|
||||||
|
|
||||||
|
let uuid = UUID().uuidString
|
||||||
|
let request = UNNotificationRequest(
|
||||||
|
identifier: uuid,
|
||||||
|
content: content,
|
||||||
|
trigger: nil
|
||||||
|
)
|
||||||
|
|
||||||
|
UNUserNotificationCenter.current().add(request) { error in
|
||||||
|
if let error = error {
|
||||||
|
AppDelegate.logger.error("Error scheduling user notification: \(error)")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
self.notificationIdentifiers.insert(uuid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handle a user notification click
|
||||||
|
func handleUserNotification(notification: UNNotification, focus: Bool) {
|
||||||
|
let id = notification.request.identifier
|
||||||
|
guard self.notificationIdentifiers.remove(id) != nil else { return }
|
||||||
|
if focus {
|
||||||
|
self.window?.makeKeyAndOrderFront(self)
|
||||||
|
Ghostty.moveFocus(to: self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - NSTextInputClient
|
||||||
|
|
||||||
|
extension Ghostty.SurfaceView: NSTextInputClient {
|
||||||
|
func hasMarkedText() -> Bool {
|
||||||
|
return markedText.length > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func markedRange() -> NSRange {
|
||||||
|
guard markedText.length > 0 else { return NSRange() }
|
||||||
|
return NSRange(0...(markedText.length-1))
|
||||||
|
}
|
||||||
|
|
||||||
|
func selectedRange() -> NSRange {
|
||||||
|
return NSRange()
|
||||||
|
}
|
||||||
|
|
||||||
|
func setMarkedText(_ string: Any, selectedRange: NSRange, replacementRange: NSRange) {
|
||||||
|
switch string {
|
||||||
|
case let v as NSAttributedString:
|
||||||
|
self.markedText = NSMutableAttributedString(attributedString: v)
|
||||||
|
|
||||||
|
case let v as String:
|
||||||
|
self.markedText = NSMutableAttributedString(string: v)
|
||||||
|
|
||||||
|
default:
|
||||||
|
print("unknown marked text: \(string)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func unmarkText() {
|
||||||
|
self.markedText.mutableString.setString("")
|
||||||
|
}
|
||||||
|
|
||||||
|
func validAttributesForMarkedText() -> [NSAttributedString.Key] {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
func attributedSubstring(forProposedRange range: NSRange, actualRange: NSRangePointer?) -> NSAttributedString? {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func characterIndex(for point: NSPoint) -> Int {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func firstRect(forCharacterRange range: NSRange, actualRange: NSRangePointer?) -> NSRect {
|
||||||
|
guard let surface = self.surface else {
|
||||||
|
return NSMakeRect(frame.origin.x, frame.origin.y, 0, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ghostty will tell us where it thinks an IME keyboard should render.
|
||||||
|
var x: Double = 0;
|
||||||
|
var y: Double = 0;
|
||||||
|
ghostty_surface_ime_point(surface, &x, &y)
|
||||||
|
|
||||||
|
// Ghostty coordinates are in top-left (0, 0) so we have to convert to
|
||||||
|
// bottom-left since that is what UIKit expects
|
||||||
|
let rect = NSMakeRect(x, frame.size.height - y, 0, 0)
|
||||||
|
|
||||||
|
// Convert from view to screen coordinates
|
||||||
|
guard let window = self.window else { return rect }
|
||||||
|
return window.convertToScreen(rect)
|
||||||
|
}
|
||||||
|
|
||||||
|
func insertText(_ string: Any, replacementRange: NSRange) {
|
||||||
|
// We must have an associated event
|
||||||
|
guard NSApp.currentEvent != nil else { return }
|
||||||
|
guard let surface = self.surface else { return }
|
||||||
|
|
||||||
|
// We want the string view of the any value
|
||||||
|
var chars = ""
|
||||||
|
switch (string) {
|
||||||
|
case let v as NSAttributedString:
|
||||||
|
chars = v.string
|
||||||
|
case let v as String:
|
||||||
|
chars = v
|
||||||
|
default:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// If insertText is called, our preedit must be over.
|
||||||
|
unmarkText()
|
||||||
|
|
||||||
|
// If we have an accumulator we're in another key event so we just
|
||||||
|
// accumulate and return.
|
||||||
|
if var acc = keyTextAccumulator {
|
||||||
|
acc.append(chars)
|
||||||
|
keyTextAccumulator = acc
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let len = chars.utf8CString.count
|
||||||
|
if (len == 0) { return }
|
||||||
|
|
||||||
|
chars.withCString { ptr in
|
||||||
|
// len includes the null terminator so we do len - 1
|
||||||
|
ghostty_surface_text(surface, ptr, UInt(len - 1))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override func doCommand(by selector: Selector) {
|
||||||
|
// This currently just prevents NSBeep from interpretKeyEvents but in the future
|
||||||
|
// we may want to make some of this work.
|
||||||
|
|
||||||
|
print("SEL: \(selector)")
|
||||||
|
}
|
||||||
|
}
|
90
macos/Sources/Ghostty/SurfaceView_UIKit.swift
Normal file
90
macos/Sources/Ghostty/SurfaceView_UIKit.swift
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
import SwiftUI
|
||||||
|
import GhosttyKit
|
||||||
|
|
||||||
|
extension Ghostty {
|
||||||
|
/// The UIView implementation for a terminal surface.
|
||||||
|
class SurfaceView: UIView, ObservableObject {
|
||||||
|
/// Unique ID per surface
|
||||||
|
let uuid: UUID
|
||||||
|
|
||||||
|
// The current title of the surface as defined by the pty. This can be
|
||||||
|
// changed with escape codes. This is public because the callbacks go
|
||||||
|
// to the app level and it is set from there.
|
||||||
|
@Published var title: String = "👻"
|
||||||
|
|
||||||
|
// The cell size of this surface. This is set by the core when the
|
||||||
|
// surface is first created and any time the cell size changes (i.e.
|
||||||
|
// when the font size changes). This is used to allow windows to be
|
||||||
|
// resized in discrete steps of a single cell.
|
||||||
|
@Published var cellSize: OSSize = .zero
|
||||||
|
|
||||||
|
// The health state of the surface. This currently only reflects the
|
||||||
|
// renderer health. In the future we may want to make this an enum.
|
||||||
|
@Published var healthy: Bool = true
|
||||||
|
|
||||||
|
// Any error while initializing the surface.
|
||||||
|
@Published var error: Error? = nil
|
||||||
|
|
||||||
|
private(set) var surface: ghostty_surface_t?
|
||||||
|
|
||||||
|
init(_ app: ghostty_app_t, baseConfig: SurfaceConfiguration? = nil, uuid: UUID? = nil) {
|
||||||
|
self.uuid = uuid ?? .init()
|
||||||
|
|
||||||
|
// Initialize with some default frame size. The important thing is that this
|
||||||
|
// is non-zero so that our layer bounds are non-zero so that our renderer
|
||||||
|
// can do SOMETHING.
|
||||||
|
super.init(frame: CGRect(x: 0, y: 0, width: 800, height: 600))
|
||||||
|
|
||||||
|
// Setup our surface. This will also initialize all the terminal IO.
|
||||||
|
let surface_cfg = baseConfig ?? SurfaceConfiguration()
|
||||||
|
var surface_cfg_c = surface_cfg.ghosttyConfig(view: self)
|
||||||
|
guard let surface = ghostty_surface_new(app, &surface_cfg_c) else {
|
||||||
|
// TODO
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.surface = surface;
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) is not supported for this view")
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
guard let surface = self.surface else { return }
|
||||||
|
ghostty_surface_free(surface)
|
||||||
|
}
|
||||||
|
|
||||||
|
func focusDidChange(_ focused: Bool) {
|
||||||
|
guard let surface = self.surface else { return }
|
||||||
|
ghostty_surface_set_focus(surface, focused)
|
||||||
|
}
|
||||||
|
|
||||||
|
func sizeDidChange(_ size: CGSize) {
|
||||||
|
guard let surface = self.surface else { return }
|
||||||
|
|
||||||
|
// Ghostty wants to know the actual framebuffer size... It is very important
|
||||||
|
// here that we use "size" and NOT the view frame. If we're in the middle of
|
||||||
|
// an animation (i.e. a fullscreen animation), the frame will not yet be updated.
|
||||||
|
// The size represents our final size we're going for.
|
||||||
|
let scale = self.contentScaleFactor
|
||||||
|
ghostty_surface_set_content_scale(surface, scale, scale)
|
||||||
|
ghostty_surface_set_size(
|
||||||
|
surface,
|
||||||
|
UInt32(size.width * scale),
|
||||||
|
UInt32(size.height * scale)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: UIView
|
||||||
|
|
||||||
|
override class var layerClass: AnyClass {
|
||||||
|
get {
|
||||||
|
return CAMetalLayer.self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override func didMoveToWindow() {
|
||||||
|
sizeDidChange(frame.size)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
54
macos/Sources/Helpers/CrossKit.swift
Normal file
54
macos/Sources/Helpers/CrossKit.swift
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
// This file is a helper to bridge some types that are effectively identical
|
||||||
|
// between AppKit and UIKit.
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
#if canImport(AppKit)
|
||||||
|
|
||||||
|
import AppKit
|
||||||
|
|
||||||
|
typealias OSView = NSView
|
||||||
|
typealias OSColor = NSColor
|
||||||
|
typealias OSSize = NSSize
|
||||||
|
|
||||||
|
protocol OSViewRepresentable: NSViewRepresentable where NSViewType == OSViewType {
|
||||||
|
associatedtype OSViewType: NSView
|
||||||
|
func makeOSView(context: Context) -> OSViewType
|
||||||
|
func updateOSView(_ osView: OSViewType, context: Context)
|
||||||
|
}
|
||||||
|
|
||||||
|
extension OSViewRepresentable {
|
||||||
|
func makeNSView(context: Context) -> OSViewType {
|
||||||
|
makeOSView(context: context)
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateNSView(_ nsView: OSViewType, context: Context) {
|
||||||
|
updateOSView(nsView, context: context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#elseif canImport(UIKit)
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
typealias OSView = UIView
|
||||||
|
typealias OSColor = UIColor
|
||||||
|
typealias OSSize = CGSize
|
||||||
|
|
||||||
|
protocol OSViewRepresentable: UIViewRepresentable {
|
||||||
|
associatedtype OSViewType: UIView
|
||||||
|
func makeOSView(context: Context) -> OSViewType
|
||||||
|
func updateOSView(_ osView: OSViewType, context: Context)
|
||||||
|
}
|
||||||
|
|
||||||
|
extension OSViewRepresentable {
|
||||||
|
func makeUIView(context: Context) -> OSViewType {
|
||||||
|
makeOSView(context: context)
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateUIView(_ uiView: OSViewType, context: Context) {
|
||||||
|
updateOSView(uiView, context: context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
@ -429,7 +429,7 @@ pub fn init(
|
|||||||
// people add other emoji fonts to their system, we always want to
|
// people add other emoji fonts to their system, we always want to
|
||||||
// prefer the official one. Users can override this by explicitly
|
// prefer the official one. Users can override this by explicitly
|
||||||
// specifying a font-family for emoji.
|
// specifying a font-family for emoji.
|
||||||
if (comptime builtin.os.tag == .macos) apple_emoji: {
|
if (comptime builtin.target.isDarwin()) apple_emoji: {
|
||||||
const disco = group.discover orelse break :apple_emoji;
|
const disco = group.discover orelse break :apple_emoji;
|
||||||
var disco_it = try disco.discover(alloc, .{
|
var disco_it = try disco.discover(alloc, .{
|
||||||
.family = "Apple Color Emoji",
|
.family = "Apple Color Emoji",
|
||||||
@ -442,7 +442,7 @@ pub fn init(
|
|||||||
|
|
||||||
// Emoji fallback. We don't include this on Mac since Mac is expected
|
// Emoji fallback. We don't include this on Mac since Mac is expected
|
||||||
// to always have the Apple Emoji available on the system.
|
// to always have the Apple Emoji available on the system.
|
||||||
if (builtin.os.tag != .macos or font.Discover == void) {
|
if (comptime !builtin.target.isDarwin() or font.Discover == void) {
|
||||||
_ = try group.addFace(
|
_ = try group.addFace(
|
||||||
.regular,
|
.regular,
|
||||||
.{ .fallback_loaded = try font.Face.init(font_lib, face_emoji_ttf, group.faceOptions()) },
|
.{ .fallback_loaded = try font.Face.init(font_lib, face_emoji_ttf, group.faceOptions()) },
|
||||||
@ -488,6 +488,7 @@ pub fn init(
|
|||||||
.balance = config.@"window-padding-balance",
|
.balance = config.@"window-padding-balance",
|
||||||
},
|
},
|
||||||
.surface_mailbox = .{ .surface = self, .app = app_mailbox },
|
.surface_mailbox = .{ .surface = self, .app = app_mailbox },
|
||||||
|
.rt_surface = rt_surface,
|
||||||
});
|
});
|
||||||
errdefer renderer_impl.deinit();
|
errdefer renderer_impl.deinit();
|
||||||
|
|
||||||
|
@ -234,6 +234,7 @@ pub const App = struct {
|
|||||||
/// Platform-specific configuration for libghostty.
|
/// Platform-specific configuration for libghostty.
|
||||||
pub const Platform = union(PlatformTag) {
|
pub const Platform = union(PlatformTag) {
|
||||||
macos: MacOS,
|
macos: MacOS,
|
||||||
|
ios: IOS,
|
||||||
|
|
||||||
// If our build target for libghostty is not darwin then we do
|
// If our build target for libghostty is not darwin then we do
|
||||||
// not include macos support at all.
|
// not include macos support at all.
|
||||||
@ -242,12 +243,21 @@ pub const Platform = union(PlatformTag) {
|
|||||||
nsview: objc.Object,
|
nsview: objc.Object,
|
||||||
} else void;
|
} else void;
|
||||||
|
|
||||||
|
pub const IOS = if (builtin.target.isDarwin()) struct {
|
||||||
|
/// The view to render the surface on.
|
||||||
|
uiview: objc.Object,
|
||||||
|
} else void;
|
||||||
|
|
||||||
// The C ABI compatible version of this union. The tag is expected
|
// The C ABI compatible version of this union. The tag is expected
|
||||||
// to be stored elsewhere.
|
// to be stored elsewhere.
|
||||||
pub const C = extern union {
|
pub const C = extern union {
|
||||||
macos: extern struct {
|
macos: extern struct {
|
||||||
nsview: ?*anyopaque,
|
nsview: ?*anyopaque,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
ios: extern struct {
|
||||||
|
uiview: ?*anyopaque,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Initialize a Platform a tag and configuration from the C ABI.
|
/// Initialize a Platform a tag and configuration from the C ABI.
|
||||||
@ -260,6 +270,13 @@ pub const Platform = union(PlatformTag) {
|
|||||||
break :macos error.NSViewMustBeSet);
|
break :macos error.NSViewMustBeSet);
|
||||||
break :macos .{ .macos = .{ .nsview = nsview } };
|
break :macos .{ .macos = .{ .nsview = nsview } };
|
||||||
} else error.UnsupportedPlatform,
|
} else error.UnsupportedPlatform,
|
||||||
|
|
||||||
|
.ios => if (IOS != void) ios: {
|
||||||
|
const config = c_platform.ios;
|
||||||
|
const uiview = objc.Object.fromId(config.uiview orelse
|
||||||
|
break :ios error.UIViewMustBeSet);
|
||||||
|
break :ios .{ .ios = .{ .uiview = uiview } };
|
||||||
|
} else error.UnsupportedPlatform,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -269,6 +286,7 @@ pub const PlatformTag = enum(c_int) {
|
|||||||
// from the C API.
|
// from the C API.
|
||||||
|
|
||||||
macos = 1,
|
macos = 1,
|
||||||
|
ios = 2,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const Surface = struct {
|
pub const Surface = struct {
|
||||||
|
@ -115,7 +115,7 @@ buf_instance: InstanceBuffer, // MTLBuffer
|
|||||||
/// Metal objects
|
/// Metal objects
|
||||||
device: objc.Object, // MTLDevice
|
device: objc.Object, // MTLDevice
|
||||||
queue: objc.Object, // MTLCommandQueue
|
queue: objc.Object, // MTLCommandQueue
|
||||||
swapchain: objc.Object, // CAMetalLayer
|
layer: objc.Object, // CAMetalLayer
|
||||||
texture_greyscale: objc.Object, // MTLTexture
|
texture_greyscale: objc.Object, // MTLTexture
|
||||||
texture_color: objc.Object, // MTLTexture
|
texture_color: objc.Object, // MTLTexture
|
||||||
|
|
||||||
@ -262,20 +262,77 @@ pub fn init(alloc: Allocator, options: renderer.Options) !Metal {
|
|||||||
defer arena.deinit();
|
defer arena.deinit();
|
||||||
const arena_alloc = arena.allocator();
|
const arena_alloc = arena.allocator();
|
||||||
|
|
||||||
|
const ViewInfo = struct {
|
||||||
|
view: objc.Object,
|
||||||
|
scaleFactor: f64,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get the metadata about our underlying view that we'll be rendering to.
|
||||||
|
const info: ViewInfo = switch (apprt.runtime) {
|
||||||
|
apprt.glfw => info: {
|
||||||
|
// Everything in glfw is window-oriented so we grab the backing
|
||||||
|
// window, then derive everything from that.
|
||||||
|
const nswindow = objc.Object.fromId(glfwNative.getCocoaWindow(
|
||||||
|
options.rt_surface.window,
|
||||||
|
).?);
|
||||||
|
|
||||||
|
const contentView = objc.Object.fromId(
|
||||||
|
nswindow.getProperty(?*anyopaque, "contentView").?,
|
||||||
|
);
|
||||||
|
const scaleFactor = nswindow.getProperty(
|
||||||
|
macos.graphics.c.CGFloat,
|
||||||
|
"backingScaleFactor",
|
||||||
|
);
|
||||||
|
|
||||||
|
break :info .{
|
||||||
|
.view = contentView,
|
||||||
|
.scaleFactor = scaleFactor,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
apprt.embedded => .{
|
||||||
|
.scaleFactor = @floatCast(options.rt_surface.content_scale.x),
|
||||||
|
.view = switch (options.rt_surface.platform) {
|
||||||
|
.macos => |v| v.nsview,
|
||||||
|
.ios => |v| v.uiview,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
else => @compileError("unsupported apprt for metal"),
|
||||||
|
};
|
||||||
|
|
||||||
// Initialize our metal stuff
|
// Initialize our metal stuff
|
||||||
const device = objc.Object.fromId(mtl.MTLCreateSystemDefaultDevice());
|
const device = objc.Object.fromId(mtl.MTLCreateSystemDefaultDevice());
|
||||||
const queue = device.msgSend(objc.Object, objc.sel("newCommandQueue"), .{});
|
const queue = device.msgSend(objc.Object, objc.sel("newCommandQueue"), .{});
|
||||||
const swapchain = swapchain: {
|
|
||||||
const CAMetalLayer = objc.getClass("CAMetalLayer").?;
|
|
||||||
const swapchain = CAMetalLayer.msgSend(objc.Object, objc.sel("layer"), .{});
|
|
||||||
swapchain.setProperty("device", device.value);
|
|
||||||
swapchain.setProperty("opaque", options.config.background_opacity >= 1);
|
|
||||||
|
|
||||||
// disable v-sync
|
// Get our CAMetalLayer
|
||||||
swapchain.setProperty("displaySyncEnabled", false);
|
const layer = switch (builtin.os.tag) {
|
||||||
|
.macos => layer: {
|
||||||
|
const CAMetalLayer = objc.getClass("CAMetalLayer").?;
|
||||||
|
break :layer CAMetalLayer.msgSend(objc.Object, objc.sel("layer"), .{});
|
||||||
|
},
|
||||||
|
|
||||||
break :swapchain swapchain;
|
// iOS is always layer-backed so we don't need to do anything here.
|
||||||
|
.ios => info.view.getProperty(objc.Object, "layer"),
|
||||||
|
|
||||||
|
else => @compileError("unsupported target for Metal"),
|
||||||
};
|
};
|
||||||
|
layer.setProperty("device", device.value);
|
||||||
|
layer.setProperty("opaque", options.config.background_opacity >= 1);
|
||||||
|
layer.setProperty("displaySyncEnabled", false); // disable v-sync
|
||||||
|
|
||||||
|
// Make our view layer-backed with our Metal layer. On iOS views are
|
||||||
|
// always layer backed so we don't need to do this. But on iOS the
|
||||||
|
// caller MUST be sure to set the layerClass to CAMetalLayer.
|
||||||
|
if (comptime builtin.os.tag == .macos) {
|
||||||
|
info.view.setProperty("layer", layer.value);
|
||||||
|
info.view.setProperty("wantsLayer", true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure that our metal layer has a content scale set to match the
|
||||||
|
// scale factor of the window. This avoids magnification issues leading
|
||||||
|
// to blurry rendering.
|
||||||
|
layer.setProperty("contentsScale", info.scaleFactor);
|
||||||
|
|
||||||
// Get our cell metrics based on a regular font ascii 'M'. Why 'M'?
|
// Get our cell metrics based on a regular font ascii 'M'. Why 'M'?
|
||||||
// Doesn't matter, any normal ASCII will do we're just trying to make
|
// Doesn't matter, any normal ASCII will do we're just trying to make
|
||||||
@ -401,7 +458,7 @@ pub fn init(alloc: Allocator, options: renderer.Options) !Metal {
|
|||||||
// Metal stuff
|
// Metal stuff
|
||||||
.device = device,
|
.device = device,
|
||||||
.queue = queue,
|
.queue = queue,
|
||||||
.swapchain = swapchain,
|
.layer = layer,
|
||||||
.texture_greyscale = texture_greyscale,
|
.texture_greyscale = texture_greyscale,
|
||||||
.texture_color = texture_color,
|
.texture_color = texture_color,
|
||||||
.custom_shader_state = custom_shader_state,
|
.custom_shader_state = custom_shader_state,
|
||||||
@ -445,45 +502,12 @@ pub fn deinit(self: *Metal) void {
|
|||||||
|
|
||||||
/// This is called just prior to spinning up the renderer thread for
|
/// This is called just prior to spinning up the renderer thread for
|
||||||
/// final main thread setup requirements.
|
/// final main thread setup requirements.
|
||||||
pub fn finalizeSurfaceInit(self: *const Metal, surface: *apprt.Surface) !void {
|
pub fn finalizeSurfaceInit(self: *Metal, surface: *apprt.Surface) !void {
|
||||||
const Info = struct {
|
_ = self;
|
||||||
view: objc.Object,
|
_ = surface;
|
||||||
scaleFactor: f64,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Get the view and scale factor for our surface.
|
// Metal doesn't have to do anything here. OpenGL has to do things
|
||||||
const info: Info = switch (apprt.runtime) {
|
// like release the context but Metal doesn't have anything like that.
|
||||||
apprt.glfw => info: {
|
|
||||||
// Everything in glfw is window-oriented so we grab the backing
|
|
||||||
// window, then derive everything from that.
|
|
||||||
const nswindow = objc.Object.fromId(glfwNative.getCocoaWindow(surface.window).?);
|
|
||||||
const contentView = objc.Object.fromId(nswindow.getProperty(?*anyopaque, "contentView").?);
|
|
||||||
const scaleFactor = nswindow.getProperty(macos.graphics.c.CGFloat, "backingScaleFactor");
|
|
||||||
break :info .{
|
|
||||||
.view = contentView,
|
|
||||||
.scaleFactor = scaleFactor,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
apprt.embedded => .{
|
|
||||||
.view = switch (surface.platform) {
|
|
||||||
.macos => |v| v.nsview,
|
|
||||||
},
|
|
||||||
.scaleFactor = @floatCast(surface.content_scale.x),
|
|
||||||
},
|
|
||||||
|
|
||||||
else => @compileError("unsupported apprt for metal"),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Make our view layer-backed with our Metal layer
|
|
||||||
info.view.setProperty("layer", self.swapchain.value);
|
|
||||||
info.view.setProperty("wantsLayer", true);
|
|
||||||
|
|
||||||
// Ensure that our metal layer has a content scale set to match the
|
|
||||||
// scale factor of the window. This avoids magnification issues leading
|
|
||||||
// to blurry rendering.
|
|
||||||
const layer = info.view.getProperty(objc.Object, "layer");
|
|
||||||
layer.setProperty("contentsScale", info.scaleFactor);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Callback called by renderer.Thread when it begins.
|
/// Callback called by renderer.Thread when it begins.
|
||||||
@ -738,7 +762,7 @@ pub fn drawFrame(self: *Metal, surface: *apprt.Surface) !void {
|
|||||||
defer pool.deinit();
|
defer pool.deinit();
|
||||||
|
|
||||||
// Get our drawable (CAMetalDrawable)
|
// Get our drawable (CAMetalDrawable)
|
||||||
const drawable = self.swapchain.msgSend(objc.Object, objc.sel("nextDrawable"), .{});
|
const drawable = self.layer.msgSend(objc.Object, objc.sel("nextDrawable"), .{});
|
||||||
|
|
||||||
// Get our screen texture. If we don't have a dedicated screen texture
|
// Get our screen texture. If we don't have a dedicated screen texture
|
||||||
// then we just use the drawable texture.
|
// then we just use the drawable texture.
|
||||||
@ -1415,7 +1439,7 @@ pub fn setScreenSize(
|
|||||||
const padded_dim = dim.subPadding(padding);
|
const padded_dim = dim.subPadding(padding);
|
||||||
|
|
||||||
// Set the size of the drawable surface to the bounds
|
// Set the size of the drawable surface to the bounds
|
||||||
self.swapchain.setProperty("drawableSize", macos.graphics.Size{
|
self.layer.setProperty("drawableSize", macos.graphics.Size{
|
||||||
.width = @floatFromInt(dim.width),
|
.width = @floatFromInt(dim.width),
|
||||||
.height = @floatFromInt(dim.height),
|
.height = @floatFromInt(dim.height),
|
||||||
});
|
});
|
||||||
|
@ -18,6 +18,9 @@ padding: Padding,
|
|||||||
/// once the thread has started and should not be used outside of the thread.
|
/// once the thread has started and should not be used outside of the thread.
|
||||||
surface_mailbox: apprt.surface.Mailbox,
|
surface_mailbox: apprt.surface.Mailbox,
|
||||||
|
|
||||||
|
/// The apprt surface.
|
||||||
|
rt_surface: *apprt.Surface,
|
||||||
|
|
||||||
pub const Padding = struct {
|
pub const Padding = struct {
|
||||||
// Explicit padding options, in pixels. The surface thread is
|
// Explicit padding options, in pixels. The surface thread is
|
||||||
// expected to convert points to pixels for a given DPI.
|
// expected to convert points to pixels for a given DPI.
|
||||||
|
Reference in New Issue
Block a user