Adopt @Observable in SwiftUI code

This moves off of ObservableObject and @Published over to @Observable, which is preferred as it allows views
to observe changes on a per-property basis rather than an entire object at a time.

I went through the app and tried to compare behavior where I could, and I couldn't spot any visual differences,
but I am sure there's good chance I have missed something.

This also requires updating the minimum macOS deployment target to 14.0. If that is unacceptable, I am happy just
closing this PR.
This commit is contained in:
Harlan Haskins
2025-01-03 10:41:41 -05:00
parent e2f9eb6a6f
commit eda7f10529
17 changed files with 92 additions and 77 deletions

View File

@ -784,7 +784,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/../Frameworks", "@executable_path/../Frameworks",
); );
MACOSX_DEPLOYMENT_TARGET = 13.0; MACOSX_DEPLOYMENT_TARGET = 14.0;
MARKETING_VERSION = 0.1; MARKETING_VERSION = 0.1;
"OTHER_LDFLAGS[arch=*]" = "-lstdc++"; "OTHER_LDFLAGS[arch=*]" = "-lstdc++";
PRODUCT_BUNDLE_IDENTIFIER = com.mitchellh.ghostty; PRODUCT_BUNDLE_IDENTIFIER = com.mitchellh.ghostty;
@ -953,7 +953,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/../Frameworks", "@executable_path/../Frameworks",
); );
MACOSX_DEPLOYMENT_TARGET = 13.0; MACOSX_DEPLOYMENT_TARGET = 14.0;
MARKETING_VERSION = 0.1; MARKETING_VERSION = 0.1;
"OTHER_LDFLAGS[arch=*]" = "-lstdc++"; "OTHER_LDFLAGS[arch=*]" = "-lstdc++";
PRODUCT_BUNDLE_IDENTIFIER = com.mitchellh.ghostty.debug; PRODUCT_BUNDLE_IDENTIFIER = com.mitchellh.ghostty.debug;
@ -1006,7 +1006,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/../Frameworks", "@executable_path/../Frameworks",
); );
MACOSX_DEPLOYMENT_TARGET = 13.0; MACOSX_DEPLOYMENT_TARGET = 14.0;
MARKETING_VERSION = 0.1; MARKETING_VERSION = 0.1;
"OTHER_LDFLAGS[arch=*]" = "-lstdc++"; "OTHER_LDFLAGS[arch=*]" = "-lstdc++";
PRODUCT_BUNDLE_IDENTIFIER = com.mitchellh.ghostty; PRODUCT_BUNDLE_IDENTIFIER = com.mitchellh.ghostty;

View File

