pythonnative 0.40.0__cp313-cp313-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.
- pythonnative/__init__.py +411 -0
- pythonnative/_ios_log.py +92 -0
- pythonnative/_yoga.cp313-win_amd64.pyd +0 -0
- pythonnative/alerts.py +236 -0
- pythonnative/animated.py +1687 -0
- pythonnative/animation_graph.py +78 -0
- pythonnative/appearance.py +124 -0
- pythonnative/bootstrap.py +100 -0
- pythonnative/bridge/__init__.py +302 -0
- pythonnative/bridge/android.py +78 -0
- pythonnative/bridge/codec.py +170 -0
- pythonnative/bridge/commits.py +114 -0
- pythonnative/bridge/fake.py +243 -0
- pythonnative/bridge/ios.py +138 -0
- pythonnative/bridge/web.py +489 -0
- pythonnative/cli/__init__.py +7 -0
- pythonnative/cli/pn.py +1214 -0
- pythonnative/component.py +271 -0
- pythonnative/components/__init__.py +111 -0
- pythonnative/components/_base.py +166 -0
- pythonnative/components/controls.py +521 -0
- pythonnative/components/layout.py +518 -0
- pythonnative/components/lists.py +785 -0
- pythonnative/components/media.py +204 -0
- pythonnative/components/overlays.py +107 -0
- pythonnative/components/pressable.py +252 -0
- pythonnative/components/structural.py +167 -0
- pythonnative/components/text.py +291 -0
- pythonnative/devclient.py +824 -0
- pythonnative/devserver/__init__.py +34 -0
- pythonnative/devserver/server.py +718 -0
- pythonnative/devserver/static/animation_graph.js +82 -0
- pythonnative/devserver/static/bridge.js +147 -0
- pythonnative/devserver/static/colors.js +70 -0
- pythonnative/devserver/static/host.js +552 -0
- pythonnative/devserver/static/index.html +60 -0
- pythonnative/devserver/static/layout.js +85 -0
- pythonnative/devserver/static/preview.css +415 -0
- pythonnative/devserver/static/renderer.js +1994 -0
- pythonnative/devserver/static/shell.js +345 -0
- pythonnative/devserver/static/yoga/LICENSE +21 -0
- pythonnative/devserver/static/yoga/binaries/yoga-wasm-base64-esm.js +75 -0
- pythonnative/devserver/static/yoga/src/generated/YGEnums.d.ts +189 -0
- pythonnative/devserver/static/yoga/src/generated/YGEnums.js +211 -0
- pythonnative/devserver/static/yoga/src/generated/YGEnums.js.map +1 -0
- pythonnative/devserver/static/yoga/src/index.d.ts +12 -0
- pythonnative/devserver/static/yoga/src/index.js +16 -0
- pythonnative/devserver/static/yoga/src/index.js.map +1 -0
- pythonnative/devserver/static/yoga/src/load.d.ts +11 -0
- pythonnative/devserver/static/yoga/src/load.js +17 -0
- pythonnative/devserver/static/yoga/src/load.js.map +1 -0
- pythonnative/devserver/static/yoga/src/wrapAssembly.d.ts +155 -0
- pythonnative/devserver/static/yoga/src/wrapAssembly.js +125 -0
- pythonnative/devserver/static/yoga/src/wrapAssembly.js.map +1 -0
- pythonnative/devserver/watcher.py +267 -0
- pythonnative/devserver/ws.py +421 -0
- pythonnative/diagnostics.py +241 -0
- pythonnative/element.py +159 -0
- pythonnative/equality.py +19 -0
- pythonnative/events.py +231 -0
- pythonnative/gestures.py +1333 -0
- pythonnative/hooks.py +1621 -0
- pythonnative/hosts/__init__.py +63 -0
- pythonnative/hosts/base.py +596 -0
- pythonnative/hosts/native.py +302 -0
- pythonnative/hot_reload.py +585 -0
- pythonnative/layout.py +328 -0
- pythonnative/mutations.py +142 -0
- pythonnative/native/android/build.gradle +52 -0
- pythonnative/native/android/gradle.properties +3 -0
- pythonnative/native/android/settings.gradle +12 -0
- pythonnative/native/android/src/main/AndroidManifest.xml +11 -0
- pythonnative/native/android/src/main/cpp/CMakeLists.txt +9 -0
- pythonnative/native/android/src/main/cpp/yoga_jni.cpp +52 -0
- pythonnative/native/android/src/main/java/com/pythonnative/generated/NativeModules.kt +5 -0
- pythonnative/native/android/src/main/java/com/pythonnative/generated/NativeProps.kt +3705 -0
- pythonnative/native/android/src/main/java/com/pythonnative/generated/PNContracts.kt +129 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/PNBridge.kt +261 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/PythonHost.kt +20 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/animation/AnimationGraph.kt +108 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/animation/AnimationSpecs.kt +141 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/animation/PNAnimator.kt +257 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/bridge/CommitState.kt +106 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/bridge/JsonUtil.kt +175 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/bridge/MainThread.kt +40 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/bridge/PNLog.kt +33 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/bridge/PNRegistry.kt +82 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/bridge/PNTransaction.kt +80 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/bridge/TransactionApplier.kt +117 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/bridge/ViewRegistry.kt +74 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/components/BuiltinComponents.kt +37 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/components/ButtonManager.kt +32 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/components/ComponentManager.kt +213 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/components/ContainerManagers.kt +69 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/components/ControlManagers.kt +186 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/components/DatePickerManager.kt +123 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/components/ImageLoader.kt +141 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/components/ImageManager.kt +162 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/components/ModalManager.kt +134 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/components/PNColor.kt +201 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/components/PickerManager.kt +86 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/components/PortalManager.kt +55 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/components/PressableManager.kt +122 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/components/ScreenStackManager.kt +110 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/components/ScrollViewManager.kt +214 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/components/SegmentedControlManager.kt +113 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/components/StatusBarManager.kt +52 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/components/TabBarManager.kt +131 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/components/TextInputManager.kt +250 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/components/TextManager.kt +233 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/components/ViewStyler.kt +355 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/components/VirtualListManager.kt +159 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/components/WebViewManager.kt +149 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/gestures/GestureArbiter.kt +233 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/gestures/GestureCoordinator.kt +127 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/gestures/GestureRecognizers.kt +527 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/layout/NativeLayout.kt +145 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/layout/YogaNode.kt +29 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/modules/BuiltinModules.kt +73 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/modules/DeviceModules.kt +316 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/modules/HostModule.kt +61 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/modules/MediaModules.kt +200 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/modules/NativeModule.kt +158 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/modules/NotificationsModule.kt +100 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/modules/PermissionsModule.kt +99 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/modules/StorageModules.kt +89 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/modules/SystemModules.kt +178 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/plugins/GeneratedPlugins.kt +16 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/screens/PNScreenFragment.kt +188 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/screens/ScreenRegistry.kt +80 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/views/PNAccessibilityDelegate.kt +48 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/views/PNBorderDrawable.kt +76 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/views/PNEditText.kt +18 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/views/PNFrameLayout.kt +49 -0
- pythonnative/native/android/src/test/java/com/pythonnative/runtime/AnimationSpecsTest.kt +75 -0
- pythonnative/native/android/src/test/java/com/pythonnative/runtime/GestureArbiterTest.kt +197 -0
- pythonnative/native/android/src/test/java/com/pythonnative/runtime/PNColorTest.kt +47 -0
- pythonnative/native/android/src/test/java/com/pythonnative/runtime/PNTransactionTest.kt +78 -0
- pythonnative/native/android/src/test/java/com/pythonnative/runtime/PromiseTest.kt +89 -0
- pythonnative/native/ios/Package.swift +23 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Animation/PNAnimationGraph.swift +199 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Animation/PNAnimator.swift +312 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Bridge/PNBridge.swift +255 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Bridge/PNCommit.swift +104 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Bridge/PNRegistry.swift +142 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Bridge/PNTransaction.swift +166 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Bridge/PNViewRegistry.swift +53 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Components/PNButtonManagers.swift +207 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Components/PNColor.swift +173 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Components/PNComponentManager.swift +183 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Components/PNContainerView.swift +56 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Components/PNControlManagers.swift +281 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Components/PNImageManager.swift +222 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Components/PNOverlayManagers.swift +243 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Components/PNPressableManager.swift +114 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Components/PNScreenStackManager.swift +107 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Components/PNScrollViewManager.swift +197 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Components/PNTabBarManager.swift +106 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Components/PNTextInputManager.swift +297 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Components/PNTextManager.swift +230 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Components/PNTransform.swift +82 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Components/PNViewManager.swift +86 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Components/PNViewState.swift +87 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Components/PNViewStyler.swift +340 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Components/PNVirtualListManager.swift +155 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Components/PNWebViewManager.swift +124 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Generated/NativeModules.swift +1 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Generated/NativeProps.swift +3775 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Generated/PNContracts.swift +46 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Gestures/PNGestureCoordinator.swift +260 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Layout/PNLayout.swift +173 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Modules/AlertModule.swift +92 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Modules/HostModule.swift +80 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Modules/LifecycleModules.swift +174 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Modules/MediaModules.swift +177 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Modules/PNNativeModule.swift +168 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Modules/PermissionModules.swift +266 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Modules/SystemModules.swift +251 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Plugins/PNPluginRegistration.swift +8 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Screens/PNScreenRegistry.swift +46 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Screens/PNViewController.swift +237 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Support/PNCompat.swift +46 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Support/PNJSON.swift +173 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Support/PNLog.swift +47 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Support/PNWindow.swift +51 -0
- pythonnative/native/ios/Tests/PythonNativeKitTests/PNAnimationGraphTests.swift +26 -0
- pythonnative/native/ios/Tests/PythonNativeKitTests/PNColorTests.swift +50 -0
- pythonnative/native/ios/Tests/PythonNativeKitTests/PNCommitTests.swift +86 -0
- pythonnative/native/ios/Tests/PythonNativeKitTests/PNGestureTests.swift +101 -0
- pythonnative/native/ios/Tests/PythonNativeKitTests/PNManagerTests.swift +126 -0
- pythonnative/native/ios/Tests/PythonNativeKitTests/PNModuleTests.swift +111 -0
- pythonnative/native/ios/Tests/PythonNativeKitTests/PNTransactionTests.swift +91 -0
- pythonnative/native/yoga/LICENSE +21 -0
- pythonnative/native/yoga/Package.swift +9 -0
- pythonnative/native/yoga/VENDORED.md +2 -0
- pythonnative/native/yoga/include/PNStyle.h +10 -0
- pythonnative/native/yoga/include/module.modulemap +5 -0
- pythonnative/native/yoga/include/yoga/YGConfig.h +158 -0
- pythonnative/native/yoga/include/yoga/YGEnums.h +142 -0
- pythonnative/native/yoga/include/yoga/YGMacros.h +99 -0
- pythonnative/native/yoga/include/yoga/YGNode.h +304 -0
- pythonnative/native/yoga/include/yoga/YGNodeLayout.h +35 -0
- pythonnative/native/yoga/include/yoga/YGNodeStyle.h +130 -0
- pythonnative/native/yoga/include/yoga/YGPixelGrid.h +29 -0
- pythonnative/native/yoga/include/yoga/YGValue.h +84 -0
- pythonnative/native/yoga/include/yoga/Yoga.h +21 -0
- pythonnative/native/yoga/python.cpp +16 -0
- pythonnative/native/yoga/style.cpp +272 -0
- pythonnative/native/yoga/yoga/CMakeLists.txt +41 -0
- pythonnative/native/yoga/yoga/YGConfig.cpp +92 -0
- pythonnative/native/yoga/yoga/YGConfig.h +158 -0
- pythonnative/native/yoga/yoga/YGEnums.cpp +262 -0
- pythonnative/native/yoga/yoga/YGEnums.h +142 -0
- pythonnative/native/yoga/yoga/YGMacros.h +99 -0
- pythonnative/native/yoga/yoga/YGNode.cpp +366 -0
- pythonnative/native/yoga/yoga/YGNode.h +304 -0
- pythonnative/native/yoga/yoga/YGNodeLayout.cpp +92 -0
- pythonnative/native/yoga/yoga/YGNodeLayout.h +35 -0
- pythonnative/native/yoga/yoga/YGNodeStyle.cpp +405 -0
- pythonnative/native/yoga/yoga/YGNodeStyle.h +130 -0
- pythonnative/native/yoga/yoga/YGPixelGrid.cpp +22 -0
- pythonnative/native/yoga/yoga/YGPixelGrid.h +29 -0
- pythonnative/native/yoga/yoga/YGValue.cpp +20 -0
- pythonnative/native/yoga/yoga/YGValue.h +84 -0
- pythonnative/native/yoga/yoga/Yoga.h +21 -0
- pythonnative/native/yoga/yoga/algorithm/AbsoluteLayout.cpp +563 -0
- pythonnative/native/yoga/yoga/algorithm/AbsoluteLayout.h +41 -0
- pythonnative/native/yoga/yoga/algorithm/Align.h +72 -0
- pythonnative/native/yoga/yoga/algorithm/Baseline.cpp +78 -0
- pythonnative/native/yoga/yoga/algorithm/Baseline.h +21 -0
- pythonnative/native/yoga/yoga/algorithm/BoundAxis.h +79 -0
- pythonnative/native/yoga/yoga/algorithm/Cache.cpp +121 -0
- pythonnative/native/yoga/yoga/algorithm/Cache.h +30 -0
- pythonnative/native/yoga/yoga/algorithm/CalculateLayout.cpp +2428 -0
- pythonnative/native/yoga/yoga/algorithm/CalculateLayout.h +38 -0
- pythonnative/native/yoga/yoga/algorithm/FlexDirection.h +120 -0
- pythonnative/native/yoga/yoga/algorithm/FlexLine.cpp +124 -0
- pythonnative/native/yoga/yoga/algorithm/FlexLine.h +75 -0
- pythonnative/native/yoga/yoga/algorithm/PixelGrid.cpp +132 -0
- pythonnative/native/yoga/yoga/algorithm/PixelGrid.h +29 -0
- pythonnative/native/yoga/yoga/algorithm/SizingMode.h +73 -0
- pythonnative/native/yoga/yoga/algorithm/TrailingPosition.h +44 -0
- pythonnative/native/yoga/yoga/config/Config.cpp +135 -0
- pythonnative/native/yoga/yoga/config/Config.h +92 -0
- pythonnative/native/yoga/yoga/debug/AssertFatal.cpp +53 -0
- pythonnative/native/yoga/yoga/debug/AssertFatal.h +29 -0
- pythonnative/native/yoga/yoga/debug/Log.cpp +107 -0
- pythonnative/native/yoga/yoga/debug/Log.h +34 -0
- pythonnative/native/yoga/yoga/enums/Align.h +47 -0
- pythonnative/native/yoga/yoga/enums/BoxSizing.h +40 -0
- pythonnative/native/yoga/yoga/enums/Dimension.h +40 -0
- pythonnative/native/yoga/yoga/enums/Direction.h +41 -0
- pythonnative/native/yoga/yoga/enums/Display.h +41 -0
- pythonnative/native/yoga/yoga/enums/Edge.h +47 -0
- pythonnative/native/yoga/yoga/enums/Errata.h +41 -0
- pythonnative/native/yoga/yoga/enums/ExperimentalFeature.h +39 -0
- pythonnative/native/yoga/yoga/enums/FlexDirection.h +42 -0
- pythonnative/native/yoga/yoga/enums/Gutter.h +41 -0
- pythonnative/native/yoga/yoga/enums/Justify.h +44 -0
- pythonnative/native/yoga/yoga/enums/LogLevel.h +44 -0
- pythonnative/native/yoga/yoga/enums/MeasureMode.h +41 -0
- pythonnative/native/yoga/yoga/enums/NodeType.h +40 -0
- pythonnative/native/yoga/yoga/enums/Overflow.h +41 -0
- pythonnative/native/yoga/yoga/enums/PhysicalEdge.h +21 -0
- pythonnative/native/yoga/yoga/enums/PositionType.h +41 -0
- pythonnative/native/yoga/yoga/enums/Unit.h +42 -0
- pythonnative/native/yoga/yoga/enums/Wrap.h +41 -0
- pythonnative/native/yoga/yoga/enums/YogaEnums.h +85 -0
- pythonnative/native/yoga/yoga/event/event.cpp +87 -0
- pythonnative/native/yoga/yoga/event/event.h +130 -0
- pythonnative/native/yoga/yoga/module.modulemap +21 -0
- pythonnative/native/yoga/yoga/node/CachedMeasurement.h +53 -0
- pythonnative/native/yoga/yoga/node/LayoutResults.cpp +48 -0
- pythonnative/native/yoga/yoga/node/LayoutResults.h +122 -0
- pythonnative/native/yoga/yoga/node/LayoutableChildren.h +148 -0
- pythonnative/native/yoga/yoga/node/Node.cpp +443 -0
- pythonnative/native/yoga/yoga/node/Node.h +337 -0
- pythonnative/native/yoga/yoga/numeric/Comparison.h +81 -0
- pythonnative/native/yoga/yoga/numeric/FloatOptional.h +93 -0
- pythonnative/native/yoga/yoga/style/SmallValueBuffer.h +133 -0
- pythonnative/native/yoga/yoga/style/Style.h +757 -0
- pythonnative/native/yoga/yoga/style/StyleLength.h +107 -0
- pythonnative/native/yoga/yoga/style/StyleValueHandle.h +98 -0
- pythonnative/native/yoga/yoga/style/StyleValuePool.h +127 -0
- pythonnative/native_modules/__init__.py +124 -0
- pythonnative/native_modules/app_state.py +99 -0
- pythonnative/native_modules/battery.py +75 -0
- pythonnative/native_modules/biometrics.py +42 -0
- pythonnative/native_modules/camera.py +65 -0
- pythonnative/native_modules/clipboard.py +46 -0
- pythonnative/native_modules/fallback.py +324 -0
- pythonnative/native_modules/file_system.py +145 -0
- pythonnative/native_modules/haptics.py +75 -0
- pythonnative/native_modules/linking.py +123 -0
- pythonnative/native_modules/location.py +75 -0
- pythonnative/native_modules/net_info.py +112 -0
- pythonnative/native_modules/notifications.py +129 -0
- pythonnative/native_modules/permissions.py +108 -0
- pythonnative/native_modules/registry.py +526 -0
- pythonnative/native_modules/secure_store.py +52 -0
- pythonnative/native_modules/share.py +51 -0
- pythonnative/native_views/__init__.py +375 -0
- pythonnative/native_views/base.py +316 -0
- pythonnative/native_views/bridge_backend.py +277 -0
- pythonnative/navigation/__init__.py +103 -0
- pythonnative/navigation/container.py +154 -0
- pythonnative/navigation/handle.py +540 -0
- pythonnative/navigation/hooks.py +133 -0
- pythonnative/navigation/host.py +58 -0
- pythonnative/navigation/linking.py +200 -0
- pythonnative/navigation/navigators.py +659 -0
- pythonnative/navigation/screen.py +149 -0
- pythonnative/navigation/state.py +269 -0
- pythonnative/net.py +248 -0
- pythonnative/platform.py +166 -0
- pythonnative/platform_metrics.py +252 -0
- pythonnative/preview.py +287 -0
- pythonnative/profiling.py +101 -0
- pythonnative/project/__init__.py +68 -0
- pythonnative/project/android.py +557 -0
- pythonnative/project/builder.py +817 -0
- pythonnative/project/config.py +773 -0
- pythonnative/project/deps.py +719 -0
- pythonnative/project/devices.py +304 -0
- pythonnative/project/doctor.py +283 -0
- pythonnative/project/fingerprint.py +171 -0
- pythonnative/project/icons.py +247 -0
- pythonnative/project/ios.py +325 -0
- pythonnative/project/lockfile.py +100 -0
- pythonnative/project/permissions.py +361 -0
- pythonnative/project/plugins.py +519 -0
- pythonnative/project/runtime_assets.py +183 -0
- pythonnative/py.typed +0 -0
- pythonnative/query.py +153 -0
- pythonnative/reconciler/__init__.py +29 -0
- pythonnative/reconciler/boundaries.py +375 -0
- pythonnative/reconciler/children.py +88 -0
- pythonnative/reconciler/core.py +1165 -0
- pythonnative/reconciler/layout_pass.py +393 -0
- pythonnative/reconciler/vnode.py +266 -0
- pythonnative/refresh.py +55 -0
- pythonnative/runtime.py +306 -0
- pythonnative/scheduler.py +159 -0
- pythonnative/sdk/__init__.py +153 -0
- pythonnative/sdk/_components.py +427 -0
- pythonnative/sdk/builtins.py +91 -0
- pythonnative/sdk/codegen.py +149 -0
- pythonnative/sdk/module_codegen.py +184 -0
- pythonnative/sdk/schema.py +224 -0
- pythonnative/sdk/templates/contracts.kt +46 -0
- pythonnative/sdk/templates/contracts.swift +46 -0
- pythonnative/storage.py +184 -0
- pythonnative/style.py +841 -0
- pythonnative/suspense.py +183 -0
- pythonnative/templates/android_template/app/build.gradle +75 -0
- pythonnative/templates/android_template/app/proguard-rules.pro +21 -0
- pythonnative/templates/android_template/app/src/androidTest/java/com/pythonnative/android_template/ExampleInstrumentedTest.kt +24 -0
- pythonnative/templates/android_template/app/src/debug/AndroidManifest.xml +7 -0
- pythonnative/templates/android_template/app/src/main/AndroidManifest.xml +32 -0
- pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/MainActivity.kt +130 -0
- pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/ScreenFragment.kt +13 -0
- pythonnative/templates/android_template/app/src/main/res/drawable/ic_launcher_background.xml +170 -0
- pythonnative/templates/android_template/app/src/main/res/drawable-v24/ic_launcher_foreground.xml +30 -0
- pythonnative/templates/android_template/app/src/main/res/layout/activity_main.xml +10 -0
- pythonnative/templates/android_template/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +6 -0
- pythonnative/templates/android_template/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +6 -0
- pythonnative/templates/android_template/app/src/main/res/mipmap-hdpi/ic_launcher.webp +0 -0
- pythonnative/templates/android_template/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp +0 -0
- pythonnative/templates/android_template/app/src/main/res/mipmap-mdpi/ic_launcher.webp +0 -0
- pythonnative/templates/android_template/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp +0 -0
- pythonnative/templates/android_template/app/src/main/res/mipmap-xhdpi/ic_launcher.webp +0 -0
- pythonnative/templates/android_template/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp +0 -0
- pythonnative/templates/android_template/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp +0 -0
- pythonnative/templates/android_template/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp +0 -0
- pythonnative/templates/android_template/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp +0 -0
- pythonnative/templates/android_template/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp +0 -0
- pythonnative/templates/android_template/app/src/main/res/navigation/nav_graph.xml +22 -0
- pythonnative/templates/android_template/app/src/main/res/values/colors.xml +5 -0
- pythonnative/templates/android_template/app/src/main/res/values/strings.xml +5 -0
- pythonnative/templates/android_template/app/src/main/res/values/themes.xml +9 -0
- pythonnative/templates/android_template/app/src/main/res/values-night/themes.xml +7 -0
- pythonnative/templates/android_template/app/src/main/res/xml/backup_rules.xml +13 -0
- pythonnative/templates/android_template/app/src/main/res/xml/data_extraction_rules.xml +19 -0
- pythonnative/templates/android_template/app/src/test/java/com/pythonnative/android_template/ExampleUnitTest.kt +17 -0
- pythonnative/templates/android_template/build.gradle +10 -0
- pythonnative/templates/android_template/gradle/wrapper/gradle-wrapper.jar +0 -0
- pythonnative/templates/android_template/gradle/wrapper/gradle-wrapper.properties +6 -0
- pythonnative/templates/android_template/gradle.properties +23 -0
- pythonnative/templates/android_template/gradlew +185 -0
- pythonnative/templates/android_template/gradlew.bat +89 -0
- pythonnative/templates/android_template/settings.gradle +17 -0
- pythonnative/templates/ios_template/ios_template/AppDelegate.swift +51 -0
- pythonnative/templates/ios_template/ios_template/Assets.xcassets/AccentColor.colorset/Contents.json +11 -0
- pythonnative/templates/ios_template/ios_template/Assets.xcassets/AppIcon.appiconset/Contents.json +13 -0
- pythonnative/templates/ios_template/ios_template/Assets.xcassets/Contents.json +6 -0
- pythonnative/templates/ios_template/ios_template/Base.lproj/LaunchScreen.storyboard +25 -0
- pythonnative/templates/ios_template/ios_template/BridgingHeader.h +14 -0
- pythonnative/templates/ios_template/ios_template/Info.plist +28 -0
- pythonnative/templates/ios_template/ios_template/PythonRuntime.swift +390 -0
- pythonnative/templates/ios_template/ios_template/SceneDelegate.swift +61 -0
- pythonnative/templates/ios_template/ios_template/ViewController.swift +23 -0
- pythonnative/templates/ios_template/ios_template.xcodeproj/project.pbxproj +745 -0
- pythonnative/templates/ios_template/ios_template.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +8 -0
- pythonnative/templates/ios_template/ios_templateTests/ios_templateTests.swift +36 -0
- pythonnative/templates/ios_template/ios_templateUITests/ios_templateUITests.swift +41 -0
- pythonnative/templates/ios_template/ios_templateUITests/ios_templateUITestsLaunchTests.swift +32 -0
- pythonnative/testing/__init__.py +53 -0
- pythonnative/testing/backend.py +276 -0
- pythonnative/testing/harness.py +369 -0
- pythonnative/utils.py +145 -0
- pythonnative-0.40.0.dist-info/DELVEWHEEL +2 -0
- pythonnative-0.40.0.dist-info/METADATA +150 -0
- pythonnative-0.40.0.dist-info/RECORD +418 -0
- pythonnative-0.40.0.dist-info/WHEEL +5 -0
- pythonnative-0.40.0.dist-info/entry_points.txt +2 -0
- pythonnative-0.40.0.dist-info/licenses/LICENSE +21 -0
- pythonnative-0.40.0.dist-info/top_level.txt +1 -0
- pythonnative.libs/msvcp140-a4c2229bdc2a2a630acdc095b4d86008.dll +0 -0
pythonnative/gestures.py
ADDED
|
@@ -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)
|