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,340 @@
|
|
|
1
|
+
import UIKit
|
|
2
|
+
|
|
3
|
+
/// Applies the visual props every element type shares.
|
|
4
|
+
///
|
|
5
|
+
/// `applyCommon` is the full container set (background, overflow,
|
|
6
|
+
/// display, opacity, z-index, pointer events, borders, shadows,
|
|
7
|
+
/// transform, accessibility). `applyDecoration` is the subset leaf
|
|
8
|
+
/// controls use (borders, shadow, transform, accessibility, opacity),
|
|
9
|
+
/// mirroring the per-handler behavior of the original iOS backend.
|
|
10
|
+
public enum PNViewStyler {
|
|
11
|
+
static let cornerRadiusKeys = [
|
|
12
|
+
"border_top_left_radius", "border_top_right_radius",
|
|
13
|
+
"border_bottom_left_radius", "border_bottom_right_radius",
|
|
14
|
+
]
|
|
15
|
+
static let sideWidthKeys = ["border_left_width", "border_top_width", "border_right_width", "border_bottom_width"]
|
|
16
|
+
static let sideColorKeys = ["border_left_color", "border_top_color", "border_right_color", "border_bottom_color"]
|
|
17
|
+
static let shadowKeys = ["shadow_color", "shadow_offset", "shadow_opacity", "shadow_radius", "elevation"]
|
|
18
|
+
static let maskedCornerBits: [CACornerMask] = [
|
|
19
|
+
.layerMinXMinYCorner, .layerMaxXMinYCorner, .layerMinXMaxYCorner, .layerMaxXMaxYCorner,
|
|
20
|
+
]
|
|
21
|
+
static let allCorners: CACornerMask = [
|
|
22
|
+
.layerMinXMinYCorner, .layerMaxXMinYCorner, .layerMinXMaxYCorner, .layerMaxXMaxYCorner,
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
// MARK: - Entry points
|
|
26
|
+
|
|
27
|
+
/// Apply every shared visual prop present in `props`.
|
|
28
|
+
public static func applyCommon(_ view: UIView, _ props: [String: Any]) {
|
|
29
|
+
if let color = PNColor.parse(PNProps.value(props, "background_color")) {
|
|
30
|
+
view.backgroundColor = color
|
|
31
|
+
}
|
|
32
|
+
if PNProps.has(props, "overflow") {
|
|
33
|
+
view.clipsToBounds = PNProps.string(PNProps.value(props, "overflow")) == "hidden"
|
|
34
|
+
}
|
|
35
|
+
if PNProps.has(props, "display") {
|
|
36
|
+
view.isHidden = PNProps.string(PNProps.value(props, "display")) == "none"
|
|
37
|
+
}
|
|
38
|
+
if let opacity = PNProps.double(PNProps.value(props, "opacity")) {
|
|
39
|
+
view.alpha = CGFloat(opacity)
|
|
40
|
+
}
|
|
41
|
+
if PNProps.has(props, "z_index") {
|
|
42
|
+
view.layer.zPosition = CGFloat(PNProps.double(PNProps.value(props, "z_index")) ?? 0)
|
|
43
|
+
}
|
|
44
|
+
if PNProps.has(props, "pointer_events") || PNProps.has(props, "hit_slop") {
|
|
45
|
+
applyInteraction(view, props)
|
|
46
|
+
}
|
|
47
|
+
applyBorder(view, props)
|
|
48
|
+
applySideBorders(view, props)
|
|
49
|
+
let merged = PNViewState.existing(for: view)?.props ?? props
|
|
50
|
+
let cornerKeysActive = cornerRadiusKeys.contains { PNProps.value(merged, $0) != nil }
|
|
51
|
+
if cornerRadiusKeys.contains(where: { PNProps.has(props, $0) }) || (PNProps.has(props, "border_radius") && cornerKeysActive) {
|
|
52
|
+
applyCornerRadii(view, merged)
|
|
53
|
+
}
|
|
54
|
+
applyShadow(view, props)
|
|
55
|
+
if PNProps.has(props, "transform") {
|
|
56
|
+
PNTransform.apply(view, spec: PNProps.value(props, "transform"))
|
|
57
|
+
}
|
|
58
|
+
applyAccessibility(view, props)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/// The leaf-control subset: border, shadow, transform, accessibility, opacity.
|
|
62
|
+
public static func applyDecoration(_ view: UIView, _ props: [String: Any]) {
|
|
63
|
+
applyBorder(view, props)
|
|
64
|
+
applyShadow(view, props)
|
|
65
|
+
if PNProps.has(props, "transform") {
|
|
66
|
+
PNTransform.apply(view, spec: PNProps.value(props, "transform"))
|
|
67
|
+
}
|
|
68
|
+
applyAccessibility(view, props)
|
|
69
|
+
if let opacity = PNProps.double(PNProps.value(props, "opacity")) {
|
|
70
|
+
view.alpha = CGFloat(opacity)
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/// Re-run the bounds-dependent parts of styling after a frame change.
|
|
75
|
+
public static func syncFrameDependentStyles(_ view: UIView, size: CGSize) {
|
|
76
|
+
guard let state = PNViewState.existing(for: view) else { return }
|
|
77
|
+
let maxRadius = max(0, min(size.width, size.height) / 2)
|
|
78
|
+
if let requested = state.requestedCornerRadius, maxRadius > 0 {
|
|
79
|
+
view.layer.cornerRadius = min(requested, maxRadius)
|
|
80
|
+
} else if state.requestedCornerRadius == nil, view.layer.cornerRadius > 0, maxRadius > 0 {
|
|
81
|
+
view.layer.cornerRadius = min(view.layer.cornerRadius, maxRadius)
|
|
82
|
+
}
|
|
83
|
+
updateCornerMask(view, state: state, size: size)
|
|
84
|
+
updateSideBorderLayers(view, state: state, size: size)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// MARK: - Borders
|
|
88
|
+
|
|
89
|
+
static func applyBorder(_ view: UIView, _ props: [String: Any]) {
|
|
90
|
+
let layer = view.layer
|
|
91
|
+
if let radius = PNProps.double(PNProps.value(props, "border_radius")) {
|
|
92
|
+
let requested = CGFloat(max(0, radius))
|
|
93
|
+
PNViewState.existing(for: view)?.requestedCornerRadius = requested
|
|
94
|
+
let size = view.bounds.size
|
|
95
|
+
var value = requested
|
|
96
|
+
if size.width > 0, size.height > 0 {
|
|
97
|
+
value = min(requested, min(size.width, size.height) / 2)
|
|
98
|
+
}
|
|
99
|
+
layer.cornerRadius = value
|
|
100
|
+
// Rounded corners clip implicitly (the React Native default).
|
|
101
|
+
layer.masksToBounds = true
|
|
102
|
+
} else if PNProps.has(props, "border_radius") {
|
|
103
|
+
PNViewState.existing(for: view)?.requestedCornerRadius = nil
|
|
104
|
+
layer.cornerRadius = 0
|
|
105
|
+
}
|
|
106
|
+
if let width = PNProps.double(PNProps.value(props, "border_width")) {
|
|
107
|
+
layer.borderWidth = CGFloat(width)
|
|
108
|
+
} else if PNProps.has(props, "border_width") {
|
|
109
|
+
layer.borderWidth = 0
|
|
110
|
+
}
|
|
111
|
+
if let color = PNColor.parse(PNProps.value(props, "border_color")) {
|
|
112
|
+
layer.borderColor = color.cgColor
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
static func applySideBorders(_ view: UIView, _ props: [String: Any]) {
|
|
117
|
+
guard sideWidthKeys.contains(where: { PNProps.value(props, $0) != nil }),
|
|
118
|
+
let state = PNViewState.existing(for: view)
|
|
119
|
+
else { return }
|
|
120
|
+
let merged = state.props
|
|
121
|
+
let baseWidth = CGFloat(PNProps.double(PNProps.value(merged, "border_width")) ?? 0)
|
|
122
|
+
let baseColor = PNColor.parse(PNProps.value(merged, "border_color")) ?? .black
|
|
123
|
+
state.sideBorderWidths = sideWidthKeys.map { key in
|
|
124
|
+
CGFloat(PNProps.double(PNProps.value(merged, key)) ?? Double(baseWidth))
|
|
125
|
+
}
|
|
126
|
+
state.sideBorderColors = sideColorKeys.map { key in
|
|
127
|
+
PNColor.parse(PNProps.value(merged, key)) ?? baseColor
|
|
128
|
+
}
|
|
129
|
+
view.layer.borderWidth = 0
|
|
130
|
+
let size = view.bounds.size
|
|
131
|
+
if size.width > 0, size.height > 0 {
|
|
132
|
+
updateSideBorderLayers(view, state: state, size: size)
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
static func updateSideBorderLayers(_ view: UIView, state: PNViewState, size: CGSize) {
|
|
137
|
+
guard let widths = state.sideBorderWidths, let colors = state.sideBorderColors else { return }
|
|
138
|
+
let frames = [
|
|
139
|
+
CGRect(x: 0, y: 0, width: widths[0], height: size.height),
|
|
140
|
+
CGRect(x: 0, y: 0, width: size.width, height: widths[1]),
|
|
141
|
+
CGRect(x: size.width - widths[2], y: 0, width: widths[2], height: size.height),
|
|
142
|
+
CGRect(x: 0, y: size.height - widths[3], width: size.width, height: widths[3]),
|
|
143
|
+
]
|
|
144
|
+
for i in 0..<4 {
|
|
145
|
+
if widths[i] <= 0 {
|
|
146
|
+
state.sideBorderLayers[i]?.removeFromSuperlayer()
|
|
147
|
+
state.sideBorderLayers[i] = nil
|
|
148
|
+
continue
|
|
149
|
+
}
|
|
150
|
+
let layer = state.sideBorderLayers[i] ?? {
|
|
151
|
+
let created = CALayer()
|
|
152
|
+
view.layer.addSublayer(created)
|
|
153
|
+
state.sideBorderLayers[i] = created
|
|
154
|
+
return created
|
|
155
|
+
}()
|
|
156
|
+
layer.backgroundColor = colors[i].cgColor
|
|
157
|
+
layer.frame = frames[i]
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// MARK: - Per-corner radii
|
|
162
|
+
|
|
163
|
+
static func applyCornerRadii(_ view: UIView, _ merged: [String: Any]) {
|
|
164
|
+
guard let state = PNViewState.existing(for: view) else { return }
|
|
165
|
+
let anyCorner = cornerRadiusKeys.contains { PNProps.value(merged, $0) != nil }
|
|
166
|
+
if !anyCorner {
|
|
167
|
+
if state.cornerRadii != nil {
|
|
168
|
+
state.cornerRadii = nil
|
|
169
|
+
view.layer.mask = nil
|
|
170
|
+
view.layer.maskedCorners = allCorners
|
|
171
|
+
applyBorder(view, merged)
|
|
172
|
+
}
|
|
173
|
+
return
|
|
174
|
+
}
|
|
175
|
+
let base = CGFloat(max(0, PNProps.double(PNProps.value(merged, "border_radius")) ?? 0))
|
|
176
|
+
let radii: [CGFloat] = cornerRadiusKeys.map { key in
|
|
177
|
+
guard let value = PNProps.double(PNProps.value(merged, key)), value.isFinite else { return base }
|
|
178
|
+
return CGFloat(max(0, value))
|
|
179
|
+
}
|
|
180
|
+
let distinct = Set(radii.filter { $0 > 0 })
|
|
181
|
+
if distinct.count <= 1 {
|
|
182
|
+
state.cornerRadii = nil
|
|
183
|
+
let value = distinct.first ?? 0
|
|
184
|
+
var mask: CACornerMask = []
|
|
185
|
+
for (bit, r) in zip(maskedCornerBits, radii) where r > 0 {
|
|
186
|
+
mask.insert(bit)
|
|
187
|
+
}
|
|
188
|
+
view.layer.mask = nil
|
|
189
|
+
view.layer.cornerRadius = value
|
|
190
|
+
view.layer.maskedCorners = mask.isEmpty ? allCorners : mask
|
|
191
|
+
view.layer.masksToBounds = true
|
|
192
|
+
state.requestedCornerRadius = value
|
|
193
|
+
return
|
|
194
|
+
}
|
|
195
|
+
state.requestedCornerRadius = nil
|
|
196
|
+
state.cornerRadii = radii
|
|
197
|
+
view.layer.cornerRadius = 0
|
|
198
|
+
view.layer.maskedCorners = allCorners
|
|
199
|
+
let size = view.bounds.size
|
|
200
|
+
if size.width > 0, size.height > 0 {
|
|
201
|
+
updateCornerMask(view, state: state, size: size)
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
static func updateCornerMask(_ view: UIView, state: PNViewState, size: CGSize) {
|
|
206
|
+
guard let radii = state.cornerRadii, size.width > 0, size.height > 0 else { return }
|
|
207
|
+
var scale: CGFloat = 1
|
|
208
|
+
let (tl0, tr0, bl0, br0) = (radii[0], radii[1], radii[2], radii[3])
|
|
209
|
+
for (sum, extent) in [(tl0 + tr0, size.width), (bl0 + br0, size.width), (tl0 + bl0, size.height), (tr0 + br0, size.height)]
|
|
210
|
+
where sum > extent && extent > 0 {
|
|
211
|
+
scale = min(scale, extent / sum)
|
|
212
|
+
}
|
|
213
|
+
let (tl, tr, bl, br) = (tl0 * scale, tr0 * scale, bl0 * scale, br0 * scale)
|
|
214
|
+
let w = size.width, h = size.height
|
|
215
|
+
let path = UIBezierPath()
|
|
216
|
+
let halfPi = CGFloat.pi / 2
|
|
217
|
+
path.move(to: CGPoint(x: tl, y: 0))
|
|
218
|
+
path.addLine(to: CGPoint(x: w - tr, y: 0))
|
|
219
|
+
if tr > 0 { path.addArc(withCenter: CGPoint(x: w - tr, y: tr), radius: tr, startAngle: -halfPi, endAngle: 0, clockwise: true) }
|
|
220
|
+
path.addLine(to: CGPoint(x: w, y: h - br))
|
|
221
|
+
if br > 0 { path.addArc(withCenter: CGPoint(x: w - br, y: h - br), radius: br, startAngle: 0, endAngle: halfPi, clockwise: true) }
|
|
222
|
+
path.addLine(to: CGPoint(x: bl, y: h))
|
|
223
|
+
if bl > 0 { path.addArc(withCenter: CGPoint(x: bl, y: h - bl), radius: bl, startAngle: halfPi, endAngle: .pi, clockwise: true) }
|
|
224
|
+
path.addLine(to: CGPoint(x: 0, y: tl))
|
|
225
|
+
if tl > 0 { path.addArc(withCenter: CGPoint(x: tl, y: tl), radius: tl, startAngle: .pi, endAngle: .pi + halfPi, clockwise: true) }
|
|
226
|
+
path.close()
|
|
227
|
+
let shape = CAShapeLayer()
|
|
228
|
+
shape.frame = CGRect(origin: .zero, size: size)
|
|
229
|
+
shape.path = path.cgPath
|
|
230
|
+
view.layer.mask = shape
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// MARK: - Shadow
|
|
234
|
+
|
|
235
|
+
static func applyShadow(_ view: UIView, _ props: [String: Any]) {
|
|
236
|
+
let layer = view.layer
|
|
237
|
+
let hasShadow = shadowKeys.contains { PNProps.value(props, $0) != nil }
|
|
238
|
+
if hasShadow, PNProps.string(PNProps.value(props, "overflow")) != "hidden" {
|
|
239
|
+
layer.masksToBounds = false
|
|
240
|
+
view.clipsToBounds = false
|
|
241
|
+
}
|
|
242
|
+
if let color = PNColor.parse(PNProps.value(props, "shadow_color")) {
|
|
243
|
+
layer.shadowColor = color.cgColor
|
|
244
|
+
}
|
|
245
|
+
if let opacity = PNProps.double(PNProps.value(props, "shadow_opacity")) {
|
|
246
|
+
layer.shadowOpacity = Float(opacity)
|
|
247
|
+
}
|
|
248
|
+
if let radius = PNProps.double(PNProps.value(props, "shadow_radius")) {
|
|
249
|
+
layer.shadowRadius = CGFloat(radius)
|
|
250
|
+
}
|
|
251
|
+
if let offset = PNProps.value(props, "shadow_offset") {
|
|
252
|
+
let (dx, dy) = shadowOffset(offset)
|
|
253
|
+
layer.shadowOffset = CGSize(width: dx, height: dy)
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/// Coerce a `shadow_offset` / `text_shadow_offset` value to `(dx, dy)`.
|
|
258
|
+
public static func shadowOffset(_ value: Any?) -> (CGFloat, CGFloat) {
|
|
259
|
+
if let dict = value as? [String: Any] {
|
|
260
|
+
return (CGFloat(PNProps.double(dict["width"]) ?? 0), CGFloat(PNProps.double(dict["height"]) ?? 0))
|
|
261
|
+
}
|
|
262
|
+
if let list = value as? [Any], list.count >= 2 {
|
|
263
|
+
return (CGFloat(PNProps.double(list[0]) ?? 0), CGFloat(PNProps.double(list[1]) ?? 0))
|
|
264
|
+
}
|
|
265
|
+
return (0, 0)
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// MARK: - Interaction
|
|
269
|
+
|
|
270
|
+
static func applyInteraction(_ view: UIView, _ props: [String: Any]) {
|
|
271
|
+
guard let container = view as? PNContainerView else {
|
|
272
|
+
PNLog.once(PNLog.components, key: "interaction:\(PNViewState.existing(for: view)?.tag ?? 0)",
|
|
273
|
+
"pointer_events / hit_slop only work on container elements; ignored")
|
|
274
|
+
return
|
|
275
|
+
}
|
|
276
|
+
if PNProps.has(props, "pointer_events") {
|
|
277
|
+
let mode = PNProps.string(PNProps.value(props, "pointer_events"))
|
|
278
|
+
container.pointerEvents = ["none", "box_none", "box_only"].contains(mode ?? "") ? mode : nil
|
|
279
|
+
}
|
|
280
|
+
if PNProps.has(props, "hit_slop") {
|
|
281
|
+
container.hitSlop = hitSlop(PNProps.value(props, "hit_slop"))
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/// Normalize a `hit_slop` prop into edge insets.
|
|
286
|
+
public static func hitSlop(_ value: Any?) -> UIEdgeInsets {
|
|
287
|
+
if let dict = value as? [String: Any] {
|
|
288
|
+
return UIEdgeInsets(
|
|
289
|
+
top: CGFloat(PNProps.double(dict["top"]) ?? 0), left: CGFloat(PNProps.double(dict["left"]) ?? 0),
|
|
290
|
+
bottom: CGFloat(PNProps.double(dict["bottom"]) ?? 0), right: CGFloat(PNProps.double(dict["right"]) ?? 0)
|
|
291
|
+
)
|
|
292
|
+
}
|
|
293
|
+
if let uniform = PNProps.double(value) {
|
|
294
|
+
let v = CGFloat(uniform)
|
|
295
|
+
return UIEdgeInsets(top: v, left: v, bottom: v, right: v)
|
|
296
|
+
}
|
|
297
|
+
return .zero
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// MARK: - Accessibility
|
|
301
|
+
|
|
302
|
+
static let traitByRole: [String: UIAccessibilityTraits] = [
|
|
303
|
+
"button": .button, "link": .link, "image": .image, "search": .searchField,
|
|
304
|
+
"keyboard_key": .keyboardKey, "static_text": .staticText, "summary_element": .summaryElement,
|
|
305
|
+
"adjustable": .adjustable, "header": .header, "selected": .selected, "checkbox": .button, "none": [],
|
|
306
|
+
]
|
|
307
|
+
|
|
308
|
+
static func applyAccessibility(_ view: UIView, _ props: [String: Any]) {
|
|
309
|
+
if PNProps.has(props, "accessible") {
|
|
310
|
+
view.isAccessibilityElement = PNProps.bool(PNProps.value(props, "accessible")) ?? false
|
|
311
|
+
}
|
|
312
|
+
if PNProps.has(props, "accessibility_label") {
|
|
313
|
+
view.accessibilityLabel = PNProps.string(PNProps.value(props, "accessibility_label")) ?? ""
|
|
314
|
+
}
|
|
315
|
+
if PNProps.has(props, "accessibility_hint") {
|
|
316
|
+
view.accessibilityHint = PNProps.string(PNProps.value(props, "accessibility_hint")) ?? ""
|
|
317
|
+
}
|
|
318
|
+
if PNProps.has(props, "test_id") {
|
|
319
|
+
view.accessibilityIdentifier = PNProps.string(PNProps.value(props, "test_id"))
|
|
320
|
+
}
|
|
321
|
+
let role = PNProps.string(PNProps.value(props, "accessibility_role"))
|
|
322
|
+
let state = PNProps.dict(PNProps.value(props, "accessibility_state"))
|
|
323
|
+
if role == nil, state == nil { return }
|
|
324
|
+
var traits: UIAccessibilityTraits = []
|
|
325
|
+
if let role = role {
|
|
326
|
+
traits.formUnion(traitByRole[role.lowercased()] ?? [])
|
|
327
|
+
}
|
|
328
|
+
if let state = state {
|
|
329
|
+
if PNProps.bool(state["selected"]) == true || PNProps.bool(state["checked"]) == true {
|
|
330
|
+
traits.insert(.selected)
|
|
331
|
+
}
|
|
332
|
+
if PNProps.bool(state["disabled"]) == true { traits.insert(.notEnabled) }
|
|
333
|
+
if PNProps.bool(state["busy"]) == true { traits.insert(.updatesFrequently) }
|
|
334
|
+
if let expanded = PNProps.bool(state["expanded"]) {
|
|
335
|
+
view.accessibilityValue = expanded ? "expanded" : "collapsed"
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
view.accessibilityTraits = traits
|
|
339
|
+
}
|
|
340
|
+
}
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import UIKit
|
|
2
|
+
|
|
3
|
+
/// `VirtualList`: a `UITableView` whose rows host PythonNative subtrees.
|
|
4
|
+
///
|
|
5
|
+
/// Rows are requested synchronously from Python: `on_bind_row` with
|
|
6
|
+
/// `[{"container": key, "index": i, "width": w, "height": h}]` returns
|
|
7
|
+
/// `{"root": tag}`, and the view with that tag is added to the cell's
|
|
8
|
+
/// content view. Recycled containers emit `on_unbind_row` with
|
|
9
|
+
/// `[{"container": key}]`.
|
|
10
|
+
public final class PNVirtualListManager: PNComponentManager {
|
|
11
|
+
public override func makeView(props: [String: Any]) -> UIView {
|
|
12
|
+
let table = UITableView(frame: .zero, style: .plain)
|
|
13
|
+
table.separatorStyle = .none
|
|
14
|
+
table.contentInsetAdjustmentBehavior = .never
|
|
15
|
+
table.register(PNVirtualListCell.self, forCellReuseIdentifier: PNVirtualListCell.reuseIdentifier)
|
|
16
|
+
table.rowHeight = 44
|
|
17
|
+
table.estimatedRowHeight = 0
|
|
18
|
+
table.estimatedSectionHeaderHeight = 0
|
|
19
|
+
table.estimatedSectionFooterHeight = 0
|
|
20
|
+
if #available(iOS 15.0, *) {
|
|
21
|
+
table.sectionHeaderTopPadding = 0
|
|
22
|
+
}
|
|
23
|
+
return table
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
public override func createView(tag: Int64, props: [String: Any]) -> UIView {
|
|
27
|
+
let view = super.createView(tag: tag, props: props)
|
|
28
|
+
if let table = view as? UITableView {
|
|
29
|
+
let source = PNVirtualListSource(table: table)
|
|
30
|
+
table.dataSource = source
|
|
31
|
+
table.delegate = source
|
|
32
|
+
PNViewState.existing(for: table)?.retained.append(source)
|
|
33
|
+
}
|
|
34
|
+
return view
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
public override func teardown(view: UIView) {
|
|
38
|
+
guard let table = view as? UITableView else { return }
|
|
39
|
+
// The list's `d` op tells Python to unmount every subtree, so the
|
|
40
|
+
// cells only need to drop their hosted views.
|
|
41
|
+
for cell in table.visibleCells {
|
|
42
|
+
(cell as? PNVirtualListCell)?.unhost()
|
|
43
|
+
}
|
|
44
|
+
table.dataSource = nil
|
|
45
|
+
table.delegate = nil
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
public override func apply(view: UIView, props: [String: Any], initial: Bool) {
|
|
49
|
+
guard let table = view as? UITableView, let state = PNViewState.existing(for: table) else { return }
|
|
50
|
+
PNViewStyler.applyCommon(table, props)
|
|
51
|
+
var needsReload = false
|
|
52
|
+
if PNProps.has(props, "count") || PNProps.has(props, "row_height") || PNProps.has(props, "row_heights") {
|
|
53
|
+
needsReload = true
|
|
54
|
+
}
|
|
55
|
+
if PNProps.has(props, "row_height") {
|
|
56
|
+
table.rowHeight = CGFloat(PNProps.double(PNProps.value(props, "row_height")) ?? 44)
|
|
57
|
+
}
|
|
58
|
+
if PNProps.has(props, "shows_scroll_indicator") {
|
|
59
|
+
table.showsVerticalScrollIndicator = PNProps.bool(PNProps.value(props, "shows_scroll_indicator")) ?? true
|
|
60
|
+
}
|
|
61
|
+
if PNProps.has(props, "scroll_enabled") {
|
|
62
|
+
table.isScrollEnabled = PNProps.bool(PNProps.value(props, "scroll_enabled")) ?? true
|
|
63
|
+
}
|
|
64
|
+
if PNProps.has(props, "bounces") {
|
|
65
|
+
table.bounces = PNProps.bool(PNProps.value(props, "bounces")) ?? true
|
|
66
|
+
}
|
|
67
|
+
if PNProps.has(props, "separator") {
|
|
68
|
+
table.separatorStyle = PNProps.bool(PNProps.value(props, "separator")) == true ? .singleLine : .none
|
|
69
|
+
}
|
|
70
|
+
if PNProps.has(props, "content_inset") {
|
|
71
|
+
let inset = PNProps.dict(PNProps.value(props, "content_inset")) ?? [:]
|
|
72
|
+
table.contentInset = UIEdgeInsets(
|
|
73
|
+
top: CGFloat(PNProps.double(inset["top"]) ?? 0), left: CGFloat(PNProps.double(inset["left"]) ?? 0),
|
|
74
|
+
bottom: CGFloat(PNProps.double(inset["bottom"]) ?? 0), right: CGFloat(PNProps.double(inset["right"]) ?? 0)
|
|
75
|
+
)
|
|
76
|
+
}
|
|
77
|
+
if let mode = PNProps.string(PNProps.value(props, "keyboard_dismiss_mode")) {
|
|
78
|
+
switch mode {
|
|
79
|
+
case "on_drag": table.keyboardDismissMode = .onDrag
|
|
80
|
+
case "interactive": table.keyboardDismissMode = .interactive
|
|
81
|
+
default: table.keyboardDismissMode = .none
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if PNProps.has(props, "generation") || PNProps.has(props, "data_version") {
|
|
85
|
+
needsReload = true
|
|
86
|
+
}
|
|
87
|
+
if needsReload, !initial {
|
|
88
|
+
state.extras["reload_pending"] = true
|
|
89
|
+
table.reloadData()
|
|
90
|
+
state.extras["reload_pending"] = false
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
public override func setFrame(view: UIView, x: Double, y: Double, w: Double, h: Double) {
|
|
95
|
+
let before = view.bounds.size
|
|
96
|
+
super.setFrame(view: view, x: x, y: y, w: w, h: h)
|
|
97
|
+
if let table = view as? UITableView, before.width != table.bounds.width, before.width > 0 {
|
|
98
|
+
table.reloadData()
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
public override func insertChild(parent: UIView, child: UIView, index: Int) {
|
|
103
|
+
// Row subtrees are attached by the data source, never by the
|
|
104
|
+
// reconciler; static children are not supported.
|
|
105
|
+
PNLog.once(PNLog.components, key: "virtuallist-child", "VirtualList ignores direct children; rows come from on_bind_row")
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
public override func command(view: UIView, name: String, args: [String: Any]) -> Any? {
|
|
109
|
+
guard let table = view as? UITableView else { return nil }
|
|
110
|
+
let animated = PNProps.bool(args["animated"]) ?? true
|
|
111
|
+
switch name {
|
|
112
|
+
case "scroll_to_offset":
|
|
113
|
+
let y = CGFloat(PNProps.finite(args["y"] ?? args["offset"]))
|
|
114
|
+
let x = CGFloat(PNProps.finite(args["x"]))
|
|
115
|
+
table.setContentOffset(CGPoint(x: x, y: y), animated: animated)
|
|
116
|
+
case "scroll_to_index":
|
|
117
|
+
let count = table.numberOfRows(inSection: 0)
|
|
118
|
+
guard count > 0, let index = PNProps.int(args["index"]) else { return nil }
|
|
119
|
+
let row = max(0, min(index, count - 1))
|
|
120
|
+
let position: UITableView.ScrollPosition
|
|
121
|
+
switch PNProps.string(args["position"]) {
|
|
122
|
+
case "middle", "center": position = .middle
|
|
123
|
+
case "bottom", "end": position = .bottom
|
|
124
|
+
case "top", "start": position = .top
|
|
125
|
+
default: position = .none
|
|
126
|
+
}
|
|
127
|
+
table.scrollToRow(at: IndexPath(row: row, section: 0), at: position, animated: animated)
|
|
128
|
+
case "scroll_to_end":
|
|
129
|
+
let count = table.numberOfRows(inSection: 0)
|
|
130
|
+
if count > 0 {
|
|
131
|
+
table.scrollToRow(at: IndexPath(row: count - 1, section: 0), at: .bottom, animated: animated)
|
|
132
|
+
}
|
|
133
|
+
case "get_scroll_offset":
|
|
134
|
+
return ["x": Double(table.contentOffset.x), "y": Double(table.contentOffset.y)]
|
|
135
|
+
case "reload":
|
|
136
|
+
table.reloadData()
|
|
137
|
+
case "flash_scroll_indicators":
|
|
138
|
+
table.flashScrollIndicators()
|
|
139
|
+
default:
|
|
140
|
+
break
|
|
141
|
+
}
|
|
142
|
+
return nil
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/// A recycled row container. `containerKey` is stable for the cell's lifetime.
|
|
147
|
+
final class PNVirtualListCell: UITableViewCell {
|
|
148
|
+
static let reuseIdentifier = "PNVirtualListCell"
|
|
149
|
+
private static var nextKey: Int64 = 1
|
|
150
|
+
|
|
151
|
+
let containerKey: Int64
|
|
152
|
+
private(set) var hosted: UIView?
|
|
153
|
+
|
|
154
|
+
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
|
155
|
+
containerKey = PNVirtualListCell.nextKey
|
|
156
|
+
PNVirtualListCell.nextKey += 1
|
|
157
|
+
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
|
158
|
+
selectionStyle = .none
|
|
159
|
+
backgroundColor = .clear
|
|
160
|
+
contentView.clipsToBounds = true
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
required init?(coder: NSCoder) {
|
|
164
|
+
containerKey = PNVirtualListCell.nextKey
|
|
165
|
+
PNVirtualListCell.nextKey += 1
|
|
166
|
+
super.init(coder: coder)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
func host(_ view: UIView) {
|
|
170
|
+
if hosted === view { return }
|
|
171
|
+
hosted?.removeFromSuperview()
|
|
172
|
+
hosted = view
|
|
173
|
+
view.translatesAutoresizingMaskIntoConstraints = true
|
|
174
|
+
contentView.addSubview(view)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
func unhost() {
|
|
178
|
+
hosted?.removeFromSuperview()
|
|
179
|
+
hosted = nil
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/// Data source and delegate for one list.
|
|
184
|
+
final class PNVirtualListSource: NSObject, UITableViewDataSource, UITableViewDelegate {
|
|
185
|
+
private weak var table: UITableView?
|
|
186
|
+
private var bound: Set<Int64> = []
|
|
187
|
+
|
|
188
|
+
init(table: UITableView) {
|
|
189
|
+
self.table = table
|
|
190
|
+
super.init()
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
private var props: [String: Any] {
|
|
194
|
+
table.flatMap { PNViewState.existing(for: $0)?.props } ?? [:]
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// MARK: UITableViewDataSource
|
|
198
|
+
|
|
199
|
+
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
|
200
|
+
max(0, PNProps.int(PNProps.value(props, "count")) ?? 0)
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
|
|
204
|
+
let props = self.props
|
|
205
|
+
if let heights = PNProps.value(props, "row_heights") as? [Any], indexPath.row < heights.count,
|
|
206
|
+
let height = PNProps.double(heights[indexPath.row])
|
|
207
|
+
{
|
|
208
|
+
return CGFloat(height)
|
|
209
|
+
}
|
|
210
|
+
return CGFloat(PNProps.double(PNProps.value(props, "row_height")) ?? Double(tableView.rowHeight))
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
|
214
|
+
let dequeued = tableView.dequeueReusableCell(withIdentifier: PNVirtualListCell.reuseIdentifier, for: indexPath)
|
|
215
|
+
guard let cell = dequeued as? PNVirtualListCell else { return dequeued }
|
|
216
|
+
let width = tableView.bounds.width
|
|
217
|
+
let height = self.tableView(tableView, heightForRowAt: indexPath)
|
|
218
|
+
bind(cell, index: indexPath.row, width: width, height: height)
|
|
219
|
+
return cell
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
private func bind(_ cell: PNVirtualListCell, index: Int, width: CGFloat, height: CGFloat) {
|
|
223
|
+
guard let table = table else { return }
|
|
224
|
+
let payload: [String: Any] = [
|
|
225
|
+
"container": cell.containerKey, "index": index, "width": Double(width), "height": Double(height),
|
|
226
|
+
]
|
|
227
|
+
bound.insert(cell.containerKey)
|
|
228
|
+
guard let reply = PNEvents.emit(table, "on_bind_row", [payload]) else {
|
|
229
|
+
cell.unhost()
|
|
230
|
+
return
|
|
231
|
+
}
|
|
232
|
+
let root = PNProps.dict(PNJSON.decode(reply))?["root"]
|
|
233
|
+
guard let tag = PNProps.int(root), let view = PNViewRegistry.shared.view(for: Int64(tag)) else {
|
|
234
|
+
cell.unhost()
|
|
235
|
+
return
|
|
236
|
+
}
|
|
237
|
+
cell.host(view)
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
func tableView(_ tableView: UITableView, didEndDisplaying cell: UITableViewCell, forRowAt indexPath: IndexPath) {
|
|
241
|
+
guard let cell = cell as? PNVirtualListCell, let table = table else { return }
|
|
242
|
+
cell.unhost()
|
|
243
|
+
if bound.remove(cell.containerKey) != nil {
|
|
244
|
+
PNEvents.emit(table, "on_unbind_row", [["container": cell.containerKey]])
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// MARK: UIScrollViewDelegate
|
|
249
|
+
|
|
250
|
+
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
|
251
|
+
PNEvents.emitIfWired(scrollView, "on_scroll", [PNScrollPayload.make(scrollView)])
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
|
|
255
|
+
PNEvents.emitIfWired(scrollView, "on_momentum_scroll_end", [PNScrollPayload.make(scrollView)])
|
|
256
|
+
}
|
|
257
|
+
}
|