pythonnative 0.36.0__py3-none-any.whl → 0.38.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 (187) hide show
  1. pythonnative/__init__.py +14 -4
  2. pythonnative/_ios_log.py +1 -1
  3. pythonnative/alerts.py +24 -86
  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 +96 -10
  11. pythonnative/components/_base.py +31 -0
  12. pythonnative/components/controls.py +19 -16
  13. pythonnative/components/layout.py +70 -11
  14. pythonnative/components/lists.py +6 -6
  15. pythonnative/gestures.py +40 -16
  16. pythonnative/hooks.py +41 -26
  17. pythonnative/hosts/__init__.py +13 -36
  18. pythonnative/hosts/native.py +348 -0
  19. pythonnative/images.py +6 -5
  20. pythonnative/native_modules/__init__.py +49 -29
  21. pythonnative/native_modules/app_state.py +10 -6
  22. pythonnative/native_modules/battery.py +22 -87
  23. pythonnative/native_modules/biometrics.py +15 -130
  24. pythonnative/native_modules/camera.py +24 -296
  25. pythonnative/native_modules/clipboard.py +14 -111
  26. pythonnative/native_modules/desktop.py +322 -0
  27. pythonnative/native_modules/file_system.py +90 -187
  28. pythonnative/native_modules/haptics.py +29 -135
  29. pythonnative/native_modules/linking.py +29 -141
  30. pythonnative/native_modules/location.py +32 -186
  31. pythonnative/native_modules/net_info.py +22 -135
  32. pythonnative/native_modules/notifications.py +37 -265
  33. pythonnative/native_modules/permissions.py +78 -194
  34. pythonnative/native_modules/registry.py +513 -0
  35. pythonnative/native_modules/secure_store.py +23 -164
  36. pythonnative/native_modules/share.py +9 -114
  37. pythonnative/native_views/__init__.py +67 -86
  38. pythonnative/native_views/bridge_backend.py +264 -0
  39. pythonnative/navigation/__init__.py +2 -1
  40. pythonnative/navigation/hooks.py +46 -7
  41. pythonnative/navigation/state.py +30 -9
  42. pythonnative/platform.py +19 -15
  43. pythonnative/platform_metrics.py +3 -3
  44. pythonnative/project/android.py +23 -3
  45. pythonnative/project/builder.py +99 -48
  46. pythonnative/project/config.py +96 -26
  47. pythonnative/project/deps.py +703 -0
  48. pythonnative/project/doctor.py +53 -4
  49. pythonnative/project/plugins.py +366 -0
  50. pythonnative/project/runtime_assets.py +13 -14
  51. pythonnative/py.typed +0 -0
  52. pythonnative/runtime.py +35 -203
  53. pythonnative/sdk/__init__.py +48 -33
  54. pythonnative/sdk/_components.py +57 -76
  55. pythonnative/storage.py +18 -243
  56. pythonnative/style.py +94 -44
  57. pythonnative/templates/android_template/app/build.gradle +11 -11
  58. pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/MainActivity.kt +34 -166
  59. pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/ScreenFragment.kt +13 -151
  60. pythonnative/templates/android_template/build.gradle +7 -4
  61. pythonnative/templates/android_template/gradle/wrapper/gradle-wrapper.properties +1 -1
  62. pythonnative/templates/android_template/pythonnative/build.gradle +48 -0
  63. pythonnative/templates/android_template/pythonnative/src/main/AndroidManifest.xml +11 -0
  64. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/PNBridge.kt +214 -0
  65. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/PythonHost.kt +20 -0
  66. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/animation/AnimationSpecs.kt +141 -0
  67. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/animation/PNAnimator.kt +249 -0
  68. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/bridge/JsonUtil.kt +175 -0
  69. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/bridge/MainThread.kt +32 -0
  70. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/bridge/PNLog.kt +33 -0
  71. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/bridge/PNRegistry.kt +82 -0
  72. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/bridge/PNTransaction.kt +80 -0
  73. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/bridge/TransactionApplier.kt +115 -0
  74. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/bridge/ViewRegistry.kt +74 -0
  75. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/BuiltinComponents.kt +35 -0
  76. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ButtonManager.kt +33 -0
  77. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ComponentManager.kt +213 -0
  78. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ContainerManagers.kt +69 -0
  79. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ControlManagers.kt +186 -0
  80. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/DatePickerManager.kt +123 -0
  81. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ImageLoader.kt +141 -0
  82. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ImageManager.kt +156 -0
  83. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ModalManager.kt +134 -0
  84. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/PNColor.kt +201 -0
  85. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/PickerManager.kt +87 -0
  86. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/PortalManager.kt +55 -0
  87. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/PressableManager.kt +122 -0
  88. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ScrollViewManager.kt +214 -0
  89. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/SegmentedControlManager.kt +113 -0
  90. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/StatusBarManager.kt +52 -0
  91. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/TabBarManager.kt +131 -0
  92. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/TextInputManager.kt +245 -0
  93. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/TextManager.kt +233 -0
  94. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ViewStyler.kt +355 -0
  95. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/VirtualListManager.kt +179 -0
  96. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/WebViewManager.kt +149 -0
  97. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/gestures/GestureArbiter.kt +233 -0
  98. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/gestures/GestureCoordinator.kt +127 -0
  99. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/gestures/GestureRecognizers.kt +527 -0
  100. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/BuiltinModules.kt +73 -0
  101. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/DeviceModules.kt +316 -0
  102. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/HostModule.kt +97 -0
  103. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/MediaModules.kt +200 -0
  104. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/NativeModule.kt +135 -0
  105. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/NotificationsModule.kt +100 -0
  106. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/PermissionsModule.kt +99 -0
  107. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/StorageModules.kt +89 -0
  108. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/SystemModules.kt +178 -0
  109. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/plugins/GeneratedPlugins.kt +16 -0
  110. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/screens/Navigator.kt +116 -0
  111. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/screens/PNScreenFragment.kt +223 -0
  112. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/screens/ScreenRegistry.kt +80 -0
  113. pythonnative/templates/android_template/{app/src/main/java/com/pythonnative/android_template → pythonnative/src/main/java/com/pythonnative/runtime/views}/PNAccessibilityDelegate.kt +5 -4
  114. pythonnative/templates/android_template/{app/src/main/java/com/pythonnative/android_template → pythonnative/src/main/java/com/pythonnative/runtime/views}/PNBorderDrawable.kt +12 -3
  115. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/views/PNEditText.kt +18 -0
  116. pythonnative/templates/android_template/{app/src/main/java/com/pythonnative/android_template → pythonnative/src/main/java/com/pythonnative/runtime/views}/PNFrameLayout.kt +9 -10
  117. pythonnative/templates/android_template/{app/src/main/java/com/pythonnative/android_template → pythonnative/src/main/java/com/pythonnative/runtime/views}/PNVirtualListView.java +9 -12
  118. pythonnative/templates/android_template/pythonnative/src/test/java/com/pythonnative/runtime/AnimationSpecsTest.kt +75 -0
  119. pythonnative/templates/android_template/pythonnative/src/test/java/com/pythonnative/runtime/GestureArbiterTest.kt +197 -0
  120. pythonnative/templates/android_template/pythonnative/src/test/java/com/pythonnative/runtime/PNColorTest.kt +47 -0
  121. pythonnative/templates/android_template/pythonnative/src/test/java/com/pythonnative/runtime/PNTransactionTest.kt +78 -0
  122. pythonnative/templates/android_template/pythonnative/src/test/java/com/pythonnative/runtime/PromiseTest.kt +75 -0
  123. pythonnative/templates/android_template/settings.gradle +1 -0
  124. pythonnative/templates/ios_template/PythonNativeKit/Package.swift +21 -0
  125. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Animation/PNAnimator.swift +301 -0
  126. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Bridge/PNBridge.swift +191 -0
  127. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Bridge/PNRegistry.swift +140 -0
  128. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Bridge/PNTransaction.swift +165 -0
  129. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Bridge/PNViewRegistry.swift +53 -0
  130. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNButtonManagers.swift +206 -0
  131. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNColor.swift +173 -0
  132. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNComponentManager.swift +183 -0
  133. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNContainerView.swift +56 -0
  134. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNControlManagers.swift +281 -0
  135. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNImageManager.swift +221 -0
  136. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNOverlayManagers.swift +243 -0
  137. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNPressableManager.swift +114 -0
  138. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNScrollViewManager.swift +197 -0
  139. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNTabBarManager.swift +106 -0
  140. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNTextInputManager.swift +289 -0
  141. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNTextManager.swift +230 -0
  142. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNTransform.swift +82 -0
  143. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNViewManager.swift +86 -0
  144. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNViewState.swift +87 -0
  145. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNViewStyler.swift +340 -0
  146. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNVirtualListManager.swift +257 -0
  147. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNWebViewManager.swift +124 -0
  148. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Gestures/PNGestureCoordinator.swift +260 -0
  149. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Modules/AlertModule.swift +92 -0
  150. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Modules/HostModule.swift +158 -0
  151. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Modules/LifecycleModules.swift +174 -0
  152. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Modules/MediaModules.swift +177 -0
  153. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Modules/PNNativeModule.swift +125 -0
  154. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Modules/PermissionModules.swift +266 -0
  155. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Modules/SystemModules.swift +251 -0
  156. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Plugins/PNPluginRegistration.swift +8 -0
  157. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Screens/PNScreenRegistry.swift +46 -0
  158. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Screens/PNViewController.swift +251 -0
  159. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Support/PNCompat.swift +46 -0
  160. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Support/PNJSON.swift +173 -0
  161. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Support/PNLog.swift +47 -0
  162. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Support/PNWindow.swift +51 -0
  163. pythonnative/templates/ios_template/PythonNativeKit/Tests/PythonNativeKitTests/PNColorTests.swift +50 -0
  164. pythonnative/templates/ios_template/PythonNativeKit/Tests/PythonNativeKitTests/PNGestureTests.swift +101 -0
  165. pythonnative/templates/ios_template/PythonNativeKit/Tests/PythonNativeKitTests/PNManagerTests.swift +124 -0
  166. pythonnative/templates/ios_template/PythonNativeKit/Tests/PythonNativeKitTests/PNModuleTests.swift +98 -0
  167. pythonnative/templates/ios_template/PythonNativeKit/Tests/PythonNativeKitTests/PNTransactionTests.swift +91 -0
  168. pythonnative/templates/ios_template/ios_template/AppDelegate.swift +8 -50
  169. pythonnative/templates/ios_template/ios_template/Info.plist +4 -4
  170. pythonnative/templates/ios_template/ios_template/PythonRuntime.swift +41 -48
  171. pythonnative/templates/ios_template/ios_template/SceneDelegate.swift +11 -16
  172. pythonnative/templates/ios_template/ios_template/ViewController.swift +12 -142
  173. pythonnative/templates/ios_template/ios_template.xcodeproj/project.pbxproj +82 -10
  174. pythonnative/utils.py +26 -109
  175. pythonnative/virtual_rows.py +13 -23
  176. {pythonnative-0.36.0.dist-info → pythonnative-0.38.0.dist-info}/METADATA +10 -12
  177. pythonnative-0.38.0.dist-info/RECORD +264 -0
  178. pythonnative/hosts/android.py +0 -269
  179. pythonnative/hosts/ios.py +0 -404
  180. pythonnative/native_views/android.py +0 -3702
  181. pythonnative/native_views/ios.py +0 -5253
  182. pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/Navigator.kt +0 -43
  183. pythonnative-0.36.0.dist-info/RECORD +0 -155
  184. {pythonnative-0.36.0.dist-info → pythonnative-0.38.0.dist-info}/WHEEL +0 -0
  185. {pythonnative-0.36.0.dist-info → pythonnative-0.38.0.dist-info}/entry_points.txt +0 -0
  186. {pythonnative-0.36.0.dist-info → pythonnative-0.38.0.dist-info}/licenses/LICENSE +0 -0
  187. {pythonnative-0.36.0.dist-info → pythonnative-0.38.0.dist-info}/top_level.txt +0 -0
