@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,885 @@
|
|
|
1
|
+
import AppKit
|
|
2
|
+
import ObjectiveC
|
|
3
|
+
|
|
4
|
+
/// Static class that applies style properties to NSViews via LayoutNode (flexbox).
|
|
5
|
+
/// Handles both layout properties (flex, padding, margin, etc.) and
|
|
6
|
+
/// NSView visual properties (backgroundColor, borderRadius, etc.).
|
|
7
|
+
///
|
|
8
|
+
/// Supports point values, percentage values, and auto for dimensions.
|
|
9
|
+
@MainActor
|
|
10
|
+
enum StyleEngine {
|
|
11
|
+
|
|
12
|
+
// MARK: - Public API
|
|
13
|
+
|
|
14
|
+
/// Apply a batch of style properties to a view.
|
|
15
|
+
static func applyStyles(_ styles: [String: Any], to view: NSView) {
|
|
16
|
+
for (key, value) in styles {
|
|
17
|
+
apply(key: key, value: value, to: view)
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/// Apply a single style property to a view.
|
|
22
|
+
/// Routes to the appropriate handler based on the property key.
|
|
23
|
+
static func apply(key: String, value: Any?, to view: NSView) {
|
|
24
|
+
// Store internal props (prefixed with "__") as associated objects
|
|
25
|
+
// so parent factories can inspect them.
|
|
26
|
+
if key.hasPrefix("__") {
|
|
27
|
+
setInternalProp(key, value: value, on: view)
|
|
28
|
+
return
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// First try layout properties (LayoutNode / flexbox)
|
|
32
|
+
if applyLayoutProp(key: key, value: value, to: view) {
|
|
33
|
+
return
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Then try visual properties (NSView / CALayer)
|
|
37
|
+
if applyVisualProp(key: key, value: value, to: view) {
|
|
38
|
+
return
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Text properties are handled by VTextFactory directly,
|
|
42
|
+
// but we handle them here as a fallback for convenience
|
|
43
|
+
if applyTextProp(key: key, value: value, to: view) {
|
|
44
|
+
return
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// MARK: - Internal Props
|
|
49
|
+
|
|
50
|
+
private static var internalPropsKey: UInt8 = 0
|
|
51
|
+
|
|
52
|
+
/// Store an internal prop (prefixed with "__") on a view as an associated object.
|
|
53
|
+
private static func setInternalProp(_ key: String, value: Any?, on view: NSView) {
|
|
54
|
+
var props = objc_getAssociatedObject(view, &internalPropsKey) as? [String: Any] ?? [:]
|
|
55
|
+
if let value = value {
|
|
56
|
+
props[key] = value
|
|
57
|
+
} else {
|
|
58
|
+
props.removeValue(forKey: key)
|
|
59
|
+
}
|
|
60
|
+
objc_setAssociatedObject(view, &internalPropsKey, props, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/// Retrieve an internal prop from a view.
|
|
64
|
+
static func getInternalProp(_ key: String, from view: NSView) -> Any? {
|
|
65
|
+
let props = objc_getAssociatedObject(view, &internalPropsKey) as? [String: Any]
|
|
66
|
+
return props?[key]
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/// Public setter for internal props (used by factories that need to store state).
|
|
70
|
+
static func setInternalPropDirect(_ key: String, value: Any?, on view: NSView) {
|
|
71
|
+
setInternalProp(key, value: value, on: view)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// MARK: - Yoga Value Helpers
|
|
75
|
+
|
|
76
|
+
/// Convert a value to CGFloat points. Supports Double and Int.
|
|
77
|
+
/// Returns nil for non-numeric values (strings like "50%", "auto").
|
|
78
|
+
static func yogaValue(_ value: Any?) -> CGFloat? {
|
|
79
|
+
if let num = value as? Double { return CGFloat(num) }
|
|
80
|
+
if let num = value as? Int { return CGFloat(num) }
|
|
81
|
+
if let num = value as? CGFloat { return num }
|
|
82
|
+
if let str = value as? String, let num = Double(str) { return CGFloat(num) }
|
|
83
|
+
return nil
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/// Check if a value represents "auto" (for dimensions that support it).
|
|
87
|
+
static func isAuto(_ value: Any?) -> Bool {
|
|
88
|
+
if let str = value as? String, str.lowercased() == "auto" {
|
|
89
|
+
return true
|
|
90
|
+
}
|
|
91
|
+
return false
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/// Extract percentage value from strings like "50%". Returns 50.0 for "50%".
|
|
95
|
+
static func asPercent(_ value: Any?) -> CGFloat? {
|
|
96
|
+
guard let str = value as? String, str.hasSuffix("%"),
|
|
97
|
+
let num = Double(str.dropLast()) else { return nil }
|
|
98
|
+
return CGFloat(num)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// MARK: - Layout Properties (LayoutNode / Flexbox)
|
|
102
|
+
|
|
103
|
+
/// Apply a layout property via LayoutNode. Returns true if the key was recognized.
|
|
104
|
+
@discardableResult
|
|
105
|
+
private static func applyLayoutProp(key: String, value: Any?, to view: NSView) -> Bool {
|
|
106
|
+
let node = view.ensureLayoutNode()
|
|
107
|
+
|
|
108
|
+
switch key {
|
|
109
|
+
|
|
110
|
+
// MARK: Flex container properties
|
|
111
|
+
|
|
112
|
+
case "flexDirection":
|
|
113
|
+
if let str = value as? String {
|
|
114
|
+
switch str {
|
|
115
|
+
case "row": node.flexDirection = .row
|
|
116
|
+
case "row-reverse", "rowReverse": node.flexDirection = .rowReverse
|
|
117
|
+
case "column-reverse", "columnReverse": node.flexDirection = .columnReverse
|
|
118
|
+
default: node.flexDirection = .column
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
node.markDirty()
|
|
122
|
+
return true
|
|
123
|
+
|
|
124
|
+
case "justifyContent":
|
|
125
|
+
if let str = value as? String {
|
|
126
|
+
switch str {
|
|
127
|
+
case "flex-start", "flexStart", "start": node.justifyContent = .flexStart
|
|
128
|
+
case "flex-end", "flexEnd", "end": node.justifyContent = .flexEnd
|
|
129
|
+
case "center": node.justifyContent = .center
|
|
130
|
+
case "space-between", "spaceBetween": node.justifyContent = .spaceBetween
|
|
131
|
+
case "space-around", "spaceAround": node.justifyContent = .spaceAround
|
|
132
|
+
case "space-evenly", "spaceEvenly": node.justifyContent = .spaceEvenly
|
|
133
|
+
default: node.justifyContent = .flexStart
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
node.markDirty()
|
|
137
|
+
return true
|
|
138
|
+
|
|
139
|
+
case "alignItems":
|
|
140
|
+
if let str = value as? String {
|
|
141
|
+
switch str {
|
|
142
|
+
case "flex-start", "flexStart", "start": node.alignItems = .flexStart
|
|
143
|
+
case "flex-end", "flexEnd", "end": node.alignItems = .flexEnd
|
|
144
|
+
case "center": node.alignItems = .center
|
|
145
|
+
case "stretch": node.alignItems = .stretch
|
|
146
|
+
case "baseline": node.alignItems = .baseline
|
|
147
|
+
default: node.alignItems = .stretch
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
node.markDirty()
|
|
151
|
+
return true
|
|
152
|
+
|
|
153
|
+
case "alignSelf":
|
|
154
|
+
if let str = value as? String {
|
|
155
|
+
switch str {
|
|
156
|
+
case "auto": node.alignSelf = .auto
|
|
157
|
+
case "flex-start", "flexStart", "start": node.alignSelf = .flexStart
|
|
158
|
+
case "flex-end", "flexEnd", "end": node.alignSelf = .flexEnd
|
|
159
|
+
case "center": node.alignSelf = .center
|
|
160
|
+
case "stretch": node.alignSelf = .stretch
|
|
161
|
+
case "baseline": node.alignSelf = .baseline
|
|
162
|
+
default: node.alignSelf = .auto
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
node.markDirty()
|
|
166
|
+
return true
|
|
167
|
+
|
|
168
|
+
case "alignContent":
|
|
169
|
+
if let str = value as? String {
|
|
170
|
+
switch str {
|
|
171
|
+
case "flex-start", "flexStart", "start": node.alignContent = .flexStart
|
|
172
|
+
case "flex-end", "flexEnd", "end": node.alignContent = .flexEnd
|
|
173
|
+
case "center": node.alignContent = .center
|
|
174
|
+
case "stretch": node.alignContent = .stretch
|
|
175
|
+
case "space-between", "spaceBetween": node.alignContent = .flexStart // Simplified
|
|
176
|
+
case "space-around", "spaceAround": node.alignContent = .flexStart // Simplified
|
|
177
|
+
default: node.alignContent = .stretch
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
node.markDirty()
|
|
181
|
+
return true
|
|
182
|
+
|
|
183
|
+
case "flexWrap":
|
|
184
|
+
if let str = value as? String {
|
|
185
|
+
switch str {
|
|
186
|
+
case "wrap": node.flexWrap = .wrap
|
|
187
|
+
case "wrap-reverse", "wrapReverse": node.flexWrap = .wrapReverse
|
|
188
|
+
default: node.flexWrap = .noWrap
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
node.markDirty()
|
|
192
|
+
return true
|
|
193
|
+
|
|
194
|
+
// MARK: Flex item properties
|
|
195
|
+
|
|
196
|
+
case "flex":
|
|
197
|
+
if let num = yogaValue(value) {
|
|
198
|
+
// CSS "flex" shorthand: when a single number, it sets flexGrow.
|
|
199
|
+
// flex: 1 => grow(1), shrink(1), basis(0)
|
|
200
|
+
node.flexGrow = num
|
|
201
|
+
if num > 0 {
|
|
202
|
+
node.flexShrink = 1
|
|
203
|
+
node.flexBasis = .points(0)
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
node.markDirty()
|
|
207
|
+
return true
|
|
208
|
+
|
|
209
|
+
case "flexGrow":
|
|
210
|
+
if let num = yogaValue(value) {
|
|
211
|
+
node.flexGrow = num
|
|
212
|
+
}
|
|
213
|
+
node.markDirty()
|
|
214
|
+
return true
|
|
215
|
+
|
|
216
|
+
case "flexShrink":
|
|
217
|
+
if let num = yogaValue(value) {
|
|
218
|
+
node.flexShrink = num
|
|
219
|
+
}
|
|
220
|
+
node.markDirty()
|
|
221
|
+
return true
|
|
222
|
+
|
|
223
|
+
case "flexBasis":
|
|
224
|
+
if isAuto(value) {
|
|
225
|
+
node.flexBasis = .auto
|
|
226
|
+
} else if let pct = asPercent(value) {
|
|
227
|
+
node.flexBasis = .percent(pct)
|
|
228
|
+
} else if let num = yogaValue(value) {
|
|
229
|
+
node.flexBasis = .points(num)
|
|
230
|
+
}
|
|
231
|
+
node.markDirty()
|
|
232
|
+
return true
|
|
233
|
+
|
|
234
|
+
// MARK: Dimensions
|
|
235
|
+
|
|
236
|
+
case "width":
|
|
237
|
+
if isAuto(value) {
|
|
238
|
+
node.width = .auto
|
|
239
|
+
} else if let pct = asPercent(value) {
|
|
240
|
+
node.width = .percent(pct)
|
|
241
|
+
} else if let num = yogaValue(value) {
|
|
242
|
+
node.width = .points(num)
|
|
243
|
+
}
|
|
244
|
+
node.markDirty()
|
|
245
|
+
return true
|
|
246
|
+
|
|
247
|
+
case "height":
|
|
248
|
+
if isAuto(value) {
|
|
249
|
+
node.height = .auto
|
|
250
|
+
} else if let pct = asPercent(value) {
|
|
251
|
+
node.height = .percent(pct)
|
|
252
|
+
} else if let num = yogaValue(value) {
|
|
253
|
+
node.height = .points(num)
|
|
254
|
+
}
|
|
255
|
+
node.markDirty()
|
|
256
|
+
return true
|
|
257
|
+
|
|
258
|
+
case "minWidth":
|
|
259
|
+
if let pct = asPercent(value) {
|
|
260
|
+
node.minWidth = .percent(pct)
|
|
261
|
+
} else if let num = yogaValue(value) {
|
|
262
|
+
node.minWidth = .points(num)
|
|
263
|
+
}
|
|
264
|
+
node.markDirty()
|
|
265
|
+
return true
|
|
266
|
+
|
|
267
|
+
case "minHeight":
|
|
268
|
+
if let pct = asPercent(value) {
|
|
269
|
+
node.minHeight = .percent(pct)
|
|
270
|
+
} else if let num = yogaValue(value) {
|
|
271
|
+
node.minHeight = .points(num)
|
|
272
|
+
}
|
|
273
|
+
node.markDirty()
|
|
274
|
+
return true
|
|
275
|
+
|
|
276
|
+
case "maxWidth":
|
|
277
|
+
if let pct = asPercent(value) {
|
|
278
|
+
node.maxWidth = .percent(pct)
|
|
279
|
+
} else if let num = yogaValue(value) {
|
|
280
|
+
node.maxWidth = .points(num)
|
|
281
|
+
}
|
|
282
|
+
node.markDirty()
|
|
283
|
+
return true
|
|
284
|
+
|
|
285
|
+
case "maxHeight":
|
|
286
|
+
if let pct = asPercent(value) {
|
|
287
|
+
node.maxHeight = .percent(pct)
|
|
288
|
+
} else if let num = yogaValue(value) {
|
|
289
|
+
node.maxHeight = .points(num)
|
|
290
|
+
}
|
|
291
|
+
node.markDirty()
|
|
292
|
+
return true
|
|
293
|
+
|
|
294
|
+
case "aspectRatio":
|
|
295
|
+
if let num = yogaValue(value) {
|
|
296
|
+
node.aspectRatio = num
|
|
297
|
+
}
|
|
298
|
+
node.markDirty()
|
|
299
|
+
return true
|
|
300
|
+
|
|
301
|
+
// MARK: Padding
|
|
302
|
+
|
|
303
|
+
case "padding":
|
|
304
|
+
if let num = yogaValue(value) {
|
|
305
|
+
node.padding = EdgeInsets(top: num, right: num, bottom: num, left: num)
|
|
306
|
+
}
|
|
307
|
+
node.markDirty()
|
|
308
|
+
return true
|
|
309
|
+
|
|
310
|
+
case "paddingTop":
|
|
311
|
+
if let num = yogaValue(value) { node.padding.top = num }
|
|
312
|
+
node.markDirty()
|
|
313
|
+
return true
|
|
314
|
+
|
|
315
|
+
case "paddingRight":
|
|
316
|
+
if let num = yogaValue(value) { node.padding.right = num }
|
|
317
|
+
node.markDirty()
|
|
318
|
+
return true
|
|
319
|
+
|
|
320
|
+
case "paddingBottom":
|
|
321
|
+
if let num = yogaValue(value) { node.padding.bottom = num }
|
|
322
|
+
node.markDirty()
|
|
323
|
+
return true
|
|
324
|
+
|
|
325
|
+
case "paddingLeft":
|
|
326
|
+
if let num = yogaValue(value) { node.padding.left = num }
|
|
327
|
+
node.markDirty()
|
|
328
|
+
return true
|
|
329
|
+
|
|
330
|
+
case "paddingHorizontal":
|
|
331
|
+
if let num = yogaValue(value) {
|
|
332
|
+
node.padding.left = num
|
|
333
|
+
node.padding.right = num
|
|
334
|
+
}
|
|
335
|
+
node.markDirty()
|
|
336
|
+
return true
|
|
337
|
+
|
|
338
|
+
case "paddingVertical":
|
|
339
|
+
if let num = yogaValue(value) {
|
|
340
|
+
node.padding.top = num
|
|
341
|
+
node.padding.bottom = num
|
|
342
|
+
}
|
|
343
|
+
node.markDirty()
|
|
344
|
+
return true
|
|
345
|
+
|
|
346
|
+
case "paddingStart":
|
|
347
|
+
if let num = yogaValue(value) { node.padding.left = num }
|
|
348
|
+
node.markDirty()
|
|
349
|
+
return true
|
|
350
|
+
|
|
351
|
+
case "paddingEnd":
|
|
352
|
+
if let num = yogaValue(value) { node.padding.right = num }
|
|
353
|
+
node.markDirty()
|
|
354
|
+
return true
|
|
355
|
+
|
|
356
|
+
// MARK: Margin
|
|
357
|
+
|
|
358
|
+
case "margin":
|
|
359
|
+
if let num = yogaValue(value) {
|
|
360
|
+
node.margin = EdgeInsets(top: num, right: num, bottom: num, left: num)
|
|
361
|
+
}
|
|
362
|
+
node.markDirty()
|
|
363
|
+
return true
|
|
364
|
+
|
|
365
|
+
case "marginTop":
|
|
366
|
+
if let num = yogaValue(value) { node.margin.top = num }
|
|
367
|
+
node.markDirty()
|
|
368
|
+
return true
|
|
369
|
+
|
|
370
|
+
case "marginRight":
|
|
371
|
+
if let num = yogaValue(value) { node.margin.right = num }
|
|
372
|
+
node.markDirty()
|
|
373
|
+
return true
|
|
374
|
+
|
|
375
|
+
case "marginBottom":
|
|
376
|
+
if let num = yogaValue(value) { node.margin.bottom = num }
|
|
377
|
+
node.markDirty()
|
|
378
|
+
return true
|
|
379
|
+
|
|
380
|
+
case "marginLeft":
|
|
381
|
+
if let num = yogaValue(value) { node.margin.left = num }
|
|
382
|
+
node.markDirty()
|
|
383
|
+
return true
|
|
384
|
+
|
|
385
|
+
case "marginHorizontal":
|
|
386
|
+
if let num = yogaValue(value) {
|
|
387
|
+
node.margin.left = num
|
|
388
|
+
node.margin.right = num
|
|
389
|
+
}
|
|
390
|
+
node.markDirty()
|
|
391
|
+
return true
|
|
392
|
+
|
|
393
|
+
case "marginVertical":
|
|
394
|
+
if let num = yogaValue(value) {
|
|
395
|
+
node.margin.top = num
|
|
396
|
+
node.margin.bottom = num
|
|
397
|
+
}
|
|
398
|
+
node.markDirty()
|
|
399
|
+
return true
|
|
400
|
+
|
|
401
|
+
case "marginStart":
|
|
402
|
+
if let num = yogaValue(value) { node.margin.left = num }
|
|
403
|
+
node.markDirty()
|
|
404
|
+
return true
|
|
405
|
+
|
|
406
|
+
case "marginEnd":
|
|
407
|
+
if let num = yogaValue(value) { node.margin.right = num }
|
|
408
|
+
node.markDirty()
|
|
409
|
+
return true
|
|
410
|
+
|
|
411
|
+
// MARK: Gap
|
|
412
|
+
|
|
413
|
+
case "gap":
|
|
414
|
+
if let num = yogaValue(value) { node.gap = num }
|
|
415
|
+
node.markDirty()
|
|
416
|
+
return true
|
|
417
|
+
|
|
418
|
+
case "rowGap":
|
|
419
|
+
if let num = yogaValue(value) { node.rowGap = num }
|
|
420
|
+
node.markDirty()
|
|
421
|
+
return true
|
|
422
|
+
|
|
423
|
+
case "columnGap":
|
|
424
|
+
if let num = yogaValue(value) { node.columnGap = num }
|
|
425
|
+
node.markDirty()
|
|
426
|
+
return true
|
|
427
|
+
|
|
428
|
+
// MARK: Position
|
|
429
|
+
|
|
430
|
+
case "position":
|
|
431
|
+
if let str = value as? String {
|
|
432
|
+
switch str {
|
|
433
|
+
case "absolute": node.positionType = .absolute
|
|
434
|
+
case "relative": node.positionType = .relative
|
|
435
|
+
default: node.positionType = .relative
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
node.markDirty()
|
|
439
|
+
return true
|
|
440
|
+
|
|
441
|
+
case "top":
|
|
442
|
+
if let num = yogaValue(value) {
|
|
443
|
+
node.positionTop = .points(num)
|
|
444
|
+
}
|
|
445
|
+
node.markDirty()
|
|
446
|
+
return true
|
|
447
|
+
|
|
448
|
+
case "right":
|
|
449
|
+
if let num = yogaValue(value) {
|
|
450
|
+
node.positionRight = .points(num)
|
|
451
|
+
}
|
|
452
|
+
node.markDirty()
|
|
453
|
+
return true
|
|
454
|
+
|
|
455
|
+
case "bottom":
|
|
456
|
+
if let num = yogaValue(value) {
|
|
457
|
+
node.positionBottom = .points(num)
|
|
458
|
+
}
|
|
459
|
+
node.markDirty()
|
|
460
|
+
return true
|
|
461
|
+
|
|
462
|
+
case "left":
|
|
463
|
+
if let num = yogaValue(value) {
|
|
464
|
+
node.positionLeft = .points(num)
|
|
465
|
+
}
|
|
466
|
+
node.markDirty()
|
|
467
|
+
return true
|
|
468
|
+
|
|
469
|
+
case "start":
|
|
470
|
+
if let num = yogaValue(value) {
|
|
471
|
+
node.positionLeft = .points(num)
|
|
472
|
+
}
|
|
473
|
+
node.markDirty()
|
|
474
|
+
return true
|
|
475
|
+
|
|
476
|
+
case "end":
|
|
477
|
+
if let num = yogaValue(value) {
|
|
478
|
+
node.positionRight = .points(num)
|
|
479
|
+
}
|
|
480
|
+
node.markDirty()
|
|
481
|
+
return true
|
|
482
|
+
|
|
483
|
+
// MARK: Overflow
|
|
484
|
+
|
|
485
|
+
case "overflow":
|
|
486
|
+
if let str = value as? String {
|
|
487
|
+
switch str {
|
|
488
|
+
case "hidden":
|
|
489
|
+
view.layer?.masksToBounds = true
|
|
490
|
+
default:
|
|
491
|
+
view.layer?.masksToBounds = false
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
return true
|
|
495
|
+
|
|
496
|
+
// MARK: Display
|
|
497
|
+
|
|
498
|
+
case "display":
|
|
499
|
+
if let str = value as? String {
|
|
500
|
+
switch str {
|
|
501
|
+
case "none":
|
|
502
|
+
node.display = .none
|
|
503
|
+
view.isHidden = true
|
|
504
|
+
default:
|
|
505
|
+
node.display = .flex
|
|
506
|
+
view.isHidden = false
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
node.markDirty()
|
|
510
|
+
return true
|
|
511
|
+
|
|
512
|
+
// MARK: Direction (RTL/LTR)
|
|
513
|
+
|
|
514
|
+
case "direction":
|
|
515
|
+
if let str = value as? String {
|
|
516
|
+
switch str {
|
|
517
|
+
case "ltr": node.layoutDirection = .leftToRight
|
|
518
|
+
case "rtl": node.layoutDirection = .rightToLeft
|
|
519
|
+
default: break
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
node.markDirty()
|
|
523
|
+
return true
|
|
524
|
+
|
|
525
|
+
default:
|
|
526
|
+
return false
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// MARK: - Visual Properties (NSView / CALayer)
|
|
531
|
+
|
|
532
|
+
/// Apply a visual property directly on the NSView. Returns true if recognized.
|
|
533
|
+
@discardableResult
|
|
534
|
+
private static func applyVisualProp(key: String, value: Any?, to view: NSView) -> Bool {
|
|
535
|
+
switch key {
|
|
536
|
+
|
|
537
|
+
case "backgroundColor":
|
|
538
|
+
if let colorStr = value as? String {
|
|
539
|
+
view.layer?.backgroundColor = NSColor.fromHex(colorStr).cgColor
|
|
540
|
+
} else {
|
|
541
|
+
view.layer?.backgroundColor = nil
|
|
542
|
+
}
|
|
543
|
+
return true
|
|
544
|
+
|
|
545
|
+
case "opacity":
|
|
546
|
+
if let num = yogaValue(value) {
|
|
547
|
+
view.alphaValue = num
|
|
548
|
+
} else {
|
|
549
|
+
view.alphaValue = 1.0
|
|
550
|
+
}
|
|
551
|
+
return true
|
|
552
|
+
|
|
553
|
+
case "borderRadius":
|
|
554
|
+
if let num = yogaValue(value) {
|
|
555
|
+
view.layer?.cornerRadius = num
|
|
556
|
+
if num > 0 {
|
|
557
|
+
view.layer?.masksToBounds = true
|
|
558
|
+
}
|
|
559
|
+
} else {
|
|
560
|
+
view.layer?.cornerRadius = 0
|
|
561
|
+
}
|
|
562
|
+
return true
|
|
563
|
+
|
|
564
|
+
case "borderTopLeftRadius":
|
|
565
|
+
if let num = yogaValue(value) {
|
|
566
|
+
applyCornerRadius(view: view, corner: .layerMinXMinYCorner, radius: num)
|
|
567
|
+
}
|
|
568
|
+
return true
|
|
569
|
+
|
|
570
|
+
case "borderTopRightRadius":
|
|
571
|
+
if let num = yogaValue(value) {
|
|
572
|
+
applyCornerRadius(view: view, corner: .layerMaxXMinYCorner, radius: num)
|
|
573
|
+
}
|
|
574
|
+
return true
|
|
575
|
+
|
|
576
|
+
case "borderBottomLeftRadius":
|
|
577
|
+
if let num = yogaValue(value) {
|
|
578
|
+
applyCornerRadius(view: view, corner: .layerMinXMaxYCorner, radius: num)
|
|
579
|
+
}
|
|
580
|
+
return true
|
|
581
|
+
|
|
582
|
+
case "borderBottomRightRadius":
|
|
583
|
+
if let num = yogaValue(value) {
|
|
584
|
+
applyCornerRadius(view: view, corner: .layerMaxXMaxYCorner, radius: num)
|
|
585
|
+
}
|
|
586
|
+
return true
|
|
587
|
+
|
|
588
|
+
case "borderWidth":
|
|
589
|
+
if let num = yogaValue(value) {
|
|
590
|
+
view.layer?.borderWidth = num
|
|
591
|
+
} else {
|
|
592
|
+
view.layer?.borderWidth = 0
|
|
593
|
+
}
|
|
594
|
+
return true
|
|
595
|
+
|
|
596
|
+
case "borderColor":
|
|
597
|
+
if let colorStr = value as? String {
|
|
598
|
+
view.layer?.borderColor = NSColor.fromHex(colorStr).cgColor
|
|
599
|
+
} else {
|
|
600
|
+
view.layer?.borderColor = nil
|
|
601
|
+
}
|
|
602
|
+
return true
|
|
603
|
+
|
|
604
|
+
case "shadowColor":
|
|
605
|
+
if let colorStr = value as? String {
|
|
606
|
+
view.layer?.shadowColor = NSColor.fromHex(colorStr).cgColor
|
|
607
|
+
}
|
|
608
|
+
return true
|
|
609
|
+
|
|
610
|
+
case "shadowOpacity":
|
|
611
|
+
if let num = yogaValue(value) {
|
|
612
|
+
view.layer?.shadowOpacity = Float(num)
|
|
613
|
+
}
|
|
614
|
+
return true
|
|
615
|
+
|
|
616
|
+
case "shadowRadius":
|
|
617
|
+
if let num = yogaValue(value) {
|
|
618
|
+
view.layer?.shadowRadius = num
|
|
619
|
+
}
|
|
620
|
+
return true
|
|
621
|
+
|
|
622
|
+
case "shadowOffsetX":
|
|
623
|
+
if let num = yogaValue(value) {
|
|
624
|
+
view.layer?.shadowOffset = CGSize(
|
|
625
|
+
width: num,
|
|
626
|
+
height: view.layer?.shadowOffset.height ?? 0
|
|
627
|
+
)
|
|
628
|
+
}
|
|
629
|
+
return true
|
|
630
|
+
|
|
631
|
+
case "shadowOffsetY":
|
|
632
|
+
if let num = yogaValue(value) {
|
|
633
|
+
view.layer?.shadowOffset = CGSize(
|
|
634
|
+
width: view.layer?.shadowOffset.width ?? 0,
|
|
635
|
+
height: num
|
|
636
|
+
)
|
|
637
|
+
}
|
|
638
|
+
return true
|
|
639
|
+
|
|
640
|
+
case "shadowOffset":
|
|
641
|
+
if let dict = value as? [String: Any] {
|
|
642
|
+
let w = (dict["width"] as? Double).map { CGFloat($0) } ?? (view.layer?.shadowOffset.width ?? 0)
|
|
643
|
+
let h = (dict["height"] as? Double).map { CGFloat($0) } ?? (view.layer?.shadowOffset.height ?? 0)
|
|
644
|
+
view.layer?.shadowOffset = CGSize(width: w, height: h)
|
|
645
|
+
}
|
|
646
|
+
return true
|
|
647
|
+
|
|
648
|
+
case "transform":
|
|
649
|
+
if let transforms = value as? [[String: Any]] {
|
|
650
|
+
var result = CGAffineTransform.identity
|
|
651
|
+
for dict in transforms {
|
|
652
|
+
if let rotateStr = dict["rotate"] as? String {
|
|
653
|
+
let angle = parseAngle(rotateStr)
|
|
654
|
+
result = result.rotated(by: angle)
|
|
655
|
+
}
|
|
656
|
+
if let scale = dict["scale"] as? Double {
|
|
657
|
+
result = result.scaledBy(x: CGFloat(scale), y: CGFloat(scale))
|
|
658
|
+
}
|
|
659
|
+
if let scaleX = dict["scaleX"] as? Double {
|
|
660
|
+
result = result.scaledBy(x: CGFloat(scaleX), y: 1)
|
|
661
|
+
}
|
|
662
|
+
if let scaleY = dict["scaleY"] as? Double {
|
|
663
|
+
result = result.scaledBy(x: 1, y: CGFloat(scaleY))
|
|
664
|
+
}
|
|
665
|
+
if let tx = dict["translateX"] as? Double {
|
|
666
|
+
result = result.translatedBy(x: CGFloat(tx), y: 0)
|
|
667
|
+
}
|
|
668
|
+
if let ty = dict["translateY"] as? Double {
|
|
669
|
+
result = result.translatedBy(x: 0, y: CGFloat(ty))
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
view.layer?.setAffineTransform(result)
|
|
673
|
+
} else {
|
|
674
|
+
view.layer?.setAffineTransform(.identity)
|
|
675
|
+
}
|
|
676
|
+
return true
|
|
677
|
+
|
|
678
|
+
case "hidden":
|
|
679
|
+
view.isHidden = (value as? Bool) ?? false
|
|
680
|
+
return true
|
|
681
|
+
|
|
682
|
+
case "isHidden":
|
|
683
|
+
if let hidden = value as? Bool {
|
|
684
|
+
view.isHidden = hidden
|
|
685
|
+
} else if let hidden = value as? Int {
|
|
686
|
+
view.isHidden = hidden != 0
|
|
687
|
+
}
|
|
688
|
+
return true
|
|
689
|
+
|
|
690
|
+
case "zIndex":
|
|
691
|
+
if let num = yogaValue(value) {
|
|
692
|
+
view.layer?.zPosition = num
|
|
693
|
+
}
|
|
694
|
+
return true
|
|
695
|
+
|
|
696
|
+
// MARK: Accessibility (NSAccessibility)
|
|
697
|
+
|
|
698
|
+
case "accessibilityLabel":
|
|
699
|
+
view.setAccessibilityLabel(value as? String)
|
|
700
|
+
return true
|
|
701
|
+
|
|
702
|
+
case "accessibilityHint":
|
|
703
|
+
// macOS uses "help" instead of "hint"
|
|
704
|
+
view.setAccessibilityHelp(value as? String)
|
|
705
|
+
return true
|
|
706
|
+
|
|
707
|
+
case "accessibilityValue":
|
|
708
|
+
view.setAccessibilityValue(value as? String)
|
|
709
|
+
return true
|
|
710
|
+
|
|
711
|
+
case "accessibilityRole":
|
|
712
|
+
if let role = value as? String {
|
|
713
|
+
switch role {
|
|
714
|
+
case "button": view.setAccessibilityRole(.button)
|
|
715
|
+
case "link": view.setAccessibilityRole(.link)
|
|
716
|
+
case "header": view.setAccessibilityRole(.staticText)
|
|
717
|
+
case "image": view.setAccessibilityRole(.image)
|
|
718
|
+
case "text": view.setAccessibilityRole(.staticText)
|
|
719
|
+
case "search": view.setAccessibilityRole(.textField)
|
|
720
|
+
case "tab": view.setAccessibilityRole(.tabGroup)
|
|
721
|
+
case "none": view.setAccessibilityRole(.unknown)
|
|
722
|
+
default: break
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
return true
|
|
726
|
+
|
|
727
|
+
case "accessible":
|
|
728
|
+
let acc = (value as? Bool) ?? (value as? NSNumber)?.boolValue ?? false
|
|
729
|
+
view.setAccessibilityElement(acc)
|
|
730
|
+
return true
|
|
731
|
+
|
|
732
|
+
default:
|
|
733
|
+
return false
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
// MARK: - Text Properties
|
|
738
|
+
|
|
739
|
+
/// Apply text-specific properties. Returns true if recognized.
|
|
740
|
+
/// This is a fallback — VTextFactory handles these directly when it can.
|
|
741
|
+
@discardableResult
|
|
742
|
+
private static func applyTextProp(key: String, value: Any?, to view: NSView) -> Bool {
|
|
743
|
+
guard let label = view as? NSTextField, !label.isEditable else { return false }
|
|
744
|
+
|
|
745
|
+
switch key {
|
|
746
|
+
case "fontSize":
|
|
747
|
+
if let num = yogaValue(value) {
|
|
748
|
+
let currentFont = label.font ?? NSFont.systemFont(ofSize: 13)
|
|
749
|
+
label.font = NSFont(descriptor: currentFont.fontDescriptor, size: num)
|
|
750
|
+
label.layoutNode?.markDirty()
|
|
751
|
+
}
|
|
752
|
+
return true
|
|
753
|
+
|
|
754
|
+
case "fontWeight":
|
|
755
|
+
if let str = value as? String {
|
|
756
|
+
let weight = VTextFactory.fontWeightMap[str] ?? .regular
|
|
757
|
+
let currentFont = label.font ?? NSFont.systemFont(ofSize: 13)
|
|
758
|
+
label.font = NSFont.systemFont(ofSize: currentFont.pointSize, weight: weight)
|
|
759
|
+
label.layoutNode?.markDirty()
|
|
760
|
+
}
|
|
761
|
+
return true
|
|
762
|
+
|
|
763
|
+
case "color":
|
|
764
|
+
if let colorStr = value as? String {
|
|
765
|
+
label.textColor = NSColor.fromHex(colorStr)
|
|
766
|
+
}
|
|
767
|
+
return true
|
|
768
|
+
|
|
769
|
+
case "textAlign":
|
|
770
|
+
if let alignStr = value as? String {
|
|
771
|
+
label.alignment = VTextFactory.textAlignMap[alignStr] ?? .natural
|
|
772
|
+
}
|
|
773
|
+
return true
|
|
774
|
+
|
|
775
|
+
case "fontStyle":
|
|
776
|
+
let currentFont = label.font ?? NSFont.systemFont(ofSize: 13)
|
|
777
|
+
if let str = value as? String, str == "italic" {
|
|
778
|
+
let descriptor = currentFont.fontDescriptor.withSymbolicTraits(.italic)
|
|
779
|
+
label.font = NSFont(descriptor: descriptor, size: currentFont.pointSize)
|
|
780
|
+
} else {
|
|
781
|
+
var traits = currentFont.fontDescriptor.symbolicTraits
|
|
782
|
+
traits.remove(.italic)
|
|
783
|
+
let descriptor = currentFont.fontDescriptor.withSymbolicTraits(traits)
|
|
784
|
+
label.font = NSFont(descriptor: descriptor, size: currentFont.pointSize)
|
|
785
|
+
}
|
|
786
|
+
label.layoutNode?.markDirty()
|
|
787
|
+
return true
|
|
788
|
+
|
|
789
|
+
case "lineHeight":
|
|
790
|
+
if let num = yogaValue(value) {
|
|
791
|
+
let paragraphStyle = NSMutableParagraphStyle()
|
|
792
|
+
paragraphStyle.minimumLineHeight = num
|
|
793
|
+
paragraphStyle.maximumLineHeight = num
|
|
794
|
+
paragraphStyle.alignment = label.alignment
|
|
795
|
+
let text = label.stringValue
|
|
796
|
+
let font = label.font ?? NSFont.systemFont(ofSize: 13)
|
|
797
|
+
let attrs: [NSAttributedString.Key: Any] = [
|
|
798
|
+
.paragraphStyle: paragraphStyle,
|
|
799
|
+
.font: font
|
|
800
|
+
]
|
|
801
|
+
label.attributedStringValue = NSAttributedString(string: text, attributes: attrs)
|
|
802
|
+
label.layoutNode?.markDirty()
|
|
803
|
+
}
|
|
804
|
+
return true
|
|
805
|
+
|
|
806
|
+
case "letterSpacing":
|
|
807
|
+
if let num = yogaValue(value) {
|
|
808
|
+
let text = label.stringValue
|
|
809
|
+
let font = label.font ?? NSFont.systemFont(ofSize: 13)
|
|
810
|
+
let attrs: [NSAttributedString.Key: Any] = [
|
|
811
|
+
.kern: num,
|
|
812
|
+
.font: font
|
|
813
|
+
]
|
|
814
|
+
label.attributedStringValue = NSAttributedString(string: text, attributes: attrs)
|
|
815
|
+
label.layoutNode?.markDirty()
|
|
816
|
+
}
|
|
817
|
+
return true
|
|
818
|
+
|
|
819
|
+
case "textDecorationLine":
|
|
820
|
+
if let str = value as? String {
|
|
821
|
+
let text = label.stringValue
|
|
822
|
+
let font = label.font ?? NSFont.systemFont(ofSize: 13)
|
|
823
|
+
var attrs: [NSAttributedString.Key: Any] = [.font: font]
|
|
824
|
+
switch str {
|
|
825
|
+
case "underline":
|
|
826
|
+
attrs[.underlineStyle] = NSUnderlineStyle.single.rawValue
|
|
827
|
+
case "line-through", "lineThrough":
|
|
828
|
+
attrs[.strikethroughStyle] = NSUnderlineStyle.single.rawValue
|
|
829
|
+
case "underline line-through":
|
|
830
|
+
attrs[.underlineStyle] = NSUnderlineStyle.single.rawValue
|
|
831
|
+
attrs[.strikethroughStyle] = NSUnderlineStyle.single.rawValue
|
|
832
|
+
default:
|
|
833
|
+
break
|
|
834
|
+
}
|
|
835
|
+
label.attributedStringValue = NSAttributedString(string: text, attributes: attrs)
|
|
836
|
+
label.layoutNode?.markDirty()
|
|
837
|
+
}
|
|
838
|
+
return true
|
|
839
|
+
|
|
840
|
+
case "textTransform":
|
|
841
|
+
if let str = value as? String {
|
|
842
|
+
let text = label.stringValue
|
|
843
|
+
switch str {
|
|
844
|
+
case "uppercase": label.stringValue = text.uppercased()
|
|
845
|
+
case "lowercase": label.stringValue = text.lowercased()
|
|
846
|
+
case "capitalize": label.stringValue = text.capitalized
|
|
847
|
+
default: break
|
|
848
|
+
}
|
|
849
|
+
label.layoutNode?.markDirty()
|
|
850
|
+
}
|
|
851
|
+
return true
|
|
852
|
+
|
|
853
|
+
default:
|
|
854
|
+
return false
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
// MARK: - Corner radius helpers
|
|
859
|
+
|
|
860
|
+
/// Apply a corner radius to a specific corner of the view.
|
|
861
|
+
private static func applyCornerRadius(view: NSView, corner: CACornerMask, radius: CGFloat) {
|
|
862
|
+
view.layer?.masksToBounds = true
|
|
863
|
+
view.layer?.maskedCorners.insert(corner)
|
|
864
|
+
view.layer?.cornerRadius = max(view.layer?.cornerRadius ?? 0, radius)
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
// MARK: - Helpers
|
|
868
|
+
|
|
869
|
+
/// Parse an angle string into radians.
|
|
870
|
+
/// Supports "45deg" (degrees) and "1.5rad" (radians).
|
|
871
|
+
private static func parseAngle(_ str: String) -> CGFloat {
|
|
872
|
+
let s = str.trimmingCharacters(in: .whitespaces).lowercased()
|
|
873
|
+
if s.hasSuffix("deg"), let num = Double(s.dropLast(3)) {
|
|
874
|
+
return CGFloat(num * .pi / 180)
|
|
875
|
+
}
|
|
876
|
+
if s.hasSuffix("rad"), let num = Double(s.dropLast(3)) {
|
|
877
|
+
return CGFloat(num)
|
|
878
|
+
}
|
|
879
|
+
// Fallback: try to parse as raw number (radians)
|
|
880
|
+
if let num = Double(s) {
|
|
881
|
+
return CGFloat(num)
|
|
882
|
+
}
|
|
883
|
+
return 0
|
|
884
|
+
}
|
|
885
|
+
}
|