@ -2,18 +2,18 @@ import SwiftUI
@main @main
struct Ghostty_iOSApp: App { struct Ghostty_iOSApp: App {
@StateObject private var ghostty_app = Ghostty.App() @State private var ghostty_app = Ghostty.App()
var body: some Scene { var body: some Scene {
WindowGroup { WindowGroup {
iOS_GhosttyTerminal() iOS_GhosttyTerminal()
.environmentObject(ghostty_app) .environment(ghostty_app)
} }
} }
} }
struct iOS_GhosttyTerminal: View { struct iOS_GhosttyTerminal: View {
@EnvironmentObject private var ghostty_app: Ghostty.App @Environment(Ghostty.App.self) private var ghostty_app: Ghostty.App
var body: some View { var body: some View {
ZStack { ZStack {
@ -26,7 +26,7 @@ struct iOS_GhosttyTerminal: View {
} }
struct iOS_GhosttyInitView: View { struct iOS_GhosttyInitView: View {
@EnvironmentObject private var ghostty_app: Ghostty.App @Environment(Ghostty.App.self) private var ghostty_app: Ghostty.App
var body: some View { var body: some View {
VStack { VStack {

View File

@ -4,8 +4,8 @@ import OSLog
import Sparkle import Sparkle
import GhosttyKit import GhosttyKit
@Observable
class AppDelegate: NSObject, class AppDelegate: NSObject,
ObservableObject,
NSApplicationDelegate, NSApplicationDelegate,
UNUserNotificationCenterDelegate, UNUserNotificationCenterDelegate,
GhosttyAppDelegate GhosttyAppDelegate
@ -99,7 +99,7 @@ class AppDelegate: NSObject,
private var appearanceObserver: NSKeyValueObservation? = nil private var appearanceObserver: NSKeyValueObservation? = nil
/// The custom app icon image that is currently in use. /// The custom app icon image that is currently in use.
@Published private(set) var appIcon: NSImage? = nil { private(set) var appIcon: NSImage? = nil {
didSet { didSet {
NSApplication.shared.applicationIconImage = appIcon NSApplication.shared.applicationIconImage = appIcon
} }

View File

@ -12,7 +12,8 @@ import OSLog
// it. You have to yield secure input on application deactivation (because // it. You have to yield secure input on application deactivation (because
// it'll affect other apps) and reacquire on reactivation, and every enable // it'll affect other apps) and reacquire on reactivation, and every enable
// needs to be balanced with a disable. // needs to be balanced with a disable.
class SecureInput : ObservableObject { @Observable
class SecureInput {
static let shared = SecureInput() static let shared = SecureInput()
private static let logger = Logger( private static let logger = Logger(
@ -28,10 +29,11 @@ class SecureInput : ObservableObject {
} }
// The scoped objects and whether they're currently in focus. // The scoped objects and whether they're currently in focus.
@ObservationIgnored
private var scoped: [ObjectIdentifier: Bool] = [:] private var scoped: [ObjectIdentifier: Bool] = [:]
// This is set to true when we've successfully called EnableSecureInput. // This is set to true when we've successfully called EnableSecureInput.
@Published private(set) var enabled: Bool = false private(set) var enabled: Bool = false
// This is true if we want to enable secure input. We want to enable // This is true if we want to enable secure input. We want to enable
// secure input if its enabled globally or any of the scoped objects are // secure input if its enabled globally or any of the scoped objects are

View File

@ -1,11 +1,11 @@
import SwiftUI import SwiftUI
protocol ConfigurationErrorsViewModel: ObservableObject { protocol ConfigurationErrorsViewModel: AnyObject, Observable {
var errors: [String] { get set } var errors: [String] { get set }
} }
struct ConfigurationErrorsView<ViewModel: ConfigurationErrorsViewModel>: View { struct ConfigurationErrorsView<ViewModel: ConfigurationErrorsViewModel>: View {
@ObservedObject var model: ViewModel var model: ViewModel
var body: some View { var body: some View {
VStack { VStack {

View File

@ -2,7 +2,7 @@ import SwiftUI
struct SettingsView: View { struct SettingsView: View {
// We need access to our app delegate to know if we're quitting or not. // We need access to our app delegate to know if we're quitting or not.
@EnvironmentObject private var appDelegate: AppDelegate @Environment(AppDelegate.self) private var appDelegate: AppDelegate
var body: some View { var body: some View {
HStack { HStack {

View File

@ -25,6 +25,7 @@ import GhosttyKit
/// ///
/// The primary idea of all the behaviors we don't implement here are that subclasses may not /// The primary idea of all the behaviors we don't implement here are that subclasses may not
/// want these behaviors. /// want these behaviors.
@Observable
class BaseTerminalController: NSWindowController, class BaseTerminalController: NSWindowController,
NSWindowDelegate, NSWindowDelegate,
TerminalViewDelegate, TerminalViewDelegate,
@ -36,12 +37,13 @@ class BaseTerminalController: NSWindowController,
let ghostty: Ghostty.App let ghostty: Ghostty.App
/// The currently focused surface. /// The currently focused surface.
@ObservationIgnored
var focusedSurface: Ghostty.SurfaceView? = nil { var focusedSurface: Ghostty.SurfaceView? = nil {
didSet { syncFocusToSurfaceTree() } didSet { syncFocusToSurfaceTree() }
} }
/// The surface tree for this window. /// The surface tree for this window.
@Published var surfaceTree: Ghostty.SplitNode? = nil { var surfaceTree: Ghostty.SplitNode? = nil {
didSet { surfaceTreeDidChange(from: oldValue, to: surfaceTree) } didSet { surfaceTreeDidChange(from: oldValue, to: surfaceTree) }
} }

View File

@ -610,21 +610,22 @@ class TerminalController: BaseTerminalController {
syncAppearance(focusedSurface.derivedConfig) syncAppearance(focusedSurface.derivedConfig)
// We also want to get notified of certain changes to update our appearance. // We also want to get notified of certain changes to update our appearance.
focusedSurface.$derivedConfig observeFocusedSurface(focusedSurface)
.sink { [weak self, weak focusedSurface] _ in self?.syncAppearanceOnPropertyChange(focusedSurface) }
.store(in: &surfaceAppearanceCancellables)
focusedSurface.$backgroundColor
.sink { [weak self, weak focusedSurface] _ in self?.syncAppearanceOnPropertyChange(focusedSurface) }
.store(in: &surfaceAppearanceCancellables)
} }
private func syncAppearanceOnPropertyChange(_ surface: Ghostty.SurfaceView?) { func observeFocusedSurface(_ focusedSurface: Ghostty.SurfaceView) {
guard let surface else { return } /// Use Observable to observe properties that will invalidate the surface
DispatchQueue.main.async { [weak self, weak surface] in /// appearance.
guard let surface else { return } withObservationTracking {
_ = focusedSurface.derivedConfig
_ = focusedSurface.backgroundColor
} onChange: { [weak self] in
guard let self else { return } guard let self else { return }
guard self.focusedSurface == surface else { return } DispatchQueue.main.async {
self.syncAppearance(surface.derivedConfig) guard self.focusedSurface == focusedSurface else { return }
self.syncAppearance(focusedSurface.derivedConfig)
self.observeFocusedSurface(focusedSurface)
}
} }
} }

View File

@ -28,18 +28,18 @@ protocol TerminalViewDelegate: AnyObject {
/// The view model is a required implementation for TerminalView callers. This contains /// The view model is a required implementation for TerminalView callers. This contains
/// the main state between the TerminalView caller and SwiftUI. This abstraction is what /// the main state between the TerminalView caller and SwiftUI. This abstraction is what
/// allows AppKit to own most of the data in SwiftUI. /// allows AppKit to own most of the data in SwiftUI.
protocol TerminalViewModel: ObservableObject { protocol TerminalViewModel: AnyObject, Observable {
/// The tree of terminal surfaces (splits) within the view. This is mutated by TerminalView /// The tree of terminal surfaces (splits) within the view. This is mutated by TerminalView
/// and children. This should be @Published. /// and children.
var surfaceTree: Ghostty.SplitNode? { get set } var surfaceTree: Ghostty.SplitNode? { get set }
} }
/// The main terminal view. This terminal view supports splits. /// The main terminal view. This terminal view supports splits.
struct TerminalView<ViewModel: TerminalViewModel>: View { struct TerminalView<ViewModel: TerminalViewModel>: View {
@ObservedObject var ghostty: Ghostty.App var ghostty: Ghostty.App
// The required view model // The required view model
@ObservedObject var viewModel: ViewModel @Bindable var viewModel: ViewModel
// An optional delegate to receive information about terminal changes. // An optional delegate to receive information about terminal changes.
weak var delegate: (any TerminalViewDelegate)? = nil weak var delegate: (any TerminalViewDelegate)? = nil
@ -86,9 +86,9 @@ struct TerminalView<ViewModel: TerminalViewModel>: View {
if (Ghostty.info.mode == GHOSTTY_BUILD_MODE_DEBUG || Ghostty.info.mode == GHOSTTY_BUILD_MODE_RELEASE_SAFE) { if (Ghostty.info.mode == GHOSTTY_BUILD_MODE_DEBUG || Ghostty.info.mode == GHOSTTY_BUILD_MODE_RELEASE_SAFE) {
DebugBuildWarningView() DebugBuildWarningView()
} }
Ghostty.TerminalSplit(node: $viewModel.surfaceTree) Ghostty.TerminalSplit(node: $viewModel.surfaceTree)
.environmentObject(ghostty) .environment(ghostty)
.focused($focused) .focused($focused)
.onAppear { self.focused = true } .onAppear { self.focused = true }
.onChange(of: focusedSurface) { newValue in .onChange(of: focusedSurface) { newValue in

View File

@ -13,7 +13,8 @@ protocol GhosttyAppDelegate: AnyObject {
extension Ghostty { extension Ghostty {
// IMPORTANT: THIS IS NOT DONE. // IMPORTANT: THIS IS NOT DONE.
// This is a refactor/redo of Ghostty.AppState so that it supports both macOS and iOS // This is a refactor/redo of Ghostty.AppState so that it supports both macOS and iOS
class App: ObservableObject { @Observable
class App {
enum Readiness: String { enum Readiness: String {
case loading, error, ready case loading, error, ready
} }
@ -22,16 +23,16 @@ extension Ghostty {
weak var delegate: GhosttyAppDelegate? weak var delegate: GhosttyAppDelegate?
/// The readiness value of the state. /// The readiness value of the state.
@Published var readiness: Readiness = .loading var readiness: Readiness = .loading
/// The global app configuration. This defines the app level configuration plus any behavior /// The global app configuration. This defines the app level configuration plus any behavior
/// for new windows, tabs, etc. Note that when creating a new window, it may inherit some /// for new windows, tabs, etc. Note that when creating a new window, it may inherit some
/// configuration (i.e. font size) from the previously focused window. This would override this. /// configuration (i.e. font size) from the previously focused window. This would override this.
@Published private(set) var config: Config private(set) var config: Config
/// The ghostty app instance. We only have one of these for the entire app, although I guess /// The ghostty app instance. We only have one of these for the entire app, although I guess
/// in theory you can have multiple... I don't know why you would... /// in theory you can have multiple... I don't know why you would...
@Published var app: ghostty_app_t? = nil { var app: ghostty_app_t? = nil {
didSet { didSet {
guard let old = oldValue else { return } guard let old = oldValue else { return }
ghostty_app_free(old) ghostty_app_free(old)

View File

@ -3,7 +3,7 @@ import GhosttyKit
extension Ghostty { extension Ghostty {
/// Maps to a `ghostty_config_t` and the various operations on that. /// Maps to a `ghostty_config_t` and the various operations on that.
class Config: ObservableObject { class Config {
// The underlying C pointer to the Ghostty config structure. This // The underlying C pointer to the Ghostty config structure. This
// should never be accessed directly. Any operations on this should // should never be accessed directly. Any operations on this should
// be called from the functions on this or another class. // be called from the functions on this or another class.

View File

@ -195,9 +195,10 @@ extension Ghostty {
} }
} }
class Leaf: ObservableObject, Equatable, Hashable, Codable { @Observable
class Leaf: Equatable, Hashable, Codable {
let app: ghostty_app_t let app: ghostty_app_t
@Published var surface: SurfaceView var surface: SurfaceView
weak var parent: SplitNode.Container? weak var parent: SplitNode.Container?
@ -250,13 +251,14 @@ extension Ghostty {
} }
} }
class Container: ObservableObject, Equatable, Hashable, Codable { @Observable
class Container: Equatable, Hashable, Codable {
let app: ghostty_app_t let app: ghostty_app_t
let direction: SplitViewDirection let direction: SplitViewDirection
@Published var topLeft: SplitNode var topLeft: SplitNode
@Published var bottomRight: SplitNode var bottomRight: SplitNode
@Published var split: CGFloat = 0.5 var split: CGFloat = 0.5
var resizeEvent: PassthroughSubject<Double, Never> = .init() var resizeEvent: PassthroughSubject<Double, Never> = .init()

View File

@ -294,11 +294,11 @@ extension Ghostty {
/// This represents a split view that is in the horizontal or vertical split state. /// This represents a split view that is in the horizontal or vertical split state.
private struct TerminalSplitContainer: View { private struct TerminalSplitContainer: View {
@EnvironmentObject var ghostty: Ghostty.App @Environment(Ghostty.App.self) var ghostty: Ghostty.App
let neighbors: SplitNode.Neighbors let neighbors: SplitNode.Neighbors
@Binding var node: SplitNode? @Binding var node: SplitNode?
@StateObject var container: SplitNode.Container @Bindable var container: SplitNode.Container
var body: some View { var body: some View {
SplitView( SplitView(

View File

@ -6,10 +6,10 @@ import GhosttyKit
extension Ghostty { extension Ghostty {
/// InspectableSurface is a type of Surface view that allows an inspector to be attached. /// InspectableSurface is a type of Surface view that allows an inspector to be attached.
struct InspectableSurface: View { struct InspectableSurface: View {
@EnvironmentObject var ghostty: Ghostty.App @Environment(Ghostty.App.self) var ghostty: Ghostty.App
/// Same as SurfaceWrapper, see the doc comments there. /// Same as SurfaceWrapper, see the doc comments there.
@ObservedObject var surfaceView: SurfaceView var surfaceView: SurfaceView
var isSplit: Bool = false var isSplit: Bool = false
// Maintain whether our view has focus or not // Maintain whether our view has focus or not

View File

@ -5,7 +5,7 @@ import GhosttyKit
extension Ghostty { extension Ghostty {
/// Render a terminal for the active app in the environment. /// Render a terminal for the active app in the environment.
struct Terminal: View { struct Terminal: View {
@EnvironmentObject private var ghostty: Ghostty.App @Environment(Ghostty.App.self) private var ghostty: Ghostty.App
@FocusedValue(\.ghosttySurfaceTitle) private var surfaceTitle: String? @FocusedValue(\.ghosttySurfaceTitle) private var surfaceTitle: String?
var body: some View { var body: some View {
@ -22,10 +22,10 @@ extension Ghostty {
struct SurfaceForApp<Content: View>: View { struct SurfaceForApp<Content: View>: View {
let content: ((SurfaceView) -> Content) let content: ((SurfaceView) -> Content)
@StateObject private var surfaceView: SurfaceView @State private var surfaceView: SurfaceView
init(_ app: ghostty_app_t, @ViewBuilder content: @escaping ((SurfaceView) -> Content)) { init(_ app: ghostty_app_t, @ViewBuilder content: @escaping ((SurfaceView) -> Content)) {
_surfaceView = StateObject(wrappedValue: SurfaceView(app)) _surfaceView = State(wrappedValue: SurfaceView(app))
self.content = content self.content = content
} }
@ -37,7 +37,7 @@ extension Ghostty {
struct SurfaceWrapper: View { struct SurfaceWrapper: View {
// The surface to create a view for. This must be created upstream. As long as this // The surface to create a view for. This must be created upstream. As long as this
// remains the same, the surface that is being rendered remains the same. // remains the same, the surface that is being rendered remains the same.
@ObservedObject var surfaceView: SurfaceView var surfaceView: SurfaceView
// True if this surface is part of a split view. This is important to know so // True if this surface is part of a split view. This is important to know so
// we know whether to dim the surface out of focus. // we know whether to dim the surface out of focus.
@ -54,10 +54,15 @@ extension Ghostty {
#if canImport(AppKit) #if canImport(AppKit)
// Observe SecureInput to detect when its enabled // Observe SecureInput to detect when its enabled
@ObservedObject private var secureInput = SecureInput.shared private var secureInput = SecureInput.shared
#endif #endif
init(surfaceView: SurfaceView, isSplit: Bool = false) {
self.surfaceView = surfaceView
self.isSplit = isSplit
}
@EnvironmentObject private var ghostty: Ghostty.App @Environment(Ghostty.App.self) private var ghostty: Ghostty.App
var body: some View { var body: some View {
let center = NotificationCenter.default let center = NotificationCenter.default

View File

@ -5,55 +5,56 @@ import GhosttyKit
extension Ghostty { extension Ghostty {
/// The NSView implementation for a terminal surface. /// The NSView implementation for a terminal surface.
class SurfaceView: OSView, ObservableObject { @Observable
class SurfaceView: OSView {
/// Unique ID per surface /// Unique ID per surface
let uuid: UUID let uuid: UUID
// The current title of the surface as defined by the pty. This can be // 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 // changed with escape codes. This is public because the callbacks go
// to the app level and it is set from there. // to the app level and it is set from there.
@Published private(set) var title: String = "👻" private(set) var title: String = "👻"
// The current pwd of the surface as defined by the pty. This can be // The current pwd of the surface as defined by the pty. This can be
// changed with escape codes. // changed with escape codes.
@Published var pwd: String? = nil var pwd: String? = nil
// The cell size of this surface. This is set by the core when the // 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. // 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 // when the font size changes). This is used to allow windows to be
// resized in discrete steps of a single cell. // resized in discrete steps of a single cell.
@Published var cellSize: NSSize = .zero var cellSize: NSSize = .zero
// The health state of the surface. This currently only reflects the // The health state of the surface. This currently only reflects the
// renderer health. In the future we may want to make this an enum. // renderer health. In the future we may want to make this an enum.
@Published var healthy: Bool = true var healthy: Bool = true
// Any error while initializing the surface. // Any error while initializing the surface.
@Published var error: Error? = nil var error: Error? = nil
// The hovered URL string // The hovered URL string
@Published var hoverUrl: String? = nil var hoverUrl: String? = nil
// The currently active key sequence. The sequence is not active if this is empty. // The currently active key sequence. The sequence is not active if this is empty.
@Published var keySequence: [Ghostty.KeyEquivalent] = [] var keySequence: [Ghostty.KeyEquivalent] = []
// The time this surface last became focused. This is a ContinuousClock.Instant // The time this surface last became focused. This is a ContinuousClock.Instant
// on supported platforms. // on supported platforms.
@Published var focusInstant: ContinuousClock.Instant? = nil var focusInstant: ContinuousClock.Instant? = nil
// Returns sizing information for the surface. This is the raw C // Returns sizing information for the surface. This is the raw C
// structure because I'm lazy. // structure because I'm lazy.
@Published var surfaceSize: ghostty_surface_size_s? = nil var surfaceSize: ghostty_surface_size_s? = nil
// Whether the pointer should be visible or not // Whether the pointer should be visible or not
@Published private(set) var pointerStyle: BackportPointerStyle = .default private(set) var pointerStyle: BackportPointerStyle = .default
/// The configuration derived from the Ghostty config so we don't need to rely on references. /// The configuration derived from the Ghostty config so we don't need to rely on references.
@Published private(set) var derivedConfig: DerivedConfig private(set) var derivedConfig: DerivedConfig
/// The background color within the color palette of the surface. This is only set if it is /// The background color within the color palette of the surface. This is only set if it is
/// dynamically updated. Otherwise, the background color is the default background color. /// dynamically updated. Otherwise, the background color is the default background color.
@Published private(set) var backgroundColor: Color? = nil private(set) var backgroundColor: Color? = nil
// An initial size to request for a window. This will only affect // An initial size to request for a window. This will only affect
// then the view is moved to a new window. // then the view is moved to a new window.
@ -89,7 +90,7 @@ extension Ghostty {
} }
// True if the inspector should be visible // True if the inspector should be visible
@Published var inspectorVisible: Bool = false { var inspectorVisible: Bool = false {
didSet { didSet {
if (oldValue && !inspectorVisible) { if (oldValue && !inspectorVisible) {
guard let surface = self.surface else { return } guard let surface = self.surface else { return }

View File

@ -3,37 +3,38 @@ import GhosttyKit
extension Ghostty { extension Ghostty {
/// The UIView implementation for a terminal surface. /// The UIView implementation for a terminal surface.
class SurfaceView: UIView, ObservableObject { @Observable
class SurfaceView: UIView {
/// Unique ID per surface /// Unique ID per surface
let uuid: UUID let uuid: UUID
// The current title of the surface as defined by the pty. This can be // 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 // changed with escape codes. This is public because the callbacks go
// to the app level and it is set from there. // to the app level and it is set from there.
@Published var title: String = "👻" var title: String = "👻"
// The current pwd of the surface. // The current pwd of the surface.
@Published var pwd: String? = nil var pwd: String? = nil
// The cell size of this surface. This is set by the core when the // 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. // 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 // when the font size changes). This is used to allow windows to be
// resized in discrete steps of a single cell. // resized in discrete steps of a single cell.
@Published var cellSize: OSSize = .zero var cellSize: OSSize = .zero
// The health state of the surface. This currently only reflects the // The health state of the surface. This currently only reflects the
// renderer health. In the future we may want to make this an enum. // renderer health. In the future we may want to make this an enum.
@Published var healthy: Bool = true var healthy: Bool = true
// Any error while initializing the surface. // Any error while initializing the surface.
@Published var error: Error? = nil var error: Error? = nil
// The hovered URL // The hovered URL
@Published var hoverUrl: String? = nil var hoverUrl: String? = nil
// The time this surface last became focused. This is a ContinuousClock.Instant // The time this surface last became focused. This is a ContinuousClock.Instant
// on supported platforms. // on supported platforms.
@Published var focusInstant: ContinuousClock.Instant? = nil var focusInstant: ContinuousClock.Instant? = nil
// Returns sizing information for the surface. This is the raw C // Returns sizing information for the surface. This is the raw C
// structure because I'm lazy. // structure because I'm lazy.