pythonnative/cli/pn.py CHANGED
@@ -6,6 +6,9 @@ The console script `pn` (declared in `pyproject.toml`) dispatches to:
6
6
  ``app/``) into ``./name/``, or into the current directory when no name
7
7
  is given.
8
8
  - `pn doctor [platform]`: diagnose the local toolchain and config.
9
+ - `pn deps [platform]`: resolve ``[requirements].packages`` for every
10
+ device target and report which wheels would be used (or why a package
11
+ can't be installed), without building anything.
9
12
  - `pn preview [component]`: render the app in a desktop (Tkinter) window
10
13
  with Fast Refresh, the fast inner dev loop, no device required.
11
14
  - `pn devices [platform]`: list connected devices, emulators, and
@@ -41,6 +44,7 @@ from pathlib import Path
41
44
  from typing import Any, Dict, List, Optional, TextIO
42
45
 
43
46
  from ..project import builder as builder_mod
47
+ from ..project import deps as deps_mod
44
48
  from ..project import devices as devices_mod
45
49
  from ..project import doctor as doctor_mod
46
50
  from ..project.android import collect_logcat_filters
@@ -54,22 +58,29 @@ HOT_RELOAD_DEV_ROOT = "pythonnative_dev"
54
58
  # init
55
59
  # ======================================================================
56
60
 
57
- _MAIN_TEMPLATE = """import pythonnative as pn
61
+ _MAIN_TEMPLATE = """from typing import TypedDict
62
+
63
+ import pythonnative as pn
58
64
 
59
65
  Stack = pn.create_stack_navigator()
60
66
 
61
67
 
68
+ class DetailParams(TypedDict):
69
+ count: int
70
+
71
+
62
72
  @pn.component
63
73
  def HomeScreen():
64
74
  count, set_count = pn.use_state(0)
65
75
  nav = pn.use_navigation()
76
+ theme = pn.use_theme()
66
77
  return pn.ScrollView(
67
78
  pn.Column(
68
- pn.Text("Hello from PythonNative!", style={"font_size": 24, "bold": True}),
79
+ pn.Text("Hello from PythonNative!", style={"font_size": theme.font_size_title, "bold": True}),
69
80
  pn.Text(f"Tapped {count} times"),
70
81
  pn.Button("Tap me", on_press=lambda: set_count(count + 1)),
71
- pn.Button("Open detail", on_press=lambda: nav.navigate("Detail", {"count": count})),
72
- style={"spacing": 12, "padding": 16, "align_items": "stretch"},
82
+ pn.Button("Open detail", on_press=lambda: nav.navigate("Detail", count=count)),
83
+ style={"spacing": theme.spacing_large, "padding": 16, "align_items": "stretch"},
73
84
  )
74
85
  )
75
86
 
@@ -77,9 +88,9 @@ def HomeScreen():
77
88
  @pn.component
78
89
  def DetailScreen():
79
90
  nav = pn.use_navigation()
80
- params = pn.use_route()
91
+ route = pn.use_route(DetailParams)
81
92
  return pn.Column(
82
- pn.Text(f"Detail: count was {params.get('count', 0)}", style={"font_size": 20}),
93
+ pn.Text(f"Detail: count was {route.params['count']}", style={"font_size": 20}),
83
94
  pn.Button("Back", on_press=nav.go_back),
84
95
  style={"spacing": 12, "padding": 16},
85
96
  )
@@ -89,8 +100,8 @@ def DetailScreen():
89
100
  def App():
90
101
  return pn.NavigationContainer(
91
102
  Stack.Navigator(
92
- Stack.Screen("Home", component=HomeScreen, options={"title": "Home"}),
93
- Stack.Screen("Detail", component=DetailScreen, options={"title": "Detail"}),
103
+ Stack.Screen("Home", HomeScreen, title="Home"),
104
+ Stack.Screen("Detail", DetailScreen, title="Detail"),
94
105
  )
95
106
  )
96
107
  """
@@ -278,6 +289,56 @@ def app_id_command(args: argparse.Namespace) -> None:
278
289
  print(config.application_id if args.platform == "android" else config.bundle_id)
279
290
 
280
291
 
292
+ # ======================================================================
293
+ # deps
294
+ # ======================================================================
295
+
296
+
297
+ def deps_command(args: argparse.Namespace) -> None:
298
+ """Report how ``[requirements].packages`` resolve for each device target.
299
+
300
+ Runs pip in its cross-platform dry-run mode once per target (iOS
301
+ device, iOS Simulator, and one per Android ABI) and prints the
302
+ wheel each package would use, flagging binary wheels and their
303
+ source index. Exits non-zero when any target can't be satisfied,
304
+ so it doubles as a CI gate. ``--json`` emits the same data as a
305
+ machine-readable document.
306
+
307
+ Args:
308
+ args: Parsed namespace with optional ``platform``, ``json``, and
309
+ ``python`` (the interpreter to run pip with).
310
+ """
311
+ platform: Optional[str] = getattr(args, "platform", None)
312
+ as_json: bool = getattr(args, "json", False)
313
+ python: Optional[str] = getattr(args, "python", None)
314
+
315
+ config = _load_config_or_exit()
316
+ targets = deps_mod.targets_for(config, platform)
317
+ runner = builder_mod.SubprocessRunner()
318
+ if not as_json and config.requirements:
319
+ print(
320
+ f"Resolving {len(config.requirements)} requirement(s) for Python {config.python_version} "
321
+ f"across {len(targets)} target(s)...\n"
322
+ )
323
+ resolutions = deps_mod.resolve_all(config, targets, runner=runner, python=python)
324
+
325
+ if as_json:
326
+ print(
327
+ json.dumps(
328
+ {
329
+ "python_version": config.python_version,
330
+ "requirements": list(config.requirements),
331
+ "targets": [res.to_dict() for res in resolutions],
332
+ },
333
+ indent=2,
334
+ )
335
+ )
336
+ else:
337
+ print(deps_mod.format_report(resolutions, requirements=config.requirements))
338
+ if any(not res.ok for res in resolutions):
339
+ sys.exit(1)
340
+
341
+
281
342
  # ======================================================================
282
343
  # preview
283
344
  # ======================================================================
@@ -426,8 +487,18 @@ def run_project(args: argparse.Namespace) -> None:
426
487
  config = _load_config_or_exit()
427
488
  builder = builder_mod.Builder(config, log=print)
428
489
 
490
+ # Resolve third-party packages only for the destination being built
491
+ # (device wheels and Simulator wheels differ); prepare-only keeps both
492
+ # slices so the staged project builds for either in Xcode.
493
+ if prepare_only:
494
+ ios_sdks: tuple = deps_mod.IOS_SDKS
495
+ elif device is not None and device.kind == "device":
496
+ ios_sdks = ("iphoneos",)
497
+ else:
498
+ ios_sdks = ("iphonesimulator",)
499
+
429
500
  try:
430
- prepared = builder.prepare(platform)
501
+ prepared = builder.prepare(platform, ios_sdks=ios_sdks)
431
502
  except builder_mod.BuildError as exc:
432
503
  print(f"Error: {exc}")
433
504
  sys.exit(1)
@@ -593,7 +664,11 @@ def build_project(args: argparse.Namespace) -> None:
593
664
  sys.exit(1)
594
665
 
595
666
  try:
596
- prepared = builder.prepare(platform, release=not debug)
667
+ prepared = builder.prepare(
668
+ platform,
669
+ release=not debug,
670
+ ios_sdks=("iphonesimulator",) if debug else ("iphoneos",),
671
+ )
597
672
  if platform == "android":
598
673
  artifacts = builder.build_android(prepared, debug=debug)
599
674
  else:
@@ -1020,6 +1095,17 @@ def _build_parser() -> argparse.ArgumentParser:
1020
1095
  parser_doctor.add_argument("platform", nargs="?", choices=["android", "ios"], help="Restrict checks to a platform")
1021
1096
  parser_doctor.set_defaults(func=doctor_command)
1022
1097
 
1098
+ parser_deps = subparsers.add_parser(
1099
+ "deps", help="Check which wheels [requirements].packages resolve to on each device target"
1100
+ )
1101
+ parser_deps.add_argument("platform", nargs="?", choices=["android", "ios"], help="Restrict to a platform")
1102
+ parser_deps.add_argument("--json", action="store_true", help="Print a JSON report for scripting")
1103
+ parser_deps.add_argument(
1104
+ "--python",
1105
+ help="Interpreter to run pip with (default: the one running pn; any version works, pip cross-resolves)",
1106
+ )
1107
+ parser_deps.set_defaults(func=deps_command)
1108
+
1023
1109
  parser_preview = subparsers.add_parser("preview", help="Render the app in a desktop window")
1024
1110
  parser_preview.add_argument(
1025
1111
  "component",
@@ -76,6 +76,37 @@ def _make_element(
76
76
  return Element(name, out, list(children), key=key)
77
77
 
78
78
 
79
+ # ======================================================================
80
+ # RefreshControl attachment
81
+ # ======================================================================
82
+
83
+ REFRESH_CONTROL_TYPE = "RefreshControl"
84
+ """Element type produced by [`RefreshControl`][pythonnative.RefreshControl]."""
85
+
86
+
87
+ def _refresh_control_props(control: Optional[Element], *, owner: str) -> Optional[Dict[str, Any]]:
88
+ """Turn a ``RefreshControl`` element into the ``refresh_control`` wire prop.
89
+
90
+ Scroll containers hold their refresh control as a prop rather than
91
+ a child (the native side attaches a ``UIRefreshControl`` /
92
+ ``SwipeRefreshLayout`` to the scroll view itself), so the element
93
+ is unwrapped here into the flat dict the bridge and the event
94
+ extractor expect. Keeping it an element on the Python side means
95
+ users build it like every other piece of UI and get a clear error
96
+ if they pass the wrong thing.
97
+
98
+ Raises:
99
+ TypeError: If ``control`` is not a ``RefreshControl`` element.
100
+ """
101
+ if control is None:
102
+ return None
103
+ if not isinstance(control, Element) or control.type != REFRESH_CONTROL_TYPE:
104
+ raise TypeError(
105
+ f"{owner}(refresh_control=...) expects pn.RefreshControl(...), got {type(control).__name__}: {control!r}"
106
+ )
107
+ return dict(control.props)
108
+
109
+
79
110
  # ======================================================================
80
111
  # Rich text spans
81
112
  # ======================================================================
@@ -2,7 +2,7 @@
2
2
 
3
3
  ``Switch``, ``Slider``, ``ProgressBar``, ``ActivityIndicator``,
4
4
  ``Checkbox``, ``SegmentedControl``, ``DatePicker``, ``Picker``, the
5
- ``RefreshControl`` spec helper, and ``StatusBar``.
5
+ ``RefreshControl``, and ``StatusBar``.
6
6
  """
7
7
 
8
8
  from typing import Any, Callable, Dict, List, Literal, Optional
@@ -10,7 +10,7 @@ from typing import Any, Callable, Dict, List, Literal, Optional
10
10
  from ..element import Element
11
11
  from ..hooks import Ref
12
12
  from ..style import AccessibilityState, Color, StyleProp
13
- from ._base import _make_element
13
+ from ._base import REFRESH_CONTROL_TYPE, _make_element
14
14
 
15
15
 
16
16
  def Switch(
@@ -426,13 +426,17 @@ def RefreshControl(
426
426
  refreshing: bool = False,
427
427
  on_refresh: Optional[Callable[[], Any]] = None,
428
428
  tint_color: Optional[Color] = None,
429
- ) -> Dict[str, Any]:
430
- """Pull-to-refresh spec for [`ScrollView`][pythonnative.ScrollView] / [`FlatList`][pythonnative.FlatList].
429
+ ) -> Element:
430
+ """Pull-to-refresh control for [`ScrollView`][pythonnative.ScrollView] and the list components.
431
431
 
432
- Returns a plain dict that should be passed as the
433
- ``refresh_control=`` prop. Modeled as a dict (not an
434
- [`Element`][pythonnative.Element]) so the host scroll container can
435
- hold one without it appearing as a child node.
432
+ Pass the result as the ``refresh_control=`` prop of a
433
+ [`ScrollView`][pythonnative.ScrollView],
434
+ [`FlatList`][pythonnative.FlatList], or
435
+ [`SectionList`][pythonnative.SectionList]. It is a regular
436
+ [`Element`][pythonnative.Element] (type ``"RefreshControl"``) built
437
+ like every other piece of UI; the scroll container attaches it to
438
+ its native scroll view rather than rendering it as a child, and
439
+ rejects anything else with a ``TypeError``.
436
440
 
437
441
  Args:
438
442
  refreshing: Drive the spinner's visibility from a use_state
@@ -443,8 +447,7 @@ def RefreshControl(
443
447
  tint_color: Color of the spinner.
444
448
 
445
449
  Returns:
446
- Dict suitable for the ``refresh_control`` prop on a scroll
447
- container.
450
+ An [`Element`][pythonnative.Element] of type ``"RefreshControl"``.
448
451
 
449
452
  Example:
450
453
  ```python
@@ -467,12 +470,12 @@ def RefreshControl(
467
470
  )
468
471
  ```
469
472
  """
470
- spec: Dict[str, Any] = {"refreshing": bool(refreshing)}
471
- if on_refresh is not None:
472
- spec["on_refresh"] = on_refresh
473
- if tint_color is not None:
474
- spec["tint_color"] = tint_color
475
- return spec
473
+ return _make_element(
474
+ REFRESH_CONTROL_TYPE,
475
+ refreshing=bool(refreshing),
476
+ on_refresh=on_refresh,
477
+ tint_color=tint_color,
478
+ )
476
479
 
477
480
 
478
481
  def StatusBar(
@@ -12,7 +12,7 @@ from ..component import component
12
12
  from ..element import Element
13
13
  from ..hooks import Ref, use_keyboard_height, use_safe_area_insets, use_state
14
14
  from ..style import AccessibilityState, StyleProp, resolve_style
15
- from ._base import _make_element
15
+ from ._base import _make_element, _refresh_control_props
16
16
 
17
17
 
18
18
  def View(
@@ -113,7 +113,16 @@ def View(
113
113
  def Column(
114
114
  *children: Element,
115
115
  style: StyleProp = None,
116
+ gestures: Optional[List[Any]] = None,
117
+ hit_slop: Optional[Union[float, Dict[str, float]]] = None,
116
118
  on_layout: Optional[Callable[[Dict[str, float]], None]] = None,
119
+ accessibility_label: Optional[str] = None,
120
+ accessibility_hint: Optional[str] = None,
121
+ accessibility_role: Optional[str] = None,
122
+ accessible: Optional[bool] = None,
123
+ accessibility_state: Optional[AccessibilityState] = None,
124
+ accessibility_live_region: Optional[Literal["none", "polite", "assertive"]] = None,
125
+ test_id: Optional[str] = None,
117
126
  ref: Optional[Ref] = None,
118
127
  key: Optional[str] = None,
119
128
  ) -> Element:
@@ -123,12 +132,25 @@ def Column(
123
132
  ``flex_direction`` locked to ``"column"``. Use ``View`` directly if
124
133
  you need to switch between row and column at runtime.
125
134
 
135
+ Accepts every [`View`][pythonnative.View] prop (gestures, hit slop,
136
+ accessibility, ``test_id``); only ``flex_direction`` is fixed.
137
+
126
138
  Args:
127
139
  *children: Child elements stacked top to bottom.
128
140
  style: Style dict (or list of dicts).
141
+ gestures: Gesture descriptors recognized natively on this view.
142
+ hit_slop: Extra touch target beyond the bounds (see ``View``).
129
143
  on_layout: Callback invoked with
130
144
  ``{"x", "y", "width", "height"}`` after layout and on
131
145
  frame changes.
146
+ accessibility_label: Spoken description for screen readers.
147
+ accessibility_hint: Spoken extra detail (iOS only).
148
+ accessibility_role: Semantic role for assistive tech.
149
+ accessible: Override whether the element is exposed to AT.
150
+ accessibility_state: Current widget state for assistive tech.
151
+ accessibility_live_region: How AT announces dynamic changes
152
+ (Android only).
153
+ test_id: Stable identifier for UI tests.
132
154
  ref: Optional [`Ref`][pythonnative.Ref] for native-view access.
133
155
  key: Stable identity for keyed reconciliation.
134
156
 
@@ -139,9 +161,18 @@ def Column(
139
161
  "Column",
140
162
  *children,
141
163
  style=style,
142
- on_layout=on_layout,
143
164
  ref=ref,
144
165
  key=key,
166
+ gestures=gestures,
167
+ hit_slop=hit_slop,
168
+ on_layout=on_layout,
169
+ accessibility_label=accessibility_label,
170
+ accessibility_hint=accessibility_hint,
171
+ accessibility_role=accessibility_role,
172
+ accessible=accessible,
173
+ accessibility_state=accessibility_state,
174
+ accessibility_live_region=accessibility_live_region,
175
+ test_id=test_id,
145
176
  _forced={"flex_direction": "column"},
146
177
  )
147
178
 
@@ -149,7 +180,16 @@ def Column(
149
180
  def Row(
150
181
  *children: Element,
151
182
  style: StyleProp = None,
183
+ gestures: Optional[List[Any]] = None,
184
+ hit_slop: Optional[Union[float, Dict[str, float]]] = None,
152
185
  on_layout: Optional[Callable[[Dict[str, float]], None]] = None,
186
+ accessibility_label: Optional[str] = None,
187
+ accessibility_hint: Optional[str] = None,
188
+ accessibility_role: Optional[str] = None,
189
+ accessible: Optional[bool] = None,
190
+ accessibility_state: Optional[AccessibilityState] = None,
191
+ accessibility_live_region: Optional[Literal["none", "polite", "assertive"]] = None,
192
+ test_id: Optional[str] = None,
153
193
  ref: Optional[Ref] = None,
154
194
  key: Optional[str] = None,
155
195
  ) -> Element:
@@ -159,12 +199,25 @@ def Row(
159
199
  ``flex_direction`` locked to ``"row"``. Use ``View`` directly if you
160
200
  need to switch between row and column at runtime.
161
201
 
202
+ Accepts every [`View`][pythonnative.View] prop (gestures, hit slop,
203
+ accessibility, ``test_id``); only ``flex_direction`` is fixed.
204
+
162
205
  Args:
163
206
  *children: Child elements arranged left to right.
164
207
  style: Style dict (or list of dicts).
208
+ gestures: Gesture descriptors recognized natively on this view.
209
+ hit_slop: Extra touch target beyond the bounds (see ``View``).
165
210
  on_layout: Callback invoked with
166
211
  ``{"x", "y", "width", "height"}`` after layout and on
167
212
  frame changes.
213
+ accessibility_label: Spoken description for screen readers.
214
+ accessibility_hint: Spoken extra detail (iOS only).
215
+ accessibility_role: Semantic role for assistive tech.
216
+ accessible: Override whether the element is exposed to AT.
217
+ accessibility_state: Current widget state for assistive tech.
218
+ accessibility_live_region: How AT announces dynamic changes
219
+ (Android only).
220
+ test_id: Stable identifier for UI tests.
168
221
  ref: Optional [`Ref`][pythonnative.Ref] for native-view access.
169
222
  key: Stable identity for keyed reconciliation.
170
223
 
@@ -175,9 +228,18 @@ def Row(
175
228
  "Row",
176
229
  *children,
177
230
  style=style,
178
- on_layout=on_layout,
179
231
  ref=ref,
180
232
  key=key,
233
+ gestures=gestures,
234
+ hit_slop=hit_slop,
235
+ on_layout=on_layout,
236
+ accessibility_label=accessibility_label,
237
+ accessibility_hint=accessibility_hint,
238
+ accessibility_role=accessibility_role,
239
+ accessible=accessible,
240
+ accessibility_state=accessibility_state,
241
+ accessibility_live_region=accessibility_live_region,
242
+ test_id=test_id,
181
243
  _forced={"flex_direction": "row"},
182
244
  )
183
245
 
@@ -219,7 +281,7 @@ def Spacer(
219
281
 
220
282
  def ScrollView(
221
283
  *children: Element,
222
- refresh_control: Optional[Dict[str, Any]] = None,
284
+ refresh_control: Optional[Element] = None,
223
285
  scroll_axis: Optional[Literal["vertical", "horizontal"]] = None,
224
286
  on_scroll: Optional[Callable[[Dict[str, float]], None]] = None,
225
287
  shows_scroll_indicator: bool = True,
@@ -240,11 +302,8 @@ def ScrollView(
240
302
 
241
303
  Args:
242
304
  *children: Child elements to scroll.
243
- refresh_control: Optional pull-to-refresh spec, typically
244
- constructed via
245
- [`RefreshControl`][pythonnative.RefreshControl]. The dict
246
- must have ``refreshing`` (bool) and ``on_refresh``
247
- (callable).
305
+ refresh_control: Optional [`RefreshControl`][pythonnative.RefreshControl]
306
+ element attached to the scroll view for pull-to-refresh.
248
307
  scroll_axis: ``"vertical"`` (default) or ``"horizontal"``.
249
308
  on_scroll: Callback invoked with ``{"x": …, "y": …}`` content
250
309
  offsets as the user scrolls.
@@ -271,7 +330,7 @@ def ScrollView(
271
330
  style=style,
272
331
  ref=ref,
273
332
  key=key,
274
- refresh_control=refresh_control,
333
+ refresh_control=_refresh_control_props(refresh_control, owner="ScrollView"),
275
334
  scroll_axis=scroll_axis,
276
335
  on_scroll=on_scroll,
277
336
  shows_scroll_indicator=False if shows_scroll_indicator is False else None,
@@ -326,7 +385,7 @@ def _SafeAreaContainer(
326
385
  for edge in _SAFE_AREA_EDGES:
327
386
  if edge not in selected:
328
387
  continue
329
- inset = float(insets.get(edge, 0.0) or 0.0)
388
+ inset = float(getattr(insets, edge, 0.0) or 0.0)
330
389
  if inset > 0:
331
390
  resolved[f"padding_{edge}"] = _numeric_edge_padding(resolved, edge) + inset
332
391
  return Element("SafeAreaView", resolved, list(children))
@@ -124,7 +124,7 @@ def _VirtualizedList(
124
124
  header: Optional[Element] = None,
125
125
  footer: Optional[Element] = None,
126
126
  empty: Optional[Element] = None,
127
- refresh_control: Optional[Dict[str, Any]] = None,
127
+ refresh_control: Optional[Element] = None,
128
128
  on_end_reached: Optional[Callable[[], Any]] = None,
129
129
  on_end_reached_threshold: Optional[float] = None,
130
130
  on_viewable_items_changed: Optional[Callable[[List[Dict[str, Any]]], None]] = None,
@@ -464,7 +464,7 @@ def FlatList(
464
464
  get_item_height: Optional[Callable[[Any, int], float]] = None,
465
465
  estimated_item_height: Optional[float] = None,
466
466
  separator_height: float = 0,
467
- refresh_control: Optional[Dict[str, Any]] = None,
467
+ refresh_control: Optional[Element] = None,
468
468
  horizontal: bool = False,
469
469
  num_columns: int = 1,
470
470
  list_header: Optional[Element] = None,
@@ -509,8 +509,8 @@ def FlatList(
509
509
  estimated_item_height: Starting extent estimate for rows whose
510
510
  true size isn't known yet (default 44).
511
511
  separator_height: Gap below each row, in points.
512
- refresh_control: Optional pull-to-refresh spec from
513
- [`RefreshControl`][pythonnative.RefreshControl].
512
+ refresh_control: Optional [`RefreshControl`][pythonnative.RefreshControl]
513
+ element for pull-to-refresh.
514
514
  horizontal: Scroll horizontally (extents become widths).
515
515
  num_columns: Render items in a grid of this many columns.
516
516
  list_header: Element rendered once before all rows.
@@ -666,7 +666,7 @@ def SectionList(
666
666
  estimated_item_height: Optional[float] = None,
667
667
  section_header_height: Optional[float] = None,
668
668
  separator_height: float = 0,
669
- refresh_control: Optional[Dict[str, Any]] = None,
669
+ refresh_control: Optional[Element] = None,
670
670
  list_header: Optional[Element] = None,
671
671
  list_footer: Optional[Element] = None,
672
672
  list_empty: Optional[Element] = None,
@@ -699,7 +699,7 @@ def SectionList(
699
699
  estimated_item_height: Starting estimate for unmeasured rows.
700
700
  section_header_height: Header extent in points, when known.
701
701
  separator_height: Gap below each item, in points.
702
- refresh_control: Optional pull-to-refresh spec.
702
+ refresh_control: Optional [`RefreshControl`][pythonnative.RefreshControl] element.
703
703
  list_header: Element rendered once before everything.
704
704
  list_footer: Element rendered once after everything.
705
705
  list_empty: Element rendered when there are no sections.
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
  -----------
@@ -75,6 +78,7 @@ from __future__ import annotations
75
78
 
76
79
  import math
77
80
  from dataclasses import dataclass, field
81
+ from enum import Enum
78
82
  from typing import Any, Callable, Dict, List, Literal, Optional, Sequence, Set, Tuple
79
83
 
80
84
  __all__ = [
@@ -96,16 +100,30 @@ __all__ = [
96
100
  ]
97
101
 
98
102
 
99
- class GestureState:
100
- """States reported on [`GestureEvent.state`][pythonnative.gestures.GestureEvent]."""
103
+ class GestureState(str, Enum):
104
+ """Lifecycle states reported on [`GestureEvent.state`][pythonnative.gestures.GestureEvent].
105
+
106
+ A ``str`` enum, so members compare equal to their wire value
107
+ (``GestureState.ENDED == "ended"``) and serialize as plain strings
108
+ across the native bridge, while callers get exhaustive
109
+ ``match`` support and autocomplete.
110
+
111
+ Attributes:
112
+ BEGAN: The gesture activated (first callback).
113
+ CHANGED: A continuous gesture updated (pan, pinch, rotation).
114
+ ENDED: The gesture completed successfully.
115
+ CANCELLED: The gesture was interrupted (lost arbitration, view
116
+ unmounted, pointer left the window).
117
+ """
101
118
 
102
119
  BEGAN = "began"
103
120
  CHANGED = "changed"
104
121
  ENDED = "ended"
105
122
  CANCELLED = "cancelled"
106
123
 
124
+ def __str__(self) -> str:
125
+ return self.value
107
126
 
108
- GestureStateName = Literal["began", "changed", "ended", "cancelled"]
109
127
 
110
128
  GestureCallback = Callable[["GestureEvent"], Any]
111
129
 
@@ -138,7 +156,7 @@ class GestureEvent:
138
156
  """
139
157
 
140
158
  kind: str
141
- state: GestureStateName
159
+ state: GestureState
142
160
  x: float = 0.0
143
161
  y: float = 0.0
144
162
  translation_x: float = 0.0
@@ -150,6 +168,12 @@ class GestureEvent:
150
168
  pointer_count: int = 1
151
169
  direction: Optional[str] = None
152
170
 
171
+ def __post_init__(self) -> None:
172
+ # Payloads from the native bridge carry plain strings; coerce
173
+ # so ``event.state`` is always a ``GestureState`` member.
174
+ if not isinstance(self.state, GestureState):
175
+ object.__setattr__(self, "state", GestureState(self.state))
176
+
153
177
 
154
178
  _EVENT_FIELDS = frozenset(
155
179
  {
@@ -518,14 +542,14 @@ def serialize_gestures(
518
542
 
519
543
 
520
544
  # ======================================================================
521
- # Pure-Python recognition engine (Android + desktop backends)
545
+ # Pure-Python recognition engine (desktop backend + reference semantics)
522
546
  # ======================================================================
523
547
  #
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.
548
+ # iOS and Android recognize natively. The desktop preview receives raw
549
+ # Tk pointer streams instead, which this arbiter turns into the same
550
+ # GestureEvent payloads. Keeping it in pure Python makes the state
551
+ # machines unit-testable with scripted event sequences; the Kotlin
552
+ # arbiter mirrors this file so the two stay in lockstep.
529
553
 
530
554
  EmitFn = Callable[[int, Dict[str, Any]], None]
531
555
  """``emit(gesture_index, payload)``: the arbiter's output channel."""
@@ -573,8 +597,8 @@ class _Recognizer:
573
597
  self.config = config
574
598
  self._emit_fn = emit
575
599
 
576
- def emit(self, state: str, **fields: Any) -> None:
577
- payload: Dict[str, Any] = {"kind": self.kind(), "state": state}
600
+ def emit(self, state: GestureState, **fields: Any) -> None:
601
+ payload: Dict[str, Any] = {"kind": self.kind(), "state": state.value}
578
602
  payload.update(fields)
579
603
  self._emit_fn(self.index, payload)
580
604