pythonnative 0.40.0__cp314-cp314-win_amd64.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 (418) hide show
  1. pythonnative/__init__.py +411 -0
  2. pythonnative/_ios_log.py +92 -0
  3. pythonnative/_yoga.cp314-win_amd64.pyd +0 -0
  4. pythonnative/alerts.py +236 -0
  5. pythonnative/animated.py +1687 -0
  6. pythonnative/animation_graph.py +78 -0
  7. pythonnative/appearance.py +124 -0
  8. pythonnative/bootstrap.py +100 -0
  9. pythonnative/bridge/__init__.py +302 -0
  10. pythonnative/bridge/android.py +78 -0
  11. pythonnative/bridge/codec.py +170 -0
  12. pythonnative/bridge/commits.py +114 -0
  13. pythonnative/bridge/fake.py +243 -0
  14. pythonnative/bridge/ios.py +138 -0
  15. pythonnative/bridge/web.py +489 -0
  16. pythonnative/cli/__init__.py +7 -0
  17. pythonnative/cli/pn.py +1214 -0
  18. pythonnative/component.py +271 -0
  19. pythonnative/components/__init__.py +111 -0
  20. pythonnative/components/_base.py +166 -0
  21. pythonnative/components/controls.py +521 -0
  22. pythonnative/components/layout.py +518 -0
  23. pythonnative/components/lists.py +785 -0
  24. pythonnative/components/media.py +204 -0
  25. pythonnative/components/overlays.py +107 -0
  26. pythonnative/components/pressable.py +252 -0
  27. pythonnative/components/structural.py +167 -0
  28. pythonnative/components/text.py +291 -0
  29. pythonnative/devclient.py +824 -0
  30. pythonnative/devserver/__init__.py +34 -0
  31. pythonnative/devserver/server.py +718 -0
  32. pythonnative/devserver/static/animation_graph.js +82 -0
  33. pythonnative/devserver/static/bridge.js +147 -0
  34. pythonnative/devserver/static/colors.js +70 -0
  35. pythonnative/devserver/static/host.js +552 -0
  36. pythonnative/devserver/static/index.html +60 -0
  37. pythonnative/devserver/static/layout.js +85 -0
  38. pythonnative/devserver/static/preview.css +415 -0
  39. pythonnative/devserver/static/renderer.js +1994 -0
  40. pythonnative/devserver/static/shell.js +345 -0
  41. pythonnative/devserver/static/yoga/LICENSE +21 -0
  42. pythonnative/devserver/static/yoga/binaries/yoga-wasm-base64-esm.js +75 -0
  43. pythonnative/devserver/static/yoga/src/generated/YGEnums.d.ts +189 -0
  44. pythonnative/devserver/static/yoga/src/generated/YGEnums.js +211 -0
  45. pythonnative/devserver/static/yoga/src/generated/YGEnums.js.map +1 -0
  46. pythonnative/devserver/static/yoga/src/index.d.ts +12 -0
  47. pythonnative/devserver/static/yoga/src/index.js +16 -0
  48. pythonnative/devserver/static/yoga/src/index.js.map +1 -0
  49. pythonnative/devserver/static/yoga/src/load.d.ts +11 -0
  50. pythonnative/devserver/static/yoga/src/load.js +17 -0
  51. pythonnative/devserver/static/yoga/src/load.js.map +1 -0
  52. pythonnative/devserver/static/yoga/src/wrapAssembly.d.ts +155 -0
  53. pythonnative/devserver/static/yoga/src/wrapAssembly.js +125 -0
  54. pythonnative/devserver/static/yoga/src/wrapAssembly.js.map +1 -0
  55. pythonnative/devserver/watcher.py +267 -0
  56. pythonnative/devserver/ws.py +421 -0
  57. pythonnative/diagnostics.py +241 -0
  58. pythonnative/element.py +159 -0
  59. pythonnative/equality.py +19 -0
  60. pythonnative/events.py +231 -0
  61. pythonnative/gestures.py +1333 -0
  62. pythonnative/hooks.py +1621 -0
  63. pythonnative/hosts/__init__.py +63 -0
  64. pythonnative/hosts/base.py +596 -0
  65. pythonnative/hosts/native.py +302 -0
  66. pythonnative/hot_reload.py +585 -0
  67. pythonnative/layout.py +328 -0
  68. pythonnative/mutations.py +142 -0
  69. pythonnative/native/android/build.gradle +52 -0
  70. pythonnative/native/android/gradle.properties +3 -0
  71. pythonnative/native/android/settings.gradle +12 -0
  72. pythonnative/native/android/src/main/AndroidManifest.xml +11 -0
  73. pythonnative/native/android/src/main/cpp/CMakeLists.txt +9 -0
  74. pythonnative/native/android/src/main/cpp/yoga_jni.cpp +52 -0
  75. pythonnative/native/android/src/main/java/com/pythonnative/generated/NativeModules.kt +5 -0
  76. pythonnative/native/android/src/main/java/com/pythonnative/generated/NativeProps.kt +3705 -0
  77. pythonnative/native/android/src/main/java/com/pythonnative/generated/PNContracts.kt +129 -0
  78. pythonnative/native/android/src/main/java/com/pythonnative/runtime/PNBridge.kt +261 -0
  79. pythonnative/native/android/src/main/java/com/pythonnative/runtime/PythonHost.kt +20 -0
  80. pythonnative/native/android/src/main/java/com/pythonnative/runtime/animation/AnimationGraph.kt +108 -0
  81. pythonnative/native/android/src/main/java/com/pythonnative/runtime/animation/AnimationSpecs.kt +141 -0
  82. pythonnative/native/android/src/main/java/com/pythonnative/runtime/animation/PNAnimator.kt +257 -0
  83. pythonnative/native/android/src/main/java/com/pythonnative/runtime/bridge/CommitState.kt +106 -0
  84. pythonnative/native/android/src/main/java/com/pythonnative/runtime/bridge/JsonUtil.kt +175 -0
  85. pythonnative/native/android/src/main/java/com/pythonnative/runtime/bridge/MainThread.kt +40 -0
  86. pythonnative/native/android/src/main/java/com/pythonnative/runtime/bridge/PNLog.kt +33 -0
  87. pythonnative/native/android/src/main/java/com/pythonnative/runtime/bridge/PNRegistry.kt +82 -0
  88. pythonnative/native/android/src/main/java/com/pythonnative/runtime/bridge/PNTransaction.kt +80 -0
  89. pythonnative/native/android/src/main/java/com/pythonnative/runtime/bridge/TransactionApplier.kt +117 -0
  90. pythonnative/native/android/src/main/java/com/pythonnative/runtime/bridge/ViewRegistry.kt +74 -0
  91. pythonnative/native/android/src/main/java/com/pythonnative/runtime/components/BuiltinComponents.kt +37 -0
  92. pythonnative/native/android/src/main/java/com/pythonnative/runtime/components/ButtonManager.kt +32 -0
  93. pythonnative/native/android/src/main/java/com/pythonnative/runtime/components/ComponentManager.kt +213 -0
  94. pythonnative/native/android/src/main/java/com/pythonnative/runtime/components/ContainerManagers.kt +69 -0
  95. pythonnative/native/android/src/main/java/com/pythonnative/runtime/components/ControlManagers.kt +186 -0
  96. pythonnative/native/android/src/main/java/com/pythonnative/runtime/components/DatePickerManager.kt +123 -0
  97. pythonnative/native/android/src/main/java/com/pythonnative/runtime/components/ImageLoader.kt +141 -0
  98. pythonnative/native/android/src/main/java/com/pythonnative/runtime/components/ImageManager.kt +162 -0
  99. pythonnative/native/android/src/main/java/com/pythonnative/runtime/components/ModalManager.kt +134 -0
  100. pythonnative/native/android/src/main/java/com/pythonnative/runtime/components/PNColor.kt +201 -0
  101. pythonnative/native/android/src/main/java/com/pythonnative/runtime/components/PickerManager.kt +86 -0
  102. pythonnative/native/android/src/main/java/com/pythonnative/runtime/components/PortalManager.kt +55 -0
  103. pythonnative/native/android/src/main/java/com/pythonnative/runtime/components/PressableManager.kt +122 -0
  104. pythonnative/native/android/src/main/java/com/pythonnative/runtime/components/ScreenStackManager.kt +110 -0
  105. pythonnative/native/android/src/main/java/com/pythonnative/runtime/components/ScrollViewManager.kt +214 -0
  106. pythonnative/native/android/src/main/java/com/pythonnative/runtime/components/SegmentedControlManager.kt +113 -0
  107. pythonnative/native/android/src/main/java/com/pythonnative/runtime/components/StatusBarManager.kt +52 -0
  108. pythonnative/native/android/src/main/java/com/pythonnative/runtime/components/TabBarManager.kt +131 -0
  109. pythonnative/native/android/src/main/java/com/pythonnative/runtime/components/TextInputManager.kt +250 -0
  110. pythonnative/native/android/src/main/java/com/pythonnative/runtime/components/TextManager.kt +233 -0
  111. pythonnative/native/android/src/main/java/com/pythonnative/runtime/components/ViewStyler.kt +355 -0
  112. pythonnative/native/android/src/main/java/com/pythonnative/runtime/components/VirtualListManager.kt +159 -0
  113. pythonnative/native/android/src/main/java/com/pythonnative/runtime/components/WebViewManager.kt +149 -0
  114. pythonnative/native/android/src/main/java/com/pythonnative/runtime/gestures/GestureArbiter.kt +233 -0
  115. pythonnative/native/android/src/main/java/com/pythonnative/runtime/gestures/GestureCoordinator.kt +127 -0
  116. pythonnative/native/android/src/main/java/com/pythonnative/runtime/gestures/GestureRecognizers.kt +527 -0
  117. pythonnative/native/android/src/main/java/com/pythonnative/runtime/layout/NativeLayout.kt +145 -0
  118. pythonnative/native/android/src/main/java/com/pythonnative/runtime/layout/YogaNode.kt +29 -0
  119. pythonnative/native/android/src/main/java/com/pythonnative/runtime/modules/BuiltinModules.kt +73 -0
  120. pythonnative/native/android/src/main/java/com/pythonnative/runtime/modules/DeviceModules.kt +316 -0
  121. pythonnative/native/android/src/main/java/com/pythonnative/runtime/modules/HostModule.kt +61 -0
  122. pythonnative/native/android/src/main/java/com/pythonnative/runtime/modules/MediaModules.kt +200 -0
  123. pythonnative/native/android/src/main/java/com/pythonnative/runtime/modules/NativeModule.kt +158 -0
  124. pythonnative/native/android/src/main/java/com/pythonnative/runtime/modules/NotificationsModule.kt +100 -0
  125. pythonnative/native/android/src/main/java/com/pythonnative/runtime/modules/PermissionsModule.kt +99 -0
  126. pythonnative/native/android/src/main/java/com/pythonnative/runtime/modules/StorageModules.kt +89 -0
  127. pythonnative/native/android/src/main/java/com/pythonnative/runtime/modules/SystemModules.kt +178 -0
  128. pythonnative/native/android/src/main/java/com/pythonnative/runtime/plugins/GeneratedPlugins.kt +16 -0
  129. pythonnative/native/android/src/main/java/com/pythonnative/runtime/screens/PNScreenFragment.kt +188 -0
  130. pythonnative/native/android/src/main/java/com/pythonnative/runtime/screens/ScreenRegistry.kt +80 -0
  131. pythonnative/native/android/src/main/java/com/pythonnative/runtime/views/PNAccessibilityDelegate.kt +48 -0
  132. pythonnative/native/android/src/main/java/com/pythonnative/runtime/views/PNBorderDrawable.kt +76 -0
  133. pythonnative/native/android/src/main/java/com/pythonnative/runtime/views/PNEditText.kt +18 -0
  134. pythonnative/native/android/src/main/java/com/pythonnative/runtime/views/PNFrameLayout.kt +49 -0
  135. pythonnative/native/android/src/test/java/com/pythonnative/runtime/AnimationSpecsTest.kt +75 -0
  136. pythonnative/native/android/src/test/java/com/pythonnative/runtime/GestureArbiterTest.kt +197 -0
  137. pythonnative/native/android/src/test/java/com/pythonnative/runtime/PNColorTest.kt +47 -0
  138. pythonnative/native/android/src/test/java/com/pythonnative/runtime/PNTransactionTest.kt +78 -0
  139. pythonnative/native/android/src/test/java/com/pythonnative/runtime/PromiseTest.kt +89 -0
  140. pythonnative/native/ios/Package.swift +23 -0
  141. pythonnative/native/ios/Sources/PythonNativeKit/Animation/PNAnimationGraph.swift +199 -0
  142. pythonnative/native/ios/Sources/PythonNativeKit/Animation/PNAnimator.swift +312 -0
  143. pythonnative/native/ios/Sources/PythonNativeKit/Bridge/PNBridge.swift +255 -0
  144. pythonnative/native/ios/Sources/PythonNativeKit/Bridge/PNCommit.swift +104 -0
  145. pythonnative/native/ios/Sources/PythonNativeKit/Bridge/PNRegistry.swift +142 -0
  146. pythonnative/native/ios/Sources/PythonNativeKit/Bridge/PNTransaction.swift +166 -0
  147. pythonnative/native/ios/Sources/PythonNativeKit/Bridge/PNViewRegistry.swift +53 -0
  148. pythonnative/native/ios/Sources/PythonNativeKit/Components/PNButtonManagers.swift +207 -0
  149. pythonnative/native/ios/Sources/PythonNativeKit/Components/PNColor.swift +173 -0
  150. pythonnative/native/ios/Sources/PythonNativeKit/Components/PNComponentManager.swift +183 -0
  151. pythonnative/native/ios/Sources/PythonNativeKit/Components/PNContainerView.swift +56 -0
  152. pythonnative/native/ios/Sources/PythonNativeKit/Components/PNControlManagers.swift +281 -0
  153. pythonnative/native/ios/Sources/PythonNativeKit/Components/PNImageManager.swift +222 -0
  154. pythonnative/native/ios/Sources/PythonNativeKit/Components/PNOverlayManagers.swift +243 -0
  155. pythonnative/native/ios/Sources/PythonNativeKit/Components/PNPressableManager.swift +114 -0
  156. pythonnative/native/ios/Sources/PythonNativeKit/Components/PNScreenStackManager.swift +107 -0
  157. pythonnative/native/ios/Sources/PythonNativeKit/Components/PNScrollViewManager.swift +197 -0
  158. pythonnative/native/ios/Sources/PythonNativeKit/Components/PNTabBarManager.swift +106 -0
  159. pythonnative/native/ios/Sources/PythonNativeKit/Components/PNTextInputManager.swift +297 -0
  160. pythonnative/native/ios/Sources/PythonNativeKit/Components/PNTextManager.swift +230 -0
  161. pythonnative/native/ios/Sources/PythonNativeKit/Components/PNTransform.swift +82 -0
  162. pythonnative/native/ios/Sources/PythonNativeKit/Components/PNViewManager.swift +86 -0
  163. pythonnative/native/ios/Sources/PythonNativeKit/Components/PNViewState.swift +87 -0
  164. pythonnative/native/ios/Sources/PythonNativeKit/Components/PNViewStyler.swift +340 -0
  165. pythonnative/native/ios/Sources/PythonNativeKit/Components/PNVirtualListManager.swift +155 -0
  166. pythonnative/native/ios/Sources/PythonNativeKit/Components/PNWebViewManager.swift +124 -0
  167. pythonnative/native/ios/Sources/PythonNativeKit/Generated/NativeModules.swift +1 -0
  168. pythonnative/native/ios/Sources/PythonNativeKit/Generated/NativeProps.swift +3775 -0
  169. pythonnative/native/ios/Sources/PythonNativeKit/Generated/PNContracts.swift +46 -0
  170. pythonnative/native/ios/Sources/PythonNativeKit/Gestures/PNGestureCoordinator.swift +260 -0
  171. pythonnative/native/ios/Sources/PythonNativeKit/Layout/PNLayout.swift +173 -0
  172. pythonnative/native/ios/Sources/PythonNativeKit/Modules/AlertModule.swift +92 -0
  173. pythonnative/native/ios/Sources/PythonNativeKit/Modules/HostModule.swift +80 -0
  174. pythonnative/native/ios/Sources/PythonNativeKit/Modules/LifecycleModules.swift +174 -0
  175. pythonnative/native/ios/Sources/PythonNativeKit/Modules/MediaModules.swift +177 -0
  176. pythonnative/native/ios/Sources/PythonNativeKit/Modules/PNNativeModule.swift +168 -0
  177. pythonnative/native/ios/Sources/PythonNativeKit/Modules/PermissionModules.swift +266 -0
  178. pythonnative/native/ios/Sources/PythonNativeKit/Modules/SystemModules.swift +251 -0
  179. pythonnative/native/ios/Sources/PythonNativeKit/Plugins/PNPluginRegistration.swift +8 -0
  180. pythonnative/native/ios/Sources/PythonNativeKit/Screens/PNScreenRegistry.swift +46 -0
  181. pythonnative/native/ios/Sources/PythonNativeKit/Screens/PNViewController.swift +237 -0
  182. pythonnative/native/ios/Sources/PythonNativeKit/Support/PNCompat.swift +46 -0
  183. pythonnative/native/ios/Sources/PythonNativeKit/Support/PNJSON.swift +173 -0
  184. pythonnative/native/ios/Sources/PythonNativeKit/Support/PNLog.swift +47 -0
  185. pythonnative/native/ios/Sources/PythonNativeKit/Support/PNWindow.swift +51 -0
  186. pythonnative/native/ios/Tests/PythonNativeKitTests/PNAnimationGraphTests.swift +26 -0
  187. pythonnative/native/ios/Tests/PythonNativeKitTests/PNColorTests.swift +50 -0
  188. pythonnative/native/ios/Tests/PythonNativeKitTests/PNCommitTests.swift +86 -0
  189. pythonnative/native/ios/Tests/PythonNativeKitTests/PNGestureTests.swift +101 -0
  190. pythonnative/native/ios/Tests/PythonNativeKitTests/PNManagerTests.swift +126 -0
  191. pythonnative/native/ios/Tests/PythonNativeKitTests/PNModuleTests.swift +111 -0
  192. pythonnative/native/ios/Tests/PythonNativeKitTests/PNTransactionTests.swift +91 -0
  193. pythonnative/native/yoga/LICENSE +21 -0
  194. pythonnative/native/yoga/Package.swift +9 -0
  195. pythonnative/native/yoga/VENDORED.md +2 -0
  196. pythonnative/native/yoga/include/PNStyle.h +10 -0
  197. pythonnative/native/yoga/include/module.modulemap +5 -0
  198. pythonnative/native/yoga/include/yoga/YGConfig.h +158 -0
  199. pythonnative/native/yoga/include/yoga/YGEnums.h +142 -0
  200. pythonnative/native/yoga/include/yoga/YGMacros.h +99 -0
  201. pythonnative/native/yoga/include/yoga/YGNode.h +304 -0
  202. pythonnative/native/yoga/include/yoga/YGNodeLayout.h +35 -0
  203. pythonnative/native/yoga/include/yoga/YGNodeStyle.h +130 -0
  204. pythonnative/native/yoga/include/yoga/YGPixelGrid.h +29 -0
  205. pythonnative/native/yoga/include/yoga/YGValue.h +84 -0
  206. pythonnative/native/yoga/include/yoga/Yoga.h +21 -0
  207. pythonnative/native/yoga/python.cpp +16 -0
  208. pythonnative/native/yoga/style.cpp +272 -0
  209. pythonnative/native/yoga/yoga/CMakeLists.txt +41 -0
  210. pythonnative/native/yoga/yoga/YGConfig.cpp +92 -0
  211. pythonnative/native/yoga/yoga/YGConfig.h +158 -0
  212. pythonnative/native/yoga/yoga/YGEnums.cpp +262 -0
  213. pythonnative/native/yoga/yoga/YGEnums.h +142 -0
  214. pythonnative/native/yoga/yoga/YGMacros.h +99 -0
  215. pythonnative/native/yoga/yoga/YGNode.cpp +366 -0
  216. pythonnative/native/yoga/yoga/YGNode.h +304 -0
  217. pythonnative/native/yoga/yoga/YGNodeLayout.cpp +92 -0
  218. pythonnative/native/yoga/yoga/YGNodeLayout.h +35 -0
  219. pythonnative/native/yoga/yoga/YGNodeStyle.cpp +405 -0
  220. pythonnative/native/yoga/yoga/YGNodeStyle.h +130 -0
  221. pythonnative/native/yoga/yoga/YGPixelGrid.cpp +22 -0
  222. pythonnative/native/yoga/yoga/YGPixelGrid.h +29 -0
  223. pythonnative/native/yoga/yoga/YGValue.cpp +20 -0
  224. pythonnative/native/yoga/yoga/YGValue.h +84 -0
  225. pythonnative/native/yoga/yoga/Yoga.h +21 -0
  226. pythonnative/native/yoga/yoga/algorithm/AbsoluteLayout.cpp +563 -0
  227. pythonnative/native/yoga/yoga/algorithm/AbsoluteLayout.h +41 -0
  228. pythonnative/native/yoga/yoga/algorithm/Align.h +72 -0
  229. pythonnative/native/yoga/yoga/algorithm/Baseline.cpp +78 -0
  230. pythonnative/native/yoga/yoga/algorithm/Baseline.h +21 -0
  231. pythonnative/native/yoga/yoga/algorithm/BoundAxis.h +79 -0
  232. pythonnative/native/yoga/yoga/algorithm/Cache.cpp +121 -0
  233. pythonnative/native/yoga/yoga/algorithm/Cache.h +30 -0
  234. pythonnative/native/yoga/yoga/algorithm/CalculateLayout.cpp +2428 -0
  235. pythonnative/native/yoga/yoga/algorithm/CalculateLayout.h +38 -0
  236. pythonnative/native/yoga/yoga/algorithm/FlexDirection.h +120 -0
  237. pythonnative/native/yoga/yoga/algorithm/FlexLine.cpp +124 -0
  238. pythonnative/native/yoga/yoga/algorithm/FlexLine.h +75 -0
  239. pythonnative/native/yoga/yoga/algorithm/PixelGrid.cpp +132 -0
  240. pythonnative/native/yoga/yoga/algorithm/PixelGrid.h +29 -0
  241. pythonnative/native/yoga/yoga/algorithm/SizingMode.h +73 -0
  242. pythonnative/native/yoga/yoga/algorithm/TrailingPosition.h +44 -0
  243. pythonnative/native/yoga/yoga/config/Config.cpp +135 -0
  244. pythonnative/native/yoga/yoga/config/Config.h +92 -0
  245. pythonnative/native/yoga/yoga/debug/AssertFatal.cpp +53 -0
  246. pythonnative/native/yoga/yoga/debug/AssertFatal.h +29 -0
  247. pythonnative/native/yoga/yoga/debug/Log.cpp +107 -0
  248. pythonnative/native/yoga/yoga/debug/Log.h +34 -0
  249. pythonnative/native/yoga/yoga/enums/Align.h +47 -0
  250. pythonnative/native/yoga/yoga/enums/BoxSizing.h +40 -0
  251. pythonnative/native/yoga/yoga/enums/Dimension.h +40 -0
  252. pythonnative/native/yoga/yoga/enums/Direction.h +41 -0
  253. pythonnative/native/yoga/yoga/enums/Display.h +41 -0
  254. pythonnative/native/yoga/yoga/enums/Edge.h +47 -0
  255. pythonnative/native/yoga/yoga/enums/Errata.h +41 -0
  256. pythonnative/native/yoga/yoga/enums/ExperimentalFeature.h +39 -0
  257. pythonnative/native/yoga/yoga/enums/FlexDirection.h +42 -0
  258. pythonnative/native/yoga/yoga/enums/Gutter.h +41 -0
  259. pythonnative/native/yoga/yoga/enums/Justify.h +44 -0
  260. pythonnative/native/yoga/yoga/enums/LogLevel.h +44 -0
  261. pythonnative/native/yoga/yoga/enums/MeasureMode.h +41 -0
  262. pythonnative/native/yoga/yoga/enums/NodeType.h +40 -0
  263. pythonnative/native/yoga/yoga/enums/Overflow.h +41 -0
  264. pythonnative/native/yoga/yoga/enums/PhysicalEdge.h +21 -0
  265. pythonnative/native/yoga/yoga/enums/PositionType.h +41 -0
  266. pythonnative/native/yoga/yoga/enums/Unit.h +42 -0
  267. pythonnative/native/yoga/yoga/enums/Wrap.h +41 -0
  268. pythonnative/native/yoga/yoga/enums/YogaEnums.h +85 -0
  269. pythonnative/native/yoga/yoga/event/event.cpp +87 -0
  270. pythonnative/native/yoga/yoga/event/event.h +130 -0
  271. pythonnative/native/yoga/yoga/module.modulemap +21 -0
  272. pythonnative/native/yoga/yoga/node/CachedMeasurement.h +53 -0
  273. pythonnative/native/yoga/yoga/node/LayoutResults.cpp +48 -0
  274. pythonnative/native/yoga/yoga/node/LayoutResults.h +122 -0
  275. pythonnative/native/yoga/yoga/node/LayoutableChildren.h +148 -0
  276. pythonnative/native/yoga/yoga/node/Node.cpp +443 -0
  277. pythonnative/native/yoga/yoga/node/Node.h +337 -0
  278. pythonnative/native/yoga/yoga/numeric/Comparison.h +81 -0
  279. pythonnative/native/yoga/yoga/numeric/FloatOptional.h +93 -0
  280. pythonnative/native/yoga/yoga/style/SmallValueBuffer.h +133 -0
  281. pythonnative/native/yoga/yoga/style/Style.h +757 -0
  282. pythonnative/native/yoga/yoga/style/StyleLength.h +107 -0
  283. pythonnative/native/yoga/yoga/style/StyleValueHandle.h +98 -0
  284. pythonnative/native/yoga/yoga/style/StyleValuePool.h +127 -0
  285. pythonnative/native_modules/__init__.py +124 -0
  286. pythonnative/native_modules/app_state.py +99 -0
  287. pythonnative/native_modules/battery.py +75 -0
  288. pythonnative/native_modules/biometrics.py +42 -0
  289. pythonnative/native_modules/camera.py +65 -0
  290. pythonnative/native_modules/clipboard.py +46 -0
  291. pythonnative/native_modules/fallback.py +324 -0
  292. pythonnative/native_modules/file_system.py +145 -0
  293. pythonnative/native_modules/haptics.py +75 -0
  294. pythonnative/native_modules/linking.py +123 -0
  295. pythonnative/native_modules/location.py +75 -0
  296. pythonnative/native_modules/net_info.py +112 -0
  297. pythonnative/native_modules/notifications.py +129 -0
  298. pythonnative/native_modules/permissions.py +108 -0
  299. pythonnative/native_modules/registry.py +526 -0
  300. pythonnative/native_modules/secure_store.py +52 -0
  301. pythonnative/native_modules/share.py +51 -0
  302. pythonnative/native_views/__init__.py +375 -0
  303. pythonnative/native_views/base.py +316 -0
  304. pythonnative/native_views/bridge_backend.py +277 -0
  305. pythonnative/navigation/__init__.py +103 -0
  306. pythonnative/navigation/container.py +154 -0
  307. pythonnative/navigation/handle.py +540 -0
  308. pythonnative/navigation/hooks.py +133 -0
  309. pythonnative/navigation/host.py +58 -0
  310. pythonnative/navigation/linking.py +200 -0
  311. pythonnative/navigation/navigators.py +659 -0
  312. pythonnative/navigation/screen.py +149 -0
  313. pythonnative/navigation/state.py +269 -0
  314. pythonnative/net.py +248 -0
  315. pythonnative/platform.py +166 -0
  316. pythonnative/platform_metrics.py +252 -0
  317. pythonnative/preview.py +287 -0
  318. pythonnative/profiling.py +101 -0
  319. pythonnative/project/__init__.py +68 -0
  320. pythonnative/project/android.py +557 -0
  321. pythonnative/project/builder.py +817 -0
  322. pythonnative/project/config.py +773 -0
  323. pythonnative/project/deps.py +719 -0
  324. pythonnative/project/devices.py +304 -0
  325. pythonnative/project/doctor.py +283 -0
  326. pythonnative/project/fingerprint.py +171 -0
  327. pythonnative/project/icons.py +247 -0
  328. pythonnative/project/ios.py +325 -0
  329. pythonnative/project/lockfile.py +100 -0
  330. pythonnative/project/permissions.py +361 -0
  331. pythonnative/project/plugins.py +519 -0
  332. pythonnative/project/runtime_assets.py +183 -0
  333. pythonnative/py.typed +0 -0
  334. pythonnative/query.py +153 -0
  335. pythonnative/reconciler/__init__.py +29 -0
  336. pythonnative/reconciler/boundaries.py +375 -0
  337. pythonnative/reconciler/children.py +88 -0
  338. pythonnative/reconciler/core.py +1165 -0
  339. pythonnative/reconciler/layout_pass.py +393 -0
  340. pythonnative/reconciler/vnode.py +266 -0
  341. pythonnative/refresh.py +55 -0
  342. pythonnative/runtime.py +306 -0
  343. pythonnative/scheduler.py +159 -0
  344. pythonnative/sdk/__init__.py +153 -0
  345. pythonnative/sdk/_components.py +427 -0
  346. pythonnative/sdk/builtins.py +91 -0
  347. pythonnative/sdk/codegen.py +149 -0
  348. pythonnative/sdk/module_codegen.py +184 -0
  349. pythonnative/sdk/schema.py +224 -0
  350. pythonnative/sdk/templates/contracts.kt +46 -0
  351. pythonnative/sdk/templates/contracts.swift +46 -0
  352. pythonnative/storage.py +184 -0
  353. pythonnative/style.py +841 -0
  354. pythonnative/suspense.py +183 -0
  355. pythonnative/templates/android_template/app/build.gradle +75 -0
  356. pythonnative/templates/android_template/app/proguard-rules.pro +21 -0
  357. pythonnative/templates/android_template/app/src/androidTest/java/com/pythonnative/android_template/ExampleInstrumentedTest.kt +24 -0
  358. pythonnative/templates/android_template/app/src/debug/AndroidManifest.xml +7 -0
  359. pythonnative/templates/android_template/app/src/main/AndroidManifest.xml +32 -0
  360. pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/MainActivity.kt +130 -0
  361. pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/ScreenFragment.kt +13 -0
  362. pythonnative/templates/android_template/app/src/main/res/drawable/ic_launcher_background.xml +170 -0
  363. pythonnative/templates/android_template/app/src/main/res/drawable-v24/ic_launcher_foreground.xml +30 -0
  364. pythonnative/templates/android_template/app/src/main/res/layout/activity_main.xml +10 -0
  365. pythonnative/templates/android_template/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +6 -0
  366. pythonnative/templates/android_template/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +6 -0
  367. pythonnative/templates/android_template/app/src/main/res/mipmap-hdpi/ic_launcher.webp +0 -0
  368. pythonnative/templates/android_template/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp +0 -0
  369. pythonnative/templates/android_template/app/src/main/res/mipmap-mdpi/ic_launcher.webp +0 -0
  370. pythonnative/templates/android_template/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp +0 -0
  371. pythonnative/templates/android_template/app/src/main/res/mipmap-xhdpi/ic_launcher.webp +0 -0
  372. pythonnative/templates/android_template/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp +0 -0
  373. pythonnative/templates/android_template/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp +0 -0
  374. pythonnative/templates/android_template/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp +0 -0
  375. pythonnative/templates/android_template/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp +0 -0
  376. pythonnative/templates/android_template/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp +0 -0
  377. pythonnative/templates/android_template/app/src/main/res/navigation/nav_graph.xml +22 -0
  378. pythonnative/templates/android_template/app/src/main/res/values/colors.xml +5 -0
  379. pythonnative/templates/android_template/app/src/main/res/values/strings.xml +5 -0
  380. pythonnative/templates/android_template/app/src/main/res/values/themes.xml +9 -0
  381. pythonnative/templates/android_template/app/src/main/res/values-night/themes.xml +7 -0
  382. pythonnative/templates/android_template/app/src/main/res/xml/backup_rules.xml +13 -0
  383. pythonnative/templates/android_template/app/src/main/res/xml/data_extraction_rules.xml +19 -0
  384. pythonnative/templates/android_template/app/src/test/java/com/pythonnative/android_template/ExampleUnitTest.kt +17 -0
  385. pythonnative/templates/android_template/build.gradle +10 -0
  386. pythonnative/templates/android_template/gradle/wrapper/gradle-wrapper.jar +0 -0
  387. pythonnative/templates/android_template/gradle/wrapper/gradle-wrapper.properties +6 -0
  388. pythonnative/templates/android_template/gradle.properties +23 -0
  389. pythonnative/templates/android_template/gradlew +185 -0
  390. pythonnative/templates/android_template/gradlew.bat +89 -0
  391. pythonnative/templates/android_template/settings.gradle +17 -0
  392. pythonnative/templates/ios_template/ios_template/AppDelegate.swift +51 -0
  393. pythonnative/templates/ios_template/ios_template/Assets.xcassets/AccentColor.colorset/Contents.json +11 -0
  394. pythonnative/templates/ios_template/ios_template/Assets.xcassets/AppIcon.appiconset/Contents.json +13 -0
  395. pythonnative/templates/ios_template/ios_template/Assets.xcassets/Contents.json +6 -0
  396. pythonnative/templates/ios_template/ios_template/Base.lproj/LaunchScreen.storyboard +25 -0
  397. pythonnative/templates/ios_template/ios_template/BridgingHeader.h +14 -0
  398. pythonnative/templates/ios_template/ios_template/Info.plist +28 -0
  399. pythonnative/templates/ios_template/ios_template/PythonRuntime.swift +390 -0
  400. pythonnative/templates/ios_template/ios_template/SceneDelegate.swift +61 -0
  401. pythonnative/templates/ios_template/ios_template/ViewController.swift +23 -0
  402. pythonnative/templates/ios_template/ios_template.xcodeproj/project.pbxproj +745 -0
  403. pythonnative/templates/ios_template/ios_template.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +8 -0
  404. pythonnative/templates/ios_template/ios_templateTests/ios_templateTests.swift +36 -0
  405. pythonnative/templates/ios_template/ios_templateUITests/ios_templateUITests.swift +41 -0
  406. pythonnative/templates/ios_template/ios_templateUITests/ios_templateUITestsLaunchTests.swift +32 -0
  407. pythonnative/testing/__init__.py +53 -0
  408. pythonnative/testing/backend.py +276 -0
  409. pythonnative/testing/harness.py +369 -0
  410. pythonnative/utils.py +145 -0
  411. pythonnative-0.40.0.dist-info/DELVEWHEEL +2 -0
  412. pythonnative-0.40.0.dist-info/METADATA +150 -0
  413. pythonnative-0.40.0.dist-info/RECORD +418 -0
  414. pythonnative-0.40.0.dist-info/WHEEL +5 -0
  415. pythonnative-0.40.0.dist-info/entry_points.txt +2 -0
  416. pythonnative-0.40.0.dist-info/licenses/LICENSE +21 -0
  417. pythonnative-0.40.0.dist-info/top_level.txt +1 -0
  418. pythonnative.libs/msvcp140-a4c2229bdc2a2a630acdc095b4d86008.dll +0 -0
