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.
Files changed (176) hide show
  1. pythonnative/__init__.py +6 -4
  2. pythonnative/_ios_log.py +1 -1
  3. pythonnative/alerts.py +34 -88
  4. pythonnative/bootstrap.py +90 -0
  5. pythonnative/bridge/__init__.py +306 -0
  6. pythonnative/bridge/android.py +78 -0
  7. pythonnative/bridge/codec.py +164 -0
  8. pythonnative/bridge/fake.py +236 -0
  9. pythonnative/bridge/ios.py +138 -0
  10. pythonnative/cli/pn.py +24 -6
  11. pythonnative/components/layout.py +65 -3
  12. pythonnative/gestures.py +13 -10
  13. pythonnative/hooks.py +41 -26
  14. pythonnative/hosts/__init__.py +13 -36
  15. pythonnative/hosts/native.py +348 -0
  16. pythonnative/images.py +6 -5
  17. pythonnative/mutations.py +21 -0
  18. pythonnative/native_modules/__init__.py +30 -29
  19. pythonnative/native_modules/app_state.py +10 -6
  20. pythonnative/native_modules/battery.py +20 -84
  21. pythonnative/native_modules/biometrics.py +11 -124
  22. pythonnative/native_modules/camera.py +15 -291
  23. pythonnative/native_modules/clipboard.py +14 -109
  24. pythonnative/native_modules/desktop.py +322 -0
  25. pythonnative/native_modules/file_system.py +15 -21
  26. pythonnative/native_modules/haptics.py +19 -129
  27. pythonnative/native_modules/linking.py +24 -137
  28. pythonnative/native_modules/location.py +28 -180
  29. pythonnative/native_modules/net_info.py +25 -135
  30. pythonnative/native_modules/notifications.py +36 -257
  31. pythonnative/native_modules/permissions.py +20 -184
  32. pythonnative/native_modules/registry.py +513 -0
  33. pythonnative/native_modules/secure_store.py +18 -155
  34. pythonnative/native_modules/share.py +8 -113
  35. pythonnative/native_views/__init__.py +67 -86
  36. pythonnative/native_views/bridge_backend.py +264 -0
  37. pythonnative/platform.py +19 -15
  38. pythonnative/platform_metrics.py +3 -3
  39. pythonnative/project/android.py +0 -3
  40. pythonnative/project/builder.py +38 -16
  41. pythonnative/project/config.py +12 -2
  42. pythonnative/project/plugins.py +366 -0
  43. pythonnative/py.typed +0 -0
  44. pythonnative/runtime.py +35 -203
  45. pythonnative/sdk/__init__.py +48 -33
  46. pythonnative/sdk/_components.py +57 -76
  47. pythonnative/storage.py +18 -243
  48. pythonnative/templates/android_template/app/build.gradle +6 -9
  49. pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/MainActivity.kt +34 -166
  50. pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/ScreenFragment.kt +13 -151
  51. pythonnative/templates/android_template/pythonnative/build.gradle +48 -0
  52. pythonnative/templates/android_template/pythonnative/src/main/AndroidManifest.xml +11 -0
  53. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/PNBridge.kt +214 -0
  54. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/PythonHost.kt +20 -0
  55. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/animation/AnimationSpecs.kt +141 -0
  56. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/animation/PNAnimator.kt +249 -0
  57. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/bridge/JsonUtil.kt +175 -0
  58. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/bridge/MainThread.kt +32 -0
  59. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/bridge/PNLog.kt +33 -0
  60. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/bridge/PNRegistry.kt +82 -0
  61. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/bridge/PNTransaction.kt +80 -0
  62. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/bridge/TransactionApplier.kt +115 -0
  63. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/bridge/ViewRegistry.kt +74 -0
  64. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/BuiltinComponents.kt +35 -0
  65. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ButtonManager.kt +33 -0
  66. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ComponentManager.kt +213 -0
  67. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ContainerManagers.kt +69 -0
  68. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ControlManagers.kt +186 -0
  69. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/DatePickerManager.kt +123 -0
  70. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ImageLoader.kt +141 -0
  71. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ImageManager.kt +156 -0
  72. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ModalManager.kt +134 -0
  73. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/PNColor.kt +201 -0
  74. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/PickerManager.kt +87 -0
  75. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/PortalManager.kt +55 -0
  76. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/PressableManager.kt +122 -0
  77. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ScrollViewManager.kt +214 -0
  78. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/SegmentedControlManager.kt +113 -0
  79. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/StatusBarManager.kt +52 -0
  80. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/TabBarManager.kt +131 -0
  81. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/TextInputManager.kt +245 -0
  82. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/TextManager.kt +233 -0
  83. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ViewStyler.kt +355 -0
  84. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/VirtualListManager.kt +179 -0
  85. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/WebViewManager.kt +149 -0
  86. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/gestures/GestureArbiter.kt +233 -0
  87. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/gestures/GestureCoordinator.kt +127 -0
  88. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/gestures/GestureRecognizers.kt +527 -0
  89. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/BuiltinModules.kt +73 -0
  90. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/DeviceModules.kt +316 -0
  91. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/HostModule.kt +97 -0
  92. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/MediaModules.kt +200 -0
  93. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/NativeModule.kt +135 -0
  94. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/NotificationsModule.kt +100 -0
  95. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/PermissionsModule.kt +88 -0
  96. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/StorageModules.kt +89 -0
  97. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/SystemModules.kt +178 -0
  98. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/plugins/GeneratedPlugins.kt +16 -0
  99. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/screens/Navigator.kt +116 -0
  100. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/screens/PNScreenFragment.kt +223 -0
  101. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/screens/ScreenRegistry.kt +80 -0
  102. pythonnative/templates/android_template/{app/src/main/java/com/pythonnative/android_template → pythonnative/src/main/java/com/pythonnative/runtime/views}/PNAccessibilityDelegate.kt +5 -4
  103. pythonnative/templates/android_template/{app/src/main/java/com/pythonnative/android_template → pythonnative/src/main/java/com/pythonnative/runtime/views}/PNBorderDrawable.kt +12 -3
  104. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/views/PNEditText.kt +18 -0
  105. pythonnative/templates/android_template/{app/src/main/java/com/pythonnative/android_template → pythonnative/src/main/java/com/pythonnative/runtime/views}/PNFrameLayout.kt +9 -10
  106. pythonnative/templates/android_template/{app/src/main/java/com/pythonnative/android_template → pythonnative/src/main/java/com/pythonnative/runtime/views}/PNVirtualListView.java +9 -12
  107. pythonnative/templates/android_template/pythonnative/src/test/java/com/pythonnative/runtime/AnimationSpecsTest.kt +75 -0
  108. pythonnative/templates/android_template/pythonnative/src/test/java/com/pythonnative/runtime/GestureArbiterTest.kt +197 -0
  109. pythonnative/templates/android_template/pythonnative/src/test/java/com/pythonnative/runtime/PNColorTest.kt +47 -0
  110. pythonnative/templates/android_template/pythonnative/src/test/java/com/pythonnative/runtime/PNTransactionTest.kt +78 -0
  111. pythonnative/templates/android_template/pythonnative/src/test/java/com/pythonnative/runtime/PromiseTest.kt +75 -0
  112. pythonnative/templates/android_template/settings.gradle +1 -0
  113. pythonnative/templates/ios_template/PythonNativeKit/Package.swift +21 -0
  114. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Animation/PNAnimator.swift +301 -0
  115. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Bridge/PNBridge.swift +191 -0
  116. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Bridge/PNRegistry.swift +140 -0
  117. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Bridge/PNTransaction.swift +165 -0
  118. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Bridge/PNViewRegistry.swift +53 -0
  119. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNButtonManagers.swift +206 -0
  120. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNColor.swift +173 -0
  121. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNComponentManager.swift +183 -0
  122. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNContainerView.swift +56 -0
  123. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNControlManagers.swift +281 -0
  124. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNImageManager.swift +221 -0
  125. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNOverlayManagers.swift +243 -0
  126. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNPressableManager.swift +114 -0
  127. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNScrollViewManager.swift +197 -0
  128. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNTabBarManager.swift +106 -0
  129. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNTextInputManager.swift +289 -0
  130. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNTextManager.swift +230 -0
  131. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNTransform.swift +82 -0
  132. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNViewManager.swift +86 -0
  133. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNViewState.swift +87 -0
  134. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNViewStyler.swift +340 -0
  135. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNVirtualListManager.swift +257 -0
  136. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNWebViewManager.swift +124 -0
  137. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Gestures/PNGestureCoordinator.swift +260 -0
  138. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Modules/AlertModule.swift +92 -0
  139. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Modules/HostModule.swift +158 -0
  140. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Modules/LifecycleModules.swift +174 -0
  141. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Modules/MediaModules.swift +177 -0
  142. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Modules/PNNativeModule.swift +125 -0
  143. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Modules/PermissionModules.swift +251 -0
  144. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Modules/SystemModules.swift +251 -0
  145. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Plugins/PNPluginRegistration.swift +8 -0
  146. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Screens/PNScreenRegistry.swift +46 -0
  147. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Screens/PNViewController.swift +251 -0
  148. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Support/PNCompat.swift +46 -0
  149. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Support/PNJSON.swift +173 -0
  150. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Support/PNLog.swift +47 -0
  151. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Support/PNWindow.swift +51 -0
  152. pythonnative/templates/ios_template/PythonNativeKit/Tests/PythonNativeKitTests/PNColorTests.swift +50 -0
  153. pythonnative/templates/ios_template/PythonNativeKit/Tests/PythonNativeKitTests/PNGestureTests.swift +101 -0
  154. pythonnative/templates/ios_template/PythonNativeKit/Tests/PythonNativeKitTests/PNManagerTests.swift +124 -0
  155. pythonnative/templates/ios_template/PythonNativeKit/Tests/PythonNativeKitTests/PNModuleTests.swift +98 -0
  156. pythonnative/templates/ios_template/PythonNativeKit/Tests/PythonNativeKitTests/PNTransactionTests.swift +91 -0
  157. pythonnative/templates/ios_template/ios_template/AppDelegate.swift +8 -50
  158. pythonnative/templates/ios_template/ios_template/Info.plist +4 -4
  159. pythonnative/templates/ios_template/ios_template/PythonRuntime.swift +38 -46
  160. pythonnative/templates/ios_template/ios_template/SceneDelegate.swift +11 -16
  161. pythonnative/templates/ios_template/ios_template/ViewController.swift +12 -142
  162. pythonnative/templates/ios_template/ios_template.xcodeproj/project.pbxproj +47 -1
  163. pythonnative/utils.py +4 -82
  164. pythonnative/virtual_rows.py +3 -3
  165. {pythonnative-0.35.0.dist-info → pythonnative-0.37.0.dist-info}/METADATA +3 -5
  166. pythonnative-0.37.0.dist-info/RECORD +263 -0
  167. pythonnative/hosts/android.py +0 -269
  168. pythonnative/hosts/ios.py +0 -404
  169. pythonnative/native_views/android.py +0 -3702
  170. pythonnative/native_views/ios.py +0 -5253
  171. pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/Navigator.kt +0 -43
  172. pythonnative-0.35.0.dist-info/RECORD +0 -155
  173. {pythonnative-0.35.0.dist-info → pythonnative-0.37.0.dist-info}/WHEEL +0 -0
  174. {pythonnative-0.35.0.dist-info → pythonnative-0.37.0.dist-info}/entry_points.txt +0 -0
  175. {pythonnative-0.35.0.dist-info → pythonnative-0.37.0.dist-info}/licenses/LICENSE +0 -0
  176. {pythonnative-0.35.0.dist-info → pythonnative-0.37.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,197 @@
1
+ import UIKit
2
+
3
+ /// `ScrollView`: a `UIScrollView` wrapping one layout-engine-sized child.
4
+ ///
5
+ /// Scroll offsets are reported through `on_scroll` with
6
+ /// `{"x", "y", "extent", "range", "content_width", "content_height"}`.
7
+ /// Commands: `scroll_to_offset`, `scroll_to_end`, `get_scroll_offset`,
8
+ /// `flash_scroll_indicators`.
9
+ public final class PNScrollViewManager: PNComponentManager {
10
+ public override func makeView(props: [String: Any]) -> UIView {
11
+ let scroll = UIScrollView(frame: .zero)
12
+ scroll.contentInsetAdjustmentBehavior = .never
13
+ return scroll
14
+ }
15
+
16
+ public override func createView(tag: Int64, props: [String: Any]) -> UIView {
17
+ let view = super.createView(tag: tag, props: props)
18
+ if let scroll = view as? UIScrollView {
19
+ let delegate = PNScrollDelegate()
20
+ scroll.delegate = delegate
21
+ PNViewState.existing(for: scroll)?.retained.append(delegate)
22
+ }
23
+ return view
24
+ }
25
+
26
+ public override func apply(view: UIView, props: [String: Any], initial: Bool) {
27
+ guard let scroll = view as? UIScrollView else { return }
28
+ PNViewStyler.applyCommon(scroll, props)
29
+ if PNProps.has(props, "refresh_control") {
30
+ applyRefresh(scroll, PNProps.value(props, "refresh_control"))
31
+ }
32
+ if PNProps.has(props, "shows_scroll_indicator") {
33
+ let visible = PNProps.bool(PNProps.value(props, "shows_scroll_indicator")) ?? true
34
+ scroll.showsVerticalScrollIndicator = visible
35
+ scroll.showsHorizontalScrollIndicator = visible
36
+ }
37
+ if PNProps.has(props, "paging_enabled") {
38
+ scroll.isPagingEnabled = PNProps.bool(PNProps.value(props, "paging_enabled")) ?? false
39
+ }
40
+ if PNProps.has(props, "bounces") {
41
+ scroll.bounces = PNProps.bool(PNProps.value(props, "bounces")) ?? true
42
+ }
43
+ if PNProps.has(props, "scroll_enabled") {
44
+ scroll.isScrollEnabled = PNProps.bool(PNProps.value(props, "scroll_enabled")) ?? true
45
+ }
46
+ if PNProps.has(props, "horizontal") {
47
+ let horizontal = PNProps.bool(PNProps.value(props, "horizontal")) ?? false
48
+ scroll.alwaysBounceHorizontal = horizontal
49
+ scroll.alwaysBounceVertical = !horizontal && scroll.refreshControl != nil
50
+ }
51
+ if let mode = PNProps.string(PNProps.value(props, "keyboard_dismiss_mode")) {
52
+ switch mode {
53
+ case "on_drag": scroll.keyboardDismissMode = .onDrag
54
+ case "interactive": scroll.keyboardDismissMode = .interactive
55
+ default: scroll.keyboardDismissMode = .none
56
+ }
57
+ }
58
+ if PNProps.has(props, "content_inset") {
59
+ let inset = PNProps.dict(PNProps.value(props, "content_inset")) ?? [:]
60
+ scroll.contentInset = UIEdgeInsets(
61
+ top: CGFloat(PNProps.double(inset["top"]) ?? 0), left: CGFloat(PNProps.double(inset["left"]) ?? 0),
62
+ bottom: CGFloat(PNProps.double(inset["bottom"]) ?? 0), right: CGFloat(PNProps.double(inset["right"]) ?? 0)
63
+ )
64
+ }
65
+ if PNProps.has(props, "scroll_indicator_insets") {
66
+ let inset = PNProps.dict(PNProps.value(props, "scroll_indicator_insets")) ?? [:]
67
+ let insets = UIEdgeInsets(
68
+ top: CGFloat(PNProps.double(inset["top"]) ?? 0), left: CGFloat(PNProps.double(inset["left"]) ?? 0),
69
+ bottom: CGFloat(PNProps.double(inset["bottom"]) ?? 0), right: CGFloat(PNProps.double(inset["right"]) ?? 0)
70
+ )
71
+ scroll.verticalScrollIndicatorInsets = insets
72
+ scroll.horizontalScrollIndicatorInsets = insets
73
+ }
74
+ if PNProps.has(props, "scroll_event_throttle") {
75
+ PNViewState.existing(for: scroll)?.extras["throttle"] = PNProps.double(PNProps.value(props, "scroll_event_throttle")) ?? 0
76
+ }
77
+ }
78
+
79
+ public override func command(view: UIView, name: String, args: [String: Any]) -> Any? {
80
+ guard let scroll = view as? UIScrollView else { return nil }
81
+ let animated = PNProps.bool(args["animated"]) ?? true
82
+ switch name {
83
+ case "scroll_to_offset":
84
+ let x = CGFloat(PNProps.finite(args["x"]))
85
+ let y = CGFloat(PNProps.finite(args["y"]))
86
+ scroll.setContentOffset(CGPoint(x: x, y: y), animated: animated)
87
+ case "scroll_to_end":
88
+ let content = scroll.contentSize
89
+ let bounds = scroll.bounds.size
90
+ let targetY = max(0, content.height - bounds.height)
91
+ let targetX = max(0, content.width - bounds.width)
92
+ let horizontal = content.width > bounds.width && content.height <= bounds.height
93
+ scroll.setContentOffset(horizontal ? CGPoint(x: targetX, y: 0) : CGPoint(x: 0, y: targetY), animated: animated)
94
+ case "get_scroll_offset":
95
+ return ["x": Double(scroll.contentOffset.x), "y": Double(scroll.contentOffset.y)]
96
+ case "flash_scroll_indicators":
97
+ scroll.flashScrollIndicators()
98
+ default:
99
+ break
100
+ }
101
+ return nil
102
+ }
103
+
104
+ // MARK: - Refresh control
105
+
106
+ private func applyRefresh(_ scroll: UIScrollView, _ spec: Any?) {
107
+ guard let spec = spec else {
108
+ if let existing = scroll.refreshControl {
109
+ existing.endRefreshing()
110
+ scroll.refreshControl = nil
111
+ scroll.alwaysBounceVertical = false
112
+ }
113
+ return
114
+ }
115
+ let control: UIRefreshControl
116
+ if let existing = scroll.refreshControl {
117
+ control = existing
118
+ } else {
119
+ control = UIRefreshControl()
120
+ scroll.refreshControl = control
121
+ // Content that fits its bounds never engages the pan gesture
122
+ // without this, making the refresh control unreachable.
123
+ scroll.alwaysBounceVertical = true
124
+ let target = PNActionTarget { [weak scroll] in
125
+ guard let scroll = scroll else { return }
126
+ PNEvents.emit(scroll, "on_refresh")
127
+ }
128
+ control.addTarget(target, action: #selector(PNActionTarget.fire(_:forEvent:)), for: .valueChanged)
129
+ PNViewState.existing(for: scroll)?.retained.append(target)
130
+ }
131
+ let dict = spec as? [String: Any] ?? [:]
132
+ if let color = PNColor.parse(PNProps.value(dict, "tint_color") ?? PNProps.value(dict, "color")) {
133
+ control.tintColor = color
134
+ }
135
+ if let title = PNProps.string(PNProps.value(dict, "title")) {
136
+ control.attributedTitle = NSAttributedString(string: title)
137
+ }
138
+ if PNProps.bool(dict["refreshing"]) == true {
139
+ if !control.isRefreshing { control.beginRefreshing() }
140
+ } else if control.isRefreshing {
141
+ control.endRefreshing()
142
+ }
143
+ }
144
+ }
145
+
146
+ /// Shared scroll payload builder.
147
+ enum PNScrollPayload {
148
+ static func make(_ scroll: UIScrollView) -> [String: Any] {
149
+ let offset = scroll.contentOffset
150
+ let bounds = scroll.bounds.size
151
+ let content = scroll.contentSize
152
+ let horizontal = content.width > bounds.width && content.height <= bounds.height
153
+ return [
154
+ "x": Double(offset.x),
155
+ "y": Double(offset.y),
156
+ "extent": Double(horizontal ? bounds.width : bounds.height),
157
+ "range": Double(horizontal ? content.width : content.height),
158
+ "content_width": Double(content.width),
159
+ "content_height": Double(content.height),
160
+ "width": Double(bounds.width),
161
+ "height": Double(bounds.height),
162
+ ]
163
+ }
164
+ }
165
+
166
+ /// Forwards `UIScrollViewDelegate` callbacks to Python events.
167
+ final class PNScrollDelegate: NSObject, UIScrollViewDelegate {
168
+ private var lastEmit: TimeInterval = 0
169
+
170
+ func scrollViewDidScroll(_ scrollView: UIScrollView) {
171
+ guard let state = PNViewState.existing(for: scrollView), state.hasEvent("on_scroll") else { return }
172
+ let throttle = (state.extras["throttle"] as? Double ?? 0) / 1000
173
+ let now = CACurrentMediaTime()
174
+ if throttle > 0, now - lastEmit < throttle, scrollView.isDragging || scrollView.isDecelerating { return }
175
+ lastEmit = now
176
+ PNEvents.emit(scrollView, "on_scroll", [PNScrollPayload.make(scrollView)])
177
+ }
178
+
179
+ func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
180
+ PNEvents.emitIfWired(scrollView, "on_scroll_begin_drag", [PNScrollPayload.make(scrollView)])
181
+ }
182
+
183
+ func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
184
+ PNEvents.emitIfWired(scrollView, "on_scroll_end_drag", [PNScrollPayload.make(scrollView)])
185
+ if !decelerate {
186
+ PNEvents.emitIfWired(scrollView, "on_momentum_scroll_end", [PNScrollPayload.make(scrollView)])
187
+ }
188
+ }
189
+
190
+ func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
191
+ PNEvents.emitIfWired(scrollView, "on_momentum_scroll_end", [PNScrollPayload.make(scrollView)])
192
+ }
193
+
194
+ func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) {
195
+ PNEvents.emitIfWired(scrollView, "on_scroll", [PNScrollPayload.make(scrollView)])
196
+ }
197
+ }
@@ -0,0 +1,106 @@
1
+ import UIKit
2
+
3
+ /// `TabBar`: a native `UITabBar`. Height is 49 pt plus the bottom safe
4
+ /// area so the bar reaches the home indicator (the screen root extends
5
+ /// past the bottom inset for this reason).
6
+ public final class PNTabBarManager: PNComponentManager {
7
+ public static let baseHeight: CGFloat = 49
8
+
9
+ /// The intrinsic bar height for the current bottom safe-area inset.
10
+ public static func height(for view: UIView?) -> CGFloat {
11
+ let bottom = view?.window?.safeAreaInsets.bottom ?? PNWindow.keyWindow()?.safeAreaInsets.bottom ?? 0
12
+ return baseHeight + bottom
13
+ }
14
+
15
+ public override func makeView(props: [String: Any]) -> UIView {
16
+ UITabBar(frame: CGRect(x: 0, y: 0, width: 0, height: PNTabBarManager.height(for: nil)))
17
+ }
18
+
19
+ public override func createView(tag: Int64, props: [String: Any]) -> UIView {
20
+ let view = super.createView(tag: tag, props: props)
21
+ if let bar = view as? UITabBar {
22
+ let delegate = PNTabBarDelegate()
23
+ bar.delegate = delegate
24
+ PNViewState.existing(for: bar)?.retained.append(delegate)
25
+ }
26
+ return view
27
+ }
28
+
29
+ public override func measure(view: UIView, maxW: CGFloat, maxH: CGFloat) -> CGSize {
30
+ let w = maxW.isFinite && maxW < 1e6 ? maxW : 320
31
+ return CGSize(width: w, height: PNTabBarManager.height(for: view))
32
+ }
33
+
34
+ public override func apply(view: UIView, props: [String: Any], initial: Bool) {
35
+ guard let bar = view as? UITabBar else { return }
36
+ let merged = mergedProps(bar)
37
+ let items = PNTabBarManager.items(merged)
38
+ if PNProps.has(props, "items") {
39
+ bar.setItems(items.enumerated().map { index, item in
40
+ let title = PNProps.string(item["title"]) ?? PNProps.string(item["name"]) ?? ""
41
+ let icon = PNTabBarManager.icon(item["icon"])
42
+ let barItem = UITabBarItem(title: title, image: icon, tag: index)
43
+ if let badge = PNProps.string(PNProps.value(item, "badge")) { barItem.badgeValue = badge }
44
+ return barItem
45
+ }, animated: false)
46
+ }
47
+ if PNProps.has(props, "active_tab") || PNProps.has(props, "active_index") || PNProps.has(props, "items") {
48
+ let activeName = PNProps.string(PNProps.value(merged, "active_tab"))
49
+ var index = PNProps.int(PNProps.value(merged, "active_index"))
50
+ if index == nil, let activeName = activeName {
51
+ index = items.firstIndex { PNProps.string($0["name"]) == activeName }
52
+ }
53
+ if let index = index, let barItems = bar.items, index >= 0, index < barItems.count {
54
+ bar.selectedItem = barItems[index]
55
+ }
56
+ }
57
+ if let color = PNColor.parse(PNProps.value(props, "active_color") ?? PNProps.value(props, "tint_color")) {
58
+ bar.tintColor = color
59
+ }
60
+ if let color = PNColor.parse(PNProps.value(props, "inactive_color")) {
61
+ bar.unselectedItemTintColor = color
62
+ }
63
+ if let color = PNColor.parse(PNProps.value(props, "background_color")) {
64
+ bar.barTintColor = color
65
+ bar.backgroundColor = color
66
+ let appearance = UITabBarAppearance()
67
+ appearance.configureWithOpaqueBackground()
68
+ appearance.backgroundColor = color
69
+ bar.standardAppearance = appearance
70
+ if #available(iOS 15.0, *) {
71
+ bar.scrollEdgeAppearance = appearance
72
+ }
73
+ }
74
+ if PNProps.has(props, "translucent") {
75
+ bar.isTranslucent = PNProps.bool(PNProps.value(props, "translucent")) ?? true
76
+ }
77
+ PNViewStyler.applyAccessibility(bar, props)
78
+ }
79
+
80
+ static func items(_ props: [String: Any]) -> [[String: Any]] {
81
+ ((PNProps.value(props, "items") as? [Any]) ?? []).compactMap { $0 as? [String: Any] }
82
+ }
83
+
84
+ /// Resolve an icon spec (SF Symbol name or `{"ios": name}`) to an image.
85
+ static func icon(_ spec: Any?) -> UIImage? {
86
+ var name: String?
87
+ if let text = spec as? String {
88
+ name = text
89
+ } else if let dict = spec as? [String: Any] {
90
+ name = PNProps.string(dict["ios"])
91
+ }
92
+ guard let name = name, !name.isEmpty else { return nil }
93
+ return UIImage(systemName: name) ?? UIImage(named: name)
94
+ }
95
+ }
96
+
97
+ /// Forwards `tabBar(_:didSelect:)` as `on_tab_select(name)` (plus `on_select(index)`).
98
+ final class PNTabBarDelegate: NSObject, UITabBarDelegate {
99
+ func tabBar(_ tabBar: UITabBar, didSelect item: UITabBarItem) {
100
+ let index = item.tag
101
+ let items = PNTabBarManager.items(PNViewState.existing(for: tabBar)?.props ?? [:])
102
+ guard index >= 0, index < items.count else { return }
103
+ PNEvents.emit(tabBar, "on_tab_select", [PNProps.string(items[index]["name"]) ?? ""])
104
+ PNEvents.emitIfWired(tabBar, "on_select", [index])
105
+ }
106
+ }
@@ -0,0 +1,289 @@
1
+ import UIKit
2
+
3
+ /// `TextInput`: a single-line `UITextField` or multiline `UITextView`.
4
+ ///
5
+ /// The class is chosen at creation from the `multiline` prop. Programmatic
6
+ /// `value` updates set a suppress flag so they don't echo into `on_change`.
7
+ public final class PNTextInputManager: PNComponentManager {
8
+ public override func makeView(props: [String: Any]) -> UIView {
9
+ if PNProps.bool(PNProps.value(props, "multiline")) == true {
10
+ let view = UITextView(frame: .zero)
11
+ view.font = UIFont.systemFont(ofSize: 17)
12
+ view.backgroundColor = .white
13
+ return view
14
+ }
15
+ let field = UITextField(frame: .zero)
16
+ field.borderStyle = .roundedRect
17
+ return field
18
+ }
19
+
20
+ public override func createView(tag: Int64, props: [String: Any]) -> UIView {
21
+ let view = super.createView(tag: tag, props: props)
22
+ let delegate = PNTextInputDelegate(view: view)
23
+ PNViewState.existing(for: view)?.retained.append(delegate)
24
+ if let field = view as? UITextField {
25
+ field.delegate = delegate
26
+ field.addTarget(delegate, action: #selector(PNTextInputDelegate.editingChanged(_:)), for: .editingChanged)
27
+ field.addTarget(delegate, action: #selector(PNTextInputDelegate.editingDidEndOnExit(_:)), for: .editingDidEndOnExit)
28
+ } else if let textView = view as? UITextView {
29
+ textView.delegate = delegate
30
+ }
31
+ return view
32
+ }
33
+
34
+ public override func apply(view: UIView, props: [String: Any], initial: Bool) {
35
+ guard let state = PNViewState.existing(for: view) else { return }
36
+ let field = view as? UITextField
37
+ let textView = view as? UITextView
38
+ if PNProps.has(props, "max_length") {
39
+ state.extras["max_length"] = PNProps.int(PNProps.value(props, "max_length")) as Any? ?? NSNull()
40
+ }
41
+ if let value = PNProps.string(PNProps.value(props, "value")) {
42
+ let current = field?.text ?? textView?.text ?? ""
43
+ if current != value {
44
+ PNTextInputManager.setText(view, value)
45
+ }
46
+ }
47
+ if let field = field {
48
+ if PNProps.has(props, "placeholder") {
49
+ field.placeholder = PNProps.string(PNProps.value(props, "placeholder")) ?? ""
50
+ }
51
+ if let color = PNColor.parse(PNProps.value(props, "placeholder_color")) {
52
+ let placeholder = PNProps.string(PNProps.value(state.props, "placeholder")) ?? ""
53
+ field.attributedPlaceholder = NSAttributedString(string: placeholder, attributes: [.foregroundColor: color])
54
+ }
55
+ if PNProps.has(props, "clear_button") {
56
+ field.clearButtonMode = PNProps.bool(PNProps.value(props, "clear_button")) == true ? .whileEditing : .never
57
+ }
58
+ }
59
+ if let size = PNProps.double(PNProps.value(props, "font_size")) {
60
+ let font = UIFont.systemFont(ofSize: CGFloat(size))
61
+ field?.font = font
62
+ textView?.font = font
63
+ }
64
+ if let color = PNColor.parse(PNProps.value(props, "color")) {
65
+ field?.textColor = color
66
+ textView?.textColor = color
67
+ }
68
+ if let color = PNColor.parse(PNProps.value(props, "background_color")) {
69
+ view.backgroundColor = color
70
+ }
71
+ if PNProps.has(props, "secure") {
72
+ let secure = PNProps.bool(PNProps.value(props, "secure")) ?? false
73
+ field?.isSecureTextEntry = secure
74
+ textView?.isSecureTextEntry = secure
75
+ }
76
+ if let keyboard = PNProps.string(PNProps.value(props, "keyboard_type")) {
77
+ let type = PNTextInputManager.keyboardType(keyboard)
78
+ field?.keyboardType = type
79
+ textView?.keyboardType = type
80
+ }
81
+ if let cap = PNProps.string(PNProps.value(props, "auto_capitalize")) {
82
+ let type = PNTextInputManager.capitalization(cap)
83
+ field?.autocapitalizationType = type
84
+ textView?.autocapitalizationType = type
85
+ }
86
+ if PNProps.has(props, "auto_correct") {
87
+ let type: UITextAutocorrectionType = PNProps.bool(PNProps.value(props, "auto_correct")) == true ? .yes : .no
88
+ field?.autocorrectionType = type
89
+ textView?.autocorrectionType = type
90
+ }
91
+ if let key = PNProps.string(PNProps.value(props, "return_key_type")) {
92
+ let type = PNTextInputManager.returnKey(key)
93
+ field?.returnKeyType = type
94
+ textView?.returnKeyType = type
95
+ }
96
+ if let color = PNColor.parse(PNProps.value(props, "selection_color")) {
97
+ view.tintColor = color
98
+ }
99
+ if PNProps.has(props, "text_content_type") {
100
+ let type = PNProps.string(PNProps.value(props, "text_content_type")).flatMap(PNTextInputManager.contentType)
101
+ field?.textContentType = type
102
+ textView?.textContentType = type
103
+ }
104
+ if PNProps.has(props, "editable") {
105
+ let editable = PNProps.bool(PNProps.value(props, "editable")) ?? true
106
+ field?.isEnabled = editable
107
+ textView?.isEditable = editable
108
+ }
109
+ if PNProps.bool(PNProps.value(props, "auto_focus")) == true {
110
+ view.becomeFirstResponder()
111
+ }
112
+ PNViewStyler.applyDecoration(view, props)
113
+ }
114
+
115
+ public override func measure(view: UIView, maxW: CGFloat, maxH: CGFloat) -> CGSize {
116
+ let size = super.measure(view: view, maxW: maxW, maxH: maxH)
117
+ return CGSize(width: max(size.width, 100), height: max(size.height, 36))
118
+ }
119
+
120
+ public override func command(view: UIView, name: String, args: [String: Any]) -> Any? {
121
+ switch name {
122
+ case "focus":
123
+ view.becomeFirstResponder()
124
+ case "blur":
125
+ view.resignFirstResponder()
126
+ case "clear":
127
+ PNTextInputManager.setText(view, "")
128
+ case "get_value":
129
+ return (view as? UITextField)?.text ?? (view as? UITextView)?.text ?? ""
130
+ case "set_selection":
131
+ if let start = PNProps.int(args["start"]) {
132
+ let end = PNProps.int(args["end"]) ?? start
133
+ PNTextInputManager.setSelection(view, start: start, end: end)
134
+ }
135
+ default:
136
+ break
137
+ }
138
+ return nil
139
+ }
140
+
141
+ // MARK: - Helpers
142
+
143
+ static func setText(_ view: UIView, _ text: String) {
144
+ guard let state = PNViewState.existing(for: view) else { return }
145
+ state.extras["suppress"] = true
146
+ (view as? UITextField)?.text = text
147
+ (view as? UITextView)?.text = text
148
+ state.extras["suppress"] = false
149
+ }
150
+
151
+ static func setSelection(_ view: UIView, start: Int, end: Int) {
152
+ guard let input = view as? UITextInput else { return }
153
+ let begin = input.beginningOfDocument
154
+ guard let from = input.position(from: begin, offset: start),
155
+ let to = input.position(from: begin, offset: end)
156
+ else { return }
157
+ input.selectedTextRange = input.textRange(from: from, to: to)
158
+ }
159
+
160
+ static func keyboardType(_ name: String) -> UIKeyboardType {
161
+ switch name {
162
+ case "ascii": return .asciiCapable
163
+ case "numbers_and_punctuation": return .numbersAndPunctuation
164
+ case "url": return .URL
165
+ case "number_pad", "numeric": return .numberPad
166
+ case "phone_pad": return .phonePad
167
+ case "email_address", "email": return .emailAddress
168
+ case "decimal_pad", "decimal": return .decimalPad
169
+ case "web_search": return .webSearch
170
+ default: return .default
171
+ }
172
+ }
173
+
174
+ static func capitalization(_ name: String) -> UITextAutocapitalizationType {
175
+ switch name {
176
+ case "none": return .none
177
+ case "words": return .words
178
+ case "characters": return .allCharacters
179
+ default: return .sentences
180
+ }
181
+ }
182
+
183
+ static func returnKey(_ name: String) -> UIReturnKeyType {
184
+ switch name {
185
+ case "go": return .go
186
+ case "google": return .google
187
+ case "join": return .join
188
+ case "next": return .next
189
+ case "route": return .route
190
+ case "search": return .search
191
+ case "send": return .send
192
+ case "yahoo": return .yahoo
193
+ case "done": return .done
194
+ default: return .default
195
+ }
196
+ }
197
+
198
+ static func contentType(_ name: String) -> UITextContentType? {
199
+ switch name.trimmingCharacters(in: .whitespaces).lowercased() {
200
+ case "username": return .username
201
+ case "password": return .password
202
+ case "new_password": return .newPassword
203
+ case "one_time_code": return .oneTimeCode
204
+ case "email", "email_address": return .emailAddress
205
+ case "name": return .name
206
+ case "url": return .URL
207
+ case "telephone", "telephone_number", "phone", "phone_number": return .telephoneNumber
208
+ default: return nil
209
+ }
210
+ }
211
+ }
212
+
213
+ /// Delegate and control target for both input classes.
214
+ final class PNTextInputDelegate: NSObject, UITextFieldDelegate, UITextViewDelegate {
215
+ private weak var view: UIView?
216
+
217
+ init(view: UIView) {
218
+ self.view = view
219
+ }
220
+
221
+ private func currentText() -> String {
222
+ (view as? UITextField)?.text ?? (view as? UITextView)?.text ?? ""
223
+ }
224
+
225
+ private func emitChange() {
226
+ guard let view = view, let state = PNViewState.existing(for: view), !state.flag("suppress") else { return }
227
+ var text = currentText()
228
+ if let maxLength = state.extras["max_length"] as? Int, maxLength >= 0, text.count > maxLength {
229
+ text = String(text.prefix(maxLength))
230
+ PNTextInputManager.setText(view, text)
231
+ }
232
+ PNEvents.emit(view, "on_change", [text])
233
+ }
234
+
235
+ private func emitSelection() {
236
+ guard let view = view, let state = PNViewState.existing(for: view), state.hasEvent("on_selection_change"),
237
+ let input = view as? UITextInput, let range = input.selectedTextRange
238
+ else { return }
239
+ let start = input.offset(from: input.beginningOfDocument, to: range.start)
240
+ let end = input.offset(from: input.beginningOfDocument, to: range.end)
241
+ PNEvents.emit(view, "on_selection_change", [["start": start, "end": end]])
242
+ }
243
+
244
+ // MARK: UITextField
245
+
246
+ @objc func editingChanged(_ sender: Any?) {
247
+ emitChange()
248
+ }
249
+
250
+ @objc func editingDidEndOnExit(_ sender: Any?) {
251
+ guard let view = view else { return }
252
+ PNEvents.emit(view, "on_submit", [currentText()])
253
+ }
254
+
255
+ func textFieldShouldReturn(_ textField: UITextField) -> Bool {
256
+ textField.resignFirstResponder()
257
+ return true
258
+ }
259
+
260
+ func textFieldDidBeginEditing(_ textField: UITextField) {
261
+ if let view = view { PNEvents.emit(view, "on_focus") }
262
+ }
263
+
264
+ func textFieldDidEndEditing(_ textField: UITextField) {
265
+ if let view = view { PNEvents.emit(view, "on_blur") }
266
+ }
267
+
268
+ func textFieldDidChangeSelection(_ textField: UITextField) {
269
+ emitSelection()
270
+ }
271
+
272
+ // MARK: UITextView
273
+
274
+ func textViewDidChange(_ textView: UITextView) {
275
+ emitChange()
276
+ }
277
+
278
+ func textViewDidBeginEditing(_ textView: UITextView) {
279
+ if let view = view { PNEvents.emit(view, "on_focus") }
280
+ }
281
+
282
+ func textViewDidEndEditing(_ textView: UITextView) {
283
+ if let view = view { PNEvents.emit(view, "on_blur") }
284
+ }
285
+
286
+ func textViewDidChangeSelection(_ textView: UITextView) {
287
+ emitSelection()
288
+ }
289
+ }