@thelacanians/vue-native-cli 0.4.15 → 0.6.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +329 -15
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Bridge/NativeBridge.kt +118 -0
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Components/Factories/VViewFactory.kt +178 -1
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Modules/GeneratedModuleRegistry.kt +28 -0
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Modules/NativeModuleRegistry.kt +3 -0
- package/native/android/VueNativeCore/src/test/kotlin/com/vuenative/core/ComponentFactoryTest.kt +674 -0
- package/native/android/VueNativeCore/src/test/kotlin/com/vuenative/core/ErrorOverlayViewTest.kt +183 -0
- package/native/android/VueNativeCore/src/test/kotlin/com/vuenative/core/EventThrottleTest.kt +203 -0
- package/native/android/VueNativeCore/src/test/kotlin/com/vuenative/core/HotReloadManagerTest.kt +162 -0
- package/native/android/VueNativeCore/src/test/kotlin/com/vuenative/core/JSPolyfillsTest.kt +153 -0
- package/native/android/VueNativeCore/src/test/kotlin/com/vuenative/core/NativeBridgeTest.kt +6 -3
- package/native/android/VueNativeCore/src/test/kotlin/com/vuenative/core/NativeModuleTest.kt +475 -0
- package/native/android/gradle.properties +1 -0
- package/native/android/gradlew +1 -1
- package/native/ios/VueNativeCore/Package.swift +1 -1
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Bridge/EventThrottle.swift +1 -0
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Bridge/NativeBridge.swift +143 -5
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Components/Factories/VTextFactory.swift +43 -0
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Components/Factories/VViewFactory.swift +116 -4
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Helpers/GestureWrapper.swift +100 -0
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Modules/GeneratedModuleRegistry.swift +28 -0
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Modules/NativeModuleRegistry.swift +3 -0
- package/native/ios/VueNativeCore/Tests/VueNativeCoreTests/CertificatePinningTests.swift +190 -0
- package/native/ios/VueNativeCore/Tests/VueNativeCoreTests/ComponentFactoryTests.swift +585 -0
- package/native/ios/VueNativeCore/Tests/VueNativeCoreTests/EventThrottleTests.swift +161 -0
- package/native/ios/VueNativeCore/Tests/VueNativeCoreTests/HotReloadManagerTests.swift +88 -0
- package/native/ios/VueNativeCore/Tests/VueNativeCoreTests/JSPolyfillsTests.swift +319 -0
- package/native/ios/VueNativeCore/Tests/VueNativeCoreTests/NativeModuleTests.swift +400 -0
- package/native/macos/VueNativeMacOS/Package.swift +34 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Bridge/ErrorOverlayView.swift +112 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Bridge/EventThrottle.swift +58 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Bridge/HotReloadManager.swift +153 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Bridge/JSPolyfills.swift +696 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Bridge/JSRuntime.swift +347 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Bridge/NativeBridge.swift +877 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Bridge/VueNativeWindowController.swift +125 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/ComponentRegistry.swift +209 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VActionSheetFactory.swift +155 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VActivityIndicatorFactory.swift +85 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VAlertDialogFactory.swift +132 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VButtonFactory.swift +83 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VCheckboxFactory.swift +108 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VDropdownFactory.swift +155 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VImageFactory.swift +270 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VInputFactory.swift +257 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VKeyboardAvoidingFactory.swift +22 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VListFactory.swift +324 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VModalFactory.swift +231 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VOutlineViewFactory.swift +276 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VPickerFactory.swift +134 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VPressableFactory.swift +120 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VProgressBarFactory.swift +71 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VRadioFactory.swift +193 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VRefreshControlFactory.swift +25 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VSafeAreaFactory.swift +46 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VScrollViewFactory.swift +190 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VSectionListFactory.swift +374 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VSegmentedControlFactory.swift +125 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VSliderFactory.swift +131 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VSplitViewFactory.swift +215 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VStatusBarFactory.swift +25 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VSwitchFactory.swift +92 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VTextFactory.swift +336 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VToolbarFactory.swift +212 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VVideoFactory.swift +245 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VViewFactory.swift +314 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VWebViewFactory.swift +162 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/NativeComponentFactory.swift +54 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Helpers/ClickableView.swift +100 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Helpers/Extensions.swift +23 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Helpers/GestureWrapper.swift +183 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Helpers/NSColor+Hex.swift +78 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Layout/FlippedView.swift +19 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Layout/LayoutNode.swift +493 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Modules/AnimationModule.swift +354 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Modules/AppStateModule.swift +62 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Modules/BiometryModule.swift +60 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Modules/CameraModule.swift +167 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Modules/ClipboardModule.swift +34 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Modules/DeviceInfoModule.swift +49 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Modules/DragDropModule.swift +50 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Modules/FileDialogModule.swift +86 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Modules/HapticsModule.swift +42 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Modules/KeyboardModule.swift +28 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Modules/LinkingModule.swift +49 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Modules/MenuModule.swift +95 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Modules/NativeModuleRegistry.swift +63 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Modules/NotificationsModule.swift +112 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Modules/PermissionsModule.swift +149 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Modules/ShareModule.swift +37 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Modules/WindowModule.swift +71 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Resources/vue-native-placeholder.js +2 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Styling/StyleEngine.swift +885 -0
- package/native/macos/VueNativeMacOS/Tests/VueNativeMacOSTests/ComponentFactoryTests.swift +80 -0
- package/native/macos/VueNativeMacOS/Tests/VueNativeMacOSTests/VueNativeMacOSTests.swift +149 -0
- package/native/shared/VueNativeShared/AGENTS.md +129 -0
- package/native/shared/VueNativeShared/Package.swift +14 -0
- package/native/shared/VueNativeShared/Sources/VueNativeShared/CertificatePinning.swift +134 -0
- package/native/shared/VueNativeShared/Sources/VueNativeShared/EventThrottle.swift +78 -0
- package/native/shared/VueNativeShared/Sources/VueNativeShared/HotReloadManager.swift +162 -0
- package/native/shared/VueNativeShared/Sources/VueNativeShared/JSRuntime.swift +412 -0
- package/native/shared/VueNativeShared/Sources/VueNativeShared/Modules/AsyncStorageModule.swift +68 -0
- package/native/shared/VueNativeShared/Sources/VueNativeShared/Modules/AudioModule.swift +359 -0
- package/native/shared/VueNativeShared/Sources/VueNativeShared/Modules/DatabaseModule.swift +259 -0
- package/native/shared/VueNativeShared/Sources/VueNativeShared/Modules/FileSystemModule.swift +233 -0
- package/native/shared/VueNativeShared/Sources/VueNativeShared/Modules/GeolocationModule.swift +156 -0
- package/native/shared/VueNativeShared/Sources/VueNativeShared/Modules/NetworkModule.swift +59 -0
- package/native/shared/VueNativeShared/Sources/VueNativeShared/Modules/PerformanceModule.swift +113 -0
- package/native/shared/VueNativeShared/Sources/VueNativeShared/Modules/SecureStorageModule.swift +119 -0
- package/native/shared/VueNativeShared/Sources/VueNativeShared/Modules/WebSocketModule.swift +212 -0
- package/native/shared/VueNativeShared/Sources/VueNativeShared/NativeEventDispatcher.swift +6 -0
- package/native/shared/VueNativeShared/Sources/VueNativeShared/NativeModule.swift +26 -0
- package/native/shared/VueNativeShared/Sources/VueNativeShared/NativeModuleRegistry.swift +37 -0
- package/native/shared/VueNativeShared/Sources/VueNativeShared/SharedJSPolyfills.swift +673 -0
- package/native/shared/VueNativeShared/Tests/VueNativeSharedTests/VueNativeSharedTests.swift +44 -0
- package/package.json +8 -2
|
@@ -0,0 +1,877 @@
|
|
|
1
|
+
import JavaScriptCore
|
|
2
|
+
import AppKit
|
|
3
|
+
import VueNativeShared
|
|
4
|
+
|
|
5
|
+
/// The main bridge between JavaScript and native AppKit.
|
|
6
|
+
///
|
|
7
|
+
/// Responsibilities:
|
|
8
|
+
/// - Registers `__VN_flushOperations` on JSContext to receive batched ops from JS
|
|
9
|
+
/// - Parses operation batches (JSON) and processes them on the main thread
|
|
10
|
+
/// - Maintains `viewRegistry` mapping node IDs to native NSViews
|
|
11
|
+
/// - Triggers layout relayout after processing each batch
|
|
12
|
+
/// - Dispatches native events back to JS via `dispatchEventToJS`
|
|
13
|
+
///
|
|
14
|
+
/// Thread safety:
|
|
15
|
+
/// - `__VN_flushOperations` is called on the JS queue
|
|
16
|
+
/// - JSON parsing happens on the JS queue
|
|
17
|
+
/// - All NSView operations dispatch to main thread
|
|
18
|
+
/// - `viewRegistry` is only accessed on the main thread
|
|
19
|
+
/// - `eventHandlers` is only accessed on the main thread
|
|
20
|
+
@MainActor
|
|
21
|
+
public final class NativeBridge: @preconcurrency NativeEventDispatcher {
|
|
22
|
+
|
|
23
|
+
// MARK: - Singleton
|
|
24
|
+
|
|
25
|
+
public static let shared = NativeBridge()
|
|
26
|
+
|
|
27
|
+
// MARK: - Properties
|
|
28
|
+
|
|
29
|
+
/// Maps node IDs to their native NSViews. Accessed only on main thread.
|
|
30
|
+
private var viewRegistry: [Int: NSView] = [:]
|
|
31
|
+
|
|
32
|
+
/// Maps node IDs to their component type strings. Accessed only on main thread.
|
|
33
|
+
private var typeRegistry: [Int: String] = [:]
|
|
34
|
+
|
|
35
|
+
/// Maps (nodeId, eventName) to event handler closures. Accessed only on main thread.
|
|
36
|
+
private var eventHandlers: [String: (Any?) -> Void] = [:]
|
|
37
|
+
|
|
38
|
+
/// Reverse index: maps nodeId to the set of event handler keys for that node.
|
|
39
|
+
private var eventKeysPerNode: [Int: Set<String>] = [:]
|
|
40
|
+
|
|
41
|
+
/// Maps each child node ID to its parent node ID.
|
|
42
|
+
private var nodeParent: [Int: Int] = [:]
|
|
43
|
+
|
|
44
|
+
/// Reverse index: maps parent node ID to an ordered list of child node IDs.
|
|
45
|
+
private var childrenOf: [Int: [Int]] = [:]
|
|
46
|
+
|
|
47
|
+
/// The root NSView that contains all rendered native views.
|
|
48
|
+
private weak var rootView: NSView?
|
|
49
|
+
|
|
50
|
+
/// The window's content view that hosts everything.
|
|
51
|
+
private weak var contentView: NSView?
|
|
52
|
+
|
|
53
|
+
/// The component registry for creating views and handling props/events.
|
|
54
|
+
private let registry = ComponentRegistry.shared
|
|
55
|
+
|
|
56
|
+
/// Reference to the JS runtime.
|
|
57
|
+
private let runtime = JSRuntime.shared
|
|
58
|
+
|
|
59
|
+
// MARK: - Teleport Support
|
|
60
|
+
|
|
61
|
+
/// Maps teleport marker IDs (start, end) for cleanup.
|
|
62
|
+
private var teleportMarkers: [Int: (start: Int, end: Int)] = [:]
|
|
63
|
+
|
|
64
|
+
/// Maps parent node IDs to their teleport containers.
|
|
65
|
+
private var teleportContainers: [Int: NSView] = [:]
|
|
66
|
+
|
|
67
|
+
/// Modal container for teleporting modal content.
|
|
68
|
+
private lazy var modalContainer: NSView = {
|
|
69
|
+
let container = FlippedView()
|
|
70
|
+
container.wantsLayer = true
|
|
71
|
+
container.layer?.backgroundColor = NSColor.clear.cgColor
|
|
72
|
+
container.translatesAutoresizingMaskIntoConstraints = false
|
|
73
|
+
return container
|
|
74
|
+
}()
|
|
75
|
+
|
|
76
|
+
// MARK: - Initialization
|
|
77
|
+
|
|
78
|
+
private init() {}
|
|
79
|
+
|
|
80
|
+
// MARK: - Bridge Function Registration
|
|
81
|
+
|
|
82
|
+
/// Register `__VN_flushOperations`, `__VN_teardown`, `__VN_log`, and `__VN_handleError`
|
|
83
|
+
/// on the given JSContext.
|
|
84
|
+
private func registerBridgeFunctions(on context: JSContext) {
|
|
85
|
+
let flushOps: @convention(block) (JSValue) -> Void = { [weak self] opsValue in
|
|
86
|
+
guard let self = self else { return }
|
|
87
|
+
guard let jsonString = opsValue.toString(), !jsonString.isEmpty else {
|
|
88
|
+
NSLog("[VueNative macOS Bridge] Warning: Empty operations batch")
|
|
89
|
+
return
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
guard let data = jsonString.data(using: .utf8),
|
|
93
|
+
let operations = try? JSONSerialization.jsonObject(with: data) as? [[String: Any]] else {
|
|
94
|
+
NSLog("[VueNative macOS Bridge] Error: Failed to parse operations JSON")
|
|
95
|
+
return
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
DispatchQueue.main.async { [weak self] in
|
|
99
|
+
self?.processOperations(operations)
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
context.setObject(flushOps, forKeyedSubscript: "__VN_flushOperations" as NSString)
|
|
103
|
+
|
|
104
|
+
let teardown: @convention(block) () -> Void = { [weak self] in
|
|
105
|
+
NSLog("[VueNative macOS] Teardown called from JS")
|
|
106
|
+
_ = self
|
|
107
|
+
}
|
|
108
|
+
context.setObject(teardown, forKeyedSubscript: "__VN_teardown" as NSString)
|
|
109
|
+
|
|
110
|
+
let log: @convention(block) (JSValue) -> Void = { message in
|
|
111
|
+
NSLog("[VueNative macOS JS] \(message.toString() ?? "")")
|
|
112
|
+
}
|
|
113
|
+
context.setObject(log, forKeyedSubscript: "__VN_log" as NSString)
|
|
114
|
+
|
|
115
|
+
let handleError: @convention(block) (JSValue) -> Void = { errorInfoValue in
|
|
116
|
+
let jsonString = errorInfoValue.toString() ?? "{}"
|
|
117
|
+
NSLog("[VueNative macOS Error] %@", jsonString)
|
|
118
|
+
|
|
119
|
+
if let data = jsonString.data(using: .utf8),
|
|
120
|
+
let info = try? JSONSerialization.jsonObject(with: data) as? [String: Any] {
|
|
121
|
+
let message = info["message"] as? String ?? "Unknown error"
|
|
122
|
+
let stack = info["stack"] as? String ?? ""
|
|
123
|
+
let componentName = info["componentName"] as? String ?? "unknown"
|
|
124
|
+
NSLog("[VueNative macOS Error] Component: %@, Message: %@", componentName, message)
|
|
125
|
+
if !stack.isEmpty {
|
|
126
|
+
NSLog("[VueNative macOS Error] Stack: %@", stack)
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
context.setObject(handleError, forKeyedSubscript: "__VN_handleError" as NSString)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// MARK: - Setup
|
|
134
|
+
|
|
135
|
+
/// Initialize the bridge. Must be called after JSRuntime.initialize().
|
|
136
|
+
public func initialize(contentView: NSView) {
|
|
137
|
+
self.contentView = contentView
|
|
138
|
+
|
|
139
|
+
let runtime = self.runtime
|
|
140
|
+
|
|
141
|
+
runtime.jsQueue.async { [weak self] in
|
|
142
|
+
guard let self = self, let context = runtime.context else { return }
|
|
143
|
+
self.registerBridgeFunctions(on: context)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Register all native modules, passing self as event dispatcher and
|
|
147
|
+
// a view lookup closure for modules that need to reference views (e.g. Animation).
|
|
148
|
+
NativeModuleRegistry.shared.registerDefaults(
|
|
149
|
+
dispatcher: self,
|
|
150
|
+
viewLookup: { [weak self] nodeId in self?.view(forId: nodeId) }
|
|
151
|
+
)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// MARK: - Operation Processing
|
|
155
|
+
|
|
156
|
+
private static let treeMutationOps: Set<String> = [
|
|
157
|
+
"create", "createText", "appendChild", "insertBefore", "removeChild",
|
|
158
|
+
"setRootView", "setText", "setElementText"
|
|
159
|
+
]
|
|
160
|
+
|
|
161
|
+
private static let layoutAffectingStyles: Set<String> = [
|
|
162
|
+
"width", "height", "minWidth", "minHeight", "maxWidth", "maxHeight",
|
|
163
|
+
"flex", "flexGrow", "flexShrink", "flexBasis", "flexDirection",
|
|
164
|
+
"flexWrap", "alignItems", "alignSelf", "alignContent", "justifyContent",
|
|
165
|
+
"padding", "paddingTop", "paddingRight", "paddingBottom", "paddingLeft",
|
|
166
|
+
"paddingHorizontal", "paddingVertical", "paddingStart", "paddingEnd",
|
|
167
|
+
"margin", "marginTop", "marginRight", "marginBottom", "marginLeft",
|
|
168
|
+
"marginHorizontal", "marginVertical", "marginStart", "marginEnd",
|
|
169
|
+
"gap", "rowGap", "columnGap",
|
|
170
|
+
"position", "top", "right", "bottom", "left", "start", "end",
|
|
171
|
+
"aspectRatio", "display", "overflow", "direction",
|
|
172
|
+
]
|
|
173
|
+
|
|
174
|
+
func processOperations(_ operations: [[String: Any]]) {
|
|
175
|
+
dispatchPrecondition(condition: .onQueue(.main))
|
|
176
|
+
#if DEBUG
|
|
177
|
+
NSLog("[VueNative macOS Bridge] Processing %d operations", operations.count)
|
|
178
|
+
#endif
|
|
179
|
+
|
|
180
|
+
var needsLayout = false
|
|
181
|
+
|
|
182
|
+
for operation in operations {
|
|
183
|
+
guard let op = operation["op"] as? String,
|
|
184
|
+
let args = operation["args"] as? [Any] else {
|
|
185
|
+
NSLog("[VueNative macOS Bridge] Warning: Invalid operation format: \(operation)")
|
|
186
|
+
continue
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if !needsLayout && NativeBridge.treeMutationOps.contains(op) {
|
|
190
|
+
needsLayout = true
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if !needsLayout && op == "updateStyle",
|
|
194
|
+
args.count >= 2,
|
|
195
|
+
let styles = args[1] as? [String: Any] {
|
|
196
|
+
for key in styles.keys {
|
|
197
|
+
if NativeBridge.layoutAffectingStyles.contains(key) {
|
|
198
|
+
needsLayout = true
|
|
199
|
+
break
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
switch op {
|
|
205
|
+
case "create":
|
|
206
|
+
handleCreate(args: args)
|
|
207
|
+
case "createText":
|
|
208
|
+
handleCreateText(args: args)
|
|
209
|
+
case "setText":
|
|
210
|
+
handleSetText(args: args)
|
|
211
|
+
case "setElementText":
|
|
212
|
+
handleSetElementText(args: args)
|
|
213
|
+
case "updateProp":
|
|
214
|
+
handleUpdateProp(args: args)
|
|
215
|
+
case "updateStyle":
|
|
216
|
+
handleUpdateStyle(args: args)
|
|
217
|
+
case "appendChild":
|
|
218
|
+
handleAppendChild(args: args)
|
|
219
|
+
case "insertBefore":
|
|
220
|
+
handleInsertBefore(args: args)
|
|
221
|
+
case "removeChild":
|
|
222
|
+
handleRemoveChild(args: args)
|
|
223
|
+
case "addEventListener":
|
|
224
|
+
handleAddEventListener(args: args)
|
|
225
|
+
case "removeEventListener":
|
|
226
|
+
handleRemoveEventListener(args: args)
|
|
227
|
+
case "setRootView":
|
|
228
|
+
handleSetRootView(args: args)
|
|
229
|
+
case "createTeleport":
|
|
230
|
+
handleCreateTeleport(args: args)
|
|
231
|
+
case "removeTeleport":
|
|
232
|
+
handleRemoveTeleport(args: args)
|
|
233
|
+
case "teleportTo":
|
|
234
|
+
handleTeleportTo(args: args)
|
|
235
|
+
case "invokeNativeModule":
|
|
236
|
+
handleInvokeNativeModule(args: args)
|
|
237
|
+
case "invokeNativeModuleSync":
|
|
238
|
+
handleInvokeNativeModuleSync(args: args)
|
|
239
|
+
default:
|
|
240
|
+
NSLog("[VueNative macOS Bridge] Warning: Unknown operation '\(op)'")
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if needsLayout {
|
|
245
|
+
triggerLayout()
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// MARK: - Operation Handlers
|
|
250
|
+
|
|
251
|
+
/// create: [nodeId: Int, type: String]
|
|
252
|
+
private func handleCreate(args: [Any]) {
|
|
253
|
+
guard args.count >= 2,
|
|
254
|
+
let nodeId = asInt(args[0]),
|
|
255
|
+
let type = args[1] as? String else {
|
|
256
|
+
NSLog("[VueNative macOS Bridge] Error: Invalid create args: \(args)")
|
|
257
|
+
return
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
guard let view = registry.createView(type: type) else {
|
|
261
|
+
NSLog("[VueNative macOS Bridge] Error: Failed to create view for type '\(type)'")
|
|
262
|
+
return
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
viewRegistry[nodeId] = view
|
|
266
|
+
typeRegistry[nodeId] = type
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/// createText: [nodeId: Int, text: String]
|
|
270
|
+
private func handleCreateText(args: [Any]) {
|
|
271
|
+
guard args.count >= 2,
|
|
272
|
+
let nodeId = asInt(args[0]),
|
|
273
|
+
let text = args[1] as? String else {
|
|
274
|
+
NSLog("[VueNative macOS Bridge] Error: Invalid createText args: \(args)")
|
|
275
|
+
return
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
guard let view = registry.createView(type: "VText") else { return }
|
|
279
|
+
|
|
280
|
+
if let textField = view as? NSTextField {
|
|
281
|
+
textField.stringValue = text
|
|
282
|
+
textField.ensureLayoutNode().markDirty()
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
viewRegistry[nodeId] = view
|
|
286
|
+
typeRegistry[nodeId] = "VText"
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/// setText: [nodeId: Int, text: String]
|
|
290
|
+
private func handleSetText(args: [Any]) {
|
|
291
|
+
guard args.count >= 2,
|
|
292
|
+
let nodeId = asInt(args[0]),
|
|
293
|
+
let text = args[1] as? String else {
|
|
294
|
+
return
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
guard let view = viewRegistry[nodeId] else { return }
|
|
298
|
+
|
|
299
|
+
if let textField = view as? NSTextField {
|
|
300
|
+
textField.stringValue = text
|
|
301
|
+
textField.ensureLayoutNode().markDirty()
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/// setElementText: [nodeId: Int, text: String]
|
|
306
|
+
private func handleSetElementText(args: [Any]) {
|
|
307
|
+
guard args.count >= 2,
|
|
308
|
+
let nodeId = asInt(args[0]),
|
|
309
|
+
let text = args[1] as? String else {
|
|
310
|
+
return
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
guard let view = viewRegistry[nodeId] else { return }
|
|
314
|
+
|
|
315
|
+
if let textField = view as? NSTextField {
|
|
316
|
+
textField.stringValue = text
|
|
317
|
+
textField.ensureLayoutNode().markDirty()
|
|
318
|
+
} else {
|
|
319
|
+
if let existingTextField = view.subviews.first(where: { $0 is NSTextField }) as? NSTextField {
|
|
320
|
+
existingTextField.stringValue = text
|
|
321
|
+
existingTextField.ensureLayoutNode().markDirty()
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/// updateProp: [nodeId: Int, key: String, value: Any?]
|
|
327
|
+
private func handleUpdateProp(args: [Any]) {
|
|
328
|
+
guard args.count >= 3,
|
|
329
|
+
let nodeId = asInt(args[0]),
|
|
330
|
+
let key = args[1] as? String else {
|
|
331
|
+
return
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
guard let view = viewRegistry[nodeId] else { return }
|
|
335
|
+
let value: Any? = (args[2] is NSNull) ? nil : args[2]
|
|
336
|
+
registry.updateProp(view: view, key: key, value: value)
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/// updateStyle: [nodeId: Int, styles: [String: Any]]
|
|
340
|
+
private func handleUpdateStyle(args: [Any]) {
|
|
341
|
+
guard args.count >= 2,
|
|
342
|
+
let nodeId = asInt(args[0]),
|
|
343
|
+
let styles = args[1] as? [String: Any] else {
|
|
344
|
+
return
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
guard let view = viewRegistry[nodeId] else { return }
|
|
348
|
+
|
|
349
|
+
for (key, value) in styles {
|
|
350
|
+
let resolvedValue: Any? = (value is NSNull) ? nil : value
|
|
351
|
+
if let type = typeRegistry[nodeId], let factory = registry.factory(for: type) {
|
|
352
|
+
factory.updateProp(view: view, key: key, value: resolvedValue)
|
|
353
|
+
} else {
|
|
354
|
+
StyleEngine.apply(key: key, value: resolvedValue, to: view)
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/// appendChild: [parentId: Int, childId: Int]
|
|
360
|
+
private func handleAppendChild(args: [Any]) {
|
|
361
|
+
guard args.count >= 2,
|
|
362
|
+
let parentId = asInt(args[0]),
|
|
363
|
+
let childId = asInt(args[1]) else {
|
|
364
|
+
return
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
guard let parentView = viewRegistry[parentId],
|
|
368
|
+
let childView = viewRegistry[childId] else {
|
|
369
|
+
return
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
nodeParent[childId] = parentId
|
|
373
|
+
childrenOf[parentId, default: []].append(childId)
|
|
374
|
+
|
|
375
|
+
let container = childContainer(for: parentView)
|
|
376
|
+
if let factory = ComponentRegistry.factory(for: parentView) {
|
|
377
|
+
factory.insertChild(childView, into: container, before: nil)
|
|
378
|
+
} else {
|
|
379
|
+
container.addSubview(childView)
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/// insertBefore: [parentId: Int, childId: Int, beforeId: Int]
|
|
384
|
+
private func handleInsertBefore(args: [Any]) {
|
|
385
|
+
guard args.count >= 3,
|
|
386
|
+
let parentId = asInt(args[0]),
|
|
387
|
+
let childId = asInt(args[1]),
|
|
388
|
+
let beforeId = asInt(args[2]) else {
|
|
389
|
+
return
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
guard let parentView = viewRegistry[parentId],
|
|
393
|
+
let childView = viewRegistry[childId],
|
|
394
|
+
let beforeView = viewRegistry[beforeId] else {
|
|
395
|
+
return
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
nodeParent[childId] = parentId
|
|
399
|
+
var siblings = childrenOf[parentId, default: []]
|
|
400
|
+
if let idx = siblings.firstIndex(of: beforeId) {
|
|
401
|
+
siblings.insert(childId, at: idx)
|
|
402
|
+
} else {
|
|
403
|
+
siblings.append(childId)
|
|
404
|
+
}
|
|
405
|
+
childrenOf[parentId] = siblings
|
|
406
|
+
|
|
407
|
+
let container = childContainer(for: parentView)
|
|
408
|
+
if let factory = ComponentRegistry.factory(for: parentView) {
|
|
409
|
+
factory.insertChild(childView, into: container, before: beforeView)
|
|
410
|
+
} else if let subviews = container.subviews as? [NSView],
|
|
411
|
+
let index = subviews.firstIndex(of: beforeView) {
|
|
412
|
+
container.addSubview(childView, positioned: .below, relativeTo: beforeView)
|
|
413
|
+
} else {
|
|
414
|
+
container.addSubview(childView)
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/// Returns the view that children should be inserted into.
|
|
419
|
+
private func childContainer(for view: NSView) -> NSView {
|
|
420
|
+
// For scroll views, children go into the documentView.
|
|
421
|
+
if let scrollView = view as? NSScrollView,
|
|
422
|
+
let docView = scrollView.documentView {
|
|
423
|
+
return docView
|
|
424
|
+
}
|
|
425
|
+
return view
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/// removeChild: [childId: Int]
|
|
429
|
+
private func handleRemoveChild(args: [Any]) {
|
|
430
|
+
guard args.count >= 1,
|
|
431
|
+
let childId = asInt(args[0]) else {
|
|
432
|
+
return
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
guard let childView = viewRegistry[childId] else { return }
|
|
436
|
+
|
|
437
|
+
removeDescendantsFromIndex(childId)
|
|
438
|
+
|
|
439
|
+
if let parentId = nodeParent[childId],
|
|
440
|
+
let parentView = viewRegistry[parentId],
|
|
441
|
+
let factory = ComponentRegistry.factory(for: parentView) {
|
|
442
|
+
let container = childContainer(for: parentView)
|
|
443
|
+
factory.removeChild(childView, from: container)
|
|
444
|
+
} else {
|
|
445
|
+
childView.removeFromSuperview()
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
if let parentId = nodeParent[childId] {
|
|
449
|
+
childrenOf[parentId]?.removeAll { $0 == childId }
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
cleanupNodeRegistries(childId)
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
private func cleanupNodeRegistries(_ nodeId: Int) {
|
|
456
|
+
nodeParent.removeValue(forKey: nodeId)
|
|
457
|
+
viewRegistry.removeValue(forKey: nodeId)
|
|
458
|
+
typeRegistry.removeValue(forKey: nodeId)
|
|
459
|
+
childrenOf.removeValue(forKey: nodeId)
|
|
460
|
+
|
|
461
|
+
if let keys = eventKeysPerNode.removeValue(forKey: nodeId) {
|
|
462
|
+
for key in keys {
|
|
463
|
+
eventHandlers.removeValue(forKey: key)
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
private func removeDescendantsFromIndex(_ nodeId: Int) {
|
|
469
|
+
guard let children = childrenOf[nodeId] else { return }
|
|
470
|
+
for childId in children {
|
|
471
|
+
removeDescendantsFromIndex(childId)
|
|
472
|
+
cleanupNodeRegistries(childId)
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/// addEventListener: [nodeId: Int, eventName: String, callbackId: Int]
|
|
477
|
+
private func handleAddEventListener(args: [Any]) {
|
|
478
|
+
guard args.count >= 2,
|
|
479
|
+
let nodeId = asInt(args[0]),
|
|
480
|
+
let eventName = args[1] as? String else {
|
|
481
|
+
return
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
guard let view = viewRegistry[nodeId] else { return }
|
|
485
|
+
|
|
486
|
+
let handlerKey = "\(nodeId):\(eventName)"
|
|
487
|
+
|
|
488
|
+
let handler: (Any?) -> Void = { [weak self] payload in
|
|
489
|
+
self?.dispatchEventToJS(nodeId: nodeId, eventName: eventName, payload: payload)
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
eventHandlers[handlerKey] = handler
|
|
493
|
+
eventKeysPerNode[nodeId, default: []].insert(handlerKey)
|
|
494
|
+
|
|
495
|
+
registry.addEventListener(view: view, event: eventName, handler: handler)
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/// removeEventListener: [nodeId: Int, eventName: String]
|
|
499
|
+
private func handleRemoveEventListener(args: [Any]) {
|
|
500
|
+
guard args.count >= 2,
|
|
501
|
+
let nodeId = asInt(args[0]),
|
|
502
|
+
let eventName = args[1] as? String else {
|
|
503
|
+
return
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
let handlerKey = "\(nodeId):\(eventName)"
|
|
507
|
+
eventHandlers.removeValue(forKey: handlerKey)
|
|
508
|
+
eventKeysPerNode[nodeId]?.remove(handlerKey)
|
|
509
|
+
|
|
510
|
+
if let view = viewRegistry[nodeId] {
|
|
511
|
+
registry.removeEventListener(view: view, event: eventName)
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
/// setRootView: [nodeId: Int]
|
|
516
|
+
private func handleSetRootView(args: [Any]) {
|
|
517
|
+
guard args.count >= 1,
|
|
518
|
+
let nodeId = asInt(args[0]) else {
|
|
519
|
+
return
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
NSLog("[VueNative macOS Bridge] handleSetRootView called with nodeId=%d", nodeId)
|
|
523
|
+
|
|
524
|
+
guard let view = viewRegistry[nodeId] else {
|
|
525
|
+
NSLog("[VueNative macOS Bridge] Error: Root view \(nodeId) not found in registry")
|
|
526
|
+
return
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
guard let contentView = contentView else {
|
|
530
|
+
NSLog("[VueNative macOS Bridge] Error: No content view set")
|
|
531
|
+
return
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
NSLog("[VueNative macOS Bridge] Setting up root view, contentView bounds: %.1f x %.1f", contentView.bounds.width, contentView.bounds.height)
|
|
535
|
+
|
|
536
|
+
rootView = view
|
|
537
|
+
view.translatesAutoresizingMaskIntoConstraints = false
|
|
538
|
+
|
|
539
|
+
contentView.addSubview(view)
|
|
540
|
+
|
|
541
|
+
// Pin root view to content view edges
|
|
542
|
+
NSLayoutConstraint.activate([
|
|
543
|
+
view.topAnchor.constraint(equalTo: contentView.topAnchor),
|
|
544
|
+
view.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
|
|
545
|
+
view.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
|
|
546
|
+
view.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
|
|
547
|
+
])
|
|
548
|
+
|
|
549
|
+
contentView.needsLayout = true
|
|
550
|
+
contentView.layoutSubtreeIfNeeded()
|
|
551
|
+
|
|
552
|
+
DispatchQueue.main.async { [weak self] in
|
|
553
|
+
self?.triggerLayoutWithRetries(remaining: 3)
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
// Monitor dark mode changes
|
|
557
|
+
setupAppearanceObserver()
|
|
558
|
+
|
|
559
|
+
installModalContainerIfNeeded()
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
/// Set up observation for dark mode (effective appearance) changes.
|
|
563
|
+
private func setupAppearanceObserver() {
|
|
564
|
+
// On macOS, observe NSApp.effectiveAppearance via KVO
|
|
565
|
+
// or use DistributedNotificationCenter for theme changes.
|
|
566
|
+
DistributedNotificationCenter.default().addObserver(
|
|
567
|
+
forName: NSNotification.Name("AppleInterfaceThemeChangedNotification"),
|
|
568
|
+
object: nil,
|
|
569
|
+
queue: .main
|
|
570
|
+
) { [weak self] _ in
|
|
571
|
+
let isDark = NSApp.effectiveAppearance.bestMatch(from: [.darkAqua, .aqua]) == .darkAqua
|
|
572
|
+
self?.dispatchGlobalEvent("colorScheme:change", payload: ["colorScheme": isDark ? "dark" : "light"])
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// MARK: - Teleport Handlers
|
|
577
|
+
|
|
578
|
+
private func handleCreateTeleport(args: [Any]) {
|
|
579
|
+
guard args.count >= 3,
|
|
580
|
+
let parentId = asInt(args[0]),
|
|
581
|
+
let startId = asInt(args[1]),
|
|
582
|
+
let endId = asInt(args[2]) else {
|
|
583
|
+
NSLog("[VueNative macOS Bridge] Error: Invalid createTeleport args: \(args)")
|
|
584
|
+
return
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
guard let parentView = viewRegistry[parentId] else {
|
|
588
|
+
NSLog("[VueNative macOS Bridge] Error: Parent view not found for teleport (id: \(parentId))")
|
|
589
|
+
return
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
teleportMarkers[parentId] = (start: startId, end: endId)
|
|
593
|
+
|
|
594
|
+
let container = FlippedView()
|
|
595
|
+
container.wantsLayer = true
|
|
596
|
+
container.layer?.backgroundColor = NSColor.clear.cgColor
|
|
597
|
+
parentView.addSubview(container)
|
|
598
|
+
teleportContainers[parentId] = container
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
private func handleRemoveTeleport(args: [Any]) {
|
|
602
|
+
guard args.count >= 1,
|
|
603
|
+
let parentId = asInt(args[0]) else {
|
|
604
|
+
NSLog("[VueNative macOS Bridge] Error: Invalid removeTeleport args: \(args)")
|
|
605
|
+
return
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
teleportContainers.removeValue(forKey: parentId)?.removeFromSuperview()
|
|
609
|
+
teleportMarkers.removeValue(forKey: parentId)
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
private func handleTeleportTo(args: [Any]) {
|
|
613
|
+
guard args.count >= 2,
|
|
614
|
+
let target = args[0] as? String,
|
|
615
|
+
let nodeId = asInt(args[1]) else {
|
|
616
|
+
NSLog("[VueNative macOS Bridge] Error: Invalid teleportTo args: \(args)")
|
|
617
|
+
return
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
guard let targetView = getTeleportTarget(target) else {
|
|
621
|
+
NSLog("[VueNative macOS Bridge] Warning: Teleport target '\(target)' not found")
|
|
622
|
+
return
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
guard let childView = viewRegistry[nodeId] else {
|
|
626
|
+
NSLog("[VueNative macOS Bridge] Warning: Node view not found for teleport (id: \(nodeId))")
|
|
627
|
+
return
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
childView.removeFromSuperview()
|
|
631
|
+
childView.translatesAutoresizingMaskIntoConstraints = false
|
|
632
|
+
targetView.addSubview(childView)
|
|
633
|
+
NSLayoutConstraint.activate([
|
|
634
|
+
childView.topAnchor.constraint(equalTo: targetView.topAnchor),
|
|
635
|
+
childView.leadingAnchor.constraint(equalTo: targetView.leadingAnchor),
|
|
636
|
+
childView.trailingAnchor.constraint(equalTo: targetView.trailingAnchor),
|
|
637
|
+
childView.bottomAnchor.constraint(equalTo: targetView.bottomAnchor),
|
|
638
|
+
])
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
private func getTeleportTarget(_ target: String) -> NSView? {
|
|
642
|
+
switch target {
|
|
643
|
+
case "root":
|
|
644
|
+
return rootView
|
|
645
|
+
case "modal":
|
|
646
|
+
installModalContainerIfNeeded()
|
|
647
|
+
return modalContainer
|
|
648
|
+
default:
|
|
649
|
+
return nil
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
private func installModalContainerIfNeeded() {
|
|
654
|
+
guard let rootView = rootView, modalContainer.superview == nil else { return }
|
|
655
|
+
rootView.addSubview(modalContainer)
|
|
656
|
+
NSLayoutConstraint.activate([
|
|
657
|
+
modalContainer.topAnchor.constraint(equalTo: rootView.topAnchor),
|
|
658
|
+
modalContainer.leadingAnchor.constraint(equalTo: rootView.leadingAnchor),
|
|
659
|
+
modalContainer.trailingAnchor.constraint(equalTo: rootView.trailingAnchor),
|
|
660
|
+
modalContainer.bottomAnchor.constraint(equalTo: rootView.bottomAnchor),
|
|
661
|
+
])
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
// MARK: - Native Module Handlers
|
|
665
|
+
|
|
666
|
+
private func handleInvokeNativeModule(args: [Any]) {
|
|
667
|
+
guard args.count >= 4,
|
|
668
|
+
let moduleName = args[0] as? String,
|
|
669
|
+
let methodName = args[1] as? String,
|
|
670
|
+
let moduleArgs = args[2] as? [Any],
|
|
671
|
+
let callbackId = asInt(args[3]) else {
|
|
672
|
+
NSLog("[VueNative macOS Bridge] Error: Invalid invokeNativeModule args: \(args)")
|
|
673
|
+
return
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
NativeModuleRegistry.shared.invoke(
|
|
677
|
+
module: moduleName,
|
|
678
|
+
method: methodName,
|
|
679
|
+
args: moduleArgs
|
|
680
|
+
) { [weak self] result, error in
|
|
681
|
+
self?.resolveNativeCallback(callbackId: callbackId, result: result, error: error)
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
private func handleInvokeNativeModuleSync(args: [Any]) {
|
|
686
|
+
guard args.count >= 3,
|
|
687
|
+
let moduleName = args[0] as? String,
|
|
688
|
+
let methodName = args[1] as? String,
|
|
689
|
+
let moduleArgs = args[2] as? [Any] else {
|
|
690
|
+
NSLog("[VueNative macOS Bridge] Error: Invalid invokeNativeModuleSync args: \(args)")
|
|
691
|
+
return
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
_ = NativeModuleRegistry.shared.invokeSync(
|
|
695
|
+
module: moduleName,
|
|
696
|
+
method: methodName,
|
|
697
|
+
args: moduleArgs
|
|
698
|
+
)
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
private func resolveNativeCallback(callbackId: Int, result: Any?, error: String?) {
|
|
702
|
+
let safeResult: Any = result ?? NSNull()
|
|
703
|
+
let safeError: Any = error ?? NSNull()
|
|
704
|
+
runtime.callFunction("__VN_resolveCallback", withArguments: [callbackId, safeResult, safeError])
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
// MARK: - Layout
|
|
708
|
+
|
|
709
|
+
/// Trigger layout on the root view using the LayoutNode system.
|
|
710
|
+
private func triggerLayout() {
|
|
711
|
+
dispatchPrecondition(condition: .onQueue(.main))
|
|
712
|
+
guard let rootView = rootView else { return }
|
|
713
|
+
|
|
714
|
+
rootView.layoutSubtreeIfNeeded()
|
|
715
|
+
|
|
716
|
+
let bounds = rootView.bounds
|
|
717
|
+
NSLog("[VueNative macOS Bridge] triggerLayout() rootView bounds: %.1f x %.1f", bounds.width, bounds.height)
|
|
718
|
+
|
|
719
|
+
guard bounds.width > 0 && bounds.height > 0 else {
|
|
720
|
+
NSLog("[VueNative macOS Bridge] triggerLayout() skipped: bounds not yet resolved")
|
|
721
|
+
return
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
// Run layout using the LayoutNode system
|
|
725
|
+
if let layoutNode = rootView.layoutNode {
|
|
726
|
+
layoutNode.layout(availableWidth: bounds.width, availableHeight: bounds.height)
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
private func triggerLayoutWithRetries(remaining: Int) {
|
|
731
|
+
dispatchPrecondition(condition: .onQueue(.main))
|
|
732
|
+
guard let rootView = rootView else { return }
|
|
733
|
+
|
|
734
|
+
rootView.layoutSubtreeIfNeeded()
|
|
735
|
+
let bounds = rootView.bounds
|
|
736
|
+
|
|
737
|
+
if bounds.width > 0 && bounds.height > 0 {
|
|
738
|
+
triggerLayout()
|
|
739
|
+
} else if remaining > 0 {
|
|
740
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in
|
|
741
|
+
self?.triggerLayoutWithRetries(remaining: remaining - 1)
|
|
742
|
+
}
|
|
743
|
+
} else {
|
|
744
|
+
NSLog("[VueNative macOS Bridge] triggerLayoutWithRetries exhausted -- root view bounds still zero")
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
// MARK: - Event Dispatch to JS
|
|
749
|
+
|
|
750
|
+
/// Dispatch a native event to the JS side.
|
|
751
|
+
public func dispatchEventToJS(nodeId: Int, eventName: String, payload: Any?) {
|
|
752
|
+
let safePayload: Any
|
|
753
|
+
if let payload = payload {
|
|
754
|
+
if JSONSerialization.isValidJSONObject(["v": payload]) {
|
|
755
|
+
safePayload = payload
|
|
756
|
+
} else if let str = payload as? String {
|
|
757
|
+
safePayload = str
|
|
758
|
+
} else if let num = payload as? NSNumber {
|
|
759
|
+
safePayload = num
|
|
760
|
+
} else {
|
|
761
|
+
safePayload = NSNull()
|
|
762
|
+
}
|
|
763
|
+
} else {
|
|
764
|
+
safePayload = NSNull()
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
runtime.callFunction("__VN_handleEvent", withArguments: [nodeId, eventName, safePayload])
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
/// Dispatch a global event to the JS side (not tied to any specific node).
|
|
771
|
+
public func dispatchGlobalEvent(_ eventName: String, payload: [String: Any] = [:]) {
|
|
772
|
+
let payloadJSON: String
|
|
773
|
+
if let data = try? JSONSerialization.data(withJSONObject: payload),
|
|
774
|
+
let str = String(data: data, encoding: .utf8) {
|
|
775
|
+
payloadJSON = str
|
|
776
|
+
} else {
|
|
777
|
+
payloadJSON = "{}"
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
runtime.callFunction("__VN_handleGlobalEvent", withArguments: [eventName, payloadJSON])
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
// MARK: - View Registry Access
|
|
784
|
+
|
|
785
|
+
/// Get a view from the registry by node ID. Must be called on main thread.
|
|
786
|
+
public func view(forNodeId nodeId: Int) -> NSView? {
|
|
787
|
+
dispatchPrecondition(condition: .onQueue(.main))
|
|
788
|
+
return viewRegistry[nodeId]
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
func view(forId id: Int) -> NSView? {
|
|
792
|
+
return viewRegistry[id]
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
public var registeredViewCount: Int {
|
|
796
|
+
return viewRegistry.count
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
// MARK: - Hot Reload
|
|
800
|
+
|
|
801
|
+
/// Reload the app with a new JavaScript bundle string.
|
|
802
|
+
public func reloadWithBundle(_ bundle: String) {
|
|
803
|
+
dispatchPrecondition(condition: .onQueue(.main))
|
|
804
|
+
NSLog("[VueNative macOS Bridge] reloadWithBundle: calling __VN_teardown on old context...")
|
|
805
|
+
|
|
806
|
+
runtime.jsQueue.sync {
|
|
807
|
+
if let context = runtime.context,
|
|
808
|
+
let teardown = context.objectForKeyedSubscript("__VN_teardown"),
|
|
809
|
+
!teardown.isUndefined {
|
|
810
|
+
teardown.call(withArguments: [])
|
|
811
|
+
context.evaluateScript("void 0;")
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
NSLog("[VueNative macOS Bridge] reloadWithBundle: clearing view hierarchy...")
|
|
816
|
+
|
|
817
|
+
for (_, view) in viewRegistry {
|
|
818
|
+
view.removeFromSuperview()
|
|
819
|
+
}
|
|
820
|
+
viewRegistry.removeAll()
|
|
821
|
+
typeRegistry.removeAll()
|
|
822
|
+
eventHandlers.removeAll()
|
|
823
|
+
eventKeysPerNode.removeAll()
|
|
824
|
+
nodeParent.removeAll()
|
|
825
|
+
childrenOf.removeAll()
|
|
826
|
+
teleportMarkers.removeAll()
|
|
827
|
+
teleportContainers.removeAll()
|
|
828
|
+
modalContainer.removeFromSuperview()
|
|
829
|
+
|
|
830
|
+
runtime.reload(bundle: bundle) { [weak self] success in
|
|
831
|
+
guard let self = self else { return }
|
|
832
|
+
guard success else {
|
|
833
|
+
NSLog("[VueNative macOS Bridge] reloadWithBundle: runtime reload failed")
|
|
834
|
+
ErrorOverlayView.show(error: "Hot reload failed.\n\nThe new bundle could not be evaluated. Check the terminal for the JS error.\n\nSave the file again to retry.")
|
|
835
|
+
return
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
guard let context = self.runtime.context else { return }
|
|
839
|
+
self.registerBridgeFunctions(on: context)
|
|
840
|
+
|
|
841
|
+
NSLog("[VueNative macOS Bridge] reloadWithBundle: bridge re-registered on new context")
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
// MARK: - Cleanup
|
|
846
|
+
|
|
847
|
+
public func reset() {
|
|
848
|
+
DispatchQueue.main.async { [weak self] in
|
|
849
|
+
guard let self = self else { return }
|
|
850
|
+
for (_, view) in self.viewRegistry {
|
|
851
|
+
view.removeFromSuperview()
|
|
852
|
+
}
|
|
853
|
+
self.viewRegistry.removeAll()
|
|
854
|
+
self.typeRegistry.removeAll()
|
|
855
|
+
self.eventHandlers.removeAll()
|
|
856
|
+
self.eventKeysPerNode.removeAll()
|
|
857
|
+
self.nodeParent.removeAll()
|
|
858
|
+
self.childrenOf.removeAll()
|
|
859
|
+
self.teleportMarkers.removeAll()
|
|
860
|
+
self.teleportContainers.removeAll()
|
|
861
|
+
self.modalContainer.removeFromSuperview()
|
|
862
|
+
self.rootView = nil
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
// MARK: - Helpers
|
|
867
|
+
|
|
868
|
+
private func asInt(_ value: Any) -> Int? {
|
|
869
|
+
if let i = value as? Int { return i }
|
|
870
|
+
if let d = value as? Double {
|
|
871
|
+
guard d.isFinite, d >= Double(Int.min), d <= Double(Int.max) else { return nil }
|
|
872
|
+
return Int(d)
|
|
873
|
+
}
|
|
874
|
+
if let n = value as? NSNumber { return n.intValue }
|
|
875
|
+
return nil
|
|
876
|
+
}
|
|
877
|
+
}
|