@@ -0,0 +1,1333 @@
1
+ """Native-backed gesture system with composition and arbitration.
2
+
3
+ Attach gestures to any view-like element via the ``gestures=`` prop:
4
+
5
+ ```python
6
+ import pythonnative as pn
7
+ from pythonnative import gestures
8
+
9
+
10
+ @pn.component
11
+ def Draggable():
12
+ tx = pn.use_animated_value(0.0)
13
+ ty = pn.use_animated_value(0.0)
14
+
15
+ def on_end(event):
16
+ pn.Animated.spring(tx, to=0.0).start()
17
+ pn.Animated.spring(ty, to=0.0).start()
18
+
19
+ return pn.Animated.View(
20
+ pn.Text("Drag me"),
21
+ style={"transform": [{"translate_x": tx}, {"translate_y": ty}], "padding": 24},
22
+ gestures=[
23
+ gestures.Pan(
24
+ on_change=pn.Animated.event(translation_x=tx, translation_y=ty),
25
+ on_end=on_end,
26
+ )
27
+ ],
28
+ )
29
+ ```
30
+
31
+ Each gesture descriptor is a frozen dataclass holding numeric
32
+ configuration plus user callbacks. The reconciler serializes the
33
+ configuration into plain dicts for the native handler (so prop diffing
34
+ never compares closures) and routes the callbacks through the
35
+ tag-based event channel. Recognition itself is native:
36
+
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
+ - **The browser preview** feeds DOM pointer events into the pure-Python
42
+ [`GestureArbiter`][pythonnative.gestures.GestureArbiter] below, which
43
+ doubles as the executable specification for the native ports.
44
+
45
+ Composition
46
+ -----------
47
+
48
+ Gestures in a plain ``gestures=[...]`` list all recognize
49
+ *simultaneously* (a press ripple plus a pan plus a pinch can all run at
50
+ once). To control how gestures interact, wrap them in composition
51
+ nodes, which nest arbitrarily:
52
+
53
+ - [`Simultaneous`][pythonnative.gestures.Simultaneous]: members may all
54
+ activate together (the flat-list default, useful inside other nodes).
55
+ - [`Race`][pythonnative.gestures.Race]: the first member to activate
56
+ wins; the rest fail for the remainder of the interaction.
57
+ - [`Exclusive`][pythonnative.gestures.Exclusive]: priority order. A
58
+ member may only activate after every member listed *before* it has
59
+ failed. ``Exclusive(double_tap, single_tap)`` is the classic
60
+ double-tap-wins arrangement: the single tap fires only after the
61
+ double-tap window expires.
62
+
63
+ ```python
64
+ gestures=[
65
+ gestures.Race(
66
+ gestures.Pan(on_change=drag),
67
+ gestures.LongPress(on_long_press=show_menu),
68
+ ),
69
+ ]
70
+ ```
71
+
72
+ Every callback receives a [`GestureEvent`][pythonnative.gestures.GestureEvent]
73
+ with position, translation, velocity, scale, and rotation populated as
74
+ appropriate for the gesture kind.
75
+ """
76
+
77
+ from __future__ import annotations
78
+
79
+ import math
80
+ from dataclasses import dataclass, field
81
+ from enum import Enum
82
+ from typing import Any, Callable, Dict, List, Literal, Optional, Sequence, Set, Tuple
83
+
84
+ __all__ = [
85
+ "GestureState",
86
+ "GestureEvent",
87
+ "Tap",
88
+ "LongPress",
89
+ "Pan",
90
+ "Swipe",
91
+ "Fling",
92
+ "Pinch",
93
+ "Rotation",
94
+ "Simultaneous",
95
+ "Race",
96
+ "Exclusive",
97
+ "GestureGroup",
98
+ "GestureArbiter",
99
+ "serialize_gestures",
100
+ ]
101
+
102
+
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
+ """
118
+
119
+ BEGAN = "began"
120
+ CHANGED = "changed"
121
+ ENDED = "ended"
122
+ CANCELLED = "cancelled"
123
+
124
+ def __str__(self) -> str:
125
+ return self.value
126
+
127
+
128
+ GestureCallback = Callable[["GestureEvent"], Any]
129
+
130
+ SwipeDirection = Literal["any", "left", "right", "up", "down"]
131
+
132
+
133
+ @dataclass(frozen=True)
134
+ class GestureEvent:
135
+ """Snapshot delivered to gesture callbacks.
136
+
137
+ Attributes:
138
+ kind: Gesture kind (``"tap"``, ``"long_press"``, ``"pan"``,
139
+ ``"swipe"``, ``"fling"``, ``"pinch"``, ``"rotation"``).
140
+ state: One of [`GestureState`][pythonnative.gestures.GestureState].
141
+ x: Pointer x-position in the view's coordinate space (points).
142
+ y: Pointer y-position in the view's coordinate space (points).
143
+ translation_x: Horizontal displacement since the gesture
144
+ activated (pan only).
145
+ translation_y: Vertical displacement since the gesture
146
+ activated (pan only).
147
+ velocity_x: Horizontal pointer velocity in points/second
148
+ (pan, swipe, and fling).
149
+ velocity_y: Vertical pointer velocity in points/second
150
+ (pan, swipe, and fling).
151
+ scale: Pinch scale factor relative to activation (pinch only).
152
+ rotation: Rotation in radians relative to activation
153
+ (rotation only).
154
+ pointer_count: Number of pointers currently down.
155
+ direction: Resolved swipe/fling direction.
156
+ """
157
+
158
+ kind: str
159
+ state: GestureState
160
+ x: float = 0.0
161
+ y: float = 0.0
162
+ translation_x: float = 0.0
163
+ translation_y: float = 0.0
164
+ velocity_x: float = 0.0
165
+ velocity_y: float = 0.0
166
+ scale: float = 1.0
167
+ rotation: float = 0.0
168
+ pointer_count: int = 1
169
+ direction: Optional[str] = None
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
+
177
+
178
+ _EVENT_FIELDS = frozenset(
179
+ {
180
+ "kind",
181
+ "state",
182
+ "x",
183
+ "y",
184
+ "translation_x",
185
+ "translation_y",
186
+ "velocity_x",
187
+ "velocity_y",
188
+ "scale",
189
+ "rotation",
190
+ "pointer_count",
191
+ "direction",
192
+ }
193
+ )
194
+
195
+
196
+ def event_from_payload(payload: Dict[str, Any]) -> GestureEvent:
197
+ """Build a [`GestureEvent`][pythonnative.gestures.GestureEvent] from a payload dict.
198
+
199
+ Unknown keys are dropped so platform handlers can attach extra
200
+ diagnostics without breaking the public dataclass.
201
+ """
202
+ return GestureEvent(**{k: v for k, v in payload.items() if k in _EVENT_FIELDS})
203
+
204
+
205
+ # ======================================================================
206
+ # Public gesture descriptors
207
+ # ======================================================================
208
+
209
+
210
+ @dataclass(frozen=True)
211
+ class _BaseGesture:
212
+ """Shared callback slots for continuous gestures."""
213
+
214
+ on_begin: Optional[GestureCallback] = None
215
+ on_change: Optional[GestureCallback] = None
216
+ on_end: Optional[GestureCallback] = None
217
+
218
+ kind: str = ""
219
+
220
+ def _config(self) -> Dict[str, Any]:
221
+ return {}
222
+
223
+ def _to_spec(self) -> Dict[str, Any]:
224
+ spec: Dict[str, Any] = {"kind": self.kind}
225
+ spec.update(self._config())
226
+ return spec
227
+
228
+ def _dispatch(self, event: GestureEvent) -> None:
229
+ if event.state == GestureState.BEGAN:
230
+ callback = self.on_begin
231
+ elif event.state == GestureState.CHANGED:
232
+ callback = self.on_change
233
+ else:
234
+ callback = self.on_end
235
+ if callback is not None:
236
+ from .runtime import invoke
237
+
238
+ invoke(callback, event)
239
+
240
+
241
+ @dataclass(frozen=True)
242
+ class Tap(_BaseGesture):
243
+ """Recognize ``n_taps`` quick taps.
244
+
245
+ Attributes:
246
+ on_tap: Called once the tap (or multi-tap) completes.
247
+ n_taps: Number of consecutive taps required (``2`` for
248
+ double-tap).
249
+ max_distance: Maximum pointer travel (points) for a touch to
250
+ still count as a tap.
251
+ """
252
+
253
+ on_tap: Optional[GestureCallback] = None
254
+ n_taps: int = 1
255
+ max_distance: float = 12.0
256
+ kind: str = "tap"
257
+
258
+ def _config(self) -> Dict[str, Any]:
259
+ return {"n_taps": int(self.n_taps), "max_distance": float(self.max_distance)}
260
+
261
+ def _dispatch(self, event: GestureEvent) -> None:
262
+ if event.state == GestureState.ENDED and self.on_tap is not None:
263
+ from .runtime import invoke
264
+
265
+ invoke(self.on_tap, event)
266
+ else:
267
+ super()._dispatch(event)
268
+
269
+
270
+ @dataclass(frozen=True)
271
+ class LongPress(_BaseGesture):
272
+ """Recognize a sustained press.
273
+
274
+ ``on_long_press`` fires as soon as the press has been held for
275
+ ``min_duration_ms`` (matching ``UILongPressGestureRecognizer``);
276
+ ``on_end`` fires when the finger lifts.
277
+
278
+ Attributes:
279
+ on_long_press: Called at activation time.
280
+ min_duration_ms: Hold duration required to activate.
281
+ max_distance: Maximum pointer travel before the press fails.
282
+ """
283
+
284
+ on_long_press: Optional[GestureCallback] = None
285
+ min_duration_ms: float = 500.0
286
+ max_distance: float = 12.0
287
+ kind: str = "long_press"
288
+
289
+ def _config(self) -> Dict[str, Any]:
290
+ return {
291
+ "min_duration_ms": float(self.min_duration_ms),
292
+ "max_distance": float(self.max_distance),
293
+ }
294
+
295
+ def _dispatch(self, event: GestureEvent) -> None:
296
+ if event.state == GestureState.BEGAN and self.on_long_press is not None:
297
+ from .runtime import invoke
298
+
299
+ invoke(self.on_long_press, event)
300
+ else:
301
+ super()._dispatch(event)
302
+
303
+
304
+ @dataclass(frozen=True)
305
+ class Pan(_BaseGesture):
306
+ """Track a drag with translation and velocity.
307
+
308
+ Activates once the pointer travels ``min_distance`` points, then
309
+ reports ``on_change`` for every movement with translation measured
310
+ from the activation point, and ``on_end`` with release velocity.
311
+
312
+ Attributes:
313
+ min_distance: Travel (points) required before the pan activates.
314
+ min_pointers: Minimum pointers that must be down.
315
+ """
316
+
317
+ min_distance: float = 10.0
318
+ min_pointers: int = 1
319
+ kind: str = "pan"
320
+
321
+ def _config(self) -> Dict[str, Any]:
322
+ return {
323
+ "min_distance": float(self.min_distance),
324
+ "min_pointers": int(self.min_pointers),
325
+ }
326
+
327
+
328
+ @dataclass(frozen=True)
329
+ class Swipe(_BaseGesture):
330
+ """Recognize a quick directional flick.
331
+
332
+ Attributes:
333
+ on_swipe: Called once on release with the resolved
334
+ ``direction`` and release velocity.
335
+ direction: Required direction, or ``"any"``.
336
+ min_velocity: Minimum release speed in points/second.
337
+ """
338
+
339
+ on_swipe: Optional[GestureCallback] = None
340
+ direction: SwipeDirection = "any"
341
+ min_velocity: float = 300.0
342
+ kind: str = "swipe"
343
+
344
+ def _config(self) -> Dict[str, Any]:
345
+ return {"direction": str(self.direction), "min_velocity": float(self.min_velocity)}
346
+
347
+ def _dispatch(self, event: GestureEvent) -> None:
348
+ if event.state == GestureState.ENDED and self.on_swipe is not None:
349
+ from .runtime import invoke
350
+
351
+ invoke(self.on_swipe, event)
352
+ else:
353
+ super()._dispatch(event)
354
+
355
+
356
+ @dataclass(frozen=True)
357
+ class Fling(_BaseGesture):
358
+ """Recognize a quick multi-pointer directional flick.
359
+
360
+ Like [`Swipe`][pythonnative.gestures.Swipe] but with a pointer-count
361
+ requirement, mirroring React Native Gesture Handler's ``Fling``
362
+ (and iOS ``UISwipeGestureRecognizer`` with
363
+ ``numberOfTouchesRequired``). A two-finger downward fling is a
364
+ common dismiss gesture:
365
+
366
+ ```python
367
+ gestures.Fling(direction="down", n_pointers=2, on_fling=dismiss)
368
+ ```
369
+
370
+ Attributes:
371
+ on_fling: Called once on release with the resolved
372
+ ``direction`` and release velocity.
373
+ direction: Required direction, or ``"any"``.
374
+ n_pointers: Number of pointers that must participate.
375
+ min_velocity: Minimum release speed in points/second.
376
+ """
377
+
378
+ on_fling: Optional[GestureCallback] = None
379
+ direction: SwipeDirection = "any"
380
+ n_pointers: int = 1
381
+ min_velocity: float = 300.0
382
+ kind: str = "fling"
383
+
384
+ def _config(self) -> Dict[str, Any]:
385
+ return {
386
+ "direction": str(self.direction),
387
+ "n_pointers": int(self.n_pointers),
388
+ "min_velocity": float(self.min_velocity),
389
+ }
390
+
391
+ def _dispatch(self, event: GestureEvent) -> None:
392
+ if event.state == GestureState.ENDED and self.on_fling is not None:
393
+ from .runtime import invoke
394
+
395
+ invoke(self.on_fling, event)
396
+ else:
397
+ super()._dispatch(event)
398
+
399
+
400
+ @dataclass(frozen=True)
401
+ class Pinch(_BaseGesture):
402
+ """Track a two-finger pinch; ``event.scale`` is relative to activation."""
403
+
404
+ kind: str = "pinch"
405
+
406
+
407
+ @dataclass(frozen=True)
408
+ class Rotation(_BaseGesture):
409
+ """Track a two-finger rotation; ``event.rotation`` is in radians."""
410
+
411
+ kind: str = "rotation"
412
+
413
+
414
+ GestureSpec = _BaseGesture
415
+ """Any gesture descriptor accepted by the ``gestures=`` prop."""
416
+
417
+
418
+ # ======================================================================
419
+ # Composition nodes
420
+ # ======================================================================
421
+
422
+
423
+ @dataclass(frozen=True)
424
+ class GestureGroup:
425
+ """A composition node relating child gestures (or nested groups).
426
+
427
+ Build instances with [`Simultaneous`][pythonnative.gestures.Simultaneous],
428
+ [`Race`][pythonnative.gestures.Race], or
429
+ [`Exclusive`][pythonnative.gestures.Exclusive] rather than directly.
430
+ """
431
+
432
+ mode: Literal["simultaneous", "race", "exclusive"]
433
+ children: Tuple[Any, ...] = field(default_factory=tuple)
434
+
435
+
436
+ def Simultaneous(*gestures: Any) -> GestureGroup:
437
+ """Compose gestures that may all be active at the same time.
438
+
439
+ This matches the flat-list default; it exists so simultaneity can
440
+ be expressed *inside* [`Race`][pythonnative.gestures.Race] or
441
+ [`Exclusive`][pythonnative.gestures.Exclusive] nodes:
442
+
443
+ ```python
444
+ gestures.Race(
445
+ gestures.Simultaneous(gestures.Pinch(...), gestures.Rotation(...)),
446
+ gestures.Pan(...),
447
+ )
448
+ ```
449
+ """
450
+ return GestureGroup("simultaneous", tuple(gestures))
451
+
452
+
453
+ def Race(*gestures: Any) -> GestureGroup:
454
+ """Compose gestures where only the first to activate wins.
455
+
456
+ As soon as one member activates, every other member fails for the
457
+ rest of the interaction (its in-progress recognition is abandoned
458
+ without firing callbacks).
459
+ """
460
+ return GestureGroup("race", tuple(gestures))
461
+
462
+
463
+ def Exclusive(*gestures: Any) -> GestureGroup:
464
+ """Compose gestures by priority: earlier members outrank later ones.
465
+
466
+ A member may only activate once every member listed before it has
467
+ *failed*. ``Exclusive(double_tap, single_tap)`` delays the single
468
+ tap until the double-tap window has expired, then fires it; if the
469
+ second tap lands in time, only the double tap fires.
470
+ """
471
+ return GestureGroup("exclusive", tuple(gestures))
472
+
473
+
474
+ def serialize_gestures(
475
+ specs: Sequence[Any],
476
+ ) -> Tuple[List[Dict[str, Any]], Dict[str, Callable[..., Any]]]:
477
+ """Flatten gesture descriptors into native config dicts and event routers.
478
+
479
+ Composition nodes ([`Simultaneous`][pythonnative.gestures.Simultaneous],
480
+ [`Race`][pythonnative.gestures.Race],
481
+ [`Exclusive`][pythonnative.gestures.Exclusive]) are flattened
482
+ depth-first; each resulting spec dict carries the relationship
483
+ metadata the recognizers need:
484
+
485
+ - ``"simultaneous"``: indices this gesture may be active alongside.
486
+ - ``"wait_for"``: indices that must *fail* before this gesture may
487
+ activate.
488
+
489
+ Two gestures that are not in each other's ``simultaneous`` sets
490
+ race: the first to activate causes the other to fail. Gestures in
491
+ the top-level list (outside any composition node) are mutually
492
+ simultaneous.
493
+
494
+ Args:
495
+ specs: The value of an element's ``gestures`` prop. Plain dicts
496
+ are passed through with relationship metadata attached (no
497
+ callbacks to route).
498
+
499
+ Returns:
500
+ ``(clean_specs, events)`` where ``clean_specs`` is a list of
501
+ JSON-ish config dicts (one per leaf gesture, depth-first) and
502
+ ``events`` maps ``"gesture:<i>"`` to a router that unpacks the
503
+ native payload into a `GestureEvent` and invokes the right
504
+ user callback.
505
+ """
506
+ leaves: List[Any] = []
507
+ sim_pairs: Set[Tuple[int, int]] = set()
508
+ wait_pairs: Set[Tuple[int, int]] = set() # (waiter, target)
509
+
510
+ def _flatten(node: Any) -> List[int]:
511
+ if isinstance(node, GestureGroup):
512
+ subtree_leaves: List[List[int]] = [_flatten(child) for child in node.children]
513
+ for a_i in range(len(subtree_leaves)):
514
+ for b_i in range(a_i + 1, len(subtree_leaves)):
515
+ for a in subtree_leaves[a_i]:
516
+ for b in subtree_leaves[b_i]:
517
+ if node.mode == "simultaneous":
518
+ sim_pairs.add((a, b))
519
+ elif node.mode == "exclusive":
520
+ # Later members wait for earlier ones.
521
+ wait_pairs.add((b, a))
522
+ return [i for group in subtree_leaves for i in group]
523
+ leaves.append(node)
524
+ return [len(leaves) - 1]
525
+
526
+ top_level: List[List[int]] = [_flatten(node) for node in specs]
527
+ # Top-level entries are mutually simultaneous (flat-list default).
528
+ for a_i in range(len(top_level)):
529
+ for b_i in range(a_i + 1, len(top_level)):
530
+ for a in top_level[a_i]:
531
+ for b in top_level[b_i]:
532
+ sim_pairs.add((a, b))
533
+
534
+ clean: List[Dict[str, Any]] = []
535
+ events: Dict[str, Callable[..., Any]] = {}
536
+ for i, leaf in enumerate(leaves):
537
+ if isinstance(leaf, _BaseGesture):
538
+ spec = leaf._to_spec()
539
+ from .animated import AnimatedEvent
540
+
541
+ animated = {}
542
+ for state, callback in (
543
+ ("began", leaf.on_begin),
544
+ ("changed", leaf.on_change),
545
+ ("ended", leaf.on_end),
546
+ ):
547
+ if isinstance(callback, AnimatedEvent):
548
+ animated[state] = {field: id(node) for field, node in callback._bindings.items()}
549
+ if animated:
550
+ spec["animated_events"] = animated
551
+
552
+ def _router(payload: Dict[str, Any], _spec: _BaseGesture = leaf) -> None:
553
+ _spec._dispatch(event_from_payload(payload))
554
+
555
+ events[f"gesture:{i}"] = _router
556
+ elif isinstance(leaf, dict):
557
+ spec = dict(leaf)
558
+ else:
559
+ spec = {"kind": ""}
560
+ spec["simultaneous"] = sorted({b for a, b in sim_pairs if a == i} | {a for a, b in sim_pairs if b == i})
561
+ spec["wait_for"] = sorted({t for w, t in wait_pairs if w == i})
562
+ clean.append(spec)
563
+ return clean, events
564
+
565
+
566
+ # ======================================================================
567
+ # Pure-Python recognition engine (browser preview + reference semantics)
568
+ # ======================================================================
569
+ #
570
+ # iOS and Android recognize natively. The browser preview receives raw
571
+ # DOM pointer streams instead, which this arbiter turns into the same
572
+ # GestureEvent payloads. Keeping it in pure Python makes the state
573
+ # machines unit-testable with scripted event sequences; the Kotlin
574
+ # arbiter mirrors this file so the two stay in lockstep.
575
+
576
+ EmitFn = Callable[[int, Dict[str, Any]], None]
577
+ """``emit(gesture_index, payload)``: the arbiter's output channel."""
578
+
579
+ # Internal (never user-visible) state used by recognizers to tell the
580
+ # arbiter they can no longer succeed for this interaction.
581
+ _FAILED = "__failed"
582
+
583
+
584
+ class _VelocityTracker:
585
+ """Estimate pointer velocity from recent samples (points/second)."""
586
+
587
+ __slots__ = ("_samples",)
588
+
589
+ _WINDOW_S = 0.1
590
+
591
+ def __init__(self) -> None:
592
+ self._samples: List[Tuple[float, float, float]] = []
593
+
594
+ def add(self, x: float, y: float, t: float) -> None:
595
+ self._samples.append((x, y, t))
596
+ cutoff = t - self._WINDOW_S
597
+ while len(self._samples) > 2 and self._samples[0][2] < cutoff:
598
+ self._samples.pop(0)
599
+
600
+ def velocity(self) -> Tuple[float, float]:
601
+ if len(self._samples) < 2:
602
+ return (0.0, 0.0)
603
+ x0, y0, t0 = self._samples[0]
604
+ x1, y1, t1 = self._samples[-1]
605
+ dt = t1 - t0
606
+ if dt <= 1e-6:
607
+ return (0.0, 0.0)
608
+ return ((x1 - x0) / dt, (y1 - y0) / dt)
609
+
610
+ def reset(self) -> None:
611
+ self._samples.clear()
612
+
613
+
614
+ class _Recognizer:
615
+ """Base class for one gesture's state machine."""
616
+
617
+ def __init__(self, index: int, config: Dict[str, Any], emit: EmitFn) -> None:
618
+ self.index = index
619
+ self.config = config
620
+ self._emit_fn = emit
621
+
622
+ def emit(self, state: GestureState, **fields: Any) -> None:
623
+ payload: Dict[str, Any] = {"kind": self.kind(), "state": state.value}
624
+ payload.update(fields)
625
+ self._emit_fn(self.index, payload)
626
+
627
+ def fail(self) -> None:
628
+ """Report that this gesture can no longer succeed this interaction."""
629
+ self._emit_fn(self.index, {"kind": self.kind(), "state": _FAILED})
630
+
631
+ def force_fail(self, t: float) -> None:
632
+ """Abandon recognition without emitting anything (lost a race)."""
633
+ self.cancel(t)
634
+
635
+ def kind(self) -> str:
636
+ return str(self.config.get("kind", ""))
637
+
638
+ # Event hooks: ``pointers`` maps pointer id -> (x, y).
639
+ def down(self, pointers: Dict[int, Tuple[float, float]], t: float) -> None:
640
+ pass
641
+
642
+ def move(self, pointers: Dict[int, Tuple[float, float]], t: float) -> None:
643
+ pass
644
+
645
+ def up(self, pointers: Dict[int, Tuple[float, float]], t: float, x: float, y: float) -> None:
646
+ pass
647
+
648
+ def cancel(self, t: float) -> None:
649
+ pass
650
+
651
+ def deadline(self) -> Optional[float]:
652
+ """Next time `poll` should run, or ``None``."""
653
+ return None
654
+
655
+ def poll(self, t: float) -> None:
656
+ pass
657
+
658
+
659
+ def _centroid(pointers: Dict[int, Tuple[float, float]]) -> Tuple[float, float]:
660
+ if not pointers:
661
+ return (0.0, 0.0)
662
+ xs = sum(p[0] for p in pointers.values())
663
+ ys = sum(p[1] for p in pointers.values())
664
+ n = len(pointers)
665
+ return (xs / n, ys / n)
666
+
667
+
668
+ class _TapRecognizer(_Recognizer):
669
+ def __init__(self, index: int, config: Dict[str, Any], emit: EmitFn) -> None:
670
+ super().__init__(index, config, emit)
671
+ self._n_taps = max(1, int(config.get("n_taps", 1)))
672
+ self._slop = float(config.get("max_distance", 12.0))
673
+ self._down_pos: Optional[Tuple[float, float]] = None
674
+ self._down_time = 0.0
675
+ self._tap_count = 0
676
+ self._last_tap_time = 0.0
677
+ self._failed = False
678
+ # Deadline for the *next* tap of a multi-tap (or None).
679
+ self._gap_deadline: Optional[float] = None
680
+
681
+ _MAX_TAP_DURATION_S = 0.4
682
+ _MULTI_TAP_GAP_S = 0.3
683
+
684
+ def down(self, pointers: Dict[int, Tuple[float, float]], t: float) -> None:
685
+ if len(pointers) != 1:
686
+ if not self._failed:
687
+ self._failed = True
688
+ self.fail()
689
+ return
690
+ if self._tap_count > 0 and t - self._last_tap_time > self._MULTI_TAP_GAP_S:
691
+ self._tap_count = 0
692
+ self._failed = False
693
+ self._gap_deadline = None
694
+ self._down_pos = _centroid(pointers)
695
+ self._down_time = t
696
+
697
+ def move(self, pointers: Dict[int, Tuple[float, float]], t: float) -> None:
698
+ if self._failed or self._down_pos is None:
699
+ return
700
+ x, y = _centroid(pointers)
701
+ if math.hypot(x - self._down_pos[0], y - self._down_pos[1]) > self._slop:
702
+ self._failed = True
703
+ self.fail()
704
+
705
+ def up(self, pointers: Dict[int, Tuple[float, float]], t: float, x: float, y: float) -> None:
706
+ if self._failed or self._down_pos is None:
707
+ self._reset()
708
+ return
709
+ if t - self._down_time > self._MAX_TAP_DURATION_S:
710
+ self._reset()
711
+ self.fail()
712
+ return
713
+ self._tap_count += 1
714
+ self._last_tap_time = t
715
+ if self._tap_count >= self._n_taps:
716
+ self.emit(GestureState.ENDED, x=x, y=y)
717
+ self._reset()
718
+ else:
719
+ # Waiting for the next tap; fail if it never arrives so
720
+ # gestures waiting on this one (Exclusive) can proceed.
721
+ self._gap_deadline = t + self._MULTI_TAP_GAP_S
722
+ self._down_pos = None
723
+
724
+ def cancel(self, t: float) -> None:
725
+ self._reset()
726
+
727
+ def deadline(self) -> Optional[float]:
728
+ return self._gap_deadline
729
+
730
+ def poll(self, t: float) -> None:
731
+ if self._gap_deadline is not None and t >= self._gap_deadline:
732
+ self._gap_deadline = None
733
+ self._tap_count = 0
734
+ self.fail()
735
+
736
+ def _reset(self) -> None:
737
+ self._down_pos = None
738
+ self._tap_count = 0 if self._tap_count >= self._n_taps else self._tap_count
739
+ self._failed = False
740
+ self._gap_deadline = None
741
+
742
+
743
+ class _LongPressRecognizer(_Recognizer):
744
+ def __init__(self, index: int, config: Dict[str, Any], emit: EmitFn) -> None:
745
+ super().__init__(index, config, emit)
746
+ self._duration_s = float(config.get("min_duration_ms", 500.0)) / 1000.0
747
+ self._slop = float(config.get("max_distance", 12.0))
748
+ self._down_pos: Optional[Tuple[float, float]] = None
749
+ self._deadline: Optional[float] = None
750
+ self._active = False
751
+
752
+ def down(self, pointers: Dict[int, Tuple[float, float]], t: float) -> None:
753
+ self._down_pos = _centroid(pointers)
754
+ self._deadline = t + self._duration_s
755
+ self._active = False
756
+
757
+ def move(self, pointers: Dict[int, Tuple[float, float]], t: float) -> None:
758
+ if self._down_pos is None:
759
+ return
760
+ x, y = _centroid(pointers)
761
+ if math.hypot(x - self._down_pos[0], y - self._down_pos[1]) > self._slop:
762
+ if self._active:
763
+ self.emit(GestureState.CANCELLED, x=x, y=y)
764
+ self._reset()
765
+ self.fail()
766
+
767
+ def up(self, pointers: Dict[int, Tuple[float, float]], t: float, x: float, y: float) -> None:
768
+ if self._active:
769
+ self.emit(GestureState.ENDED, x=x, y=y)
770
+ elif self._down_pos is not None:
771
+ self.fail()
772
+ self._reset()
773
+
774
+ def cancel(self, t: float) -> None:
775
+ if self._active:
776
+ self.emit(GestureState.CANCELLED)
777
+ self._reset()
778
+
779
+ def deadline(self) -> Optional[float]:
780
+ return self._deadline
781
+
782
+ def poll(self, t: float) -> None:
783
+ if self._deadline is None or self._down_pos is None or self._active:
784
+ return
785
+ if t >= self._deadline:
786
+ self._active = True
787
+ self._deadline = None
788
+ self.emit(GestureState.BEGAN, x=self._down_pos[0], y=self._down_pos[1])
789
+
790
+ def _reset(self) -> None:
791
+ self._down_pos = None
792
+ self._deadline = None
793
+ self._active = False
794
+
795
+
796
+ class _PanRecognizer(_Recognizer):
797
+ def __init__(self, index: int, config: Dict[str, Any], emit: EmitFn) -> None:
798
+ super().__init__(index, config, emit)
799
+ self._min_distance = float(config.get("min_distance", 10.0))
800
+ self._min_pointers = max(1, int(config.get("min_pointers", 1)))
801
+ self._origin: Optional[Tuple[float, float]] = None
802
+ self._anchor: Optional[Tuple[float, float]] = None
803
+ self._active = False
804
+ self._velocity = _VelocityTracker()
805
+ self._last_translation: Tuple[float, float] = (0.0, 0.0)
806
+
807
+ @property
808
+ def active(self) -> bool:
809
+ return self._active
810
+
811
+ def down(self, pointers: Dict[int, Tuple[float, float]], t: float) -> None:
812
+ if len(pointers) < self._min_pointers:
813
+ return
814
+ if self._origin is None:
815
+ self._origin = _centroid(pointers)
816
+ self._velocity.reset()
817
+ x, y = self._origin
818
+ self._velocity.add(x, y, t)
819
+ else:
820
+ # Pointer count changed; re-anchor so the centroid jump
821
+ # doesn't teleport the translation.
822
+ self._rebase(pointers)
823
+
824
+ def move(self, pointers: Dict[int, Tuple[float, float]], t: float) -> None:
825
+ if self._origin is None or len(pointers) < self._min_pointers:
826
+ return
827
+ x, y = _centroid(pointers)
828
+ self._velocity.add(x, y, t)
829
+ if not self._active:
830
+ if math.hypot(x - self._origin[0], y - self._origin[1]) < self._min_distance:
831
+ return
832
+ self._active = True
833
+ self._anchor = (x, y)
834
+ self.emit(GestureState.BEGAN, x=x, y=y, pointer_count=len(pointers))
835
+ return
836
+ assert self._anchor is not None
837
+ vx, vy = self._velocity.velocity()
838
+ self._last_translation = (x - self._anchor[0], y - self._anchor[1])
839
+ self.emit(
840
+ GestureState.CHANGED,
841
+ x=x,
842
+ y=y,
843
+ translation_x=self._last_translation[0],
844
+ translation_y=self._last_translation[1],
845
+ velocity_x=vx,
846
+ velocity_y=vy,
847
+ pointer_count=len(pointers),
848
+ )
849
+
850
+ def up(self, pointers: Dict[int, Tuple[float, float]], t: float, x: float, y: float) -> None:
851
+ if self._active and len(pointers) < self._min_pointers:
852
+ vx, vy = self._velocity.velocity()
853
+ anchor = self._anchor or (x, y)
854
+ self.emit(
855
+ GestureState.ENDED,
856
+ x=x,
857
+ y=y,
858
+ translation_x=x - anchor[0],
859
+ translation_y=y - anchor[1],
860
+ velocity_x=vx,
861
+ velocity_y=vy,
862
+ pointer_count=len(pointers),
863
+ )
864
+ self._reset()
865
+ elif not pointers:
866
+ if not self._active and self._origin is not None:
867
+ self.fail()
868
+ self._reset()
869
+ elif self._active:
870
+ self._rebase(pointers)
871
+
872
+ def cancel(self, t: float) -> None:
873
+ if self._active:
874
+ self.emit(GestureState.CANCELLED)
875
+ self._reset()
876
+
877
+ def _rebase(self, pointers: Dict[int, Tuple[float, float]]) -> None:
878
+ """Re-anchor after a pointer-count change, preserving translation."""
879
+ if not self._active or self._anchor is None:
880
+ self._origin = _centroid(pointers)
881
+ return
882
+ x, y = _centroid(pointers)
883
+ prev_tx, prev_ty = self._last_translation
884
+ self._anchor = (x - prev_tx, y - prev_ty)
885
+
886
+ def _reset(self) -> None:
887
+ self._origin = None
888
+ self._anchor = None
889
+ self._active = False
890
+ self._velocity.reset()
891
+ self._last_translation = (0.0, 0.0)
892
+
893
+
894
+ class _SwipeRecognizer(_Recognizer):
895
+ """Directional flick recognizer; also serves ``fling`` (adds pointer count)."""
896
+
897
+ def __init__(self, index: int, config: Dict[str, Any], emit: EmitFn) -> None:
898
+ super().__init__(index, config, emit)
899
+ self._direction = str(config.get("direction", "any"))
900
+ self._min_velocity = float(config.get("min_velocity", 300.0))
901
+ self._n_pointers = max(1, int(config.get("n_pointers", 1)))
902
+ self._velocity = _VelocityTracker()
903
+ self._tracking = False
904
+ self._max_pointers = 0
905
+
906
+ def down(self, pointers: Dict[int, Tuple[float, float]], t: float) -> None:
907
+ if not self._tracking:
908
+ self._velocity.reset()
909
+ self._tracking = True
910
+ self._max_pointers = 0
911
+ self._max_pointers = max(self._max_pointers, len(pointers))
912
+ x, y = _centroid(pointers)
913
+ self._velocity.add(x, y, t)
914
+
915
+ def move(self, pointers: Dict[int, Tuple[float, float]], t: float) -> None:
916
+ if not self._tracking:
917
+ return
918
+ self._max_pointers = max(self._max_pointers, len(pointers))
919
+ x, y = _centroid(pointers)
920
+ self._velocity.add(x, y, t)
921
+
922
+ def up(self, pointers: Dict[int, Tuple[float, float]], t: float, x: float, y: float) -> None:
923
+ if not self._tracking or pointers:
924
+ return
925
+ self._tracking = False
926
+ self._velocity.add(x, y, t)
927
+ vx, vy = self._velocity.velocity()
928
+ speed = math.hypot(vx, vy)
929
+ if speed < self._min_velocity or self._max_pointers < self._n_pointers:
930
+ self.fail()
931
+ return
932
+ if abs(vx) >= abs(vy):
933
+ direction = "right" if vx > 0 else "left"
934
+ else:
935
+ direction = "down" if vy > 0 else "up"
936
+ if self._direction not in ("any", direction):
937
+ self.fail()
938
+ return
939
+ self.emit(
940
+ GestureState.ENDED,
941
+ x=x,
942
+ y=y,
943
+ velocity_x=vx,
944
+ velocity_y=vy,
945
+ direction=direction,
946
+ pointer_count=self._max_pointers,
947
+ )
948
+
949
+ def cancel(self, t: float) -> None:
950
+ self._tracking = False
951
+ self._max_pointers = 0
952
+ self._velocity.reset()
953
+
954
+
955
+ class _PinchRecognizer(_Recognizer):
956
+ def __init__(self, index: int, config: Dict[str, Any], emit: EmitFn) -> None:
957
+ super().__init__(index, config, emit)
958
+ self._initial: Optional[float] = None
959
+ self._active = False
960
+ self._scale = 1.0
961
+
962
+ def _span(self, pointers: Dict[int, Tuple[float, float]]) -> Optional[float]:
963
+ if len(pointers) < 2:
964
+ return None
965
+ pts = list(pointers.values())[:2]
966
+ return math.hypot(pts[1][0] - pts[0][0], pts[1][1] - pts[0][1])
967
+
968
+ def down(self, pointers: Dict[int, Tuple[float, float]], t: float) -> None:
969
+ span = self._span(pointers)
970
+ if span is not None and span > 0 and self._initial is None:
971
+ self._initial = span
972
+ self._active = True
973
+ x, y = _centroid(pointers)
974
+ self.emit(GestureState.BEGAN, x=x, y=y, scale=1.0, pointer_count=len(pointers))
975
+
976
+ def move(self, pointers: Dict[int, Tuple[float, float]], t: float) -> None:
977
+ if not self._active or self._initial is None:
978
+ return
979
+ span = self._span(pointers)
980
+ if span is None or span <= 0:
981
+ return
982
+ self._scale = span / self._initial
983
+ x, y = _centroid(pointers)
984
+ self.emit(
985
+ GestureState.CHANGED,
986
+ x=x,
987
+ y=y,
988
+ scale=self._scale,
989
+ pointer_count=len(pointers),
990
+ )
991
+
992
+ def up(self, pointers: Dict[int, Tuple[float, float]], t: float, x: float, y: float) -> None:
993
+ if self._active and len(pointers) < 2:
994
+ self.emit(GestureState.ENDED, x=x, y=y, scale=self._scale, pointer_count=len(pointers))
995
+ self._reset()
996
+ elif not self._active and not pointers:
997
+ self.fail()
998
+
999
+ def cancel(self, t: float) -> None:
1000
+ if self._active:
1001
+ self.emit(GestureState.CANCELLED, scale=self._scale)
1002
+ self._reset()
1003
+
1004
+ def _reset(self) -> None:
1005
+ self._initial = None
1006
+ self._active = False
1007
+ self._scale = 1.0
1008
+
1009
+
1010
+ class _RotationRecognizer(_Recognizer):
1011
+ def __init__(self, index: int, config: Dict[str, Any], emit: EmitFn) -> None:
1012
+ super().__init__(index, config, emit)
1013
+ self._initial: Optional[float] = None
1014
+ self._active = False
1015
+ self._rotation = 0.0
1016
+
1017
+ def _angle(self, pointers: Dict[int, Tuple[float, float]]) -> Optional[float]:
1018
+ if len(pointers) < 2:
1019
+ return None
1020
+ pts = list(pointers.values())[:2]
1021
+ return math.atan2(pts[1][1] - pts[0][1], pts[1][0] - pts[0][0])
1022
+
1023
+ def down(self, pointers: Dict[int, Tuple[float, float]], t: float) -> None:
1024
+ angle = self._angle(pointers)
1025
+ if angle is not None and self._initial is None:
1026
+ self._initial = angle
1027
+ self._active = True
1028
+ x, y = _centroid(pointers)
1029
+ self.emit(GestureState.BEGAN, x=x, y=y, rotation=0.0, pointer_count=len(pointers))
1030
+
1031
+ def move(self, pointers: Dict[int, Tuple[float, float]], t: float) -> None:
1032
+ if not self._active or self._initial is None:
1033
+ return
1034
+ angle = self._angle(pointers)
1035
+ if angle is None:
1036
+ return
1037
+ delta = angle - self._initial
1038
+ while delta > math.pi:
1039
+ delta -= 2 * math.pi
1040
+ while delta < -math.pi:
1041
+ delta += 2 * math.pi
1042
+ self._rotation = delta
1043
+ x, y = _centroid(pointers)
1044
+ self.emit(
1045
+ GestureState.CHANGED,
1046
+ x=x,
1047
+ y=y,
1048
+ rotation=self._rotation,
1049
+ pointer_count=len(pointers),
1050
+ )
1051
+
1052
+ def up(self, pointers: Dict[int, Tuple[float, float]], t: float, x: float, y: float) -> None:
1053
+ if self._active and len(pointers) < 2:
1054
+ self.emit(GestureState.ENDED, x=x, y=y, rotation=self._rotation, pointer_count=len(pointers))
1055
+ self._reset()
1056
+ elif not self._active and not pointers:
1057
+ self.fail()
1058
+
1059
+ def cancel(self, t: float) -> None:
1060
+ if self._active:
1061
+ self.emit(GestureState.CANCELLED, rotation=self._rotation)
1062
+ self._reset()
1063
+
1064
+ def _reset(self) -> None:
1065
+ self._initial = None
1066
+ self._active = False
1067
+ self._rotation = 0.0
1068
+
1069
+
1070
+ _RECOGNIZERS: Dict[str, Any] = {
1071
+ "tap": _TapRecognizer,
1072
+ "long_press": _LongPressRecognizer,
1073
+ "pan": _PanRecognizer,
1074
+ "swipe": _SwipeRecognizer,
1075
+ "fling": _SwipeRecognizer,
1076
+ "pinch": _PinchRecognizer,
1077
+ "rotation": _RotationRecognizer,
1078
+ }
1079
+
1080
+ # Arbitration states for one recognizer within one interaction.
1081
+ _POSSIBLE = "possible"
1082
+ _WAITING = "waiting" # activation buffered, awaiting wait_for targets
1083
+ _ACTIVE = "active"
1084
+ _DONE = "done"
1085
+ _ST_FAILED = "failed"
1086
+
1087
+
1088
+ class GestureArbiter:
1089
+ """Turn a raw pointer-event stream into arbitrated gesture payloads.
1090
+
1091
+ One arbiter serves one view. The host backend feeds it normalized
1092
+ pointer events (positions in the view's coordinate space, times in
1093
+ seconds, any monotonic clock) and provides an ``emit`` callback
1094
+ that forwards ``(gesture_index, payload)`` pairs to
1095
+ [`dispatch_event`][pythonnative.events.dispatch_event].
1096
+
1097
+ Beyond running each gesture's state machine, the arbiter enforces
1098
+ the relationships computed by
1099
+ [`serialize_gestures`][pythonnative.gestures.serialize_gestures]:
1100
+
1101
+ - Two gestures not in each other's ``"simultaneous"`` sets race;
1102
+ when one activates, the other is force-failed for the rest of
1103
+ the interaction.
1104
+ - A gesture with a ``"wait_for"`` set may only activate after all
1105
+ of those gestures have failed. Its output (including a buffered
1106
+ discrete completion, like a single tap waiting out a double-tap
1107
+ window) is held and either flushed on failure of the targets or
1108
+ discarded if a target succeeds.
1109
+
1110
+ Specs without relationship metadata (hand-built dicts) default to
1111
+ fully simultaneous, matching pre-composition behavior.
1112
+
1113
+ Timing: after each pointer event, hosts should check
1114
+ [`next_deadline`][pythonnative.gestures.GestureArbiter.next_deadline]
1115
+ and schedule a [`poll`][pythonnative.gestures.GestureArbiter.poll]
1116
+ call for that time (long-press activation and multi-tap windows).
1117
+ """
1118
+
1119
+ _DISCRETE_KINDS = frozenset({"tap", "swipe", "fling"})
1120
+
1121
+ def __init__(self, specs: Sequence[Dict[str, Any]], emit: EmitFn) -> None:
1122
+ self._emit_out = emit
1123
+ self._pointers: Dict[int, Tuple[float, float]] = {}
1124
+ self._recognizers: List[_Recognizer] = []
1125
+ self._indices: List[int] = []
1126
+ self._sim: Dict[int, Optional[Set[int]]] = {}
1127
+ self._wait_for: Dict[int, Set[int]] = {}
1128
+ self._states: Dict[int, str] = {}
1129
+ self._buffers: Dict[int, List[Dict[str, Any]]] = {}
1130
+ self._last_t = 0.0
1131
+ for i, spec in enumerate(specs):
1132
+ recognizer_cls = _RECOGNIZERS.get(str(spec.get("kind", "")))
1133
+ if recognizer_cls is None:
1134
+ continue
1135
+ self._recognizers.append(recognizer_cls(i, spec, self._mediate))
1136
+ self._indices.append(i)
1137
+ sim = spec.get("simultaneous")
1138
+ # None means "simultaneous with everything" (back-compat
1139
+ # for hand-built spec dicts without metadata).
1140
+ self._sim[i] = None if sim is None else set(int(s) for s in sim)
1141
+ self._wait_for[i] = set(int(w) for w in spec.get("wait_for", ()) or ())
1142
+ self._states[i] = _POSSIBLE
1143
+
1144
+ # -- pointer input ---------------------------------------------------
1145
+
1146
+ def pointer_down(self, pointer_id: int, x: float, y: float, t: float) -> None:
1147
+ """Record a pointer press and advance every recognizer."""
1148
+ self._last_t = t
1149
+ if not self._pointers and not any(s == _WAITING for s in self._states.values()):
1150
+ # Fresh interaction: clear per-interaction verdicts.
1151
+ for i in self._indices:
1152
+ self._states[i] = _POSSIBLE
1153
+ self._buffers.clear()
1154
+ self._pointers[pointer_id] = (x, y)
1155
+ for recognizer in self._recognizers:
1156
+ recognizer.down(self._pointers, t)
1157
+
1158
+ def pointer_move(self, pointer_id: int, x: float, y: float, t: float) -> None:
1159
+ """Record pointer travel and advance every recognizer."""
1160
+ self._last_t = t
1161
+ if pointer_id not in self._pointers:
1162
+ return
1163
+ self._pointers[pointer_id] = (x, y)
1164
+ for recognizer in self._recognizers:
1165
+ recognizer.move(self._pointers, t)
1166
+
1167
+ def pointer_up(self, pointer_id: int, x: float, y: float, t: float) -> None:
1168
+ """Record a pointer release and advance every recognizer."""
1169
+ self._last_t = t
1170
+ self._pointers.pop(pointer_id, None)
1171
+ for recognizer in self._recognizers:
1172
+ recognizer.up(self._pointers, t, x, y)
1173
+
1174
+ def cancel(self, t: float) -> None:
1175
+ """Abort every in-flight gesture (e.g. touch stolen by a scroll parent)."""
1176
+ self._last_t = t
1177
+ self._pointers.clear()
1178
+ self._buffers.clear()
1179
+ for i in self._indices:
1180
+ if self._states[i] == _WAITING:
1181
+ self._states[i] = _ST_FAILED
1182
+ for recognizer in self._recognizers:
1183
+ recognizer.cancel(t)
1184
+
1185
+ def poll(self, t: float) -> None:
1186
+ """Advance time-based recognizers (long-press, multi-tap windows)."""
1187
+ self._last_t = t
1188
+ for recognizer in self._recognizers:
1189
+ recognizer.poll(t)
1190
+
1191
+ def next_deadline(self) -> Optional[float]:
1192
+ """Earliest time `poll` should be called, or ``None``."""
1193
+ deadlines = [d for r in self._recognizers if (d := r.deadline()) is not None]
1194
+ return min(deadlines) if deadlines else None
1195
+
1196
+ def has_active_pan(self) -> bool:
1197
+ """Whether a pan gesture is currently activated.
1198
+
1199
+ Android handlers use this to call
1200
+ ``requestDisallowInterceptTouchEvent`` so an enclosing
1201
+ ScrollView doesn't steal the drag.
1202
+ """
1203
+ return any(isinstance(r, _PanRecognizer) and r.active for r in self._recognizers)
1204
+
1205
+ # -- arbitration -------------------------------------------------------
1206
+
1207
+ def _recognizer_for(self, index: int) -> Optional[_Recognizer]:
1208
+ for r in self._recognizers:
1209
+ if r.index == index:
1210
+ return r
1211
+ return None
1212
+
1213
+ def _is_simultaneous(self, a: int, b: int) -> bool:
1214
+ sim_a = self._sim.get(a)
1215
+ sim_b = self._sim.get(b)
1216
+ if sim_a is None or sim_b is None:
1217
+ return True
1218
+ return b in sim_a and a in sim_b
1219
+
1220
+ def _mediate(self, index: int, payload: Dict[str, Any]) -> None:
1221
+ state = payload.get("state")
1222
+ current = self._states.get(index, _POSSIBLE)
1223
+
1224
+ if state == _FAILED:
1225
+ if current in (_POSSIBLE, _WAITING):
1226
+ self._set_failed(index, discard_buffer=True)
1227
+ return
1228
+
1229
+ if current == _ST_FAILED:
1230
+ return
1231
+
1232
+ if current == _ACTIVE:
1233
+ self._emit_out(index, payload)
1234
+ if state == GestureState.ENDED:
1235
+ self._states[index] = _DONE
1236
+ self._on_resolved(index, succeeded=True)
1237
+ elif state == GestureState.CANCELLED:
1238
+ self._states[index] = _ST_FAILED
1239
+ self._on_resolved(index, succeeded=False)
1240
+ return
1241
+
1242
+ if current == _WAITING:
1243
+ # Recognition continues while the activation is parked;
1244
+ # keep buffering so a flush replays the full stream.
1245
+ self._buffers.setdefault(index, []).append(payload)
1246
+ return
1247
+
1248
+ # current == _POSSIBLE: this payload is an activation attempt
1249
+ # (BEGAN, or a discrete completion such as a tap's ENDED).
1250
+ self._request_activation(index, payload)
1251
+
1252
+ def _request_activation(self, index: int, payload: Dict[str, Any]) -> None:
1253
+ # Race check: blocked by any non-simultaneous gesture that has
1254
+ # already activated (or completed) this interaction.
1255
+ for j in self._indices:
1256
+ if j == index:
1257
+ continue
1258
+ if self._states[j] in (_ACTIVE, _DONE) and not self._is_simultaneous(index, j):
1259
+ self._set_failed(index, discard_buffer=True)
1260
+ return
1261
+
1262
+ targets = [t for t in self._wait_for.get(index, ()) if t in self._states]
1263
+ if any(self._states[t] in (_ACTIVE, _DONE) for t in targets):
1264
+ self._set_failed(index, discard_buffer=True)
1265
+ return
1266
+ if any(self._states[t] in (_POSSIBLE, _WAITING) for t in targets):
1267
+ self._states[index] = _WAITING
1268
+ self._buffers.setdefault(index, []).append(payload)
1269
+ return
1270
+
1271
+ self._activate(index, [payload])
1272
+
1273
+ def _activate(self, index: int, payloads: List[Dict[str, Any]]) -> None:
1274
+ discrete_done = bool(payloads) and payloads[-1].get("state") in (
1275
+ GestureState.ENDED,
1276
+ GestureState.CANCELLED,
1277
+ )
1278
+ self._states[index] = _DONE if discrete_done else _ACTIVE
1279
+ # Winning a race force-fails every unresolved non-simultaneous
1280
+ # competitor before any output, so their recognizers stand down
1281
+ # (no stray callbacks later in the interaction).
1282
+ for j in self._indices:
1283
+ if j == index:
1284
+ continue
1285
+ if self._states[j] in (_POSSIBLE, _WAITING) and not self._is_simultaneous(index, j):
1286
+ recognizer = self._recognizer_for(j)
1287
+ if recognizer is not None:
1288
+ recognizer.force_fail(self._last_t)
1289
+ self._set_failed(j, discard_buffer=True)
1290
+ for payload in payloads:
1291
+ self._emit_out(index, payload)
1292
+ if self._states[index] == _DONE:
1293
+ self._on_resolved(index, succeeded=True)
1294
+
1295
+ def _set_failed(self, index: int, discard_buffer: bool) -> None:
1296
+ if self._states.get(index) == _ST_FAILED:
1297
+ return
1298
+ self._states[index] = _ST_FAILED
1299
+ if discard_buffer:
1300
+ self._buffers.pop(index, None)
1301
+ self._on_resolved(index, succeeded=False)
1302
+
1303
+ def _on_resolved(self, index: int, succeeded: bool) -> None:
1304
+ """Re-evaluate gestures waiting on ``index`` after it resolves."""
1305
+ for waiter in self._indices:
1306
+ if self._states.get(waiter) != _WAITING:
1307
+ continue
1308
+ if index not in self._wait_for.get(waiter, ()):
1309
+ continue
1310
+ if succeeded:
1311
+ recognizer = self._recognizer_for(waiter)
1312
+ if recognizer is not None:
1313
+ recognizer.force_fail(self._last_t)
1314
+ self._set_failed(waiter, discard_buffer=True)
1315
+ continue
1316
+ targets = [t for t in self._wait_for.get(waiter, ()) if t in self._states]
1317
+ if any(self._states[t] in (_POSSIBLE, _WAITING) for t in targets):
1318
+ continue # still waiting on someone else
1319
+ if any(self._states[t] in (_ACTIVE, _DONE) for t in targets):
1320
+ recognizer = self._recognizer_for(waiter)
1321
+ if recognizer is not None:
1322
+ recognizer.force_fail(self._last_t)
1323
+ self._set_failed(waiter, discard_buffer=True)
1324
+ continue
1325
+ payloads = self._buffers.pop(waiter, [])
1326
+ self._states[waiter] = _POSSIBLE
1327
+ self._activate(waiter, payloads)
1328
+
1329
+
1330
+ # Re-exported via ``pythonnative.gestures`` for handler-side construction.
1331
+ def make_arbiter(specs: Sequence[Dict[str, Any]], emit: EmitFn) -> GestureArbiter:
1332
+ """Build a [`GestureArbiter`][pythonnative.gestures.GestureArbiter] from serialized specs."""
1333
+ return GestureArbiter(specs, emit)