pythonnative 0.36.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 (174) 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/components/layout.py +65 -3
  11. pythonnative/gestures.py +13 -10
  12. pythonnative/hooks.py +41 -26
  13. pythonnative/hosts/__init__.py +13 -36
  14. pythonnative/hosts/native.py +348 -0
  15. pythonnative/images.py +6 -5
  16. pythonnative/native_modules/__init__.py +30 -29
  17. pythonnative/native_modules/app_state.py +10 -6
  18. pythonnative/native_modules/battery.py +20 -84
  19. pythonnative/native_modules/biometrics.py +11 -124
  20. pythonnative/native_modules/camera.py +15 -291
  21. pythonnative/native_modules/clipboard.py +14 -109
  22. pythonnative/native_modules/desktop.py +322 -0
  23. pythonnative/native_modules/file_system.py +15 -21
  24. pythonnative/native_modules/haptics.py +19 -129
  25. pythonnative/native_modules/linking.py +24 -137
  26. pythonnative/native_modules/location.py +28 -180
  27. pythonnative/native_modules/net_info.py +25 -135
  28. pythonnative/native_modules/notifications.py +36 -257
  29. pythonnative/native_modules/permissions.py +20 -184
  30. pythonnative/native_modules/registry.py +513 -0
  31. pythonnative/native_modules/secure_store.py +18 -155
  32. pythonnative/native_modules/share.py +8 -113
  33. pythonnative/native_views/__init__.py +67 -86
  34. pythonnative/native_views/bridge_backend.py +264 -0
  35. pythonnative/platform.py +19 -15
  36. pythonnative/platform_metrics.py +3 -3
  37. pythonnative/project/android.py +0 -3
  38. pythonnative/project/builder.py +38 -16
  39. pythonnative/project/config.py +12 -2
  40. pythonnative/project/plugins.py +366 -0
  41. pythonnative/py.typed +0 -0
  42. pythonnative/runtime.py +35 -203
  43. pythonnative/sdk/__init__.py +48 -33
  44. pythonnative/sdk/_components.py +57 -76
  45. pythonnative/storage.py +18 -243
  46. pythonnative/templates/android_template/app/build.gradle +6 -9
  47. pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/MainActivity.kt +34 -166
  48. pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/ScreenFragment.kt +13 -151
  49. pythonnative/templates/android_template/pythonnative/build.gradle +48 -0
  50. pythonnative/templates/android_template/pythonnative/src/main/AndroidManifest.xml +11 -0
  51. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/PNBridge.kt +214 -0
  52. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/PythonHost.kt +20 -0
  53. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/animation/AnimationSpecs.kt +141 -0
  54. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/animation/PNAnimator.kt +249 -0
  55. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/bridge/JsonUtil.kt +175 -0
  56. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/bridge/MainThread.kt +32 -0
  57. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/bridge/PNLog.kt +33 -0
  58. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/bridge/PNRegistry.kt +82 -0
  59. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/bridge/PNTransaction.kt +80 -0
  60. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/bridge/TransactionApplier.kt +115 -0
  61. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/bridge/ViewRegistry.kt +74 -0
  62. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/BuiltinComponents.kt +35 -0
  63. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ButtonManager.kt +33 -0
  64. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ComponentManager.kt +213 -0
  65. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ContainerManagers.kt +69 -0
  66. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ControlManagers.kt +186 -0
  67. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/DatePickerManager.kt +123 -0
  68. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ImageLoader.kt +141 -0
  69. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ImageManager.kt +156 -0
  70. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ModalManager.kt +134 -0
  71. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/PNColor.kt +201 -0
  72. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/PickerManager.kt +87 -0
  73. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/PortalManager.kt +55 -0
  74. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/PressableManager.kt +122 -0
  75. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ScrollViewManager.kt +214 -0
  76. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/SegmentedControlManager.kt +113 -0
  77. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/StatusBarManager.kt +52 -0
  78. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/TabBarManager.kt +131 -0
  79. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/TextInputManager.kt +245 -0
  80. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/TextManager.kt +233 -0
  81. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ViewStyler.kt +355 -0
  82. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/VirtualListManager.kt +179 -0
  83. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/WebViewManager.kt +149 -0
  84. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/gestures/GestureArbiter.kt +233 -0
  85. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/gestures/GestureCoordinator.kt +127 -0
  86. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/gestures/GestureRecognizers.kt +527 -0
  87. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/BuiltinModules.kt +73 -0
  88. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/DeviceModules.kt +316 -0
  89. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/HostModule.kt +97 -0
  90. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/MediaModules.kt +200 -0
  91. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/NativeModule.kt +135 -0
  92. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/NotificationsModule.kt +100 -0
  93. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/PermissionsModule.kt +88 -0
  94. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/StorageModules.kt +89 -0
  95. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/SystemModules.kt +178 -0
  96. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/plugins/GeneratedPlugins.kt +16 -0
  97. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/screens/Navigator.kt +116 -0
  98. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/screens/PNScreenFragment.kt +223 -0
  99. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/screens/ScreenRegistry.kt +80 -0
  100. pythonnative/templates/android_template/{app/src/main/java/com/pythonnative/android_template → pythonnative/src/main/java/com/pythonnative/runtime/views}/PNAccessibilityDelegate.kt +5 -4
  101. pythonnative/templates/android_template/{app/src/main/java/com/pythonnative/android_template → pythonnative/src/main/java/com/pythonnative/runtime/views}/PNBorderDrawable.kt +12 -3
  102. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/views/PNEditText.kt +18 -0
  103. pythonnative/templates/android_template/{app/src/main/java/com/pythonnative/android_template → pythonnative/src/main/java/com/pythonnative/runtime/views}/PNFrameLayout.kt +9 -10
  104. pythonnative/templates/android_template/{app/src/main/java/com/pythonnative/android_template → pythonnative/src/main/java/com/pythonnative/runtime/views}/PNVirtualListView.java +9 -12
  105. pythonnative/templates/android_template/pythonnative/src/test/java/com/pythonnative/runtime/AnimationSpecsTest.kt +75 -0
  106. pythonnative/templates/android_template/pythonnative/src/test/java/com/pythonnative/runtime/GestureArbiterTest.kt +197 -0
  107. pythonnative/templates/android_template/pythonnative/src/test/java/com/pythonnative/runtime/PNColorTest.kt +47 -0
  108. pythonnative/templates/android_template/pythonnative/src/test/java/com/pythonnative/runtime/PNTransactionTest.kt +78 -0
  109. pythonnative/templates/android_template/pythonnative/src/test/java/com/pythonnative/runtime/PromiseTest.kt +75 -0
  110. pythonnative/templates/android_template/settings.gradle +1 -0
  111. pythonnative/templates/ios_template/PythonNativeKit/Package.swift +21 -0
  112. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Animation/PNAnimator.swift +301 -0
  113. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Bridge/PNBridge.swift +191 -0
  114. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Bridge/PNRegistry.swift +140 -0
  115. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Bridge/PNTransaction.swift +165 -0
  116. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Bridge/PNViewRegistry.swift +53 -0
  117. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNButtonManagers.swift +206 -0
  118. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNColor.swift +173 -0
  119. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNComponentManager.swift +183 -0
  120. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNContainerView.swift +56 -0
  121. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNControlManagers.swift +281 -0
  122. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNImageManager.swift +221 -0
  123. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNOverlayManagers.swift +243 -0
  124. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNPressableManager.swift +114 -0
  125. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNScrollViewManager.swift +197 -0
  126. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNTabBarManager.swift +106 -0
  127. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNTextInputManager.swift +289 -0
  128. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNTextManager.swift +230 -0
  129. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNTransform.swift +82 -0
  130. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNViewManager.swift +86 -0
  131. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNViewState.swift +87 -0
  132. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNViewStyler.swift +340 -0
  133. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNVirtualListManager.swift +257 -0
  134. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNWebViewManager.swift +124 -0
  135. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Gestures/PNGestureCoordinator.swift +260 -0
  136. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Modules/AlertModule.swift +92 -0
  137. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Modules/HostModule.swift +158 -0
  138. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Modules/LifecycleModules.swift +174 -0
  139. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Modules/MediaModules.swift +177 -0
  140. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Modules/PNNativeModule.swift +125 -0
  141. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Modules/PermissionModules.swift +251 -0
  142. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Modules/SystemModules.swift +251 -0
  143. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Plugins/PNPluginRegistration.swift +8 -0
  144. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Screens/PNScreenRegistry.swift +46 -0
  145. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Screens/PNViewController.swift +251 -0
  146. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Support/PNCompat.swift +46 -0
  147. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Support/PNJSON.swift +173 -0
  148. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Support/PNLog.swift +47 -0
  149. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Support/PNWindow.swift +51 -0
  150. pythonnative/templates/ios_template/PythonNativeKit/Tests/PythonNativeKitTests/PNColorTests.swift +50 -0
  151. pythonnative/templates/ios_template/PythonNativeKit/Tests/PythonNativeKitTests/PNGestureTests.swift +101 -0
  152. pythonnative/templates/ios_template/PythonNativeKit/Tests/PythonNativeKitTests/PNManagerTests.swift +124 -0
  153. pythonnative/templates/ios_template/PythonNativeKit/Tests/PythonNativeKitTests/PNModuleTests.swift +98 -0
  154. pythonnative/templates/ios_template/PythonNativeKit/Tests/PythonNativeKitTests/PNTransactionTests.swift +91 -0
  155. pythonnative/templates/ios_template/ios_template/AppDelegate.swift +8 -50
  156. pythonnative/templates/ios_template/ios_template/Info.plist +4 -4
  157. pythonnative/templates/ios_template/ios_template/PythonRuntime.swift +38 -46
  158. pythonnative/templates/ios_template/ios_template/SceneDelegate.swift +11 -16
  159. pythonnative/templates/ios_template/ios_template/ViewController.swift +12 -142
  160. pythonnative/templates/ios_template/ios_template.xcodeproj/project.pbxproj +47 -1
  161. pythonnative/utils.py +4 -82
  162. pythonnative/virtual_rows.py +3 -3
  163. {pythonnative-0.36.0.dist-info → pythonnative-0.37.0.dist-info}/METADATA +3 -5
  164. pythonnative-0.37.0.dist-info/RECORD +263 -0
  165. pythonnative/hosts/android.py +0 -269
  166. pythonnative/hosts/ios.py +0 -404
  167. pythonnative/native_views/android.py +0 -3702
  168. pythonnative/native_views/ios.py +0 -5253
  169. pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/Navigator.kt +0 -43
  170. pythonnative-0.36.0.dist-info/RECORD +0 -155
  171. {pythonnative-0.36.0.dist-info → pythonnative-0.37.0.dist-info}/WHEEL +0 -0
  172. {pythonnative-0.36.0.dist-info → pythonnative-0.37.0.dist-info}/entry_points.txt +0 -0
  173. {pythonnative-0.36.0.dist-info → pythonnative-0.37.0.dist-info}/licenses/LICENSE +0 -0
  174. {pythonnative-0.36.0.dist-info → pythonnative-0.37.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,230 @@
1
+ import UIKit
2
+
3
+ /// `Text`: a `UILabel` with rich spans, text transforms, shadows, and the
4
+ /// full font prop set.
5
+ public final class PNTextManager: PNComponentManager {
6
+ static let textShadowKeys = ["text_shadow_color", "text_shadow_offset", "text_shadow_radius"]
7
+ static let attributedKeys = ["letter_spacing", "line_height", "text_decoration"] + textShadowKeys
8
+ static let fontKeys = ["font_size", "font_weight", "font_family", "italic", "bold", "font_style"]
9
+ static let spanRebuildKeys = ["spans", "text", "text_transform", "color"] + fontKeys + attributedKeys
10
+
11
+ public override func makeView(props: [String: Any]) -> UIView {
12
+ let label = UILabel(frame: .zero)
13
+ label.numberOfLines = 0
14
+ label.translatesAutoresizingMaskIntoConstraints = true
15
+ return label
16
+ }
17
+
18
+ public override func apply(view: UIView, props: [String: Any], initial: Bool) {
19
+ guard let label = view as? UILabel else { return }
20
+ let merged = mergedProps(label)
21
+ let hasSpans = !((PNProps.value(merged, "spans") as? [Any]) ?? []).isEmpty
22
+ let textChanged = PNProps.has(props, "text") || PNProps.has(props, "text_transform")
23
+ if textChanged, !hasSpans {
24
+ label.text = PNTextManager.transform(PNProps.string(PNProps.value(merged, "text")), mode: PNProps.string(PNProps.value(merged, "text_transform")))
25
+ }
26
+ if PNTextManager.fontKeys.contains(where: { PNProps.has(props, $0) }) {
27
+ label.font = PNTextManager.font(from: merged, base: label.font)
28
+ }
29
+ if let color = PNColor.parse(PNProps.value(props, "color")) {
30
+ label.textColor = color
31
+ }
32
+ if let color = PNColor.parse(PNProps.value(props, "background_color")) {
33
+ label.backgroundColor = color
34
+ }
35
+ if PNProps.has(props, "max_lines") || PNProps.has(props, "number_of_lines") {
36
+ let lines = PNProps.int(PNProps.value(props, "max_lines")) ?? PNProps.int(PNProps.value(props, "number_of_lines")) ?? 0
37
+ label.numberOfLines = max(0, lines)
38
+ }
39
+ if PNProps.has(props, "text_align") {
40
+ label.textAlignment = PNTextManager.alignment(PNProps.string(PNProps.value(props, "text_align")))
41
+ }
42
+ if PNProps.has(props, "selectable") {
43
+ label.isUserInteractionEnabled = PNProps.bool(PNProps.value(props, "selectable")) ?? false
44
+ }
45
+ if hasSpans {
46
+ if PNTextManager.spanRebuildKeys.contains(where: { PNProps.has(props, $0) }) {
47
+ applySpans(label, merged)
48
+ }
49
+ } else if PNProps.has(props, "spans") {
50
+ label.text = PNTextManager.transform(PNProps.string(PNProps.value(merged, "text")), mode: PNProps.string(PNProps.value(merged, "text_transform")))
51
+ } else if PNTextManager.attributedKeys.contains(where: { PNProps.has(props, $0) })
52
+ || (textChanged && PNTextManager.attributedKeys.contains { PNProps.value(merged, $0) != nil })
53
+ {
54
+ applyAttributed(label, merged)
55
+ }
56
+ PNViewStyler.applyDecoration(label, props)
57
+ }
58
+
59
+ // MARK: - Fonts
60
+
61
+ /// Resolve a `UIFont` from the element's font props.
62
+ static func font(from props: [String: Any], base: UIFont?) -> UIFont {
63
+ let currentSize = base?.pointSize ?? 17
64
+ let size = CGFloat(PNProps.double(PNProps.value(props, "font_size")) ?? Double(currentSize))
65
+ var weight: Any? = PNProps.value(props, "font_weight")
66
+ if weight == nil, PNProps.bool(PNProps.value(props, "bold")) == true { weight = "bold" }
67
+ let italic = PNProps.bool(PNProps.value(props, "italic")) == true
68
+ || PNProps.string(PNProps.value(props, "font_style")) == "italic"
69
+ return font(size: size, weight: weight, family: PNProps.string(PNProps.value(props, "font_family")), italic: italic)
70
+ }
71
+
72
+ static func font(size: CGFloat, weight: Any?, family: String?, italic: Bool) -> UIFont {
73
+ if let family = family, !family.isEmpty, let named = UIFont(name: family, size: size) {
74
+ return italic ? italicized(named, size: size) : named
75
+ }
76
+ let font = UIFont.systemFont(ofSize: size, weight: fontWeight(weight))
77
+ return italic ? italicized(font, size: size) : font
78
+ }
79
+
80
+ static func italicized(_ font: UIFont, size: CGFloat) -> UIFont {
81
+ var traits = font.fontDescriptor.symbolicTraits
82
+ traits.insert(.traitItalic)
83
+ guard let descriptor = font.fontDescriptor.withSymbolicTraits(traits) else { return font }
84
+ return UIFont(descriptor: descriptor, size: size)
85
+ }
86
+
87
+ static func fontWeight(_ value: Any?) -> UIFont.Weight {
88
+ if let name = value as? String {
89
+ switch name.lowercased() {
90
+ case "ultralight": return .ultraLight
91
+ case "thin": return .thin
92
+ case "light": return .light
93
+ case "medium": return .medium
94
+ case "semibold": return .semibold
95
+ case "bold": return .bold
96
+ case "heavy": return .heavy
97
+ case "black": return .black
98
+ default:
99
+ if let numeric = Double(name) { return fontWeight(numeric) }
100
+ return .regular
101
+ }
102
+ }
103
+ guard let numeric = PNProps.double(value) else { return .regular }
104
+ let n = max(100, min(900, numeric))
105
+ switch n {
106
+ case ...100: return .ultraLight
107
+ case ...200: return .thin
108
+ case ...300: return .light
109
+ case ...400: return .regular
110
+ case ...500: return .medium
111
+ case ...600: return .semibold
112
+ case ...700: return .bold
113
+ case ...800: return .heavy
114
+ default: return .black
115
+ }
116
+ }
117
+
118
+ static func alignment(_ value: String?) -> NSTextAlignment {
119
+ switch value {
120
+ case "center": return .center
121
+ case "right": return .right
122
+ case "justify": return .justified
123
+ case "natural": return .natural
124
+ default: return .left
125
+ }
126
+ }
127
+
128
+ // MARK: - Transforms
129
+
130
+ /// Apply a `text_transform` mode (`uppercase`, `lowercase`, `capitalize`).
131
+ public static func transform(_ text: String?, mode: String?) -> String {
132
+ let s = text ?? ""
133
+ switch mode {
134
+ case "uppercase": return s.uppercased()
135
+ case "lowercase": return s.lowercased()
136
+ case "capitalize":
137
+ var out = ""
138
+ var atWordStart = true
139
+ for ch in s {
140
+ if ch.isWhitespace {
141
+ atWordStart = true
142
+ out.append(ch)
143
+ } else if atWordStart {
144
+ out.append(contentsOf: String(ch).uppercased())
145
+ atWordStart = false
146
+ } else {
147
+ out.append(ch)
148
+ }
149
+ }
150
+ return out
151
+ default: return s
152
+ }
153
+ }
154
+
155
+ // MARK: - Attributed rendering
156
+
157
+ static func baseAttributes(_ props: [String: Any], font: UIFont?) -> [NSAttributedString.Key: Any] {
158
+ var attrs: [NSAttributedString.Key: Any] = [:]
159
+ if let font = font { attrs[.font] = font }
160
+ if let kern = PNProps.double(PNProps.value(props, "letter_spacing")) { attrs[.kern] = kern }
161
+ if let lineHeight = PNProps.double(PNProps.value(props, "line_height")) {
162
+ let style = NSMutableParagraphStyle()
163
+ style.minimumLineHeight = CGFloat(lineHeight)
164
+ style.maximumLineHeight = CGFloat(lineHeight)
165
+ attrs[.paragraphStyle] = style
166
+ }
167
+ switch PNProps.string(PNProps.value(props, "text_decoration")) {
168
+ case "underline": attrs[.underlineStyle] = NSUnderlineStyle.single.rawValue
169
+ case "line_through": attrs[.strikethroughStyle] = NSUnderlineStyle.single.rawValue
170
+ default: break
171
+ }
172
+ if let shadow = textShadow(props) { attrs[.shadow] = shadow }
173
+ return attrs
174
+ }
175
+
176
+ static func textShadow(_ props: [String: Any]) -> NSShadow? {
177
+ guard textShadowKeys.contains(where: { PNProps.value(props, $0) != nil }) else { return nil }
178
+ let shadow = NSShadow()
179
+ shadow.shadowColor = PNColor.parse(PNProps.value(props, "text_shadow_color")) ?? UIColor.black
180
+ let (dx, dy) = PNViewStyler.shadowOffset(PNProps.value(props, "text_shadow_offset"))
181
+ shadow.shadowOffset = CGSize(width: dx, height: dy)
182
+ shadow.shadowBlurRadius = CGFloat(PNProps.double(PNProps.value(props, "text_shadow_radius")) ?? 0)
183
+ return shadow
184
+ }
185
+
186
+ func applyAttributed(_ label: UILabel, _ props: [String: Any]) {
187
+ guard let text = label.text, !text.isEmpty else { return }
188
+ label.attributedText = NSAttributedString(string: text, attributes: PNTextManager.baseAttributes(props, font: label.font))
189
+ }
190
+
191
+ func applySpans(_ label: UILabel, _ merged: [String: Any]) {
192
+ let mode = PNProps.string(PNProps.value(merged, "text_transform"))
193
+ let spans = ((PNProps.value(merged, "spans") as? [Any]) ?? []).compactMap { $0 as? [String: Any] }
194
+ let texts = spans.map { PNTextManager.transform(PNProps.string($0["text"]), mode: mode) }
195
+ let full = NSMutableAttributedString(string: texts.joined(), attributes: PNTextManager.baseAttributes(merged, font: label.font))
196
+ let baseSize = label.font?.pointSize ?? 17
197
+ var location = 0
198
+ for (span, text) in zip(spans, texts) {
199
+ let length = (text as NSString).length
200
+ let range = NSRange(location: location, length: length)
201
+ location += length
202
+ if length == 0 { continue }
203
+ if PNTextManager.fontKeys.contains(where: { PNProps.value(span, $0) != nil }) {
204
+ let size = CGFloat(PNProps.double(PNProps.value(span, "font_size")) ?? Double(baseSize))
205
+ var weight: Any? = PNProps.value(span, "font_weight")
206
+ if weight == nil, PNProps.bool(PNProps.value(span, "bold")) == true { weight = "bold" }
207
+ let font = PNTextManager.font(
208
+ size: size, weight: weight, family: PNProps.string(PNProps.value(span, "font_family")),
209
+ italic: PNProps.bool(PNProps.value(span, "italic")) == true
210
+ )
211
+ full.addAttribute(.font, value: font, range: range)
212
+ }
213
+ if let color = PNColor.parse(PNProps.value(span, "color")) {
214
+ full.addAttribute(.foregroundColor, value: color, range: range)
215
+ }
216
+ if let color = PNColor.parse(PNProps.value(span, "background_color")) {
217
+ full.addAttribute(.backgroundColor, value: color, range: range)
218
+ }
219
+ if let kern = PNProps.double(PNProps.value(span, "letter_spacing")) {
220
+ full.addAttribute(.kern, value: kern, range: range)
221
+ }
222
+ switch PNProps.string(PNProps.value(span, "text_decoration")) {
223
+ case "underline": full.addAttribute(.underlineStyle, value: NSUnderlineStyle.single.rawValue, range: range)
224
+ case "line_through": full.addAttribute(.strikethroughStyle, value: NSUnderlineStyle.single.rawValue, range: range)
225
+ default: break
226
+ }
227
+ }
228
+ label.attributedText = full
229
+ }
230
+ }
@@ -0,0 +1,82 @@
1
+ import UIKit
2
+
3
+ /// Builds `CGAffineTransform`s from the `transform` style prop.
4
+ ///
5
+ /// The prop is a list of single-key dicts: `rotate` (degrees, or a
6
+ /// `"45deg"` / `"0.5rad"` string), `scale`, `scale_x`, `scale_y`,
7
+ /// `translate_x`, `translate_y`, `skew_x`, `skew_y` (degrees).
8
+ public enum PNTransform {
9
+ /// Build a transform from a spec list (or single dict). `nil` is identity.
10
+ public static func make(_ spec: Any?) -> CGAffineTransform {
11
+ var transform = CGAffineTransform.identity
12
+ guard let spec = spec, !(spec is NSNull) else { return transform }
13
+ let entries: [Any] = (spec as? [Any]) ?? [spec]
14
+ for entry in entries {
15
+ guard let dict = entry as? [String: Any] else { continue }
16
+ if let rotate = dict["rotate"] {
17
+ transform = transform.rotated(by: angle(rotate))
18
+ }
19
+ if let scale = PNProps.double(dict["scale"]) {
20
+ transform = transform.scaledBy(x: CGFloat(scale), y: CGFloat(scale))
21
+ }
22
+ if let sx = PNProps.double(dict["scale_x"]) {
23
+ transform = transform.scaledBy(x: CGFloat(sx), y: 1)
24
+ }
25
+ if let sy = PNProps.double(dict["scale_y"]) {
26
+ transform = transform.scaledBy(x: 1, y: CGFloat(sy))
27
+ }
28
+ if dict["translate_x"] != nil || dict["translate_y"] != nil {
29
+ let tx = PNProps.double(dict["translate_x"]) ?? 0
30
+ let ty = PNProps.double(dict["translate_y"]) ?? 0
31
+ transform = transform.translatedBy(x: CGFloat(tx), y: CGFloat(ty))
32
+ }
33
+ if dict["skew_x"] != nil || dict["skew_y"] != nil {
34
+ let skew = CGAffineTransform(
35
+ a: 1, b: tan(angle(dict["skew_y"] ?? 0)),
36
+ c: tan(angle(dict["skew_x"] ?? 0)), d: 1,
37
+ tx: 0, ty: 0
38
+ )
39
+ transform = skew.concatenating(transform)
40
+ }
41
+ }
42
+ return transform
43
+ }
44
+
45
+ /// Whether every component of `t` is finite.
46
+ public static func isFinite(_ t: CGAffineTransform) -> Bool {
47
+ t.a.isFinite && t.b.isFinite && t.c.isFinite && t.d.isFinite && t.tx.isFinite && t.ty.isFinite
48
+ }
49
+
50
+ /// Apply the `transform` prop to `view` (identity when `null`).
51
+ public static func apply(_ view: UIView, spec: Any?) {
52
+ let transform = make(spec)
53
+ guard isFinite(transform) else {
54
+ PNLog.rateLimited(PNLog.components, key: "set_transform:nan", "[set_transform:nan] spec=\(String(describing: spec))")
55
+ view.transform = .identity
56
+ return
57
+ }
58
+ view.transform = transform
59
+ }
60
+
61
+ /// Parse a rotation value into radians (`"90deg"`, `"1.5rad"`, or numeric degrees).
62
+ public static func angle(_ value: Any) -> CGFloat {
63
+ if let text = value as? String {
64
+ let lower = text.lowercased()
65
+ if lower.hasSuffix("deg"), let degrees = Double(lower.dropLast(3)) {
66
+ return CGFloat(degrees * .pi / 180)
67
+ }
68
+ if lower.hasSuffix("rad"), let radians = Double(lower.dropLast(3)) {
69
+ return CGFloat(radians)
70
+ }
71
+ }
72
+ return CGFloat((PNProps.double(value) ?? 0) * .pi / 180)
73
+ }
74
+
75
+ /// Decompose an affine transform into the animatable channels.
76
+ public static func decompose(_ t: CGAffineTransform) -> (translateX: CGFloat, translateY: CGFloat, scaleX: CGFloat, scaleY: CGFloat, rotateDegrees: CGFloat) {
77
+ let scaleX = sqrt(t.a * t.a + t.b * t.b)
78
+ let scaleY = sqrt(t.c * t.c + t.d * t.d)
79
+ let rotation = atan2(t.b, t.a)
80
+ return (t.tx, t.ty, scaleX, scaleY, rotation * 180 / .pi)
81
+ }
82
+ }
@@ -0,0 +1,86 @@
1
+ import UIKit
2
+
3
+ /// `View`, `Row`, and `Column`: a hit-test-aware flex container. All
4
+ /// layout semantics live in the Python layout engine; the manager only
5
+ /// applies visual props and frames.
6
+ public final class PNViewManager: PNComponentManager {
7
+ public override func makeView(props: [String: Any]) -> UIView {
8
+ PNContainerView(frame: .zero)
9
+ }
10
+ }
11
+
12
+ /// `Spacer`: an empty layout placeholder.
13
+ public final class PNSpacerManager: PNComponentManager {
14
+ public override func makeView(props: [String: Any]) -> UIView {
15
+ UIView(frame: .zero)
16
+ }
17
+
18
+ public override func apply(view: UIView, props: [String: Any], initial: Bool) {
19
+ // Sizing is entirely the layout engine's; nothing visual to apply.
20
+ }
21
+ }
22
+
23
+ /// `SafeAreaView`: a plain container; insets are applied by the layout engine.
24
+ public final class PNSafeAreaViewManager: PNComponentManager {
25
+ public override func makeView(props: [String: Any]) -> UIView {
26
+ PNContainerView(frame: .zero)
27
+ }
28
+ }
29
+
30
+ /// `KeyboardAvoidingView`: a container that keeps the keyboard observer
31
+ /// installed so Python receives keyboard height updates.
32
+ public final class PNKeyboardAvoidingViewManager: PNComponentManager {
33
+ public override func makeView(props: [String: Any]) -> UIView {
34
+ PNKeyboardObserver.shared.start()
35
+ return PNContainerView(frame: .zero)
36
+ }
37
+ }
38
+
39
+ /// Publishes the keyboard height through
40
+ /// `callback("module", 0, "Host", {"event": "keyboard", "payload": {"height": h}})`.
41
+ public final class PNKeyboardObserver {
42
+ public static let shared = PNKeyboardObserver()
43
+
44
+ private var started = false
45
+ private(set) public var height: CGFloat = 0
46
+
47
+ private init() {}
48
+
49
+ /// Begin observing keyboard notifications (idempotent).
50
+ public func start() {
51
+ if started { return }
52
+ started = true
53
+ let center = NotificationCenter.default
54
+ center.addObserver(self, selector: #selector(willShow(_:)), name: UIResponder.keyboardWillShowNotification, object: nil)
55
+ center.addObserver(self, selector: #selector(willChange(_:)), name: UIResponder.keyboardWillChangeFrameNotification, object: nil)
56
+ center.addObserver(self, selector: #selector(willHide(_:)), name: UIResponder.keyboardWillHideNotification, object: nil)
57
+ }
58
+
59
+ @objc private func willShow(_ note: Notification) {
60
+ publish(Self.keyboardHeight(from: note))
61
+ }
62
+
63
+ @objc private func willChange(_ note: Notification) {
64
+ publish(Self.keyboardHeight(from: note))
65
+ }
66
+
67
+ @objc private func willHide(_ note: Notification) {
68
+ publish(0)
69
+ }
70
+
71
+ private func publish(_ value: CGFloat) {
72
+ let clamped = max(0, value)
73
+ if clamped == height { return }
74
+ height = clamped
75
+ PNModuleEvents.emit(module: "Host", event: "keyboard", payload: ["height": Double(clamped)])
76
+ }
77
+
78
+ static func keyboardHeight(from note: Notification) -> CGFloat {
79
+ guard let frame = (note.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue else {
80
+ return 0
81
+ }
82
+ let screen = PNWindow.screenBounds()
83
+ if screen.height > 0, frame.origin.y >= screen.height { return 0 }
84
+ return frame.height
85
+ }
86
+ }
@@ -0,0 +1,87 @@
1
+ import UIKit
2
+
3
+ /// Per-view state attached to every bridge-created `UIView`.
4
+ ///
5
+ /// Managers are shared per element type, so anything a manager needs
6
+ /// to remember about one view (its merged props, style bookkeeping,
7
+ /// recognizers, suppress flags) lives here. The state is stored as an
8
+ /// associated object so it can be reached from any `UIView` reference.
9
+ public final class PNViewState {
10
+ /// The reconciler tag of the view.
11
+ public let tag: Int64
12
+ /// Element type name.
13
+ public let typeName: String
14
+ /// Every prop the view has received so far (updates merge; `null` removes).
15
+ public var props: [String: Any] = [:]
16
+ /// Free-form storage for manager-specific values (suppress flags, delegates, ...).
17
+ public var extras: [String: Any] = [:]
18
+ /// Strong references the view's manager needs to keep alive (delegates, targets).
19
+ public var retained: [AnyObject] = []
20
+ /// Recognizers installed from the `gestures` prop.
21
+ public var gestureRecognizers: [UIGestureRecognizer] = []
22
+
23
+ // Style bookkeeping used by `PNViewStyler` at frame time.
24
+ var requestedCornerRadius: CGFloat?
25
+ var cornerRadii: [CGFloat]?
26
+ var sideBorderWidths: [CGFloat]?
27
+ var sideBorderColors: [UIColor]?
28
+ var sideBorderLayers: [CALayer?] = [nil, nil, nil, nil]
29
+
30
+ init(tag: Int64, typeName: String) {
31
+ self.tag = tag
32
+ self.typeName = typeName
33
+ }
34
+
35
+ /// Names of the events wired on the element this render (`_pn_events`).
36
+ public var eventNames: Set<String> {
37
+ guard let list = props["_pn_events"] as? [Any] else { return [] }
38
+ return Set(list.compactMap { $0 as? String })
39
+ }
40
+
41
+ /// Whether the element wired a callback named `name`.
42
+ public func hasEvent(_ name: String) -> Bool {
43
+ eventNames.contains(name)
44
+ }
45
+
46
+ /// Typed access to `extras`.
47
+ public func flag(_ key: String) -> Bool {
48
+ (extras[key] as? Bool) ?? false
49
+ }
50
+
51
+ private static var key: UInt8 = 0
52
+
53
+ /// Attach a fresh state object to `view`.
54
+ @discardableResult
55
+ static func attach(_ view: UIView, tag: Int64, typeName: String) -> PNViewState {
56
+ let state = PNViewState(tag: tag, typeName: typeName)
57
+ objc_setAssociatedObject(view, &key, state, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
58
+ return state
59
+ }
60
+
61
+ /// The state attached to `view`, if any.
62
+ public static func existing(for view: UIView) -> PNViewState? {
63
+ objc_getAssociatedObject(view, &key) as? PNViewState
64
+ }
65
+
66
+ static func detach(_ view: UIView) {
67
+ objc_setAssociatedObject(view, &key, nil, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
68
+ }
69
+ }
70
+
71
+ /// Event emission keyed by view.
72
+ public enum PNEvents {
73
+ /// Emit `name` for `view` with positional `args`. Returns Python's
74
+ /// JSON reply for request-style events, else `nil`.
75
+ @discardableResult
76
+ public static func emit(_ view: UIView, _ name: String, _ args: [Any?] = []) -> String? {
77
+ guard let state = PNViewState.existing(for: view) else { return nil }
78
+ return PNBridge.shared.emitEvent(tag: state.tag, name: name, args: args)
79
+ }
80
+
81
+ /// Emit only when the element wired a handler for `name`.
82
+ @discardableResult
83
+ public static func emitIfWired(_ view: UIView, _ name: String, _ args: [Any?] = []) -> String? {
84
+ guard let state = PNViewState.existing(for: view), state.hasEvent(name) else { return nil }
85
+ return PNBridge.shared.emitEvent(tag: state.tag, name: name, args: args)
86
+ }
87
+ }