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
pythonnative/gestures.py CHANGED
@@ -34,10 +34,13 @@ configuration into plain dicts for the native handler (so prop diffing
34
34
  never compares closures) and routes the callbacks through the
35
35
  tag-based event channel. Recognition itself is native:
36
36
 
37
- - **iOS** attaches real ``UIGestureRecognizer`` instances.
38
- - **Android** feeds raw ``MotionEvent`` streams into the pure-Python
39
- [`GestureArbiter`][pythonnative.gestures.GestureArbiter] below.
40
- - **Desktop** feeds Tk pointer events into the same arbiter.
37
+ - **iOS** attaches real ``UIGestureRecognizer`` instances
38
+ (``PythonNativeKit``).
39
+ - **Android** runs an equivalent recognizer set in Kotlin (the
40
+ ``pythonnative`` Gradle module) on top of ``MotionEvent`` streams.
41
+ - **Desktop** feeds Tk pointer events into the pure-Python
42
+ [`GestureArbiter`][pythonnative.gestures.GestureArbiter] below, which
43
+ doubles as the executable specification for the native ports.
41
44
 
42
45
  Composition
43
46
  -----------
@@ -518,14 +521,14 @@ def serialize_gestures(
518
521
 
519
522
 
520
523
  # ======================================================================
521
- # Pure-Python recognition engine (Android + desktop backends)
524
+ # Pure-Python recognition engine (desktop backend + reference semantics)
522
525
  # ======================================================================
523
526
  #
524
- # iOS uses real UIGestureRecognizers. Android and the desktop preview
525
- # receive raw pointer streams instead, which this arbiter turns into
526
- # the same GestureEvent payloads. Keeping it in pure Python makes the
527
- # state machines unit-testable with scripted event sequences and
528
- # guarantees identical semantics on both backends.
527
+ # iOS and Android recognize natively. The desktop preview receives raw
528
+ # Tk pointer streams instead, which this arbiter turns into the same
529
+ # GestureEvent payloads. Keeping it in pure Python makes the state
530
+ # machines unit-testable with scripted event sequences; the Kotlin
531
+ # arbiter mirrors this file so the two stay in lockstep.
529
532
 
530
533
  EmitFn = Callable[[int, Dict[str, Any]], None]
531
534
  """``emit(gesture_index, payload)``: the arbiter's output channel."""
pythonnative/hooks.py CHANGED
@@ -67,6 +67,7 @@ from typing import (
67
67
 
68
68
  from . import diagnostics
69
69
  from .element import Element, Node
70
+ from .platform_metrics import SafeAreaInsets, WindowDimensions
70
71
  from .scheduler import TransitionQueue, in_transition, run_in_transition, schedule_trigger
71
72
  from .suspense import CoroDriver, Resource
72
73
 
@@ -1235,27 +1236,39 @@ def use_mutation(
1235
1236
  from .runtime import run_async
1236
1237
 
1237
1238
  state, set_state = use_state(lambda: MutationState[T]())
1239
+ # The trigger is identity-stable across renders (like ``set_state``)
1240
+ # so it can sit in effect deps or be passed to memoized children;
1241
+ # it always calls the latest ``mutator``.
1242
+ latest = use_ref(mutator)
1243
+ latest.current = mutator
1244
+
1245
+ def _make_mutate() -> Callable[..., MutationCall[T]]:
1246
+ def mutate(*args: Any, **kwargs: Any) -> MutationCall[T]:
1247
+ set_state(lambda s: replace(s, loading=True, error=None))
1248
+ fn = latest.current
1238
1249
 
1239
- def mutate(*args: Any, **kwargs: Any) -> MutationCall[T]:
1240
- set_state(lambda s: replace(s, loading=True, error=None))
1250
+ async def _runner() -> T:
1251
+ try:
1252
+ data = await fn(*args, **kwargs)
1253
+ set_state(lambda s: replace(s, data=data, loading=False, error=None))
1254
+ return data
1255
+ except asyncio.CancelledError:
1256
+ set_state(lambda s: replace(s, loading=False))
1257
+ raise
1258
+ except BaseException as exc:
1259
+ failure = exc
1260
+ set_state(lambda s: replace(s, loading=False, error=failure))
1261
+ raise
1241
1262
 
1242
- async def _runner() -> T:
1243
- try:
1244
- data = await mutator(*args, **kwargs)
1245
- set_state(lambda s: replace(s, data=data, loading=False, error=None))
1246
- return data
1247
- except asyncio.CancelledError:
1248
- set_state(lambda s: replace(s, loading=False))
1249
- raise
1250
- except BaseException as exc:
1251
- failure = exc
1252
- set_state(lambda s: replace(s, loading=False, error=failure))
1253
- raise
1263
+ future = run_async(_runner())
1264
+ return MutationCall[T](future)
1254
1265
 
1255
- future = run_async(_runner())
1256
- return MutationCall[T](future)
1266
+ return mutate
1257
1267
 
1258
- return state, mutate
1268
+ mutate_ref: Ref[Optional[Callable[..., MutationCall[T]]]] = use_ref(None)
1269
+ if mutate_ref.current is None:
1270
+ mutate_ref.current = _make_mutate()
1271
+ return state, mutate_ref.current
1259
1272
 
1260
1273
 
1261
1274
  # ======================================================================
@@ -1294,7 +1307,7 @@ def use_subscription(subscribe: Callable[[Callable[[], None]], Callable[[], None
1294
1307
  return get_snapshot()
1295
1308
 
1296
1309
 
1297
- def use_window_dimensions() -> Dict[str, float]:
1310
+ def use_window_dimensions() -> WindowDimensions:
1298
1311
  """Return the current viewport size and re-render when it changes.
1299
1312
 
1300
1313
  Equivalent to React Native's ``useWindowDimensions``. The values
@@ -1302,26 +1315,29 @@ def use_window_dimensions() -> Dict[str, float]:
1302
1315
  size (initial layout, rotation, multitasking split-view).
1303
1316
 
1304
1317
  Returns:
1305
- A dict with ``"width"`` and ``"height"`` floats in layout
1318
+ A [`WindowDimensions`][pythonnative.platform_metrics.WindowDimensions]
1319
+ named tuple with ``width`` and ``height`` floats in layout
1306
1320
  units (pt on iOS, dp on Android). Both are ``0.0`` until the
1307
- screen host has run its first layout pass.
1321
+ screen host has run its first layout pass. Being a tuple, it
1322
+ unpacks (``width, height = pn.use_window_dimensions()``) and
1323
+ compares by value.
1308
1324
 
1309
1325
  Raises:
1310
1326
  RuntimeError: If called outside a ``@component`` function.
1311
1327
  """
1312
1328
  from . import platform_metrics
1313
1329
 
1314
- dims = use_subscription(platform_metrics.subscribe, platform_metrics.get_window_dimensions)
1315
- return {"width": dims.width, "height": dims.height}
1330
+ return use_subscription(platform_metrics.subscribe, platform_metrics.get_window_dimensions)
1316
1331
 
1317
1332
 
1318
- def use_safe_area_insets() -> Dict[str, float]:
1333
+ def use_safe_area_insets() -> SafeAreaInsets:
1319
1334
  """Return the current safe-area insets and re-render on change.
1320
1335
 
1321
1336
  Mirrors ``react-native-safe-area-context``'s ``useSafeAreaInsets``.
1322
1337
 
1323
1338
  Returns:
1324
- A dict with ``"top"``, ``"bottom"``, ``"left"``, and ``"right"``
1339
+ A [`SafeAreaInsets`][pythonnative.platform_metrics.SafeAreaInsets]
1340
+ named tuple with ``top``, ``left``, ``bottom``, and ``right``
1325
1341
  floats in layout units (pt on iOS, dp on Android).
1326
1342
 
1327
1343
  Raises:
@@ -1329,8 +1345,7 @@ def use_safe_area_insets() -> Dict[str, float]:
1329
1345
  """
1330
1346
  from . import platform_metrics
1331
1347
 
1332
- insets = use_subscription(platform_metrics.subscribe, platform_metrics.get_safe_area_insets)
1333
- return {"top": insets.top, "bottom": insets.bottom, "left": insets.left, "right": insets.right}
1348
+ return use_subscription(platform_metrics.subscribe, platform_metrics.get_safe_area_insets)
1334
1349
 
1335
1350
 
1336
1351
  def use_keyboard_height() -> float:
@@ -1,18 +1,13 @@
1
1
  """Screen hosts: the bridge between a native screen and the reconciler.
2
2
 
3
- Native templates create one host per screen:
4
-
5
- ```python
6
- host = pythonnative.hosts.create_screen("app.main", native_instance, args_json)
7
- host.on_create()
8
- ```
9
-
10
- and forward lifecycle events to it. The concrete class depends on the
11
- runtime ([`AndroidScreenHost`][pythonnative.hosts.android.AndroidScreenHost],
12
- [`IOSScreenHost`][pythonnative.hosts.ios.IOSScreenHost],
13
- [`DesktopScreenHost`][pythonnative.hosts.desktop.DesktopScreenHost]);
14
- the headless base [`ScreenHost`][pythonnative.hosts.ScreenHost] is used
15
- in unit tests with a fake backend.
3
+ On device the native runtime creates screens through the bridge
4
+ (``callback("host", screen_id, "create", {...})``), which lands in
5
+ [`NativeScreenHost`][pythonnative.hosts.native.NativeScreenHost]. Off device,
6
+ ``pn preview`` creates a
7
+ [`DesktopScreenHost`][pythonnative.hosts.desktop.DesktopScreenHost]
8
+ through [`create_screen`][pythonnative.hosts.create_screen], and unit
9
+ tests use the headless base [`ScreenHost`][pythonnative.hosts.ScreenHost]
10
+ with a fake backend.
16
11
  """
17
12
 
18
13
  from __future__ import annotations
@@ -27,18 +22,14 @@ __all__ = ["ScreenHost", "create_screen", "host_class", "import_component"]
27
22
 
28
23
  def host_class() -> Type[ScreenHost]:
29
24
  """The host class for the current runtime."""
30
- if IS_ANDROID:
31
- from .android import AndroidScreenHost
25
+ if IS_ANDROID or IS_IOS:
26
+ from .native import NativeScreenHost
32
27
 
33
- return AndroidScreenHost
28
+ return NativeScreenHost
34
29
  if IS_DESKTOP:
35
30
  from .desktop import DesktopScreenHost
36
31
 
37
32
  return DesktopScreenHost
38
- if IS_IOS:
39
- from .ios import IOSScreenHost
40
-
41
- return IOSScreenHost
42
33
  return ScreenHost
43
34
 
44
35
 
@@ -49,8 +40,8 @@ def create_screen(component_path: str, native_instance: Any = None, args_json: O
49
40
  component_path: ``"app.main"`` (the module's ``App`` is used) or a
50
41
  dotted path like ``"app.main.RootScreen"``. Imported lazily
51
42
  so the dev server can reload it.
52
- native_instance: The platform object owning the screen
53
- (``Activity``, ``UIViewController`` pointer, ``DesktopApp``).
43
+ native_instance: The platform object owning the screen (the
44
+ integer screen id on device, ``DesktopApp`` in the preview).
54
45
  args_json: Optional JSON launch arguments (pushed screens
55
46
  receive their navigation history here).
56
47
 
@@ -64,20 +55,6 @@ def create_screen(component_path: str, native_instance: Any = None, args_json: O
64
55
  return host
65
56
 
66
57
 
67
- def drain_ios_scheduled_renders() -> None:
68
- """Drain deferred iOS renders (called by the Swift template on the main thread)."""
69
- from .ios import drain_ios_scheduled_renders as drain
70
-
71
- drain()
72
-
73
-
74
- def forward_lifecycle(native_addr: int, event: str) -> None:
75
- """Forward a Swift view-controller lifecycle event to its host."""
76
- from .ios import forward_lifecycle as forward
77
-
78
- forward(native_addr, event)
79
-
80
-
81
58
  def drain_desktop_scheduled_renders() -> None:
82
59
  """Drain deferred desktop renders (called by the preview's Tk loop)."""
83
60
  from .desktop import drain_desktop_scheduled_renders as drain
@@ -0,0 +1,348 @@
1
+ """On-device screen host, shared by iOS and Android.
2
+
3
+ One [`NativeScreenHost`][pythonnative.hosts.native.NativeScreenHost]
4
+ exists per native screen (``UIViewController`` / ``Fragment``). The
5
+ native side never holds Python objects: screens are addressed by an
6
+ integer id it assigns, lifecycle arrives through
7
+ ``callback("host", screen_id, event, payload)`` (routed here by
8
+ [`dispatch_host_event`][pythonnative.hosts.native.dispatch_host_event]),
9
+ and everything the host needs from the platform goes out through the
10
+ ``Host`` native module (``attach_root``, ``push``, ``pop``, ...). See
11
+ ``docs/concepts/bridge.md`` for the payloads.
12
+
13
+ Because native reports viewport size, safe-area insets, keyboard
14
+ height, and color scheme *with* each lifecycle event, this module
15
+ never queries the platform; it only publishes what it is told.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import json
21
+ import threading
22
+ from typing import Any, Dict, Optional, Sequence, Tuple
23
+
24
+ from .. import appearance, diagnostics, platform_metrics
25
+ from ..bridge import codec
26
+ from ..native_modules.registry import native_module
27
+ from .base import ScreenHost, flush_hosts, log_pn
28
+
29
+ __all__ = ["NativeScreenHost", "dispatch_host_event", "host_for_screen", "live_hosts"]
30
+
31
+ _HOSTS: Dict[int, "NativeScreenHost"] = {}
32
+ _SCHEDULED: Dict[int, ScreenHost] = {}
33
+ _flush_pending = False
34
+
35
+
36
+ def host_for_screen(screen_id: int) -> Optional["NativeScreenHost"]:
37
+ """Return the host registered for ``screen_id`` (``None`` if unknown)."""
38
+ return _HOSTS.get(int(screen_id))
39
+
40
+
41
+ def live_hosts() -> Sequence["NativeScreenHost"]:
42
+ """Every host currently registered, in creation order."""
43
+ return list(_HOSTS.values())
44
+
45
+
46
+ def _host_module() -> Any:
47
+ return native_module("Host")
48
+
49
+
50
+ # ======================================================================
51
+ # Deferred renders
52
+ # ======================================================================
53
+
54
+
55
+ def _flush_scheduled() -> None:
56
+ global _flush_pending
57
+ _flush_pending = False
58
+ hosts = list(_SCHEDULED.values())
59
+ _SCHEDULED.clear()
60
+ if hosts:
61
+ log_pn(f"render_scheduler: flushing {len(hosts)} host(s)")
62
+ flush_hosts(hosts)
63
+
64
+
65
+ def _request_flush() -> None:
66
+ """Queue one main-thread flush of every scheduled host."""
67
+ global _flush_pending
68
+ if _flush_pending:
69
+ return
70
+ _flush_pending = True
71
+ from ..bridge import post_to_main
72
+
73
+ post_to_main(_flush_scheduled)
74
+
75
+
76
+ # ======================================================================
77
+ # Payload helpers
78
+ # ======================================================================
79
+
80
+
81
+ def _publish_metrics(payload: Any) -> Tuple[float, float]:
82
+ """Publish viewport, insets, keyboard, and color scheme from a host payload.
83
+
84
+ Returns the ``(width, height)`` it found (``0, 0`` when absent).
85
+ """
86
+ if not isinstance(payload, dict):
87
+ return (0.0, 0.0)
88
+ width = float(payload.get("width") or 0.0)
89
+ height = float(payload.get("height") or 0.0)
90
+ insets = payload.get("insets")
91
+ if isinstance(insets, dict):
92
+ platform_metrics.set_safe_area_insets(
93
+ float(insets.get("top") or 0.0),
94
+ float(insets.get("left") or 0.0),
95
+ float(insets.get("bottom") or 0.0),
96
+ float(insets.get("right") or 0.0),
97
+ )
98
+ if "keyboard_height" in payload:
99
+ platform_metrics.set_keyboard_height(float(payload.get("keyboard_height") or 0.0))
100
+ scheme = payload.get("color_scheme")
101
+ if scheme in ("light", "dark"):
102
+ appearance.set_system_color_scheme(str(scheme))
103
+ if width > 0 and height > 0:
104
+ platform_metrics.set_window_dimensions(width, height)
105
+ return (width, height)
106
+
107
+
108
+ # ======================================================================
109
+ # Host
110
+ # ======================================================================
111
+
112
+
113
+ class NativeScreenHost(ScreenHost):
114
+ """Screen host addressed by an integer ``screen_id`` on the bridge."""
115
+
116
+ def __init__(self, screen_id: int, component_path: str, component: Any) -> None:
117
+ super().__init__(int(screen_id), component_path, component)
118
+ self.screen_id = int(screen_id)
119
+ self._pending_viewport: Optional[Tuple[float, float]] = None
120
+ _HOSTS[self.screen_id] = self
121
+
122
+ # -- lifecycle ------------------------------------------------------
123
+
124
+ def on_destroy(self) -> None:
125
+ """Forget the screen id, then tear down the tree."""
126
+ _HOSTS.pop(self.screen_id, None)
127
+ _SCHEDULED.pop(id(self), None)
128
+ super().on_destroy()
129
+
130
+ def apply_metrics(self, payload: Any) -> None:
131
+ """Publish the metrics carried by a host event and resize the viewport."""
132
+ width, height = _publish_metrics(payload)
133
+ if width > 0 and height > 0:
134
+ self._pending_viewport = (width, height)
135
+ self.set_viewport_size(width, height)
136
+
137
+ # -- platform primitives -------------------------------------------
138
+
139
+ def _initial_viewport_size(self) -> Optional[Tuple[float, float]]:
140
+ if self._pending_viewport is not None:
141
+ return self._pending_viewport
142
+ dims = platform_metrics.get_window_dimensions()
143
+ if dims.width > 0 and dims.height > 0:
144
+ return (dims.width, dims.height)
145
+ return None
146
+
147
+ def _schedule_render_async(self) -> bool:
148
+ if self._render_scheduled:
149
+ return True
150
+ self._render_scheduled = True
151
+ _SCHEDULED[id(self)] = self
152
+ try:
153
+ _request_flush()
154
+ except Exception as exc:
155
+ self._render_scheduled = False
156
+ _SCHEDULED.pop(id(self), None)
157
+ log_pn(f"request_render: bridge defer failed ({exc!r}); rendering synchronously")
158
+ return False
159
+ return True
160
+
161
+ def _attach_root(self, native_view: Any) -> None:
162
+ tag = getattr(native_view, "tag", None)
163
+ if tag is None:
164
+ return
165
+ # Native answers with the viewport the root now occupies, which
166
+ # is the earliest exact size available on iOS.
167
+ result = _host_module().call("attach_root", screen=self.screen_id, tag=int(tag))
168
+ if isinstance(result, dict):
169
+ self.apply_metrics(result)
170
+
171
+ def _detach_root(self, native_view: Any) -> None:
172
+ tag = getattr(native_view, "tag", None)
173
+ if tag is None:
174
+ return
175
+ try:
176
+ _host_module().call("detach_root", screen=self.screen_id, tag=int(tag))
177
+ except Exception:
178
+ diagnostics.swallowed("hosts.native.detach_root")
179
+
180
+ def _native_push(self, component_path: str, args: Dict[str, Any], options: Dict[str, Any]) -> None:
181
+ _host_module().call(
182
+ "push",
183
+ screen=self.screen_id,
184
+ path=component_path,
185
+ args=json.dumps(args) if args else None,
186
+ options=codec.to_jsonable(options or {}),
187
+ )
188
+
189
+ def _native_pop(self, count: int) -> None:
190
+ _host_module().call("pop", screen=self.screen_id, count=int(count))
191
+
192
+ def _native_pop_to_root(self) -> None:
193
+ _host_module().call("pop_to_root", screen=self.screen_id)
194
+
195
+ def _native_replace(self, component_path: str, args: Dict[str, Any], options: Dict[str, Any]) -> None:
196
+ _host_module().call(
197
+ "replace",
198
+ screen=self.screen_id,
199
+ path=component_path,
200
+ args=json.dumps(args) if args else None,
201
+ options=codec.to_jsonable(options or {}),
202
+ )
203
+
204
+ def _native_reset(self, component_path: str, screens: Sequence[Tuple[Dict[str, Any], Dict[str, Any]]]) -> None:
205
+ _host_module().call(
206
+ "reset",
207
+ screen=self.screen_id,
208
+ path=component_path,
209
+ screens=[
210
+ {
211
+ "path": component_path,
212
+ "args": json.dumps(args) if args else None,
213
+ "options": codec.to_jsonable(options or {}),
214
+ }
215
+ for args, options in screens
216
+ ],
217
+ )
218
+
219
+ def _native_set_options(self, options: Dict[str, Any]) -> None:
220
+ _host_module().call("set_options", screen=self.screen_id, options=codec.to_jsonable(options or {}))
221
+
222
+
223
+ # ======================================================================
224
+ # Inbound host events
225
+ # ======================================================================
226
+
227
+ _create_lock = threading.Lock()
228
+
229
+
230
+ def dispatch_host_event(screen_id: int, event: str, payload: Any) -> Optional[str]:
231
+ """Route ``callback("host", screen_id, event, payload)`` to a host.
232
+
233
+ Events (payloads are what ``PNViewController`` and
234
+ ``PNScreenFragment`` send):
235
+
236
+ - ``create`` ``{"path", "args", "dev_root", "restored_state",
237
+ ...metrics}``: import the component, create the host, and mount.
238
+ ``path`` is the dotted component path (``None`` means the app
239
+ entry module), ``args`` a JSON string, ``dev_root`` the hot-reload
240
+ overlay directory for debug builds. Returns ``{"root": tag}``.
241
+ - ``start`` / ``resume`` / ``pause`` / ``stop`` / ``destroy``:
242
+ lifecycle. ``resume`` and ``layout`` carry metrics.
243
+ - ``layout`` ``{width, height, insets, keyboard_height, color_scheme}``.
244
+ - ``appearance`` ``{"color_scheme"}``.
245
+ - ``back_pressed``: returns ``"true"`` when a handler consumed it.
246
+ - ``hot_reload_tick``: poll the reload manifest; returns ``"true"``
247
+ when a reload ran.
248
+ - ``save_state`` / ``restore_state``: instance-state hooks.
249
+ - ``flush``: run deferred renders now.
250
+
251
+ Returns a JSON string for request-style events, else ``None``.
252
+ """
253
+ screen_id = int(screen_id)
254
+ if event == "create":
255
+ return _create(screen_id, payload if isinstance(payload, dict) else {})
256
+ if event == "flush":
257
+ _flush_scheduled()
258
+ return None
259
+ if event == "appearance":
260
+ _publish_metrics(payload)
261
+ return None
262
+ host = _HOSTS.get(screen_id)
263
+ if host is None:
264
+ log_pn(f"dispatch_host_event: no host for screen={screen_id} event={event!r}")
265
+ return None
266
+ if event == "layout":
267
+ host.apply_metrics(payload)
268
+ host.on_layout()
269
+ return None
270
+ if event == "resume":
271
+ host.apply_metrics(payload)
272
+ host.on_resume()
273
+ return None
274
+ if event == "back_pressed":
275
+ return "true" if host.on_back_pressed() else "false"
276
+ if event in ("hot_reload_tick", "tick"):
277
+ return "true" if host.hot_reload_tick() else "false"
278
+ if event == "save_state":
279
+ host.on_save_instance_state()
280
+ return None
281
+ if event == "restore_state":
282
+ host.on_restore_instance_state()
283
+ return None
284
+ if event == "destroy":
285
+ host.on_destroy()
286
+ return None
287
+ handler = getattr(host, f"on_{event}", None)
288
+ if handler is None:
289
+ log_pn(f"dispatch_host_event: unknown event {event!r}")
290
+ return None
291
+ handler()
292
+ return None
293
+
294
+
295
+ def _create(screen_id: int, payload: Dict[str, Any]) -> Optional[str]:
296
+ from . import import_component
297
+
298
+ component_path = str(payload.get("path") or payload.get("component") or _entry_module())
299
+ with _create_lock:
300
+ existing = _HOSTS.get(screen_id)
301
+ if existing is not None:
302
+ existing.apply_metrics(payload)
303
+ existing.on_create()
304
+ return _root_json(existing)
305
+ component = import_component(component_path)
306
+ host = NativeScreenHost(screen_id, component_path, component)
307
+ args = payload.get("args")
308
+ if args:
309
+ host.set_args(args)
310
+ dev_root = payload.get("dev_root")
311
+ if dev_root:
312
+ from .. import hot_reload
313
+
314
+ host.enable_hot_reload(hot_reload.manifest_path_for(str(dev_root)), str(dev_root))
315
+ if not (payload.get("width") and payload.get("height")):
316
+ # ``create`` is sent before the first layout pass; ask native for
317
+ # its best guess so the first commit lays out at the right size.
318
+ try:
319
+ viewport = _host_module().call("viewport", screen=screen_id)
320
+ except Exception:
321
+ viewport = None
322
+ if isinstance(viewport, dict):
323
+ payload = {**viewport, **payload}
324
+ _publish_metrics(payload)
325
+ width = float(payload.get("width") or 0.0)
326
+ height = float(payload.get("height") or 0.0)
327
+ if width > 0 and height > 0:
328
+ host._pending_viewport = (width, height)
329
+ host.on_create()
330
+ return _root_json(host)
331
+
332
+
333
+ def _entry_module() -> str:
334
+ import os
335
+
336
+ return os.environ.get("PN_ENTRY_MODULE") or "app.main"
337
+
338
+
339
+ def _root_json(host: ScreenHost) -> str:
340
+ tag = getattr(host.root_native_view, "tag", None)
341
+ return codec.dumps({"root": None if tag is None else int(tag)})
342
+
343
+
344
+ def _reset_for_tests() -> None:
345
+ global _flush_pending
346
+ _HOSTS.clear()
347
+ _SCHEDULED.clear()
348
+ _flush_pending = False
pythonnative/images.py CHANGED
@@ -38,13 +38,14 @@ def _default_cache_dir() -> str:
38
38
  backups). Desktop: a per-user directory under the system temp dir.
39
39
  """
40
40
  try:
41
- from .utils import IS_ANDROID
41
+ from .utils import IS_ANDROID, IS_IOS
42
42
 
43
- if IS_ANDROID:
44
- from .utils import get_android_context
43
+ if IS_ANDROID or IS_IOS:
44
+ from .native_modules.registry import native_module
45
45
 
46
- base = str(get_android_context().getCacheDir().getAbsolutePath())
47
- return os.path.join(base, "pn_images")
46
+ info = native_module("Device").call("info")
47
+ if isinstance(info, dict) and info.get("cache_dir"):
48
+ return os.path.join(str(info["cache_dir"]), "pn_images")
48
49
  except Exception:
49
50
  pass
50
51
  home = os.path.expanduser("~")