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,221 @@
1
+ import CryptoKit
2
+ import UIKit
3
+
4
+ /// `Image`: a `UIImageView` loading from http(s) URLs (with memory and
5
+ /// disk caching), local file paths, bundle asset names, and base64 data URIs.
6
+ public final class PNImageManager: PNComponentManager {
7
+ public override func makeView(props: [String: Any]) -> UIView {
8
+ let view = UIImageView(frame: .zero)
9
+ view.clipsToBounds = true
10
+ view.contentMode = .scaleAspectFit
11
+ return view
12
+ }
13
+
14
+ public override func apply(view: UIView, props: [String: Any], initial: Bool) {
15
+ guard let imageView = view as? UIImageView else { return }
16
+ if let tint = PNColor.parse(PNProps.value(props, "tint_color") ?? PNProps.value(props, "tint")) {
17
+ imageView.tintColor = tint
18
+ if let image = imageView.image {
19
+ imageView.image = image.withRenderingMode(.alwaysTemplate)
20
+ }
21
+ }
22
+ if let placeholder = PNColor.parse(PNProps.value(props, "placeholder_color")) {
23
+ imageView.backgroundColor = placeholder
24
+ }
25
+ if let source = PNProps.string(PNProps.value(props, "source")), !source.isEmpty {
26
+ load(imageView, source: source)
27
+ }
28
+ if let mode = PNProps.string(PNProps.value(props, "scale_type") ?? PNProps.value(props, "resize_mode")) {
29
+ imageView.contentMode = PNImageManager.contentMode(mode)
30
+ }
31
+ PNViewStyler.applyCommon(imageView, props)
32
+ }
33
+
34
+ public override func measure(view: UIView, maxW: CGFloat, maxH: CGFloat) -> CGSize {
35
+ guard let image = (view as? UIImageView)?.image else { return .zero }
36
+ var size = image.size
37
+ if maxW.isFinite, maxW > 0, size.width > maxW {
38
+ let scale = maxW / size.width
39
+ size = CGSize(width: maxW, height: size.height * scale)
40
+ }
41
+ return size
42
+ }
43
+
44
+ static func contentMode(_ name: String) -> UIView.ContentMode {
45
+ switch name {
46
+ case "cover": return .scaleAspectFill
47
+ case "stretch": return .scaleToFill
48
+ case "center": return .center
49
+ case "repeat": return .scaleAspectFill
50
+ default: return .scaleAspectFit
51
+ }
52
+ }
53
+
54
+ // MARK: - Loading
55
+
56
+ private func load(_ imageView: UIImageView, source: String) {
57
+ guard let state = PNViewState.existing(for: imageView) else { return }
58
+ state.extras["pending_source"] = source
59
+ if source.hasPrefix("data:") {
60
+ loadDataURI(imageView, source)
61
+ } else if source.hasPrefix("http://") || source.hasPrefix("https://") {
62
+ PNImageLoader.shared.fetch(source) { [weak imageView] result in
63
+ guard let imageView = imageView, PNViewState.existing(for: imageView)?.extras["pending_source"] as? String == source else { return }
64
+ switch result {
65
+ case .success(let data):
66
+ if let image = PNImageManager.decode(data, targetSize: imageView.bounds.size) {
67
+ self.setImage(imageView, image)
68
+ PNEvents.emit(imageView, "on_load", [["width": Double(image.size.width), "height": Double(image.size.height)]])
69
+ } else {
70
+ PNEvents.emit(imageView, "on_error", ["decode failed"])
71
+ }
72
+ case .failure(let error):
73
+ PNEvents.emit(imageView, "on_error", [error.localizedDescription])
74
+ }
75
+ }
76
+ } else {
77
+ var image = UIImage(named: source)
78
+ if image == nil {
79
+ let path = source.hasPrefix("file://") ? (URL(string: source)?.path ?? source) : source
80
+ image = UIImage(contentsOfFile: path)
81
+ }
82
+ if let image = image {
83
+ setImage(imageView, image)
84
+ PNEvents.emit(imageView, "on_load", [["width": Double(image.size.width), "height": Double(image.size.height)]])
85
+ } else {
86
+ PNEvents.emit(imageView, "on_error", ["image '\(source)' not found"])
87
+ }
88
+ }
89
+ }
90
+
91
+ private func setImage(_ imageView: UIImageView, _ image: UIImage) {
92
+ let tinted = PNProps.value(mergedProps(imageView), "tint_color") ?? PNProps.value(mergedProps(imageView), "tint")
93
+ imageView.image = tinted != nil ? image.withRenderingMode(.alwaysTemplate) : image
94
+ }
95
+
96
+ private func loadDataURI(_ imageView: UIImageView, _ source: String) {
97
+ guard let comma = source.firstIndex(of: ","),
98
+ let data = Data(base64Encoded: String(source[source.index(after: comma)...]), options: [.ignoreUnknownCharacters]),
99
+ let image = UIImage(data: data)
100
+ else {
101
+ PNEvents.emit(imageView, "on_error", ["data URI decode failed"])
102
+ return
103
+ }
104
+ setImage(imageView, image)
105
+ PNEvents.emit(imageView, "on_load", [["width": Double(image.size.width), "height": Double(image.size.height)]])
106
+ }
107
+
108
+ /// Decode `data`, downsampling when the bitmap is more than 2x the target.
109
+ static func decode(_ data: Data, targetSize: CGSize) -> UIImage? {
110
+ guard let image = UIImage(data: data) else { return nil }
111
+ guard targetSize.width > 0, targetSize.height > 0 else { return image }
112
+ let size = image.size
113
+ if size.width <= targetSize.width * 2, size.height <= targetSize.height * 2 { return image }
114
+ let scale = min(targetSize.width * 2 / size.width, targetSize.height * 2 / size.height)
115
+ let newSize = CGSize(width: size.width * scale, height: size.height * scale)
116
+ let renderer = UIGraphicsImageRenderer(size: newSize)
117
+ return renderer.image { _ in image.draw(in: CGRect(origin: .zero, size: newSize)) }
118
+ }
119
+ }
120
+
121
+ /// Shared image pipeline: memory LRU + disk cache under `Caches/pn_images`,
122
+ /// request deduplication, delivery on the main queue.
123
+ public final class PNImageLoader {
124
+ public static let shared = PNImageLoader()
125
+
126
+ public enum LoadError: Error, LocalizedError {
127
+ case badURL
128
+ case http(Int)
129
+ case empty
130
+
131
+ public var errorDescription: String? {
132
+ switch self {
133
+ case .badURL: return "invalid image URL"
134
+ case .http(let code): return "HTTP \(code)"
135
+ case .empty: return "empty response"
136
+ }
137
+ }
138
+ }
139
+
140
+ private let memory = NSCache<NSString, NSData>()
141
+ private var inflight: [String: [(Result<Data, Error>) -> Void]] = [:]
142
+ private let session: URLSession
143
+ private let queue = DispatchQueue(label: "com.pythonnative.images", qos: .utility)
144
+ private lazy var directory: URL = {
145
+ let caches = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first ?? URL(fileURLWithPath: NSTemporaryDirectory())
146
+ let dir = caches.appendingPathComponent("pn_images", isDirectory: true)
147
+ try? FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true)
148
+ return dir
149
+ }()
150
+
151
+ private init() {
152
+ memory.totalCostLimit = 16 * 1024 * 1024
153
+ let config = URLSessionConfiguration.default
154
+ config.requestCachePolicy = .returnCacheDataElseLoad
155
+ session = URLSession(configuration: config)
156
+ }
157
+
158
+ /// Fetch `url`, delivering the raw bytes on the main queue.
159
+ public func fetch(_ url: String, completion: @escaping (Result<Data, Error>) -> Void) {
160
+ if let cached = memory.object(forKey: url as NSString) {
161
+ completion(.success(cached as Data))
162
+ return
163
+ }
164
+ if inflight[url] != nil {
165
+ inflight[url]?.append(completion)
166
+ return
167
+ }
168
+ inflight[url] = [completion]
169
+ let path = cachePath(for: url)
170
+ queue.async {
171
+ if let data = try? Data(contentsOf: path), !data.isEmpty {
172
+ self.deliver(url, .success(data))
173
+ return
174
+ }
175
+ guard let remote = URL(string: url) else {
176
+ self.deliver(url, .failure(LoadError.badURL))
177
+ return
178
+ }
179
+ let task = self.session.dataTask(with: remote) { data, response, error in
180
+ if let error = error {
181
+ self.deliver(url, .failure(error))
182
+ return
183
+ }
184
+ if let http = response as? HTTPURLResponse, !(200..<300).contains(http.statusCode) {
185
+ self.deliver(url, .failure(LoadError.http(http.statusCode)))
186
+ return
187
+ }
188
+ guard let data = data, !data.isEmpty else {
189
+ self.deliver(url, .failure(LoadError.empty))
190
+ return
191
+ }
192
+ try? data.write(to: path, options: [.atomic])
193
+ self.deliver(url, .success(data))
194
+ }
195
+ task.resume()
196
+ }
197
+ }
198
+
199
+ /// Remove every cached image (memory and disk).
200
+ public func clear() {
201
+ memory.removeAllObjects()
202
+ try? FileManager.default.removeItem(at: directory)
203
+ try? FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)
204
+ }
205
+
206
+ private func deliver(_ url: String, _ result: Result<Data, Error>) {
207
+ DispatchQueue.main.async {
208
+ if case .success(let data) = result {
209
+ self.memory.setObject(data as NSData, forKey: url as NSString, cost: data.count)
210
+ }
211
+ let waiters = self.inflight.removeValue(forKey: url) ?? []
212
+ for waiter in waiters { waiter(result) }
213
+ }
214
+ }
215
+
216
+ private func cachePath(for url: String) -> URL {
217
+ let digest = SHA256.hash(data: Data(url.utf8)).map { String(format: "%02x", $0) }.joined()
218
+ let ext = URL(string: url)?.pathExtension ?? ""
219
+ return directory.appendingPathComponent(ext.isEmpty ? digest : "\(digest).\(ext)")
220
+ }
221
+ }
@@ -0,0 +1,243 @@
1
+ import UIKit
2
+
3
+ /// `Modal`: a hidden on-tree placeholder whose children mount into a
4
+ /// presented `UIViewController` while `visible` is true.
5
+ public final class PNModalManager: PNComponentManager {
6
+ final class Presentation {
7
+ let controller: UIViewController
8
+ let content: UIView
9
+ init(controller: UIViewController, content: UIView) {
10
+ self.controller = controller
11
+ self.content = content
12
+ }
13
+ }
14
+
15
+ public override func makeView(props: [String: Any]) -> UIView {
16
+ let placeholder = UIView(frame: .zero)
17
+ placeholder.isHidden = true
18
+ return placeholder
19
+ }
20
+
21
+ public override func apply(view: UIView, props: [String: Any], initial: Bool) {
22
+ guard let state = PNViewState.existing(for: view) else { return }
23
+ let visible = PNProps.bool(PNProps.value(state.props, "visible")) ?? false
24
+ let presented = state.extras["modal"] is Presentation
25
+ if visible, !presented {
26
+ present(view, state)
27
+ } else if !visible, presented {
28
+ dismiss(view, state, emit: true)
29
+ }
30
+ }
31
+
32
+ public override func teardown(view: UIView) {
33
+ if let state = PNViewState.existing(for: view), state.extras["modal"] is Presentation {
34
+ dismiss(view, state, emit: false)
35
+ }
36
+ }
37
+
38
+ public override func childContainer(for view: UIView) -> UIView {
39
+ guard let state = PNViewState.existing(for: view), let modal = state.extras["modal"] as? Presentation else {
40
+ return view
41
+ }
42
+ return modal.content
43
+ }
44
+
45
+ public override func insertChild(parent: UIView, child: UIView, index: Int) {
46
+ if let state = PNViewState.existing(for: parent), !(state.extras["modal"] is Presentation) {
47
+ var pending = (state.extras["pending_children"] as? [UIView]) ?? []
48
+ pending.removeAll { $0 === child }
49
+ pending.insert(child, at: max(0, min(index, pending.count)))
50
+ state.extras["pending_children"] = pending
51
+ return
52
+ }
53
+ super.insertChild(parent: parent, child: child, index: index)
54
+ }
55
+
56
+ public override func removeChild(parent: UIView, child: UIView) {
57
+ super.removeChild(parent: parent, child: child)
58
+ if let state = PNViewState.existing(for: parent), var pending = state.extras["pending_children"] as? [UIView] {
59
+ pending.removeAll { $0 === child }
60
+ state.extras["pending_children"] = pending
61
+ }
62
+ }
63
+
64
+ public override func setFrame(view: UIView, x: Double, y: Double, w: Double, h: Double) {}
65
+
66
+ public override func measure(view: UIView, maxW: CGFloat, maxH: CGFloat) -> CGSize { .zero }
67
+
68
+ private func present(_ placeholder: UIView, _ state: PNViewState) {
69
+ let props = state.props
70
+ let controller = PNModalViewController()
71
+ controller.onDismissed = { [weak placeholder] in
72
+ guard let placeholder = placeholder, let state = PNViewState.existing(for: placeholder),
73
+ state.extras["modal"] is Presentation
74
+ else { return }
75
+ state.extras.removeValue(forKey: "modal")
76
+ PNEvents.emit(placeholder, "on_dismiss")
77
+ PNEvents.emitIfWired(placeholder, "on_request_close")
78
+ }
79
+ let style = PNProps.string(PNProps.value(props, "presentation_style")) ?? "page_sheet"
80
+ let isOverlay = style == "overlay" || PNProps.bool(PNProps.value(props, "transparent")) == true
81
+ let content = UIView(frame: controller.view.bounds)
82
+ content.backgroundColor = isOverlay ? .clear : PNColor.parse(PNProps.value(props, "background_color")) ?? .systemBackground
83
+ content.autoresizingMask = [.flexibleWidth, .flexibleHeight]
84
+ controller.view.addSubview(content)
85
+ controller.view.backgroundColor = isOverlay ? UIColor.black.withAlphaComponent(0.4) : content.backgroundColor
86
+ switch isOverlay ? "overlay" : style {
87
+ case "full_screen": controller.modalPresentationStyle = .fullScreen
88
+ case "form_sheet": controller.modalPresentationStyle = .formSheet
89
+ case "overlay": controller.modalPresentationStyle = .overCurrentContext
90
+ default: controller.modalPresentationStyle = .pageSheet
91
+ }
92
+ switch PNProps.string(PNProps.value(props, "animation_type")) ?? PNProps.string(PNProps.value(props, "animation")) {
93
+ case "fade": controller.modalTransitionStyle = .crossDissolve
94
+ case "flip": controller.modalTransitionStyle = .flipHorizontal
95
+ default: controller.modalTransitionStyle = .coverVertical
96
+ }
97
+ if !isOverlay, PNProps.bool(PNProps.value(props, "dismiss_on_backdrop")) == false {
98
+ controller.isModalInPresentation = true
99
+ }
100
+ let presentation = Presentation(controller: controller, content: content)
101
+ state.extras["modal"] = presentation
102
+ for child in (state.extras.removeValue(forKey: "pending_children") as? [UIView]) ?? [] {
103
+ child.translatesAutoresizingMaskIntoConstraints = true
104
+ content.addSubview(child)
105
+ }
106
+ let animated = PNProps.string(PNProps.value(props, "animation_type")) != "none"
107
+ guard let top = PNWindow.topViewController() else {
108
+ state.extras.removeValue(forKey: "modal")
109
+ return
110
+ }
111
+ top.present(controller, animated: animated) { [weak placeholder] in
112
+ if let placeholder = placeholder { PNEvents.emit(placeholder, "on_show") }
113
+ }
114
+ }
115
+
116
+ private func dismiss(_ placeholder: UIView, _ state: PNViewState, emit: Bool) {
117
+ guard let modal = state.extras.removeValue(forKey: "modal") as? Presentation else { return }
118
+ (modal.controller as? PNModalViewController)?.onDismissed = nil
119
+ let animated = PNProps.string(PNProps.value(state.props, "animation_type")) != "none"
120
+ modal.controller.dismiss(animated: animated)
121
+ if emit { PNEvents.emit(placeholder, "on_dismiss") }
122
+ }
123
+ }
124
+
125
+ /// Reports interactive dismissal (sheet swipe) back to the manager.
126
+ final class PNModalViewController: UIViewController {
127
+ var onDismissed: (() -> Void)?
128
+
129
+ override func viewDidDisappear(_ animated: Bool) {
130
+ super.viewDidDisappear(animated)
131
+ if isBeingDismissed || presentingViewController == nil {
132
+ onDismissed?()
133
+ }
134
+ }
135
+ }
136
+
137
+ /// Overlay view that only claims touches landing on one of its subviews.
138
+ final class PNPortalView: UIView {
139
+ override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
140
+ for sub in subviews where !sub.isHidden && sub.alpha > 0.01 && sub.isUserInteractionEnabled {
141
+ if sub.frame.contains(point) { return true }
142
+ }
143
+ return false
144
+ }
145
+ }
146
+
147
+ /// `Portal`: floats children over the screen in a top-level overlay that
148
+ /// mirrors the screen host's root frame.
149
+ public final class PNPortalManager: PNComponentManager {
150
+ public override func makeView(props: [String: Any]) -> UIView {
151
+ let overlay = PNPortalView(frame: .zero)
152
+ overlay.backgroundColor = .clear
153
+ return overlay
154
+ }
155
+
156
+ public override func apply(view: UIView, props: [String: Any], initial: Bool) {
157
+ PNViewStyler.applyCommon(view, props)
158
+ if !initial { ensureAttached(view) }
159
+ }
160
+
161
+ public override func insertChild(parent: UIView, child: UIView, index: Int) {
162
+ ensureAttached(parent)
163
+ super.insertChild(parent: parent, child: child, index: index)
164
+ }
165
+
166
+ public override func setFrame(view: UIView, x: Double, y: Double, w: Double, h: Double) {}
167
+
168
+ public override func measure(view: UIView, maxW: CGFloat, maxH: CGFloat) -> CGSize { .zero }
169
+
170
+ private func ensureAttached(_ overlay: UIView) {
171
+ guard let host = PNWindow.topViewController()?.view else { return }
172
+ if overlay.superview === host {
173
+ if host.subviews.last !== overlay {
174
+ host.bringSubviewToFront(overlay)
175
+ }
176
+ syncFrame(overlay, host: host)
177
+ return
178
+ }
179
+ overlay.removeFromSuperview()
180
+ host.addSubview(overlay)
181
+ syncFrame(overlay, host: host)
182
+ }
183
+
184
+ static func rootFrame(in host: UIView) -> CGRect {
185
+ let bounds = host.bounds
186
+ let insets = host.safeAreaInsets
187
+ let w = max(0, bounds.width - insets.left - insets.right)
188
+ let h = max(0, bounds.height - insets.top)
189
+ return CGRect(x: insets.left, y: insets.top, width: w, height: h)
190
+ }
191
+
192
+ private func syncFrame(_ overlay: UIView, host: UIView) {
193
+ overlay.frame = PNPortalManager.rootFrame(in: host)
194
+ overlay.autoresizingMask = [.flexibleWidth, .flexibleHeight]
195
+ }
196
+ }
197
+
198
+ /// Process-wide status bar state consulted by `PNViewController`.
199
+ public enum PNStatusBarState {
200
+ public static var hidden = false
201
+ public static var style: UIStatusBarStyle = .default
202
+ public static var animation: UIStatusBarAnimation = .fade
203
+
204
+ static func refresh() {
205
+ var controller = PNWindow.keyWindow()?.rootViewController
206
+ if let nav = controller as? UINavigationController { controller = nav.topViewController }
207
+ controller?.setNeedsStatusBarAppearanceUpdate()
208
+ }
209
+ }
210
+
211
+ /// `StatusBar`: a global side effect (hidden / style), no on-screen view.
212
+ public final class PNStatusBarManager: PNComponentManager {
213
+ public override func makeView(props: [String: Any]) -> UIView {
214
+ let placeholder = UIView(frame: .zero)
215
+ placeholder.isHidden = true
216
+ return placeholder
217
+ }
218
+
219
+ public override func apply(view: UIView, props: [String: Any], initial: Bool) {
220
+ if let hidden = PNProps.bool(PNProps.value(props, "hidden")) {
221
+ PNStatusBarState.hidden = hidden
222
+ }
223
+ if let style = PNProps.string(PNProps.value(props, "bar_style")) {
224
+ switch style {
225
+ case "light", "light_content": PNStatusBarState.style = .lightContent
226
+ case "dark", "dark_content": PNStatusBarState.style = .darkContent
227
+ default: PNStatusBarState.style = .default
228
+ }
229
+ }
230
+ if let animation = PNProps.string(PNProps.value(props, "animation")) {
231
+ switch animation {
232
+ case "slide": PNStatusBarState.animation = .slide
233
+ case "none": PNStatusBarState.animation = .none
234
+ default: PNStatusBarState.animation = .fade
235
+ }
236
+ }
237
+ PNStatusBarState.refresh()
238
+ }
239
+
240
+ public override func setFrame(view: UIView, x: Double, y: Double, w: Double, h: Double) {}
241
+
242
+ public override func measure(view: UIView, maxW: CGFloat, maxH: CGFloat) -> CGSize { .zero }
243
+ }
@@ -0,0 +1,114 @@
1
+ import UIKit
2
+
3
+ /// `Pressable`: a touchable container with press feedback.
4
+ ///
5
+ /// A zero-duration long-press recognizer tracks the raw touch for
6
+ /// `on_press_in` / `on_press_out` and the pressed-opacity feedback; a tap
7
+ /// recognizer fires `on_press` and a standard long press fires
8
+ /// `on_long_press`. Hit slop and pointer events come from
9
+ /// `PNContainerView`, so hit testing is entirely UIKit's.
10
+ public final class PNPressableManager: PNComponentManager {
11
+ public override func makeView(props: [String: Any]) -> UIView {
12
+ let view = PNContainerView(frame: .zero)
13
+ view.isUserInteractionEnabled = true
14
+ return view
15
+ }
16
+
17
+ public override func createView(tag: Int64, props: [String: Any]) -> UIView {
18
+ let view = super.createView(tag: tag, props: props)
19
+ let handler = PNPressHandler(view: view)
20
+ PNViewState.existing(for: view)?.retained.append(handler)
21
+ handler.install()
22
+ return view
23
+ }
24
+
25
+ public override func apply(view: UIView, props: [String: Any], initial: Bool) {
26
+ PNViewStyler.applyCommon(view, props)
27
+ if PNProps.has(props, "enabled") {
28
+ view.isUserInteractionEnabled = PNProps.bool(PNProps.value(props, "enabled")) ?? true
29
+ }
30
+ if PNProps.has(props, "disabled") {
31
+ view.isUserInteractionEnabled = !(PNProps.bool(PNProps.value(props, "disabled")) ?? false)
32
+ }
33
+ if let delay = PNProps.double(PNProps.value(props, "delay_long_press")),
34
+ let handler = PNViewState.existing(for: view)?.retained.compactMap({ $0 as? PNPressHandler }).first
35
+ {
36
+ handler.longPress.minimumPressDuration = max(0.05, delay / 1000)
37
+ }
38
+ }
39
+ }
40
+
41
+ /// Owns the three press recognizers of one Pressable.
42
+ final class PNPressHandler: NSObject, UIGestureRecognizerDelegate {
43
+ private weak var view: UIView?
44
+ let tap = UITapGestureRecognizer()
45
+ let longPress = UILongPressGestureRecognizer()
46
+ let touch = UILongPressGestureRecognizer()
47
+
48
+ init(view: UIView) {
49
+ self.view = view
50
+ super.init()
51
+ }
52
+
53
+ func install() {
54
+ guard let view = view else { return }
55
+ touch.minimumPressDuration = 0
56
+ tap.addTarget(self, action: #selector(onTap(_:)))
57
+ longPress.addTarget(self, action: #selector(onLongPress(_:)))
58
+ touch.addTarget(self, action: #selector(onTouch(_:)))
59
+ for recognizer in [tap, longPress, touch] {
60
+ recognizer.cancelsTouchesInView = false
61
+ recognizer.delegate = self
62
+ view.addGestureRecognizer(recognizer)
63
+ }
64
+ }
65
+
66
+ private var merged: [String: Any] {
67
+ view.flatMap { PNViewState.existing(for: $0)?.props } ?? [:]
68
+ }
69
+
70
+ @objc private func onTap(_ recognizer: UITapGestureRecognizer) {
71
+ guard let view = view, recognizer.state == .ended else { return }
72
+ PNEvents.emit(view, "on_press")
73
+ }
74
+
75
+ @objc private func onLongPress(_ recognizer: UILongPressGestureRecognizer) {
76
+ guard let view = view, recognizer.state == .began else { return }
77
+ PNEvents.emit(view, "on_long_press")
78
+ }
79
+
80
+ @objc private func onTouch(_ recognizer: UILongPressGestureRecognizer) {
81
+ guard let view = view else { return }
82
+ switch recognizer.state {
83
+ case .began:
84
+ feedback(pressed: true)
85
+ PNEvents.emit(view, "on_press_in")
86
+ case .ended, .cancelled, .failed:
87
+ feedback(pressed: false)
88
+ PNEvents.emit(view, "on_press_out")
89
+ default:
90
+ break
91
+ }
92
+ }
93
+
94
+ private func feedback(pressed: Bool) {
95
+ guard let view = view else { return }
96
+ let props = merged
97
+ let target: CGFloat
98
+ let duration: TimeInterval
99
+ if pressed {
100
+ target = CGFloat(PNProps.double(PNProps.value(props, "pressed_opacity")) ?? 0.6)
101
+ duration = 0.05
102
+ } else {
103
+ target = CGFloat(PNProps.double(PNProps.value(props, "opacity")) ?? 1)
104
+ duration = 0.1
105
+ }
106
+ UIView.animate(withDuration: duration) { view.alpha = target }
107
+ }
108
+
109
+ // Press recognizers recognize alongside everything, including the
110
+ // declarative `gestures` recognizers and a parent scroll view's pan.
111
+ func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith other: UIGestureRecognizer) -> Bool {
112
+ true
113
+ }
114
+ }