pythonnative 0.35.0__py3-none-any.whl → 0.37.0__py3-none-any.whl
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.
- pythonnative/__init__.py +6 -4
- pythonnative/_ios_log.py +1 -1
- pythonnative/alerts.py +34 -88
- pythonnative/bootstrap.py +90 -0
- pythonnative/bridge/__init__.py +306 -0
- pythonnative/bridge/android.py +78 -0
- pythonnative/bridge/codec.py +164 -0
- pythonnative/bridge/fake.py +236 -0
- pythonnative/bridge/ios.py +138 -0
- pythonnative/cli/pn.py +24 -6
- pythonnative/components/layout.py +65 -3
- pythonnative/gestures.py +13 -10
- pythonnative/hooks.py +41 -26
- pythonnative/hosts/__init__.py +13 -36
- pythonnative/hosts/native.py +348 -0
- pythonnative/images.py +6 -5
- pythonnative/mutations.py +21 -0
- pythonnative/native_modules/__init__.py +30 -29
- pythonnative/native_modules/app_state.py +10 -6
- pythonnative/native_modules/battery.py +20 -84
- pythonnative/native_modules/biometrics.py +11 -124
- pythonnative/native_modules/camera.py +15 -291
- pythonnative/native_modules/clipboard.py +14 -109
- pythonnative/native_modules/desktop.py +322 -0
- pythonnative/native_modules/file_system.py +15 -21
- pythonnative/native_modules/haptics.py +19 -129
- pythonnative/native_modules/linking.py +24 -137
- pythonnative/native_modules/location.py +28 -180
- pythonnative/native_modules/net_info.py +25 -135
- pythonnative/native_modules/notifications.py +36 -257
- pythonnative/native_modules/permissions.py +20 -184
- pythonnative/native_modules/registry.py +513 -0
- pythonnative/native_modules/secure_store.py +18 -155
- pythonnative/native_modules/share.py +8 -113
- pythonnative/native_views/__init__.py +67 -86
- pythonnative/native_views/bridge_backend.py +264 -0
- pythonnative/platform.py +19 -15
- pythonnative/platform_metrics.py +3 -3
- pythonnative/project/android.py +0 -3
- pythonnative/project/builder.py +38 -16
- pythonnative/project/config.py +12 -2
- pythonnative/project/plugins.py +366 -0
- pythonnative/py.typed +0 -0
- pythonnative/runtime.py +35 -203
- pythonnative/sdk/__init__.py +48 -33
- pythonnative/sdk/_components.py +57 -76
- pythonnative/storage.py +18 -243
- pythonnative/templates/android_template/app/build.gradle +6 -9
- pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/MainActivity.kt +34 -166
- pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/ScreenFragment.kt +13 -151
- pythonnative/templates/android_template/pythonnative/build.gradle +48 -0
- pythonnative/templates/android_template/pythonnative/src/main/AndroidManifest.xml +11 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/PNBridge.kt +214 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/PythonHost.kt +20 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/animation/AnimationSpecs.kt +141 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/animation/PNAnimator.kt +249 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/bridge/JsonUtil.kt +175 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/bridge/MainThread.kt +32 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/bridge/PNLog.kt +33 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/bridge/PNRegistry.kt +82 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/bridge/PNTransaction.kt +80 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/bridge/TransactionApplier.kt +115 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/bridge/ViewRegistry.kt +74 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/BuiltinComponents.kt +35 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ButtonManager.kt +33 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ComponentManager.kt +213 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ContainerManagers.kt +69 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ControlManagers.kt +186 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/DatePickerManager.kt +123 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ImageLoader.kt +141 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ImageManager.kt +156 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ModalManager.kt +134 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/PNColor.kt +201 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/PickerManager.kt +87 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/PortalManager.kt +55 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/PressableManager.kt +122 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ScrollViewManager.kt +214 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/SegmentedControlManager.kt +113 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/StatusBarManager.kt +52 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/TabBarManager.kt +131 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/TextInputManager.kt +245 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/TextManager.kt +233 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ViewStyler.kt +355 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/VirtualListManager.kt +179 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/WebViewManager.kt +149 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/gestures/GestureArbiter.kt +233 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/gestures/GestureCoordinator.kt +127 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/gestures/GestureRecognizers.kt +527 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/BuiltinModules.kt +73 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/DeviceModules.kt +316 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/HostModule.kt +97 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/MediaModules.kt +200 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/NativeModule.kt +135 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/NotificationsModule.kt +100 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/PermissionsModule.kt +88 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/StorageModules.kt +89 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/SystemModules.kt +178 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/plugins/GeneratedPlugins.kt +16 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/screens/Navigator.kt +116 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/screens/PNScreenFragment.kt +223 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/screens/ScreenRegistry.kt +80 -0
- pythonnative/templates/android_template/{app/src/main/java/com/pythonnative/android_template → pythonnative/src/main/java/com/pythonnative/runtime/views}/PNAccessibilityDelegate.kt +5 -4
- pythonnative/templates/android_template/{app/src/main/java/com/pythonnative/android_template → pythonnative/src/main/java/com/pythonnative/runtime/views}/PNBorderDrawable.kt +12 -3
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/views/PNEditText.kt +18 -0
- pythonnative/templates/android_template/{app/src/main/java/com/pythonnative/android_template → pythonnative/src/main/java/com/pythonnative/runtime/views}/PNFrameLayout.kt +9 -10
- pythonnative/templates/android_template/{app/src/main/java/com/pythonnative/android_template → pythonnative/src/main/java/com/pythonnative/runtime/views}/PNVirtualListView.java +9 -12
- pythonnative/templates/android_template/pythonnative/src/test/java/com/pythonnative/runtime/AnimationSpecsTest.kt +75 -0
- pythonnative/templates/android_template/pythonnative/src/test/java/com/pythonnative/runtime/GestureArbiterTest.kt +197 -0
- pythonnative/templates/android_template/pythonnative/src/test/java/com/pythonnative/runtime/PNColorTest.kt +47 -0
- pythonnative/templates/android_template/pythonnative/src/test/java/com/pythonnative/runtime/PNTransactionTest.kt +78 -0
- pythonnative/templates/android_template/pythonnative/src/test/java/com/pythonnative/runtime/PromiseTest.kt +75 -0
- pythonnative/templates/android_template/settings.gradle +1 -0
- pythonnative/templates/ios_template/PythonNativeKit/Package.swift +21 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Animation/PNAnimator.swift +301 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Bridge/PNBridge.swift +191 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Bridge/PNRegistry.swift +140 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Bridge/PNTransaction.swift +165 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Bridge/PNViewRegistry.swift +53 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNButtonManagers.swift +206 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNColor.swift +173 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNComponentManager.swift +183 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNContainerView.swift +56 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNControlManagers.swift +281 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNImageManager.swift +221 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNOverlayManagers.swift +243 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNPressableManager.swift +114 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNScrollViewManager.swift +197 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNTabBarManager.swift +106 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNTextInputManager.swift +289 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNTextManager.swift +230 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNTransform.swift +82 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNViewManager.swift +86 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNViewState.swift +87 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNViewStyler.swift +340 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNVirtualListManager.swift +257 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNWebViewManager.swift +124 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Gestures/PNGestureCoordinator.swift +260 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Modules/AlertModule.swift +92 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Modules/HostModule.swift +158 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Modules/LifecycleModules.swift +174 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Modules/MediaModules.swift +177 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Modules/PNNativeModule.swift +125 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Modules/PermissionModules.swift +251 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Modules/SystemModules.swift +251 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Plugins/PNPluginRegistration.swift +8 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Screens/PNScreenRegistry.swift +46 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Screens/PNViewController.swift +251 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Support/PNCompat.swift +46 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Support/PNJSON.swift +173 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Support/PNLog.swift +47 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Support/PNWindow.swift +51 -0
- pythonnative/templates/ios_template/PythonNativeKit/Tests/PythonNativeKitTests/PNColorTests.swift +50 -0
- pythonnative/templates/ios_template/PythonNativeKit/Tests/PythonNativeKitTests/PNGestureTests.swift +101 -0
- pythonnative/templates/ios_template/PythonNativeKit/Tests/PythonNativeKitTests/PNManagerTests.swift +124 -0
- pythonnative/templates/ios_template/PythonNativeKit/Tests/PythonNativeKitTests/PNModuleTests.swift +98 -0
- pythonnative/templates/ios_template/PythonNativeKit/Tests/PythonNativeKitTests/PNTransactionTests.swift +91 -0
- pythonnative/templates/ios_template/ios_template/AppDelegate.swift +8 -50
- pythonnative/templates/ios_template/ios_template/Info.plist +4 -4
- pythonnative/templates/ios_template/ios_template/PythonRuntime.swift +38 -46
- pythonnative/templates/ios_template/ios_template/SceneDelegate.swift +11 -16
- pythonnative/templates/ios_template/ios_template/ViewController.swift +12 -142
- pythonnative/templates/ios_template/ios_template.xcodeproj/project.pbxproj +47 -1
- pythonnative/utils.py +4 -82
- pythonnative/virtual_rows.py +3 -3
- {pythonnative-0.35.0.dist-info → pythonnative-0.37.0.dist-info}/METADATA +3 -5
- pythonnative-0.37.0.dist-info/RECORD +263 -0
- pythonnative/hosts/android.py +0 -269
- pythonnative/hosts/ios.py +0 -404
- pythonnative/native_views/android.py +0 -3702
- pythonnative/native_views/ios.py +0 -5253
- pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/Navigator.kt +0 -43
- pythonnative-0.35.0.dist-info/RECORD +0 -155
- {pythonnative-0.35.0.dist-info → pythonnative-0.37.0.dist-info}/WHEEL +0 -0
- {pythonnative-0.35.0.dist-info → pythonnative-0.37.0.dist-info}/entry_points.txt +0 -0
- {pythonnative-0.35.0.dist-info → pythonnative-0.37.0.dist-info}/licenses/LICENSE +0 -0
- {pythonnative-0.35.0.dist-info → pythonnative-0.37.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import UIKit
|
|
2
|
+
|
|
3
|
+
/// Decodes and applies reconciler transactions.
|
|
4
|
+
///
|
|
5
|
+
/// A transaction is a JSON array of ops:
|
|
6
|
+
///
|
|
7
|
+
/// - `["c", tag, "Type", {props}]` create a view
|
|
8
|
+
/// - `["u", tag, {changed}]` apply changed props (`null` removes a prop)
|
|
9
|
+
/// - `["i", parent, child, index]` ensure `child` is at `index` under `parent`
|
|
10
|
+
/// - `["d", tag]` destroy the view
|
|
11
|
+
/// - `["f", tag, x, y, w, h]` set the frame in points
|
|
12
|
+
///
|
|
13
|
+
/// Ops are applied strictly in order; a failing op is logged and skipped
|
|
14
|
+
/// so one bad prop can't desync the rest of the tree.
|
|
15
|
+
public enum PNTransaction {
|
|
16
|
+
/// A decoded op.
|
|
17
|
+
public enum Op: Equatable {
|
|
18
|
+
case create(tag: Int64, type: String, props: [String: Any])
|
|
19
|
+
case update(tag: Int64, changed: [String: Any])
|
|
20
|
+
case insert(parent: Int64, child: Int64, index: Int)
|
|
21
|
+
case destroy(tag: Int64)
|
|
22
|
+
case frame(tag: Int64, x: Double, y: Double, w: Double, h: Double)
|
|
23
|
+
|
|
24
|
+
public static func == (lhs: Op, rhs: Op) -> Bool {
|
|
25
|
+
switch (lhs, rhs) {
|
|
26
|
+
case let (.create(t1, ty1, p1), .create(t2, ty2, p2)):
|
|
27
|
+
return t1 == t2 && ty1 == ty2 && NSDictionary(dictionary: p1).isEqual(to: p2)
|
|
28
|
+
case let (.update(t1, p1), .update(t2, p2)):
|
|
29
|
+
return t1 == t2 && NSDictionary(dictionary: p1).isEqual(to: p2)
|
|
30
|
+
case let (.insert(p1, c1, i1), .insert(p2, c2, i2)):
|
|
31
|
+
return p1 == p2 && c1 == c2 && i1 == i2
|
|
32
|
+
case let (.destroy(t1), .destroy(t2)):
|
|
33
|
+
return t1 == t2
|
|
34
|
+
case let (.frame(t1, x1, y1, w1, h1), .frame(t2, x2, y2, w2, h2)):
|
|
35
|
+
return t1 == t2 && x1 == x2 && y1 == y2 && w1 == w2 && h1 == h2
|
|
36
|
+
default:
|
|
37
|
+
return false
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/// Errors raised while decoding a single op.
|
|
43
|
+
public enum DecodeError: Error, Equatable {
|
|
44
|
+
case notAnArray
|
|
45
|
+
case malformedOp(index: Int)
|
|
46
|
+
case unknownOpcode(String)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// MARK: - Decoding
|
|
50
|
+
|
|
51
|
+
/// Decode a transaction document into ops. Malformed ops are skipped
|
|
52
|
+
/// (and logged) rather than failing the whole batch.
|
|
53
|
+
public static func decode(_ json: String) throws -> [Op] {
|
|
54
|
+
guard let root = PNJSON.decode(json) as? [Any] else { throw DecodeError.notAnArray }
|
|
55
|
+
var ops: [Op] = []
|
|
56
|
+
ops.reserveCapacity(root.count)
|
|
57
|
+
for (index, raw) in root.enumerated() {
|
|
58
|
+
do {
|
|
59
|
+
ops.append(try decodeOp(raw, index: index))
|
|
60
|
+
} catch {
|
|
61
|
+
PNLog.rateLimited(PNLog.bridge, key: "decode-op", "skipping malformed op #\(index): \(error)")
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return ops
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
static func decodeOp(_ raw: Any, index: Int) throws -> Op {
|
|
68
|
+
guard let parts = raw as? [Any], let code = parts.first as? String else {
|
|
69
|
+
throw DecodeError.malformedOp(index: index)
|
|
70
|
+
}
|
|
71
|
+
func tag(_ i: Int) throws -> Int64 {
|
|
72
|
+
guard i < parts.count, let value = PNProps.double(parts[i]), value.isFinite else {
|
|
73
|
+
throw DecodeError.malformedOp(index: index)
|
|
74
|
+
}
|
|
75
|
+
return Int64(value)
|
|
76
|
+
}
|
|
77
|
+
func number(_ i: Int) throws -> Double {
|
|
78
|
+
guard i < parts.count, let value = PNProps.double(parts[i]) else {
|
|
79
|
+
throw DecodeError.malformedOp(index: index)
|
|
80
|
+
}
|
|
81
|
+
return value
|
|
82
|
+
}
|
|
83
|
+
switch code {
|
|
84
|
+
case "c":
|
|
85
|
+
guard parts.count >= 4, let type = parts[2] as? String else { throw DecodeError.malformedOp(index: index) }
|
|
86
|
+
let props = (PNJSON.resolveInfinity(parts[3]) as? [String: Any]) ?? [:]
|
|
87
|
+
return .create(tag: try tag(1), type: type, props: props)
|
|
88
|
+
case "u":
|
|
89
|
+
guard parts.count >= 3 else { throw DecodeError.malformedOp(index: index) }
|
|
90
|
+
let changed = (PNJSON.resolveInfinity(parts[2]) as? [String: Any]) ?? [:]
|
|
91
|
+
return .update(tag: try tag(1), changed: changed)
|
|
92
|
+
case "i":
|
|
93
|
+
guard parts.count >= 4 else { throw DecodeError.malformedOp(index: index) }
|
|
94
|
+
return .insert(parent: try tag(1), child: try tag(2), index: Int(try number(3)))
|
|
95
|
+
case "d":
|
|
96
|
+
return .destroy(tag: try tag(1))
|
|
97
|
+
case "f":
|
|
98
|
+
guard parts.count >= 6 else { throw DecodeError.malformedOp(index: index) }
|
|
99
|
+
return .frame(tag: try tag(1), x: try number(2), y: try number(3), w: try number(4), h: try number(5))
|
|
100
|
+
default:
|
|
101
|
+
throw DecodeError.unknownOpcode(code)
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// MARK: - Applying
|
|
106
|
+
|
|
107
|
+
/// Decode and apply a transaction document.
|
|
108
|
+
public static func apply(_ json: String) {
|
|
109
|
+
let ops: [Op]
|
|
110
|
+
do {
|
|
111
|
+
ops = try decode(json)
|
|
112
|
+
} catch {
|
|
113
|
+
PNLog.bridge.error("transaction rejected: \(String(describing: error))")
|
|
114
|
+
return
|
|
115
|
+
}
|
|
116
|
+
apply(ops)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/// Apply already-decoded ops in order with per-op error isolation.
|
|
120
|
+
public static func apply(_ ops: [Op]) {
|
|
121
|
+
let registry = PNViewRegistry.shared
|
|
122
|
+
for op in ops {
|
|
123
|
+
switch op {
|
|
124
|
+
case let .create(tag, type, props):
|
|
125
|
+
if registry.resolve(tag) != nil {
|
|
126
|
+
PNLog.rateLimited(PNLog.bridge, key: "dup-create", "create for tag \(tag) ignored: already exists")
|
|
127
|
+
continue
|
|
128
|
+
}
|
|
129
|
+
let manager = PNRegistry.shared.manager(for: type)
|
|
130
|
+
let view = manager.createView(tag: tag, props: props)
|
|
131
|
+
registry.register(PNViewRecord(tag: tag, typeName: type, view: view, manager: manager))
|
|
132
|
+
manager.didCreate(view: view, tag: tag, props: props)
|
|
133
|
+
|
|
134
|
+
case let .update(tag, changed):
|
|
135
|
+
guard let record = registry.resolve(tag) else {
|
|
136
|
+
PNLog.rateLimited(PNLog.bridge, key: "missing-update", "update for unknown tag \(tag)")
|
|
137
|
+
continue
|
|
138
|
+
}
|
|
139
|
+
record.manager.update(view: record.view, changed: changed)
|
|
140
|
+
|
|
141
|
+
case let .insert(parent, child, index):
|
|
142
|
+
guard let parentRecord = registry.resolve(parent), let childRecord = registry.resolve(child) else {
|
|
143
|
+
PNLog.rateLimited(PNLog.bridge, key: "missing-insert", "insert \(child) into \(parent): unknown tag")
|
|
144
|
+
continue
|
|
145
|
+
}
|
|
146
|
+
parentRecord.manager.insertChild(parent: parentRecord.view, child: childRecord.view, index: index)
|
|
147
|
+
|
|
148
|
+
case let .destroy(tag):
|
|
149
|
+
guard let record = registry.unregister(tag) else { continue }
|
|
150
|
+
if let parent = record.view.superview,
|
|
151
|
+
let parentState = PNViewState.existing(for: parent),
|
|
152
|
+
let parentRecord = registry.resolve(parentState.tag)
|
|
153
|
+
{
|
|
154
|
+
parentRecord.manager.removeChild(parent: parent, child: record.view)
|
|
155
|
+
}
|
|
156
|
+
record.manager.destroy(view: record.view)
|
|
157
|
+
PNViewState.detach(record.view)
|
|
158
|
+
|
|
159
|
+
case let .frame(tag, x, y, w, h):
|
|
160
|
+
guard let record = registry.resolve(tag) else { continue }
|
|
161
|
+
record.manager.setFrame(view: record.view, x: x, y: y, w: w, h: h)
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import UIKit
|
|
2
|
+
|
|
3
|
+
/// One live native view addressed by its reconciler tag.
|
|
4
|
+
public final class PNViewRecord {
|
|
5
|
+
public let tag: Int64
|
|
6
|
+
public let typeName: String
|
|
7
|
+
public let view: UIView
|
|
8
|
+
public let manager: PNComponentManager
|
|
9
|
+
|
|
10
|
+
init(tag: Int64, typeName: String, view: UIView, manager: PNComponentManager) {
|
|
11
|
+
self.tag = tag
|
|
12
|
+
self.typeName = typeName
|
|
13
|
+
self.view = view
|
|
14
|
+
self.manager = manager
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/// Tag -> view table. Every view created through a transaction is
|
|
19
|
+
/// registered here until its `d` op arrives.
|
|
20
|
+
public final class PNViewRegistry {
|
|
21
|
+
public static let shared = PNViewRegistry()
|
|
22
|
+
|
|
23
|
+
private var records: [Int64: PNViewRecord] = [:]
|
|
24
|
+
|
|
25
|
+
private init() {}
|
|
26
|
+
|
|
27
|
+
/// The record for `tag`, or `nil` when no such view exists.
|
|
28
|
+
public func resolve(_ tag: Int64) -> PNViewRecord? {
|
|
29
|
+
records[tag]
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/// The registered view for `tag`.
|
|
33
|
+
public func view(for tag: Int64) -> UIView? {
|
|
34
|
+
records[tag]?.view
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/// Number of live records (used by tests and diagnostics).
|
|
38
|
+
public var count: Int { records.count }
|
|
39
|
+
|
|
40
|
+
func register(_ record: PNViewRecord) {
|
|
41
|
+
records[record.tag] = record
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
@discardableResult
|
|
45
|
+
func unregister(_ tag: Int64) -> PNViewRecord? {
|
|
46
|
+
records.removeValue(forKey: tag)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/// Drop every record. Intended for tests.
|
|
50
|
+
public func removeAll() {
|
|
51
|
+
records.removeAll()
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import UIKit
|
|
2
|
+
|
|
3
|
+
/// A closure-backed `UIControl` target with the duplicate-send filter the
|
|
4
|
+
/// simulator's event injection needs (one `UIEvent`, two action sends).
|
|
5
|
+
public final class PNActionTarget: NSObject {
|
|
6
|
+
private let handler: () -> Void
|
|
7
|
+
private var lastTimestamp: TimeInterval?
|
|
8
|
+
|
|
9
|
+
public init(_ handler: @escaping () -> Void) {
|
|
10
|
+
self.handler = handler
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/// Attach `handler` to `control` for `events`, retained by the view's state.
|
|
14
|
+
@discardableResult
|
|
15
|
+
public static func attach(_ control: UIControl, events: UIControl.Event, _ handler: @escaping () -> Void) -> PNActionTarget {
|
|
16
|
+
let target = PNActionTarget(handler)
|
|
17
|
+
control.addTarget(target, action: #selector(fire(_:forEvent:)), for: events)
|
|
18
|
+
PNViewState.existing(for: control)?.retained.append(target)
|
|
19
|
+
return target
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
@objc func fire(_ sender: Any?, forEvent event: UIEvent?) {
|
|
23
|
+
if let timestamp = event?.timestamp {
|
|
24
|
+
if lastTimestamp == timestamp {
|
|
25
|
+
PNLog.once(PNLog.components, key: "control-dedupe", "dropped a duplicated control action (one UIEvent, two sends)")
|
|
26
|
+
return
|
|
27
|
+
}
|
|
28
|
+
lastTimestamp = timestamp
|
|
29
|
+
}
|
|
30
|
+
handler()
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/// `Button`: a system `UIButton` emitting `on_press`.
|
|
35
|
+
public final class PNButtonManager: PNComponentManager {
|
|
36
|
+
public override func makeView(props: [String: Any]) -> UIView {
|
|
37
|
+
UIButton(type: .system)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
public override func createView(tag: Int64, props: [String: Any]) -> UIView {
|
|
41
|
+
let view = super.createView(tag: tag, props: props)
|
|
42
|
+
if let button = view as? UIButton {
|
|
43
|
+
PNActionTarget.attach(button, events: .touchUpInside) { [weak button] in
|
|
44
|
+
guard let button = button else { return }
|
|
45
|
+
PNEvents.emit(button, "on_press")
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return view
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
public override func apply(view: UIView, props: [String: Any], initial: Bool) {
|
|
52
|
+
guard let button = view as? UIButton else { return }
|
|
53
|
+
if PNProps.has(props, "title") {
|
|
54
|
+
button.setTitle(PNProps.string(PNProps.value(props, "title")) ?? "", for: .normal)
|
|
55
|
+
}
|
|
56
|
+
if let size = PNProps.double(PNProps.value(props, "font_size")) {
|
|
57
|
+
button.titleLabel?.font = UIFont.systemFont(ofSize: CGFloat(size))
|
|
58
|
+
}
|
|
59
|
+
if let color = PNColor.parse(PNProps.value(props, "background_color")) {
|
|
60
|
+
button.backgroundColor = color
|
|
61
|
+
if !PNProps.has(props, "color") {
|
|
62
|
+
button.setTitleColor(.white, for: .normal)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if let color = PNColor.parse(PNProps.value(props, "color")) {
|
|
66
|
+
button.setTitleColor(color, for: .normal)
|
|
67
|
+
}
|
|
68
|
+
if PNProps.has(props, "enabled") {
|
|
69
|
+
button.isEnabled = PNProps.bool(PNProps.value(props, "enabled")) ?? true
|
|
70
|
+
}
|
|
71
|
+
PNViewStyler.applyDecoration(button, props)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
public override func measure(view: UIView, maxW: CGFloat, maxH: CGFloat) -> CGSize {
|
|
75
|
+
let intrinsic = view.intrinsicContentSize
|
|
76
|
+
var w = intrinsic.width + 24
|
|
77
|
+
var h = intrinsic.height + 12
|
|
78
|
+
if maxW.isFinite { w = min(w, maxW) }
|
|
79
|
+
if maxH.isFinite { h = min(h, maxH) }
|
|
80
|
+
return CGSize(width: max(w, 44), height: max(h, 32))
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/// `Checkbox`: an SF Symbol button toggling checked / unchecked.
|
|
85
|
+
public final class PNCheckboxManager: PNComponentManager {
|
|
86
|
+
public override func makeView(props: [String: Any]) -> UIView {
|
|
87
|
+
UIButton(type: .custom)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
public override func createView(tag: Int64, props: [String: Any]) -> UIView {
|
|
91
|
+
let view = super.createView(tag: tag, props: props)
|
|
92
|
+
if let button = view as? UIButton {
|
|
93
|
+
PNActionTarget.attach(button, events: .touchUpInside) { [weak self, weak button] in
|
|
94
|
+
guard let self = self, let button = button else { return }
|
|
95
|
+
self.toggle(button)
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return view
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
public override func apply(view: UIView, props: [String: Any], initial: Bool) {
|
|
102
|
+
guard let button = view as? UIButton, let state = PNViewState.existing(for: button) else { return }
|
|
103
|
+
if initial {
|
|
104
|
+
let ink = PNColor.parse("#111111") ?? .label
|
|
105
|
+
button.setTitleColor(ink, for: .normal)
|
|
106
|
+
button.tintColor = ink
|
|
107
|
+
}
|
|
108
|
+
if PNProps.has(props, "value") {
|
|
109
|
+
state.extras["value"] = PNProps.bool(PNProps.value(props, "value")) ?? false
|
|
110
|
+
}
|
|
111
|
+
if PNProps.has(props, "label") {
|
|
112
|
+
let label = PNProps.string(PNProps.value(props, "label")) ?? ""
|
|
113
|
+
button.setTitle(label, for: .normal)
|
|
114
|
+
button.accessibilityLabel = label
|
|
115
|
+
}
|
|
116
|
+
if PNProps.has(props, "disabled") {
|
|
117
|
+
button.isEnabled = !(PNProps.bool(PNProps.value(props, "disabled")) ?? false)
|
|
118
|
+
}
|
|
119
|
+
updateImage(button)
|
|
120
|
+
PNViewStyler.applyAccessibility(button, props)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
private func toggle(_ button: UIButton) {
|
|
124
|
+
guard let state = PNViewState.existing(for: button) else { return }
|
|
125
|
+
if PNProps.bool(PNProps.value(state.props, "disabled")) == true { return }
|
|
126
|
+
let newValue = !state.flag("value")
|
|
127
|
+
// Optimistic flip so the box feels instant; the `value` prop re-syncs it.
|
|
128
|
+
state.extras["value"] = newValue
|
|
129
|
+
updateImage(button)
|
|
130
|
+
PNEvents.emit(button, "on_change", [newValue])
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
private func updateImage(_ button: UIButton) {
|
|
134
|
+
guard let state = PNViewState.existing(for: button) else { return }
|
|
135
|
+
let checked = state.flag("value")
|
|
136
|
+
guard var image = UIImage(systemName: checked ? "checkmark.square.fill" : "square") else { return }
|
|
137
|
+
if checked, let color = PNColor.parse(PNProps.value(state.props, "color")) {
|
|
138
|
+
image = image.withTintColor(color, renderingMode: .alwaysOriginal)
|
|
139
|
+
}
|
|
140
|
+
button.setImage(image, for: .normal)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
public override func measure(view: UIView, maxW: CGFloat, maxH: CGFloat) -> CGSize {
|
|
144
|
+
let size = view.sizeThatFits(CGSize(width: PNComponentManager.clampConstraint(maxW), height: PNComponentManager.clampConstraint(maxH)))
|
|
145
|
+
var w = max(size.width + 8, 28)
|
|
146
|
+
if maxW.isFinite { w = min(w, maxW) }
|
|
147
|
+
return CGSize(width: w, height: max(size.height, 28))
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/// `Picker`: a button that presents an action sheet listing the options.
|
|
152
|
+
public final class PNPickerManager: PNComponentManager {
|
|
153
|
+
public override func makeView(props: [String: Any]) -> UIView {
|
|
154
|
+
UIButton(type: .system)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
public override func createView(tag: Int64, props: [String: Any]) -> UIView {
|
|
158
|
+
let view = super.createView(tag: tag, props: props)
|
|
159
|
+
if let button = view as? UIButton {
|
|
160
|
+
PNActionTarget.attach(button, events: .touchUpInside) { [weak self, weak button] in
|
|
161
|
+
guard let self = self, let button = button else { return }
|
|
162
|
+
self.presentSheet(button)
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return view
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
public override func apply(view: UIView, props: [String: Any], initial: Bool) {
|
|
169
|
+
guard let button = view as? UIButton else { return }
|
|
170
|
+
button.setTitle(PNPickerManager.title(for: mergedProps(button)), for: .normal)
|
|
171
|
+
PNViewStyler.applyAccessibility(button, props)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
static func items(_ props: [String: Any]) -> [[String: Any]] {
|
|
175
|
+
((PNProps.value(props, "items") as? [Any]) ?? []).compactMap { $0 as? [String: Any] }
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
static func title(for props: [String: Any]) -> String {
|
|
179
|
+
let selected = PNProps.value(props, "value")
|
|
180
|
+
for item in items(props) {
|
|
181
|
+
if let value = PNProps.value(item, "value"), PNJSON.encode(value) == PNJSON.encode(selected) {
|
|
182
|
+
return PNProps.string(item["label"]) ?? PNProps.string(item["value"]) ?? ""
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return PNProps.string(PNProps.value(props, "placeholder")) ?? "Select…"
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
private func presentSheet(_ button: UIButton) {
|
|
189
|
+
let merged = mergedProps(button)
|
|
190
|
+
let items = PNPickerManager.items(merged)
|
|
191
|
+
var buttons: [[String: Any]] = items.map { ["label": PNProps.string($0["label"]) ?? PNProps.string($0["value"]) ?? ""] }
|
|
192
|
+
buttons.append(["label": "Cancel", "style": "cancel"])
|
|
193
|
+
PNAlertPresenter.present(
|
|
194
|
+
title: PNProps.string(PNProps.value(merged, "placeholder")) ?? "Select…",
|
|
195
|
+
message: nil, buttons: buttons, style: "action_sheet"
|
|
196
|
+
) { index in
|
|
197
|
+
guard index >= 0, index < items.count else { return }
|
|
198
|
+
PNEvents.emit(button, "on_change", [PNProps.value(items[index], "value")])
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
public override func measure(view: UIView, maxW: CGFloat, maxH: CGFloat) -> CGSize {
|
|
203
|
+
let size = view.sizeThatFits(CGSize(width: PNComponentManager.clampConstraint(maxW), height: PNComponentManager.clampConstraint(maxH)))
|
|
204
|
+
return CGSize(width: size.width + 16, height: size.height + 8)
|
|
205
|
+
}
|
|
206
|
+
}
|
pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNColor.swift
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import UIKit
|
|
2
|
+
|
|
3
|
+
/// Color parsing shared by every manager.
|
|
4
|
+
///
|
|
5
|
+
/// Accepts `#RGB`, `#RGBA`, `#RRGGBB`, `#AARRGGBB` (the PythonNative
|
|
6
|
+
/// convention: alpha first), `rgb(r, g, b)`, `rgba(r, g, b, a)`, CSS
|
|
7
|
+
/// named colors, `"transparent"`, raw ARGB integers, and
|
|
8
|
+
/// `{"light": c, "dark": c}` dictionaries for dynamic colors.
|
|
9
|
+
public enum PNColor {
|
|
10
|
+
/// Parse `value` into a `UIColor`, or `nil` when it isn't a color.
|
|
11
|
+
public static func parse(_ value: Any?) -> UIColor? {
|
|
12
|
+
switch value {
|
|
13
|
+
case nil, is NSNull:
|
|
14
|
+
return nil
|
|
15
|
+
case let dict as [String: Any]:
|
|
16
|
+
return dynamic(dict)
|
|
17
|
+
case let number as NSNumber where !(value is Bool):
|
|
18
|
+
return fromARGB(number.int64Value)
|
|
19
|
+
case let int as Int:
|
|
20
|
+
return fromARGB(Int64(int))
|
|
21
|
+
case let string as String:
|
|
22
|
+
return parse(string: string)
|
|
23
|
+
default:
|
|
24
|
+
return nil
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/// Parse a color string; see `parse(_:)` for the accepted grammar.
|
|
29
|
+
public static func parse(string raw: String) -> UIColor? {
|
|
30
|
+
let text = raw.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
31
|
+
if text.isEmpty { return nil }
|
|
32
|
+
let lower = text.lowercased()
|
|
33
|
+
if lower == "transparent" || lower == "clear" || lower == "none" {
|
|
34
|
+
return UIColor.clear
|
|
35
|
+
}
|
|
36
|
+
if text.hasPrefix("#") {
|
|
37
|
+
return parseHex(String(text.dropFirst()))
|
|
38
|
+
}
|
|
39
|
+
if lower.hasPrefix("rgb") {
|
|
40
|
+
return parseRGBFunction(lower)
|
|
41
|
+
}
|
|
42
|
+
if let named = PNNamedColors.table[lower] {
|
|
43
|
+
return fromARGB(named)
|
|
44
|
+
}
|
|
45
|
+
if let hex = parseHex(text), text.count == 6 || text.count == 8 {
|
|
46
|
+
return hex
|
|
47
|
+
}
|
|
48
|
+
return nil
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/// Components (0...1) of `color` in the sRGB space.
|
|
52
|
+
public static func components(_ color: UIColor) -> (r: CGFloat, g: CGFloat, b: CGFloat, a: CGFloat) {
|
|
53
|
+
var r: CGFloat = 0, g: CGFloat = 0, b: CGFloat = 0, a: CGFloat = 0
|
|
54
|
+
if color.getRed(&r, green: &g, blue: &b, alpha: &a) {
|
|
55
|
+
return (r, g, b, a)
|
|
56
|
+
}
|
|
57
|
+
var white: CGFloat = 0
|
|
58
|
+
if color.getWhite(&white, alpha: &a) {
|
|
59
|
+
return (white, white, white, a)
|
|
60
|
+
}
|
|
61
|
+
return (0, 0, 0, 1)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/// `#AARRGGBB` string for `color`, matching the Python interpolation output.
|
|
65
|
+
public static func hexString(_ color: UIColor) -> String {
|
|
66
|
+
let c = components(color)
|
|
67
|
+
func byte(_ v: CGFloat) -> Int { Int((max(0, min(1, v)) * 255).rounded()) }
|
|
68
|
+
return String(format: "#%02X%02X%02X%02X", byte(c.a), byte(c.r), byte(c.g), byte(c.b))
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/// Build a color from a 32-bit ARGB integer (signed or unsigned).
|
|
72
|
+
public static func fromARGB(_ value: Int64) -> UIColor {
|
|
73
|
+
var argb = value
|
|
74
|
+
if argb < 0 { argb += 0x1_0000_0000 }
|
|
75
|
+
let a = CGFloat((argb >> 24) & 0xFF) / 255
|
|
76
|
+
let r = CGFloat((argb >> 16) & 0xFF) / 255
|
|
77
|
+
let g = CGFloat((argb >> 8) & 0xFF) / 255
|
|
78
|
+
let b = CGFloat(argb & 0xFF) / 255
|
|
79
|
+
return UIColor(red: r, green: g, blue: b, alpha: a)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// MARK: - Private
|
|
83
|
+
|
|
84
|
+
private static func parseHex(_ digits: String) -> UIColor? {
|
|
85
|
+
var hex = digits
|
|
86
|
+
switch hex.count {
|
|
87
|
+
case 3:
|
|
88
|
+
hex = "FF" + hex.map { "\($0)\($0)" }.joined()
|
|
89
|
+
case 4:
|
|
90
|
+
// #RGBA short form (CSS): expand then move alpha to the front.
|
|
91
|
+
let expanded = hex.map { "\($0)\($0)" }
|
|
92
|
+
hex = expanded[3] + expanded[0] + expanded[1] + expanded[2]
|
|
93
|
+
case 6:
|
|
94
|
+
hex = "FF" + hex
|
|
95
|
+
case 8:
|
|
96
|
+
break
|
|
97
|
+
default:
|
|
98
|
+
return nil
|
|
99
|
+
}
|
|
100
|
+
guard let value = Int64(hex, radix: 16) else { return nil }
|
|
101
|
+
return fromARGB(value)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
private static func parseRGBFunction(_ text: String) -> UIColor? {
|
|
105
|
+
guard let open = text.firstIndex(of: "("), let close = text.lastIndex(of: ")"), open < close else { return nil }
|
|
106
|
+
let inner = text[text.index(after: open)..<close]
|
|
107
|
+
let parts = inner.split(whereSeparator: { $0 == "," || $0 == " " || $0 == "/" })
|
|
108
|
+
.map { $0.trimmingCharacters(in: .whitespaces) }
|
|
109
|
+
.filter { !$0.isEmpty }
|
|
110
|
+
guard parts.count == 3 || parts.count == 4 else { return nil }
|
|
111
|
+
func channel(_ s: String) -> CGFloat? {
|
|
112
|
+
if s.hasSuffix("%"), let v = Double(s.dropLast()) { return CGFloat(v / 100) }
|
|
113
|
+
guard let v = Double(s) else { return nil }
|
|
114
|
+
return CGFloat(v / 255)
|
|
115
|
+
}
|
|
116
|
+
func alpha(_ s: String) -> CGFloat? {
|
|
117
|
+
if s.hasSuffix("%"), let v = Double(s.dropLast()) { return CGFloat(v / 100) }
|
|
118
|
+
guard let v = Double(s) else { return nil }
|
|
119
|
+
return CGFloat(v)
|
|
120
|
+
}
|
|
121
|
+
guard let r = channel(parts[0]), let g = channel(parts[1]), let b = channel(parts[2]) else { return nil }
|
|
122
|
+
let a = parts.count == 4 ? (alpha(parts[3]) ?? 1) : 1
|
|
123
|
+
return UIColor(red: clamp(r), green: clamp(g), blue: clamp(b), alpha: clamp(a))
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
private static func dynamic(_ dict: [String: Any]) -> UIColor? {
|
|
127
|
+
let light = parse(dict["light"] ?? dict["default"])
|
|
128
|
+
let dark = parse(dict["dark"])
|
|
129
|
+
guard light != nil || dark != nil else { return nil }
|
|
130
|
+
return UIColor { traits in
|
|
131
|
+
if traits.userInterfaceStyle == .dark {
|
|
132
|
+
return dark ?? light ?? .clear
|
|
133
|
+
}
|
|
134
|
+
return light ?? dark ?? .clear
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
private static func clamp(_ v: CGFloat) -> CGFloat { max(0, min(1, v)) }
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/// CSS named colors as ARGB integers.
|
|
142
|
+
enum PNNamedColors {
|
|
143
|
+
static let table: [String: Int64] = [
|
|
144
|
+
"black": 0xFF00_0000, "white": 0xFFFF_FFFF, "red": 0xFFFF_0000, "green": 0xFF00_8000,
|
|
145
|
+
"blue": 0xFF00_00FF, "yellow": 0xFFFF_FF00, "cyan": 0xFF00_FFFF, "aqua": 0xFF00_FFFF,
|
|
146
|
+
"magenta": 0xFFFF_00FF, "fuchsia": 0xFFFF_00FF, "gray": 0xFF80_8080, "grey": 0xFF80_8080,
|
|
147
|
+
"silver": 0xFFC0_C0C0, "maroon": 0xFF80_0000, "olive": 0xFF80_8000, "lime": 0xFF00_FF00,
|
|
148
|
+
"teal": 0xFF00_8080, "navy": 0xFF00_0080, "purple": 0xFF80_0080, "orange": 0xFFFF_A500,
|
|
149
|
+
"pink": 0xFFFF_C0CB, "brown": 0xFFA5_2A2A, "gold": 0xFFFF_D700, "indigo": 0xFF4B_0082,
|
|
150
|
+
"violet": 0xFFEE_82EE, "tomato": 0xFFFF_6347, "coral": 0xFFFF_7F50, "salmon": 0xFFFA_8072,
|
|
151
|
+
"crimson": 0xFFDC_143C, "khaki": 0xFFF0_E68C, "beige": 0xFFF5_F5DC, "ivory": 0xFFFF_FFF0,
|
|
152
|
+
"lavender": 0xFFE6_E6FA, "plum": 0xFFDD_A0DD, "orchid": 0xFFDA_70D6, "turquoise": 0xFF40_E0D0,
|
|
153
|
+
"tan": 0xFFD2_B48C, "chocolate": 0xFFD2_691E, "firebrick": 0xFFB2_2222, "darkred": 0xFF8B_0000,
|
|
154
|
+
"darkgreen": 0xFF00_6400, "darkblue": 0xFF00_008B, "darkgray": 0xFFA9_A9A9, "darkgrey": 0xFFA9_A9A9,
|
|
155
|
+
"dimgray": 0xFF69_6969, "dimgrey": 0xFF69_6969, "lightgray": 0xFFD3_D3D3, "lightgrey": 0xFFD3_D3D3,
|
|
156
|
+
"lightblue": 0xFFAD_D8E6, "lightgreen": 0xFF90_EE90, "lightyellow": 0xFFFF_FFE0, "lightpink": 0xFFFF_B6C1,
|
|
157
|
+
"skyblue": 0xFF87_CEEB, "steelblue": 0xFF46_82B4, "royalblue": 0xFF41_69E1, "dodgerblue": 0xFF1E_90FF,
|
|
158
|
+
"deepskyblue": 0xFF00_BFFF, "slategray": 0xFF70_8090, "slategrey": 0xFF70_8090, "whitesmoke": 0xFFF5_F5F5,
|
|
159
|
+
"gainsboro": 0xFFDC_DCDC, "snow": 0xFFFF_FAFA, "seagreen": 0xFF2E_8B57, "forestgreen": 0xFF22_8B22,
|
|
160
|
+
"limegreen": 0xFF32_CD32, "springgreen": 0xFF00_FF7F, "mintcream": 0xFFF5_FFFA, "aquamarine": 0xFF7F_FFD4,
|
|
161
|
+
"hotpink": 0xFFFF_69B4, "deeppink": 0xFFFF_1493, "orangered": 0xFFFF_4500, "darkorange": 0xFFFF_8C00,
|
|
162
|
+
"goldenrod": 0xFFDA_A520, "sienna": 0xFFA0_522D, "peru": 0xFFCD_853F, "wheat": 0xFFF5_DEB3,
|
|
163
|
+
"linen": 0xFFFA_F0E6, "azure": 0xFFF0_FFFF, "honeydew": 0xFFF0_FFF0, "midnightblue": 0xFF19_1970,
|
|
164
|
+
"rebeccapurple": 0xFF66_3399, "mediumpurple": 0xFF93_70DB, "slateblue": 0xFF6A_5ACD, "cadetblue": 0xFF5F_9EA0,
|
|
165
|
+
"cornflowerblue": 0xFF64_95ED, "darkcyan": 0xFF00_8B8B, "darkmagenta": 0xFF8B_008B, "darkviolet": 0xFF94_00D3,
|
|
166
|
+
"darkslategray": 0xFF2F_4F4F, "darkslategrey": 0xFF2F_4F4F, "lightcoral": 0xFFF0_8080, "lightsalmon": 0xFFFF_A07A,
|
|
167
|
+
"lightseagreen": 0xFF20_B2AA, "lightskyblue": 0xFF87_CEFA, "lightslategray": 0xFF77_8899, "lightslategrey": 0xFF77_8899,
|
|
168
|
+
"lightsteelblue": 0xFFB0_C4DE, "mediumblue": 0xFF00_00CD, "mediumseagreen": 0xFF3C_B371, "mediumvioletred": 0xFFC7_1585,
|
|
169
|
+
"olivedrab": 0xFF6B_8E23, "palegreen": 0xFF98_FB98, "paleturquoise": 0xFFAF_EEEE, "palevioletred": 0xFFDB_7093,
|
|
170
|
+
"powderblue": 0xFFB0_E0E6, "rosybrown": 0xFFBC_8F8F, "saddlebrown": 0xFF8B_4513, "sandybrown": 0xFFF4_A460,
|
|
171
|
+
"thistle": 0xFFD8_BFD8, "yellowgreen": 0xFF9A_CD32, "chartreuse": 0xFF7F_FF00, "greenyellow": 0xFFAD_FF2F,
|
|
172
|
+
]
|
|
173
|
+
}
|