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/animated.py
ADDED
|
@@ -0,0 +1,1687 @@
|
|
|
1
|
+
"""Animated values, derived animated nodes, and native-driven animation.
|
|
2
|
+
|
|
3
|
+
Modeled on React Native's ``Animated`` API with an ``async``-aware
|
|
4
|
+
completion contract. The core primitives are:
|
|
5
|
+
|
|
6
|
+
- [`AnimatedValue`][pythonnative.animated.AnimatedValue]: a numeric
|
|
7
|
+
cell attached to native view properties; animations drive it over
|
|
8
|
+
time.
|
|
9
|
+
- **Derived nodes**: every animated node supports
|
|
10
|
+
[`interpolate`][pythonnative.animated.AnimatedNode.interpolate]
|
|
11
|
+
(range mapping with numeric, color, and angle outputs) and Python
|
|
12
|
+
arithmetic (``opacity * 0.5``, ``x + y``, ``-value``), producing
|
|
13
|
+
read-only [`AnimatedNode`][pythonnative.animated.AnimatedNode]
|
|
14
|
+
instances that update whenever their inputs change.
|
|
15
|
+
- ``Animated.timing`` / ``Animated.spring`` / ``Animated.decay``:
|
|
16
|
+
animation factories. The objects they return implement
|
|
17
|
+
``__await__``, so you can write ``await Animated.timing(v, to=1.0)``
|
|
18
|
+
to suspend until the animation finishes.
|
|
19
|
+
- ``Animated.sequence`` / ``Animated.parallel`` / ``Animated.stagger``
|
|
20
|
+
/ ``Animated.delay`` / ``Animated.loop``: composition; also
|
|
21
|
+
awaitable.
|
|
22
|
+
- ``Animated.event``: build an event-prop callback that copies event
|
|
23
|
+
fields into animated values (``on_scroll=pn.Animated.event(y=v)``).
|
|
24
|
+
- ``Animated.diff_clamp``: accumulate an input's *deltas* into a
|
|
25
|
+
clamped range (the collapsing-header primitive).
|
|
26
|
+
- ``Animated.View`` / ``Animated.Text`` / ``Animated.Image``:
|
|
27
|
+
components whose ``style`` may contain animated nodes, including
|
|
28
|
+
inside ``transform`` entries.
|
|
29
|
+
|
|
30
|
+
Driver architecture (the **native driver**):
|
|
31
|
+
|
|
32
|
+
When an animation starts, PythonNative compiles its spec (curve,
|
|
33
|
+
duration, target value) and offers it to the platform handler of every
|
|
34
|
+
native view the value is attached to
|
|
35
|
+
([`ViewHandler.start_animation`][pythonnative.native_views.base.ViewHandler.start_animation]).
|
|
36
|
+
|
|
37
|
+
- **Accepted** (iOS Core Animation, Android ``ViewPropertyAnimator`` /
|
|
38
|
+
``DynamicAnimation``): the platform animates the property entirely
|
|
39
|
+
natively; no Python code runs per frame. Python receives exactly one
|
|
40
|
+
callback when the animation settles, updates the
|
|
41
|
+
[`AnimatedValue`][pythonnative.animated.AnimatedValue], and resolves
|
|
42
|
+
any awaiting tasks.
|
|
43
|
+
- **Declined** (unattached values, callable easings,
|
|
44
|
+
values feeding Python-side listeners or derived nodes): a single
|
|
45
|
+
background thread ticks the animation at ~60 Hz from Python, pushing
|
|
46
|
+
each frame through ``set_animated_property``. Semantics are
|
|
47
|
+
identical; only the frame source differs.
|
|
48
|
+
|
|
49
|
+
Values driven by *events* (scroll offsets via ``Animated.event``,
|
|
50
|
+
gesture translations) flow through Python: the native listener fires,
|
|
51
|
+
the bound values update, and every attachment (including derived
|
|
52
|
+
nodes) is pushed in the same call.
|
|
53
|
+
|
|
54
|
+
Example:
|
|
55
|
+
```python
|
|
56
|
+
import pythonnative as pn
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@pn.component
|
|
60
|
+
def FadeIn():
|
|
61
|
+
opacity = pn.use_animated_value(0.0)
|
|
62
|
+
|
|
63
|
+
async def fade_in():
|
|
64
|
+
await pn.Animated.timing(opacity, to=1.0, duration=400)
|
|
65
|
+
await pn.Animated.timing(opacity, to=0.5, duration=200)
|
|
66
|
+
|
|
67
|
+
pn.use_effect(fade_in, [])
|
|
68
|
+
|
|
69
|
+
return pn.Animated.View(
|
|
70
|
+
pn.Text("Hello!"),
|
|
71
|
+
style={"opacity": opacity, "padding": 20},
|
|
72
|
+
)
|
|
73
|
+
```
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
from __future__ import annotations
|
|
77
|
+
|
|
78
|
+
import asyncio
|
|
79
|
+
import bisect
|
|
80
|
+
import itertools
|
|
81
|
+
import math
|
|
82
|
+
import threading
|
|
83
|
+
import time
|
|
84
|
+
import weakref
|
|
85
|
+
from typing import Any, Callable, Dict, List, Optional, Sequence, Tuple
|
|
86
|
+
|
|
87
|
+
from .element import Element
|
|
88
|
+
from .hooks import Ref, use_effect, use_ref
|
|
89
|
+
from .runtime import resolve_future
|
|
90
|
+
from .style import StyleProp, resolve_style
|
|
91
|
+
|
|
92
|
+
# Maximum frame rate at which the Python fallback ticker drives
|
|
93
|
+
# animations (native-driven animations run at the display's refresh
|
|
94
|
+
# rate, managed by the platform).
|
|
95
|
+
_TARGET_FPS = 60.0
|
|
96
|
+
_FRAME_DT = 1.0 / _TARGET_FPS
|
|
97
|
+
|
|
98
|
+
# Upper bound on how much wall-clock time the fallback loop will try to
|
|
99
|
+
# catch up on in a single iteration after thread starvation. At 60 fps
|
|
100
|
+
# this is ~333 ms of simulated motion; further drift is dropped to keep
|
|
101
|
+
# the loop responsive.
|
|
102
|
+
_MAX_CATCHUP_FRAMES = 20
|
|
103
|
+
|
|
104
|
+
_EASINGS: Dict[str, Callable[[float], float]] = {
|
|
105
|
+
"linear": lambda t: t,
|
|
106
|
+
"ease_in": lambda t: t * t,
|
|
107
|
+
"ease_out": lambda t: 1.0 - (1.0 - t) * (1.0 - t),
|
|
108
|
+
"ease_in_out": lambda t: 3.0 * t * t - 2.0 * t * t * t,
|
|
109
|
+
"ease_in_quad": lambda t: t * t,
|
|
110
|
+
"ease_out_quad": lambda t: 1.0 - (1.0 - t) * (1.0 - t),
|
|
111
|
+
"bounce": lambda t: (
|
|
112
|
+
# Robert Penner's bounce out, common easing.
|
|
113
|
+
7.5625 * t * t
|
|
114
|
+
if t < 1 / 2.75
|
|
115
|
+
else (
|
|
116
|
+
7.5625 * (t - 1.5 / 2.75) * (t - 1.5 / 2.75) + 0.75
|
|
117
|
+
if t < 2 / 2.75
|
|
118
|
+
else (
|
|
119
|
+
7.5625 * (t - 2.25 / 2.75) * (t - 2.25 / 2.75) + 0.9375
|
|
120
|
+
if t < 2.5 / 2.75
|
|
121
|
+
else 7.5625 * (t - 2.625 / 2.75) * (t - 2.625 / 2.75) + 0.984375
|
|
122
|
+
)
|
|
123
|
+
)
|
|
124
|
+
),
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _resolve_easing(name: Any) -> Callable[[float], float]:
|
|
129
|
+
if callable(name):
|
|
130
|
+
return name
|
|
131
|
+
return _EASINGS.get(str(name), _EASINGS["ease_in_out"])
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _backend() -> Any:
|
|
135
|
+
"""Return the active native-view registry (the animation backend)."""
|
|
136
|
+
from .native_views import get_registry
|
|
137
|
+
|
|
138
|
+
return get_registry()
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
# Process-unique ids for native animations, so completion callbacks can
|
|
142
|
+
# be routed without holding references on the native side.
|
|
143
|
+
_anim_id_counter = itertools.count(1)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
# ======================================================================
|
|
147
|
+
# AnimatedNode: the shared graph-node base
|
|
148
|
+
# ======================================================================
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
class AnimatedNode:
|
|
152
|
+
"""Base class for every animated node (settable leaves and derived nodes).
|
|
153
|
+
|
|
154
|
+
An animated node holds a current output value and a set of
|
|
155
|
+
``(tag, prop)`` **attachments** binding it to native view
|
|
156
|
+
properties. Whenever the node's output changes (a leaf was set or
|
|
157
|
+
animated, or an input of a derived node changed), the new value is
|
|
158
|
+
pushed to every attachment through the registry's
|
|
159
|
+
``set_animated_property`` and to every Python-side listener, then
|
|
160
|
+
propagated to derived nodes built from this one.
|
|
161
|
+
|
|
162
|
+
Derived nodes are constructed with
|
|
163
|
+
[`interpolate`][pythonnative.animated.AnimatedNode.interpolate],
|
|
164
|
+
with Python arithmetic operators (``+``, ``-``, ``*``, ``/``,
|
|
165
|
+
``%``, unary ``-``), or with ``Animated.diff_clamp``. They are
|
|
166
|
+
read-only: only [`AnimatedValue`][pythonnative.AnimatedValue]
|
|
167
|
+
leaves can be set or animated directly.
|
|
168
|
+
"""
|
|
169
|
+
|
|
170
|
+
__slots__ = ("_subscribers", "_attachments", "_lock", "_children", "__weakref__")
|
|
171
|
+
|
|
172
|
+
def __init__(self) -> None:
|
|
173
|
+
self._subscribers: List[Tuple[str, Callable[[Any], None]]] = []
|
|
174
|
+
self._attachments: List[Tuple[int, str]] = []
|
|
175
|
+
self._lock = threading.Lock()
|
|
176
|
+
# Derived nodes built from this one. Weak so a discarded
|
|
177
|
+
# interpolation doesn't keep receiving pushes forever.
|
|
178
|
+
self._children: "weakref.WeakSet[AnimatedNode]" = weakref.WeakSet()
|
|
179
|
+
|
|
180
|
+
# -- value -----------------------------------------------------------
|
|
181
|
+
|
|
182
|
+
@property
|
|
183
|
+
def value(self) -> Any:
|
|
184
|
+
"""Return the node's current output value."""
|
|
185
|
+
raise NotImplementedError
|
|
186
|
+
|
|
187
|
+
def __float__(self) -> float:
|
|
188
|
+
try:
|
|
189
|
+
return float(self.value)
|
|
190
|
+
except (TypeError, ValueError):
|
|
191
|
+
return 0.0
|
|
192
|
+
|
|
193
|
+
# -- graph -----------------------------------------------------------
|
|
194
|
+
|
|
195
|
+
def _adopt_child(self, child: "AnimatedNode") -> None:
|
|
196
|
+
with self._lock:
|
|
197
|
+
self._children.add(child)
|
|
198
|
+
|
|
199
|
+
def _has_dependents(self) -> bool:
|
|
200
|
+
"""Whether any derived node consumes this node's output."""
|
|
201
|
+
with self._lock:
|
|
202
|
+
return len(self._children) > 0
|
|
203
|
+
|
|
204
|
+
def _refresh(self) -> None:
|
|
205
|
+
"""Hook for stateful derived nodes to update from their inputs."""
|
|
206
|
+
|
|
207
|
+
def _propagate(self) -> None:
|
|
208
|
+
"""Push the current output to attachments/listeners and descend."""
|
|
209
|
+
self._refresh()
|
|
210
|
+
current = self.value
|
|
211
|
+
with self._lock:
|
|
212
|
+
subs = list(self._subscribers)
|
|
213
|
+
attachments = list(self._attachments)
|
|
214
|
+
children = list(self._children)
|
|
215
|
+
if attachments:
|
|
216
|
+
try:
|
|
217
|
+
backend = _backend()
|
|
218
|
+
for tag, prop in attachments:
|
|
219
|
+
backend.set_animated_property(tag, prop, current)
|
|
220
|
+
except Exception:
|
|
221
|
+
pass
|
|
222
|
+
for _prop, cb in subs:
|
|
223
|
+
try:
|
|
224
|
+
cb(current)
|
|
225
|
+
except Exception:
|
|
226
|
+
pass
|
|
227
|
+
for child in children:
|
|
228
|
+
child._propagate()
|
|
229
|
+
|
|
230
|
+
# -- bindings ----------------------------------------------------------
|
|
231
|
+
|
|
232
|
+
def attach(self, tag: int, prop: str) -> Callable[[], None]:
|
|
233
|
+
"""Bind this node to ``prop`` of the native view under ``tag``.
|
|
234
|
+
|
|
235
|
+
The current value is pushed immediately so the view reflects it
|
|
236
|
+
even if no animation is running. Returns a detach callable.
|
|
237
|
+
"""
|
|
238
|
+
binding = (tag, prop)
|
|
239
|
+
with self._lock:
|
|
240
|
+
self._attachments.append(binding)
|
|
241
|
+
try:
|
|
242
|
+
_backend().set_animated_property(tag, prop, self.value)
|
|
243
|
+
except Exception:
|
|
244
|
+
pass
|
|
245
|
+
|
|
246
|
+
from .animation_graph import install
|
|
247
|
+
|
|
248
|
+
install(self)
|
|
249
|
+
|
|
250
|
+
def _detach() -> None:
|
|
251
|
+
with self._lock:
|
|
252
|
+
try:
|
|
253
|
+
self._attachments.remove(binding)
|
|
254
|
+
except ValueError:
|
|
255
|
+
pass
|
|
256
|
+
install(self, detached_tag=tag)
|
|
257
|
+
|
|
258
|
+
return _detach
|
|
259
|
+
|
|
260
|
+
def attachments(self) -> List[Tuple[int, str]]:
|
|
261
|
+
"""Snapshot of the current ``(tag, prop)`` bindings."""
|
|
262
|
+
with self._lock:
|
|
263
|
+
return list(self._attachments)
|
|
264
|
+
|
|
265
|
+
# -- listeners ---------------------------------------------------------
|
|
266
|
+
|
|
267
|
+
def add_listener(self, prop: str, callback: Callable[[Any], None]) -> Callable[[], None]:
|
|
268
|
+
"""Register ``callback`` for Python-driven changes to this node.
|
|
269
|
+
|
|
270
|
+
Returns an unsubscribe callable. ``prop`` is metadata only; it
|
|
271
|
+
lets the subscriber differentiate this binding from others on
|
|
272
|
+
the same node.
|
|
273
|
+
"""
|
|
274
|
+
with self._lock:
|
|
275
|
+
self._subscribers.append((prop, callback))
|
|
276
|
+
|
|
277
|
+
def _unsubscribe() -> None:
|
|
278
|
+
with self._lock:
|
|
279
|
+
try:
|
|
280
|
+
self._subscribers.remove((prop, callback))
|
|
281
|
+
except ValueError:
|
|
282
|
+
pass
|
|
283
|
+
|
|
284
|
+
return _unsubscribe
|
|
285
|
+
|
|
286
|
+
def has_listeners(self) -> bool:
|
|
287
|
+
"""Whether any Python-side listeners are registered."""
|
|
288
|
+
with self._lock:
|
|
289
|
+
return bool(self._subscribers)
|
|
290
|
+
|
|
291
|
+
# -- derivation --------------------------------------------------------
|
|
292
|
+
|
|
293
|
+
def interpolate(
|
|
294
|
+
self,
|
|
295
|
+
input_range: Sequence[float],
|
|
296
|
+
output_range: Sequence[Any],
|
|
297
|
+
extrapolate: str = "extend",
|
|
298
|
+
extrapolate_left: Optional[str] = None,
|
|
299
|
+
extrapolate_right: Optional[str] = None,
|
|
300
|
+
) -> "AnimatedInterpolation":
|
|
301
|
+
"""Map this node's value through an input/output range.
|
|
302
|
+
|
|
303
|
+
Mirrors React Native's ``interpolate``. ``output_range`` may
|
|
304
|
+
contain numbers, colors (``"#RRGGBB"`` / ``"#AARRGGBB"``), or
|
|
305
|
+
angle strings (``"45deg"`` / ``"0.5rad"``, emitted as numeric
|
|
306
|
+
degrees for the ``rotate`` transform).
|
|
307
|
+
|
|
308
|
+
Args:
|
|
309
|
+
input_range: Monotonically non-decreasing breakpoints for
|
|
310
|
+
this node's value. At least two entries.
|
|
311
|
+
output_range: Output breakpoints, same length as
|
|
312
|
+
``input_range``.
|
|
313
|
+
extrapolate: Behavior outside the input range:
|
|
314
|
+
``"extend"`` (continue the edge segment's slope,
|
|
315
|
+
default), ``"clamp"`` (pin to the edge output), or
|
|
316
|
+
``"identity"`` (return the input unchanged).
|
|
317
|
+
extrapolate_left: Override ``extrapolate`` below the range.
|
|
318
|
+
extrapolate_right: Override ``extrapolate`` above the range.
|
|
319
|
+
|
|
320
|
+
Returns:
|
|
321
|
+
A derived, read-only animated node.
|
|
322
|
+
|
|
323
|
+
Example:
|
|
324
|
+
```python
|
|
325
|
+
header_height = scroll_y.interpolate(
|
|
326
|
+
input_range=[0, 120],
|
|
327
|
+
output_range=[160, 56],
|
|
328
|
+
extrapolate="clamp",
|
|
329
|
+
)
|
|
330
|
+
```
|
|
331
|
+
"""
|
|
332
|
+
return AnimatedInterpolation(
|
|
333
|
+
self,
|
|
334
|
+
input_range,
|
|
335
|
+
output_range,
|
|
336
|
+
extrapolate=extrapolate,
|
|
337
|
+
extrapolate_left=extrapolate_left,
|
|
338
|
+
extrapolate_right=extrapolate_right,
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
# -- arithmetic --------------------------------------------------------
|
|
342
|
+
|
|
343
|
+
def __add__(self, other: Any) -> "_AnimatedOperation":
|
|
344
|
+
return _AnimatedOperation("add", lambda a, b: a + b, [self, other])
|
|
345
|
+
|
|
346
|
+
def __radd__(self, other: Any) -> "_AnimatedOperation":
|
|
347
|
+
return _AnimatedOperation("add", lambda a, b: a + b, [other, self])
|
|
348
|
+
|
|
349
|
+
def __sub__(self, other: Any) -> "_AnimatedOperation":
|
|
350
|
+
return _AnimatedOperation("subtract", lambda a, b: a - b, [self, other])
|
|
351
|
+
|
|
352
|
+
def __rsub__(self, other: Any) -> "_AnimatedOperation":
|
|
353
|
+
return _AnimatedOperation("subtract", lambda a, b: a - b, [other, self])
|
|
354
|
+
|
|
355
|
+
def __mul__(self, other: Any) -> "_AnimatedOperation":
|
|
356
|
+
return _AnimatedOperation("multiply", lambda a, b: a * b, [self, other])
|
|
357
|
+
|
|
358
|
+
def __rmul__(self, other: Any) -> "_AnimatedOperation":
|
|
359
|
+
return _AnimatedOperation("multiply", lambda a, b: a * b, [other, self])
|
|
360
|
+
|
|
361
|
+
def __truediv__(self, other: Any) -> "_AnimatedOperation":
|
|
362
|
+
return _AnimatedOperation("divide", lambda a, b: a / b if b else 0.0, [self, other])
|
|
363
|
+
|
|
364
|
+
def __rtruediv__(self, other: Any) -> "_AnimatedOperation":
|
|
365
|
+
return _AnimatedOperation("divide", lambda a, b: a / b if b else 0.0, [other, self])
|
|
366
|
+
|
|
367
|
+
def __mod__(self, other: Any) -> "_AnimatedOperation":
|
|
368
|
+
return _AnimatedOperation("modulo", lambda a, b: math.fmod(a, b) if b else 0.0, [self, other])
|
|
369
|
+
|
|
370
|
+
def __neg__(self) -> "_AnimatedOperation":
|
|
371
|
+
return _AnimatedOperation("negate", lambda a: -a, [self])
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
# ======================================================================
|
|
375
|
+
# AnimatedValue: the settable leaf
|
|
376
|
+
# ======================================================================
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
class AnimatedValue(AnimatedNode):
|
|
380
|
+
"""A numeric cell that can be attached to native view properties.
|
|
381
|
+
|
|
382
|
+
Animated components (``Animated.View`` et al.) **attach** the value
|
|
383
|
+
to ``(tag, prop)`` bindings after mount. Setting the value pushes
|
|
384
|
+
the new number to every attached native view through the registry's
|
|
385
|
+
``set_animated_property`` (and through every derived node built
|
|
386
|
+
from this value), and when an animation can be driven natively, the
|
|
387
|
+
platform animates those same bindings directly.
|
|
388
|
+
|
|
389
|
+
Python-side listeners registered via
|
|
390
|
+
[`add_listener`][pythonnative.animated.AnimatedNode.add_listener]
|
|
391
|
+
observe every Python-driven change. Natively-driven animations
|
|
392
|
+
intentionally skip per-frame Python callbacks (that's the point);
|
|
393
|
+
listeners see the final settled value.
|
|
394
|
+
"""
|
|
395
|
+
|
|
396
|
+
__slots__ = ("_value", "_native_group")
|
|
397
|
+
|
|
398
|
+
def __init__(self, initial: float = 0.0) -> None:
|
|
399
|
+
super().__init__()
|
|
400
|
+
self._value = float(initial)
|
|
401
|
+
# The in-flight native animation group driving this value, if any.
|
|
402
|
+
self._native_group: Optional["_NativeAnimationGroup"] = None
|
|
403
|
+
|
|
404
|
+
@property
|
|
405
|
+
def value(self) -> float:
|
|
406
|
+
"""Return the current numeric value (without subscribing)."""
|
|
407
|
+
return self._value
|
|
408
|
+
|
|
409
|
+
def set_value(self, new_value: float) -> None:
|
|
410
|
+
"""Set the value immediately, pushing to native views and listeners."""
|
|
411
|
+
self._apply(float(new_value), push_native=True)
|
|
412
|
+
from .animation_graph import install
|
|
413
|
+
|
|
414
|
+
graph = install(self)
|
|
415
|
+
if graph and graph["bindings"]:
|
|
416
|
+
backend = _backend()
|
|
417
|
+
backend.set_animated_property(graph["bindings"][0][0], f"_pn_graph:{id(self)}", float(new_value))
|
|
418
|
+
|
|
419
|
+
def _apply(self, new_value: float, push_native: bool) -> None:
|
|
420
|
+
with self._lock:
|
|
421
|
+
self._value = new_value
|
|
422
|
+
subs = list(self._subscribers)
|
|
423
|
+
attachments = list(self._attachments)
|
|
424
|
+
children = list(self._children)
|
|
425
|
+
if push_native and attachments:
|
|
426
|
+
try:
|
|
427
|
+
backend = _backend()
|
|
428
|
+
for tag, prop in attachments:
|
|
429
|
+
backend.set_animated_property(tag, prop, new_value)
|
|
430
|
+
except Exception:
|
|
431
|
+
pass
|
|
432
|
+
for prop, cb in subs:
|
|
433
|
+
try:
|
|
434
|
+
cb(new_value)
|
|
435
|
+
except Exception:
|
|
436
|
+
pass
|
|
437
|
+
for child in children:
|
|
438
|
+
child._propagate()
|
|
439
|
+
|
|
440
|
+
# -- native handoff ------------------------------------------------
|
|
441
|
+
|
|
442
|
+
def _adopt_native_group(self, group: Optional["_NativeAnimationGroup"]) -> None:
|
|
443
|
+
previous = self._native_group
|
|
444
|
+
self._native_group = group
|
|
445
|
+
if previous is not None and previous is not group:
|
|
446
|
+
previous.cancel()
|
|
447
|
+
|
|
448
|
+
def stop_animation(self) -> None:
|
|
449
|
+
"""Cancel any in-flight animation on this value (native or Python)."""
|
|
450
|
+
self._adopt_native_group(None)
|
|
451
|
+
_manager.cancel_for_value(self)
|
|
452
|
+
|
|
453
|
+
def __repr__(self) -> str:
|
|
454
|
+
return f"AnimatedValue({self._value:g})"
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
# ======================================================================
|
|
458
|
+
# Derived nodes
|
|
459
|
+
# ======================================================================
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
def _parse_color_output(value: str) -> Optional[Tuple[int, int, int, int]]:
|
|
463
|
+
"""Parse ``"#RRGGBB"`` / ``"#AARRGGBB"`` into an ``(a, r, g, b)`` tuple."""
|
|
464
|
+
c = value.strip().lstrip("#")
|
|
465
|
+
if len(c) == 6:
|
|
466
|
+
c = "FF" + c
|
|
467
|
+
if len(c) != 8:
|
|
468
|
+
return None
|
|
469
|
+
try:
|
|
470
|
+
raw = int(c, 16)
|
|
471
|
+
except ValueError:
|
|
472
|
+
return None
|
|
473
|
+
return ((raw >> 24) & 0xFF, (raw >> 16) & 0xFF, (raw >> 8) & 0xFF, raw & 0xFF)
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
def _parse_angle_output(value: str) -> Optional[float]:
|
|
477
|
+
"""Parse ``"45deg"`` / ``"0.5rad"`` into numeric degrees."""
|
|
478
|
+
text = value.strip()
|
|
479
|
+
try:
|
|
480
|
+
if text.endswith("deg"):
|
|
481
|
+
return float(text[:-3])
|
|
482
|
+
if text.endswith("rad"):
|
|
483
|
+
return math.degrees(float(text[:-3]))
|
|
484
|
+
except ValueError:
|
|
485
|
+
return None
|
|
486
|
+
return None
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
class AnimatedInterpolation(AnimatedNode):
|
|
490
|
+
"""Read-only node mapping a parent node through an input/output range.
|
|
491
|
+
|
|
492
|
+
Built via
|
|
493
|
+
[`AnimatedNode.interpolate`][pythonnative.animated.AnimatedNode.interpolate];
|
|
494
|
+
see that method for the semantics of the arguments.
|
|
495
|
+
"""
|
|
496
|
+
|
|
497
|
+
__slots__ = (
|
|
498
|
+
"_parent",
|
|
499
|
+
"_inputs",
|
|
500
|
+
"_outputs",
|
|
501
|
+
"_kind",
|
|
502
|
+
"_left",
|
|
503
|
+
"_right",
|
|
504
|
+
)
|
|
505
|
+
|
|
506
|
+
def __init__(
|
|
507
|
+
self,
|
|
508
|
+
parent: AnimatedNode,
|
|
509
|
+
input_range: Sequence[float],
|
|
510
|
+
output_range: Sequence[Any],
|
|
511
|
+
extrapolate: str = "extend",
|
|
512
|
+
extrapolate_left: Optional[str] = None,
|
|
513
|
+
extrapolate_right: Optional[str] = None,
|
|
514
|
+
) -> None:
|
|
515
|
+
super().__init__()
|
|
516
|
+
inputs = [float(v) for v in input_range]
|
|
517
|
+
outputs = list(output_range)
|
|
518
|
+
if len(inputs) < 2:
|
|
519
|
+
raise ValueError("interpolate() needs at least two input_range entries")
|
|
520
|
+
if len(inputs) != len(outputs):
|
|
521
|
+
raise ValueError("interpolate() input_range and output_range must have the same length")
|
|
522
|
+
for a, b in zip(inputs, inputs[1:]):
|
|
523
|
+
if b < a:
|
|
524
|
+
raise ValueError("interpolate() input_range must be monotonically non-decreasing")
|
|
525
|
+
|
|
526
|
+
kind = "number"
|
|
527
|
+
first = outputs[0]
|
|
528
|
+
if isinstance(first, str):
|
|
529
|
+
if _parse_color_output(first) is not None:
|
|
530
|
+
kind = "color"
|
|
531
|
+
outputs = [_parse_color_output(str(v)) for v in outputs]
|
|
532
|
+
if any(v is None for v in outputs):
|
|
533
|
+
raise ValueError("interpolate() color output_range entries must all be colors")
|
|
534
|
+
else:
|
|
535
|
+
angles = [_parse_angle_output(str(v)) for v in outputs]
|
|
536
|
+
if any(v is None for v in angles):
|
|
537
|
+
raise ValueError(f"interpolate() cannot parse output value {first!r}")
|
|
538
|
+
outputs = angles
|
|
539
|
+
else:
|
|
540
|
+
outputs = [float(v) for v in outputs]
|
|
541
|
+
|
|
542
|
+
self._parent = parent
|
|
543
|
+
self._inputs = inputs
|
|
544
|
+
self._outputs: List[Any] = outputs
|
|
545
|
+
self._kind = kind
|
|
546
|
+
self._left = extrapolate_left or extrapolate
|
|
547
|
+
self._right = extrapolate_right or extrapolate
|
|
548
|
+
parent._adopt_child(self)
|
|
549
|
+
|
|
550
|
+
@property
|
|
551
|
+
def value(self) -> Any:
|
|
552
|
+
"""Return the interpolated output for the parent's current value."""
|
|
553
|
+
return self._compute(float(self._parent))
|
|
554
|
+
|
|
555
|
+
def _compute(self, x: float) -> Any:
|
|
556
|
+
inputs = self._inputs
|
|
557
|
+
n = len(inputs)
|
|
558
|
+
if x < inputs[0]:
|
|
559
|
+
if self._left == "identity":
|
|
560
|
+
return x
|
|
561
|
+
if self._left == "clamp":
|
|
562
|
+
x = inputs[0]
|
|
563
|
+
i = 0
|
|
564
|
+
elif x > inputs[-1]:
|
|
565
|
+
if self._right == "identity":
|
|
566
|
+
return x
|
|
567
|
+
if self._right == "clamp":
|
|
568
|
+
x = inputs[-1]
|
|
569
|
+
i = n - 2
|
|
570
|
+
else:
|
|
571
|
+
i = max(0, min(n - 2, bisect.bisect_right(inputs, x) - 1))
|
|
572
|
+
|
|
573
|
+
x0, x1 = inputs[i], inputs[i + 1]
|
|
574
|
+
span = x1 - x0
|
|
575
|
+
t = 0.0 if span <= 0 else (x - x0) / span
|
|
576
|
+
|
|
577
|
+
if self._kind == "color":
|
|
578
|
+
c0 = self._outputs[i]
|
|
579
|
+
c1 = self._outputs[i + 1]
|
|
580
|
+
t_cl = max(0.0, min(1.0, t))
|
|
581
|
+
channels = [int(round(c0[j] + (c1[j] - c0[j]) * t_cl)) for j in range(4)]
|
|
582
|
+
a, r, g, b = (max(0, min(255, ch)) for ch in channels)
|
|
583
|
+
return f"#{a:02X}{r:02X}{g:02X}{b:02X}"
|
|
584
|
+
|
|
585
|
+
y0 = self._outputs[i]
|
|
586
|
+
y1 = self._outputs[i + 1]
|
|
587
|
+
return y0 + (y1 - y0) * t
|
|
588
|
+
|
|
589
|
+
def __repr__(self) -> str:
|
|
590
|
+
return f"AnimatedInterpolation({self._inputs} -> {self._outputs})"
|
|
591
|
+
|
|
592
|
+
|
|
593
|
+
class _AnimatedOperation(AnimatedNode):
|
|
594
|
+
"""Read-only node computed from other nodes (and constants) by ``fn``."""
|
|
595
|
+
|
|
596
|
+
__slots__ = ("_op", "_fn", "_parents")
|
|
597
|
+
|
|
598
|
+
def __init__(self, op: str, fn: Callable[..., float], parents: List[Any]) -> None:
|
|
599
|
+
super().__init__()
|
|
600
|
+
self._op = op
|
|
601
|
+
self._fn = fn
|
|
602
|
+
self._parents = list(parents)
|
|
603
|
+
for parent in self._parents:
|
|
604
|
+
if isinstance(parent, AnimatedNode):
|
|
605
|
+
parent._adopt_child(self)
|
|
606
|
+
|
|
607
|
+
@property
|
|
608
|
+
def value(self) -> float:
|
|
609
|
+
args = [float(p) if isinstance(p, AnimatedNode) else float(p) for p in self._parents]
|
|
610
|
+
try:
|
|
611
|
+
return float(self._fn(*args))
|
|
612
|
+
except Exception:
|
|
613
|
+
return 0.0
|
|
614
|
+
|
|
615
|
+
def __repr__(self) -> str:
|
|
616
|
+
return f"AnimatedOperation({self._op})"
|
|
617
|
+
|
|
618
|
+
|
|
619
|
+
class _AnimatedDiffClamp(AnimatedNode):
|
|
620
|
+
"""Accumulate a parent's *deltas* into a clamped range.
|
|
621
|
+
|
|
622
|
+
Mirrors React Native's ``Animated.diffClamp``: the output moves by
|
|
623
|
+
the same amount as the input but is pinned to ``[min, max]``, so
|
|
624
|
+
scrolling far down then slightly up immediately re-reveals a
|
|
625
|
+
collapsing header regardless of absolute offset.
|
|
626
|
+
"""
|
|
627
|
+
|
|
628
|
+
__slots__ = ("_parent", "_min", "_max", "_last_input", "_current")
|
|
629
|
+
|
|
630
|
+
def __init__(self, parent: AnimatedNode, min_value: float, max_value: float) -> None:
|
|
631
|
+
super().__init__()
|
|
632
|
+
if max_value < min_value:
|
|
633
|
+
raise ValueError("diff_clamp() requires min_value <= max_value")
|
|
634
|
+
self._parent = parent
|
|
635
|
+
self._min = float(min_value)
|
|
636
|
+
self._max = float(max_value)
|
|
637
|
+
self._last_input = float(parent)
|
|
638
|
+
self._current = max(self._min, min(self._max, self._last_input))
|
|
639
|
+
parent._adopt_child(self)
|
|
640
|
+
|
|
641
|
+
@property
|
|
642
|
+
def value(self) -> float:
|
|
643
|
+
return self._current
|
|
644
|
+
|
|
645
|
+
def _refresh(self) -> None:
|
|
646
|
+
latest = float(self._parent)
|
|
647
|
+
delta = latest - self._last_input
|
|
648
|
+
self._last_input = latest
|
|
649
|
+
self._current = max(self._min, min(self._max, self._current + delta))
|
|
650
|
+
|
|
651
|
+
|
|
652
|
+
# ======================================================================
|
|
653
|
+
# Animated.event
|
|
654
|
+
# ======================================================================
|
|
655
|
+
|
|
656
|
+
|
|
657
|
+
class AnimatedEvent:
|
|
658
|
+
"""Callable event handler copying event fields into animated values.
|
|
659
|
+
|
|
660
|
+
Built via ``pn.Animated.event(...)``. Each keyword argument names a
|
|
661
|
+
field on the incoming event payload (a dict key for scroll payloads
|
|
662
|
+
such as ``{"x": ..., "y": ...}``, or an attribute for
|
|
663
|
+
[`GestureEvent`][pythonnative.gestures.GestureEvent] instances) and
|
|
664
|
+
maps it onto an [`AnimatedValue`][pythonnative.AnimatedValue].
|
|
665
|
+
|
|
666
|
+
Because the result is an ordinary callable, it can be passed to any
|
|
667
|
+
event prop:
|
|
668
|
+
|
|
669
|
+
```python
|
|
670
|
+
scroll_y = pn.use_animated_value(0.0)
|
|
671
|
+
pn.ScrollView(..., on_scroll=pn.Animated.event(y=scroll_y))
|
|
672
|
+
|
|
673
|
+
tx = pn.use_animated_value(0.0)
|
|
674
|
+
gestures.Pan(on_change=pn.Animated.event(translation_x=tx))
|
|
675
|
+
```
|
|
676
|
+
"""
|
|
677
|
+
|
|
678
|
+
__slots__ = ("_bindings", "_listener")
|
|
679
|
+
|
|
680
|
+
def __init__(self, listener: Optional[Callable[..., None]] = None, **bindings: AnimatedValue) -> None:
|
|
681
|
+
for name, node in bindings.items():
|
|
682
|
+
if not isinstance(node, AnimatedValue):
|
|
683
|
+
raise TypeError(
|
|
684
|
+
f"Animated.event() field {name!r} must map to an AnimatedValue "
|
|
685
|
+
f"(got {type(node).__name__}); derived nodes are read-only."
|
|
686
|
+
)
|
|
687
|
+
self._bindings = dict(bindings)
|
|
688
|
+
self._listener = listener
|
|
689
|
+
|
|
690
|
+
def __call__(self, payload: Any = None, *args: Any) -> None:
|
|
691
|
+
"""Write bound payload fields into their values, then run the listener.
|
|
692
|
+
|
|
693
|
+
Args:
|
|
694
|
+
payload: The event payload; a dict is read by key, any
|
|
695
|
+
other object by attribute. Missing or non-numeric
|
|
696
|
+
fields are skipped.
|
|
697
|
+
*args: Extra positional arguments forwarded to the
|
|
698
|
+
listener.
|
|
699
|
+
"""
|
|
700
|
+
for name, node in self._bindings.items():
|
|
701
|
+
raw: Any = None
|
|
702
|
+
if isinstance(payload, dict):
|
|
703
|
+
raw = payload.get(name)
|
|
704
|
+
elif payload is not None:
|
|
705
|
+
raw = getattr(payload, name, None)
|
|
706
|
+
if raw is None:
|
|
707
|
+
continue
|
|
708
|
+
try:
|
|
709
|
+
if getattr(_backend(), "install_animation_graph", None) is not None:
|
|
710
|
+
# Native already evaluated the graph at the input timestamp.
|
|
711
|
+
# Python observes the sample without echoing an older frame.
|
|
712
|
+
node._value = float(raw)
|
|
713
|
+
else:
|
|
714
|
+
node.set_value(float(raw))
|
|
715
|
+
except (TypeError, ValueError):
|
|
716
|
+
continue
|
|
717
|
+
if self._listener is not None:
|
|
718
|
+
try:
|
|
719
|
+
self._listener(payload, *args)
|
|
720
|
+
except Exception:
|
|
721
|
+
pass
|
|
722
|
+
|
|
723
|
+
|
|
724
|
+
# ======================================================================
|
|
725
|
+
# Python fallback driver
|
|
726
|
+
# ======================================================================
|
|
727
|
+
|
|
728
|
+
|
|
729
|
+
class _AnimationManager:
|
|
730
|
+
"""Single-threaded fallback driver for Python-ticked animations.
|
|
731
|
+
|
|
732
|
+
Holds a list of ``_RunningAnimation`` instances and ticks them at
|
|
733
|
+
~60 Hz. The thread starts on first use and idles when nothing is
|
|
734
|
+
active. Native-driven animations never touch this loop.
|
|
735
|
+
"""
|
|
736
|
+
|
|
737
|
+
def __init__(self) -> None:
|
|
738
|
+
self._lock = threading.Lock()
|
|
739
|
+
self._animations: List[_RunningAnimation] = []
|
|
740
|
+
self._thread: Optional[threading.Thread] = None
|
|
741
|
+
self._stopped = False
|
|
742
|
+
|
|
743
|
+
def add(self, anim: "_RunningAnimation") -> None:
|
|
744
|
+
with self._lock:
|
|
745
|
+
self._animations.append(anim)
|
|
746
|
+
self._ensure_thread_locked()
|
|
747
|
+
|
|
748
|
+
def remove(self, anim: "_RunningAnimation") -> None:
|
|
749
|
+
with self._lock:
|
|
750
|
+
try:
|
|
751
|
+
self._animations.remove(anim)
|
|
752
|
+
except ValueError:
|
|
753
|
+
pass
|
|
754
|
+
|
|
755
|
+
def cancel_for_value(self, value: AnimatedValue) -> None:
|
|
756
|
+
"""Cancel every queued/running Python-driven animation on ``value``."""
|
|
757
|
+
with self._lock:
|
|
758
|
+
stale = [a for a in self._animations if a.value is value]
|
|
759
|
+
for anim in stale:
|
|
760
|
+
self._animations.remove(anim)
|
|
761
|
+
for anim in stale:
|
|
762
|
+
anim._finish()
|
|
763
|
+
|
|
764
|
+
def _ensure_thread_locked(self) -> None:
|
|
765
|
+
if self._thread is not None and self._thread.is_alive():
|
|
766
|
+
return
|
|
767
|
+
self._thread = threading.Thread(target=self._loop, daemon=True, name="pn-animated")
|
|
768
|
+
self._thread.start()
|
|
769
|
+
|
|
770
|
+
def _loop(self) -> None:
|
|
771
|
+
last = time.monotonic()
|
|
772
|
+
# Clamping the per-tick dt is important for numerical stability:
|
|
773
|
+
# an underdamped spring with a 0.3 s step explodes immediately,
|
|
774
|
+
# and the animation thread can be starved for several frames
|
|
775
|
+
# during render bursts. We integrate physics on a clamped dt
|
|
776
|
+
# (max 2 target frames) and sub-step when wall-clock has
|
|
777
|
+
# advanced more than that, so the perceived motion still tracks
|
|
778
|
+
# real time at most a couple of frames behind. After an extreme
|
|
779
|
+
# starvation (e.g. the app was backgrounded for seconds) we cap
|
|
780
|
+
# the catch-up at ``_MAX_CATCHUP_FRAMES`` worth of physics; any
|
|
781
|
+
# further wall-clock drift is dropped on the floor, which keeps
|
|
782
|
+
# the loop responsive instead of spinning forward through
|
|
783
|
+
# hundreds of substeps.
|
|
784
|
+
max_step = _FRAME_DT * 2.0
|
|
785
|
+
max_catchup = _FRAME_DT * _MAX_CATCHUP_FRAMES
|
|
786
|
+
while not self._stopped:
|
|
787
|
+
now = time.monotonic()
|
|
788
|
+
dt = now - last
|
|
789
|
+
last = now
|
|
790
|
+
with self._lock:
|
|
791
|
+
active = list(self._animations)
|
|
792
|
+
if not active:
|
|
793
|
+
time.sleep(0.05)
|
|
794
|
+
last = time.monotonic()
|
|
795
|
+
continue
|
|
796
|
+
remaining = min(dt, max_catchup)
|
|
797
|
+
while remaining > 0.0:
|
|
798
|
+
step = remaining if remaining <= max_step else max_step
|
|
799
|
+
remaining -= step
|
|
800
|
+
for anim in active:
|
|
801
|
+
if getattr(anim, "_completed", False):
|
|
802
|
+
continue
|
|
803
|
+
try:
|
|
804
|
+
finished = anim.advance(step)
|
|
805
|
+
except Exception:
|
|
806
|
+
finished = True
|
|
807
|
+
if finished:
|
|
808
|
+
self.remove(anim)
|
|
809
|
+
time.sleep(_FRAME_DT)
|
|
810
|
+
|
|
811
|
+
|
|
812
|
+
_manager = _AnimationManager()
|
|
813
|
+
|
|
814
|
+
|
|
815
|
+
# ======================================================================
|
|
816
|
+
# Python-driven animation primitives (the fallback path)
|
|
817
|
+
# ======================================================================
|
|
818
|
+
|
|
819
|
+
|
|
820
|
+
class _RunningAnimation:
|
|
821
|
+
"""Base class for Python-ticked animations; ``advance()`` returns True when done."""
|
|
822
|
+
|
|
823
|
+
def __init__(self, value: AnimatedValue) -> None:
|
|
824
|
+
self.value = value
|
|
825
|
+
self._completion_futures: List[asyncio.Future[None]] = []
|
|
826
|
+
self._completed = False
|
|
827
|
+
|
|
828
|
+
def add_completion_future(self, future: asyncio.Future[None]) -> None:
|
|
829
|
+
"""Register ``future`` to be resolved when the animation ends."""
|
|
830
|
+
self._completion_futures.append(future)
|
|
831
|
+
if self._completed:
|
|
832
|
+
resolve_future(future, None)
|
|
833
|
+
|
|
834
|
+
def advance(self, dt: float) -> bool:
|
|
835
|
+
raise NotImplementedError
|
|
836
|
+
|
|
837
|
+
def _finish(self) -> None:
|
|
838
|
+
if self._completed:
|
|
839
|
+
return
|
|
840
|
+
self._completed = True
|
|
841
|
+
for fut in self._completion_futures:
|
|
842
|
+
resolve_future(fut, None)
|
|
843
|
+
|
|
844
|
+
|
|
845
|
+
class _TimingAnimation(_RunningAnimation):
|
|
846
|
+
def __init__(
|
|
847
|
+
self,
|
|
848
|
+
value: AnimatedValue,
|
|
849
|
+
to: float,
|
|
850
|
+
duration: float,
|
|
851
|
+
easing: Callable[[float], float],
|
|
852
|
+
) -> None:
|
|
853
|
+
super().__init__(value)
|
|
854
|
+
self._from = value.value
|
|
855
|
+
self._to = float(to)
|
|
856
|
+
self._duration = max(0.001, float(duration) / 1000.0)
|
|
857
|
+
self._easing = easing
|
|
858
|
+
self._elapsed = 0.0
|
|
859
|
+
|
|
860
|
+
def advance(self, dt: float) -> bool:
|
|
861
|
+
self._elapsed += dt
|
|
862
|
+
progress = min(1.0, self._elapsed / self._duration)
|
|
863
|
+
eased = self._easing(progress)
|
|
864
|
+
new_val = self._from + (self._to - self._from) * eased
|
|
865
|
+
self.value.set_value(new_val)
|
|
866
|
+
if progress >= 1.0:
|
|
867
|
+
self._finish()
|
|
868
|
+
return True
|
|
869
|
+
return False
|
|
870
|
+
|
|
871
|
+
|
|
872
|
+
class _SpringAnimation(_RunningAnimation):
|
|
873
|
+
"""Damped harmonic spring driver."""
|
|
874
|
+
|
|
875
|
+
def __init__(
|
|
876
|
+
self,
|
|
877
|
+
value: AnimatedValue,
|
|
878
|
+
to: float,
|
|
879
|
+
stiffness: float,
|
|
880
|
+
damping: float,
|
|
881
|
+
mass: float,
|
|
882
|
+
initial_velocity: float = 0.0,
|
|
883
|
+
) -> None:
|
|
884
|
+
super().__init__(value)
|
|
885
|
+
self._to = float(to)
|
|
886
|
+
self._velocity = float(initial_velocity)
|
|
887
|
+
self._stiffness = float(stiffness)
|
|
888
|
+
self._damping = float(damping)
|
|
889
|
+
self._mass = float(mass)
|
|
890
|
+
self._rest_threshold = 0.001
|
|
891
|
+
|
|
892
|
+
def advance(self, dt: float) -> bool:
|
|
893
|
+
x = self.value.value
|
|
894
|
+
a = (-self._stiffness * (x - self._to) - self._damping * self._velocity) / self._mass
|
|
895
|
+
self._velocity += a * dt
|
|
896
|
+
new_x = x + self._velocity * dt
|
|
897
|
+
self.value.set_value(new_x)
|
|
898
|
+
if abs(new_x - self._to) < self._rest_threshold and abs(self._velocity) < self._rest_threshold:
|
|
899
|
+
self.value.set_value(self._to)
|
|
900
|
+
self._finish()
|
|
901
|
+
return True
|
|
902
|
+
return False
|
|
903
|
+
|
|
904
|
+
|
|
905
|
+
class _DecayAnimation(_RunningAnimation):
|
|
906
|
+
def __init__(self, value: AnimatedValue, velocity: float, deceleration: float) -> None:
|
|
907
|
+
super().__init__(value)
|
|
908
|
+
self._velocity = float(velocity)
|
|
909
|
+
self._deceleration = float(deceleration)
|
|
910
|
+
self._rest_threshold = 0.001
|
|
911
|
+
|
|
912
|
+
def advance(self, dt: float) -> bool:
|
|
913
|
+
self._velocity *= math.exp(-self._deceleration * dt * 1000.0)
|
|
914
|
+
new_x = self.value.value + self._velocity * dt
|
|
915
|
+
self.value.set_value(new_x)
|
|
916
|
+
if abs(self._velocity) < self._rest_threshold:
|
|
917
|
+
self._finish()
|
|
918
|
+
return True
|
|
919
|
+
return False
|
|
920
|
+
|
|
921
|
+
|
|
922
|
+
class _DelayAnimation(_RunningAnimation):
|
|
923
|
+
def __init__(self, duration_ms: float) -> None:
|
|
924
|
+
super().__init__(AnimatedValue(0.0))
|
|
925
|
+
self._elapsed = 0.0
|
|
926
|
+
self._duration = max(0.001, duration_ms / 1000.0)
|
|
927
|
+
|
|
928
|
+
def advance(self, dt: float) -> bool:
|
|
929
|
+
self._elapsed += dt
|
|
930
|
+
if self._elapsed >= self._duration:
|
|
931
|
+
self._finish()
|
|
932
|
+
return True
|
|
933
|
+
return False
|
|
934
|
+
|
|
935
|
+
|
|
936
|
+
# ======================================================================
|
|
937
|
+
# Native-driven animation group
|
|
938
|
+
# ======================================================================
|
|
939
|
+
|
|
940
|
+
|
|
941
|
+
class _NativeAnimationGroup:
|
|
942
|
+
"""One logical animation fanned out to N natively-animated views.
|
|
943
|
+
|
|
944
|
+
Each attached ``(tag, prop)`` binding gets its own ``anim_id``; the
|
|
945
|
+
group completes when the platform reports completion for all of
|
|
946
|
+
them. Cancellation asks each platform handler for the current
|
|
947
|
+
presentation value so the ``AnimatedValue`` lands wherever the view
|
|
948
|
+
visually was.
|
|
949
|
+
"""
|
|
950
|
+
|
|
951
|
+
def __init__(self, value: AnimatedValue, final_value: float) -> None:
|
|
952
|
+
self.value = value
|
|
953
|
+
self.final_value = final_value
|
|
954
|
+
self._targets: Dict[int, Tuple[int, str]] = {} # anim_id -> (tag, prop)
|
|
955
|
+
self._pending: set = set()
|
|
956
|
+
self._completion_futures: List[asyncio.Future[None]] = []
|
|
957
|
+
self._completed = False
|
|
958
|
+
self._lock = threading.Lock()
|
|
959
|
+
|
|
960
|
+
def add_target(self, anim_id: int, tag: int, prop: str) -> None:
|
|
961
|
+
with self._lock:
|
|
962
|
+
self._targets[anim_id] = (tag, prop)
|
|
963
|
+
self._pending.add(anim_id)
|
|
964
|
+
_native_groups[anim_id] = self
|
|
965
|
+
|
|
966
|
+
def add_completion_future(self, future: asyncio.Future[None]) -> None:
|
|
967
|
+
with self._lock:
|
|
968
|
+
done = self._completed
|
|
969
|
+
if not done:
|
|
970
|
+
self._completion_futures.append(future)
|
|
971
|
+
if done:
|
|
972
|
+
resolve_future(future, None)
|
|
973
|
+
|
|
974
|
+
def target_completed(self, anim_id: int, finished: bool) -> None:
|
|
975
|
+
with self._lock:
|
|
976
|
+
self._pending.discard(anim_id)
|
|
977
|
+
remaining = len(self._pending)
|
|
978
|
+
_native_groups.pop(anim_id, None)
|
|
979
|
+
if remaining == 0:
|
|
980
|
+
self._settle(self.final_value if finished else None)
|
|
981
|
+
|
|
982
|
+
def cancel(self) -> None:
|
|
983
|
+
"""Cancel all in-flight native animations, syncing to presentation values."""
|
|
984
|
+
with self._lock:
|
|
985
|
+
targets = dict(self._targets)
|
|
986
|
+
self._pending.clear()
|
|
987
|
+
presentation: Optional[float] = None
|
|
988
|
+
try:
|
|
989
|
+
backend = _backend()
|
|
990
|
+
for anim_id, (tag, _prop) in targets.items():
|
|
991
|
+
_native_groups.pop(anim_id, None)
|
|
992
|
+
current = backend.cancel_animation(tag, anim_id)
|
|
993
|
+
if current is not None:
|
|
994
|
+
try:
|
|
995
|
+
presentation = float(current)
|
|
996
|
+
except (TypeError, ValueError):
|
|
997
|
+
pass
|
|
998
|
+
except Exception:
|
|
999
|
+
pass
|
|
1000
|
+
self._settle(presentation)
|
|
1001
|
+
|
|
1002
|
+
def _settle(self, end_value: Optional[float]) -> None:
|
|
1003
|
+
with self._lock:
|
|
1004
|
+
if self._completed:
|
|
1005
|
+
return
|
|
1006
|
+
self._completed = True
|
|
1007
|
+
futures = list(self._completion_futures)
|
|
1008
|
+
self._completion_futures.clear()
|
|
1009
|
+
if self.value._native_group is self:
|
|
1010
|
+
self.value._native_group = None
|
|
1011
|
+
if end_value is not None:
|
|
1012
|
+
# The native side already shows this value; update the
|
|
1013
|
+
# Python cell (and listeners) without re-pushing.
|
|
1014
|
+
self.value._apply(end_value, push_native=False)
|
|
1015
|
+
for fut in futures:
|
|
1016
|
+
resolve_future(fut, None)
|
|
1017
|
+
|
|
1018
|
+
|
|
1019
|
+
# anim_id -> group, for routing completion callbacks from platform handlers.
|
|
1020
|
+
_native_groups: Dict[int, _NativeAnimationGroup] = {}
|
|
1021
|
+
|
|
1022
|
+
|
|
1023
|
+
def native_animation_completed(anim_id: int, finished: bool = True) -> None:
|
|
1024
|
+
"""Report a natively-driven animation as settled.
|
|
1025
|
+
|
|
1026
|
+
Called by platform handlers from their completion callbacks (iOS
|
|
1027
|
+
``UIView`` completion blocks, Android ``withEndAction`` /
|
|
1028
|
+
``DynamicAnimation.OnAnimationEndListener``). Safe to call from any
|
|
1029
|
+
thread; unknown ids are ignored (e.g. an animation cancelled
|
|
1030
|
+
moments before its completion fired).
|
|
1031
|
+
|
|
1032
|
+
Args:
|
|
1033
|
+
anim_id: The id passed to ``ViewHandler.start_animation``.
|
|
1034
|
+
finished: ``False`` when the platform reports the animation was
|
|
1035
|
+
interrupted rather than running to completion.
|
|
1036
|
+
"""
|
|
1037
|
+
group = _native_groups.get(anim_id)
|
|
1038
|
+
if group is not None:
|
|
1039
|
+
group.target_completed(anim_id, finished)
|
|
1040
|
+
|
|
1041
|
+
|
|
1042
|
+
def _projected_final_value(spec: Dict[str, Any]) -> float:
|
|
1043
|
+
"""Compute where an animation will settle, from its spec."""
|
|
1044
|
+
kind = spec.get("kind")
|
|
1045
|
+
if kind == "decay":
|
|
1046
|
+
# v(t) = v0 · e^(−k·1000·t) ⇒ ∫v dt = v0 / (k·1000)
|
|
1047
|
+
v0 = float(spec.get("velocity", 0.0))
|
|
1048
|
+
k = max(1e-6, float(spec.get("deceleration", 0.997)))
|
|
1049
|
+
return float(spec.get("from", 0.0)) + v0 / (k * 1000.0)
|
|
1050
|
+
return float(spec.get("to", spec.get("from", 0.0)))
|
|
1051
|
+
|
|
1052
|
+
|
|
1053
|
+
def _start_native(value: AnimatedValue, spec: Dict[str, Any]) -> Optional[_NativeAnimationGroup]:
|
|
1054
|
+
"""Offer ``spec`` to the platform for every binding of ``value``.
|
|
1055
|
+
|
|
1056
|
+
Returns the live group when **all** bindings accepted the native
|
|
1057
|
+
animation; otherwise rolls back any accepted targets and returns
|
|
1058
|
+
``None`` so the caller falls back to the Python ticker.
|
|
1059
|
+
"""
|
|
1060
|
+
try:
|
|
1061
|
+
backend = _backend()
|
|
1062
|
+
except Exception:
|
|
1063
|
+
return None
|
|
1064
|
+
from .animation_graph import install
|
|
1065
|
+
|
|
1066
|
+
graph = install(value)
|
|
1067
|
+
if graph and graph["bindings"]:
|
|
1068
|
+
targets = [(graph["bindings"][0][0], f"_pn_graph:{id(value)}")]
|
|
1069
|
+
else:
|
|
1070
|
+
targets = value.attachments()
|
|
1071
|
+
if value._has_dependents():
|
|
1072
|
+
return None
|
|
1073
|
+
if not targets or value.has_listeners():
|
|
1074
|
+
return None
|
|
1075
|
+
|
|
1076
|
+
group = _NativeAnimationGroup(value, _projected_final_value(spec))
|
|
1077
|
+
accepted: List[Tuple[int, int]] = [] # (anim_id, tag)
|
|
1078
|
+
for tag, prop in targets:
|
|
1079
|
+
anim_id = next(_anim_id_counter)
|
|
1080
|
+
try:
|
|
1081
|
+
ok = backend.start_animation(tag, anim_id, prop, spec)
|
|
1082
|
+
except Exception:
|
|
1083
|
+
ok = False
|
|
1084
|
+
if not ok:
|
|
1085
|
+
for prev_id, prev_tag in accepted:
|
|
1086
|
+
_native_groups.pop(prev_id, None)
|
|
1087
|
+
try:
|
|
1088
|
+
backend.cancel_animation(prev_tag, prev_id)
|
|
1089
|
+
except Exception:
|
|
1090
|
+
pass
|
|
1091
|
+
return None
|
|
1092
|
+
group.add_target(anim_id, tag, prop)
|
|
1093
|
+
accepted.append((anim_id, tag))
|
|
1094
|
+
return group
|
|
1095
|
+
|
|
1096
|
+
|
|
1097
|
+
# ======================================================================
|
|
1098
|
+
# Public animation handles
|
|
1099
|
+
# ======================================================================
|
|
1100
|
+
|
|
1101
|
+
|
|
1102
|
+
class _AwaitableAnimation:
|
|
1103
|
+
"""Base for awaitable animation handles.
|
|
1104
|
+
|
|
1105
|
+
Subclasses implement :meth:`start` and :meth:`stop`. Awaiting the
|
|
1106
|
+
handle (``await handle``) starts the animation if necessary and
|
|
1107
|
+
suspends until it completes. Cancelling the awaiting task calls
|
|
1108
|
+
:meth:`stop`.
|
|
1109
|
+
|
|
1110
|
+
Calling :meth:`start` returns ``self`` so handles can be chained
|
|
1111
|
+
or stashed: ``handle = pn.Animated.timing(...).start()``.
|
|
1112
|
+
"""
|
|
1113
|
+
|
|
1114
|
+
def start(self) -> "_AwaitableAnimation":
|
|
1115
|
+
raise NotImplementedError
|
|
1116
|
+
|
|
1117
|
+
def stop(self) -> None:
|
|
1118
|
+
raise NotImplementedError
|
|
1119
|
+
|
|
1120
|
+
def run(self) -> "_AwaitableAnimation":
|
|
1121
|
+
"""Return ``self`` for explicit ``await handle.run()`` style.
|
|
1122
|
+
|
|
1123
|
+
Equivalent to ``await handle`` directly; provided because some
|
|
1124
|
+
readers prefer the slightly more explicit form, particularly
|
|
1125
|
+
when storing the awaitable before resolving it.
|
|
1126
|
+
"""
|
|
1127
|
+
return self
|
|
1128
|
+
|
|
1129
|
+
async def _drive(self) -> None:
|
|
1130
|
+
raise NotImplementedError
|
|
1131
|
+
|
|
1132
|
+
def __await__(self) -> Any:
|
|
1133
|
+
try:
|
|
1134
|
+
asyncio.get_running_loop()
|
|
1135
|
+
except RuntimeError as exc:
|
|
1136
|
+
raise RuntimeError(
|
|
1137
|
+
"Animations can only be awaited from inside an asyncio task; "
|
|
1138
|
+
"use handle.start() to fire-and-forget instead."
|
|
1139
|
+
) from exc
|
|
1140
|
+
|
|
1141
|
+
async def _runner() -> None:
|
|
1142
|
+
try:
|
|
1143
|
+
await self._drive()
|
|
1144
|
+
except asyncio.CancelledError:
|
|
1145
|
+
self.stop()
|
|
1146
|
+
raise
|
|
1147
|
+
|
|
1148
|
+
return _runner().__await__()
|
|
1149
|
+
|
|
1150
|
+
|
|
1151
|
+
class _AnimationHandle(_AwaitableAnimation):
|
|
1152
|
+
"""Public handle returned by ``Animated.timing`` / ``.spring`` / ``.decay``.
|
|
1153
|
+
|
|
1154
|
+
Each ``.start()`` call snapshots the value's current state, prefers
|
|
1155
|
+
the native driver, and falls back to a fresh Python-ticked
|
|
1156
|
+
animation otherwise (matches React Native: the ``Animated.timing``
|
|
1157
|
+
return value is reusable).
|
|
1158
|
+
"""
|
|
1159
|
+
|
|
1160
|
+
def __init__(
|
|
1161
|
+
self,
|
|
1162
|
+
value: Optional[AnimatedValue],
|
|
1163
|
+
spec_factory: Callable[[], Dict[str, Any]],
|
|
1164
|
+
fallback_factory: Callable[[], _RunningAnimation],
|
|
1165
|
+
native_eligible: bool = True,
|
|
1166
|
+
) -> None:
|
|
1167
|
+
self._value = value
|
|
1168
|
+
self._spec_factory = spec_factory
|
|
1169
|
+
self._fallback_factory = fallback_factory
|
|
1170
|
+
self._native_eligible = native_eligible
|
|
1171
|
+
self._python_anim: Optional[_RunningAnimation] = None
|
|
1172
|
+
self._native_group: Optional[_NativeAnimationGroup] = None
|
|
1173
|
+
|
|
1174
|
+
def start(self) -> "_AnimationHandle":
|
|
1175
|
+
"""Begin the animation. Returns ``self`` for chaining."""
|
|
1176
|
+
self.stop()
|
|
1177
|
+
if self._value is not None and self._native_eligible:
|
|
1178
|
+
spec = self._spec_factory()
|
|
1179
|
+
group = _start_native(self._value, spec)
|
|
1180
|
+
if group is not None:
|
|
1181
|
+
self._native_group = group
|
|
1182
|
+
self._value._adopt_native_group(group)
|
|
1183
|
+
return self
|
|
1184
|
+
anim = self._fallback_factory()
|
|
1185
|
+
self._python_anim = anim
|
|
1186
|
+
_manager.add(anim)
|
|
1187
|
+
return self
|
|
1188
|
+
|
|
1189
|
+
def stop(self) -> None:
|
|
1190
|
+
"""Cancel the running instance (no-op if not running)."""
|
|
1191
|
+
if self._native_group is not None:
|
|
1192
|
+
group = self._native_group
|
|
1193
|
+
self._native_group = None
|
|
1194
|
+
if self._value is not None and self._value._native_group is group:
|
|
1195
|
+
self._value._native_group = None
|
|
1196
|
+
group.cancel()
|
|
1197
|
+
if self._python_anim is not None:
|
|
1198
|
+
anim = self._python_anim
|
|
1199
|
+
self._python_anim = None
|
|
1200
|
+
anim._finish()
|
|
1201
|
+
_manager.remove(anim)
|
|
1202
|
+
|
|
1203
|
+
def _is_running(self) -> bool:
|
|
1204
|
+
if self._native_group is not None and not self._native_group._completed:
|
|
1205
|
+
return True
|
|
1206
|
+
if self._python_anim is not None and not self._python_anim._completed:
|
|
1207
|
+
return True
|
|
1208
|
+
return False
|
|
1209
|
+
|
|
1210
|
+
async def _drive(self) -> None:
|
|
1211
|
+
# (Re)start unless an instance is currently mid-flight, so a
|
|
1212
|
+
# reused handle (``Animated.loop``, awaiting the same handle
|
|
1213
|
+
# twice) runs a fresh animation instead of resolving instantly
|
|
1214
|
+
# against the finished previous instance.
|
|
1215
|
+
if not self._is_running():
|
|
1216
|
+
self.start()
|
|
1217
|
+
loop = asyncio.get_running_loop()
|
|
1218
|
+
future: asyncio.Future[None] = loop.create_future()
|
|
1219
|
+
if self._native_group is not None:
|
|
1220
|
+
self._native_group.add_completion_future(future)
|
|
1221
|
+
elif self._python_anim is not None:
|
|
1222
|
+
self._python_anim.add_completion_future(future)
|
|
1223
|
+
else:
|
|
1224
|
+
return
|
|
1225
|
+
await future
|
|
1226
|
+
|
|
1227
|
+
|
|
1228
|
+
class _CompositeAnimation(_AwaitableAnimation):
|
|
1229
|
+
"""Run a list of animations in sequence, in parallel, or staggered."""
|
|
1230
|
+
|
|
1231
|
+
def __init__(self, items: List[Any], mode: str, stagger_ms: float = 0.0) -> None:
|
|
1232
|
+
self._items = list(items)
|
|
1233
|
+
self._mode = mode
|
|
1234
|
+
self._stagger_ms = float(stagger_ms)
|
|
1235
|
+
|
|
1236
|
+
def start(self) -> "_CompositeAnimation":
|
|
1237
|
+
"""Schedule the composite on the framework runtime, fire-and-forget."""
|
|
1238
|
+
from .runtime import run_async
|
|
1239
|
+
|
|
1240
|
+
run_async(self._drive())
|
|
1241
|
+
return self
|
|
1242
|
+
|
|
1243
|
+
def stop(self) -> None:
|
|
1244
|
+
for item in self._items:
|
|
1245
|
+
try:
|
|
1246
|
+
item.stop()
|
|
1247
|
+
except Exception:
|
|
1248
|
+
pass
|
|
1249
|
+
|
|
1250
|
+
async def _drive(self) -> None:
|
|
1251
|
+
if self._mode == "parallel":
|
|
1252
|
+
await asyncio.gather(*(self._await_item(item) for item in self._items))
|
|
1253
|
+
return
|
|
1254
|
+
if self._mode == "stagger":
|
|
1255
|
+
delay_s = max(0.0, self._stagger_ms) / 1000.0
|
|
1256
|
+
|
|
1257
|
+
async def _delayed(index: int, item: Any) -> None:
|
|
1258
|
+
if index > 0 and delay_s > 0.0:
|
|
1259
|
+
await asyncio.sleep(delay_s * index)
|
|
1260
|
+
await self._await_item(item)
|
|
1261
|
+
|
|
1262
|
+
await asyncio.gather(*(_delayed(i, item) for i, item in enumerate(self._items)))
|
|
1263
|
+
return
|
|
1264
|
+
for item in self._items:
|
|
1265
|
+
await self._await_item(item)
|
|
1266
|
+
|
|
1267
|
+
@staticmethod
|
|
1268
|
+
async def _await_item(item: Any) -> None:
|
|
1269
|
+
if item is None:
|
|
1270
|
+
return
|
|
1271
|
+
# ``_AwaitableAnimation`` and plain awaitables/coroutines are
|
|
1272
|
+
# both supported: lets users mix in ``asyncio.sleep``.
|
|
1273
|
+
await item
|
|
1274
|
+
|
|
1275
|
+
|
|
1276
|
+
class _LoopAnimation(_AwaitableAnimation):
|
|
1277
|
+
"""Repeat an animation, resetting its values before each iteration."""
|
|
1278
|
+
|
|
1279
|
+
def __init__(self, animation: Any, iterations: int = -1, reset: bool = True) -> None:
|
|
1280
|
+
self._animation = animation
|
|
1281
|
+
self._iterations = int(iterations)
|
|
1282
|
+
self._reset = bool(reset)
|
|
1283
|
+
self._stopped = False
|
|
1284
|
+
|
|
1285
|
+
def start(self) -> "_LoopAnimation":
|
|
1286
|
+
from .runtime import run_async
|
|
1287
|
+
|
|
1288
|
+
self._stopped = False
|
|
1289
|
+
run_async(self._drive())
|
|
1290
|
+
return self
|
|
1291
|
+
|
|
1292
|
+
def stop(self) -> None:
|
|
1293
|
+
self._stopped = True
|
|
1294
|
+
try:
|
|
1295
|
+
self._animation.stop()
|
|
1296
|
+
except Exception:
|
|
1297
|
+
pass
|
|
1298
|
+
|
|
1299
|
+
def _collect_values(self, item: Any, out: List[AnimatedValue]) -> None:
|
|
1300
|
+
if isinstance(item, _AnimationHandle):
|
|
1301
|
+
if item._value is not None and item._value not in out:
|
|
1302
|
+
out.append(item._value)
|
|
1303
|
+
elif isinstance(item, _CompositeAnimation):
|
|
1304
|
+
for sub in item._items:
|
|
1305
|
+
self._collect_values(sub, out)
|
|
1306
|
+
elif isinstance(item, _LoopAnimation):
|
|
1307
|
+
self._collect_values(item._animation, out)
|
|
1308
|
+
|
|
1309
|
+
async def _drive(self) -> None:
|
|
1310
|
+
self._stopped = False
|
|
1311
|
+
values: List[AnimatedValue] = []
|
|
1312
|
+
self._collect_values(self._animation, values)
|
|
1313
|
+
origins = [(v, v.value) for v in values]
|
|
1314
|
+
count = 0
|
|
1315
|
+
while not self._stopped and (self._iterations < 0 or count < self._iterations):
|
|
1316
|
+
if self._reset and count > 0:
|
|
1317
|
+
for value, origin in origins:
|
|
1318
|
+
value.set_value(origin)
|
|
1319
|
+
await self._animation
|
|
1320
|
+
count += 1
|
|
1321
|
+
|
|
1322
|
+
|
|
1323
|
+
# ======================================================================
|
|
1324
|
+
# Animated component wrappers
|
|
1325
|
+
# ======================================================================
|
|
1326
|
+
|
|
1327
|
+
# Transform-entry keys that may carry animated nodes; the key doubles
|
|
1328
|
+
# as the ``set_animated_property`` prop name.
|
|
1329
|
+
_ANIMATED_TRANSFORM_KEYS = frozenset(
|
|
1330
|
+
{"translate_x", "translate_y", "scale", "scale_x", "scale_y", "rotate"},
|
|
1331
|
+
)
|
|
1332
|
+
|
|
1333
|
+
|
|
1334
|
+
def _resolve_style_with_values(style: StyleProp) -> Tuple[Dict[str, Any], Dict[str, AnimatedNode]]:
|
|
1335
|
+
"""Split ``style`` into a plain dict and animated bindings.
|
|
1336
|
+
|
|
1337
|
+
Animated nodes in the style (top-level values *and* values inside
|
|
1338
|
+
``transform`` entries) are replaced with their current numeric
|
|
1339
|
+
value in ``plain_style`` and recorded in ``animated_bindings`` so
|
|
1340
|
+
the wrapping component can attach them after mount.
|
|
1341
|
+
"""
|
|
1342
|
+
flat = resolve_style(style)
|
|
1343
|
+
bindings: Dict[str, AnimatedNode] = {}
|
|
1344
|
+
plain: Dict[str, Any] = {}
|
|
1345
|
+
for k, v in flat.items():
|
|
1346
|
+
if isinstance(v, AnimatedNode):
|
|
1347
|
+
bindings[k] = v
|
|
1348
|
+
plain[k] = v.value
|
|
1349
|
+
elif k == "transform" and v is not None:
|
|
1350
|
+
entries = v if isinstance(v, list) else [v]
|
|
1351
|
+
plain_entries: List[Any] = []
|
|
1352
|
+
for entry in entries:
|
|
1353
|
+
if not isinstance(entry, dict):
|
|
1354
|
+
plain_entries.append(entry)
|
|
1355
|
+
continue
|
|
1356
|
+
clean_entry: Dict[str, Any] = {}
|
|
1357
|
+
for prop, val in entry.items():
|
|
1358
|
+
if isinstance(val, AnimatedNode) and prop in _ANIMATED_TRANSFORM_KEYS:
|
|
1359
|
+
bindings[prop] = val
|
|
1360
|
+
clean_entry[prop] = val.value
|
|
1361
|
+
else:
|
|
1362
|
+
clean_entry[prop] = val
|
|
1363
|
+
plain_entries.append(clean_entry)
|
|
1364
|
+
plain[k] = plain_entries
|
|
1365
|
+
else:
|
|
1366
|
+
plain[k] = v
|
|
1367
|
+
return plain, bindings
|
|
1368
|
+
|
|
1369
|
+
|
|
1370
|
+
def _make_animated_factory(
|
|
1371
|
+
element_type: str,
|
|
1372
|
+
accept_children: bool,
|
|
1373
|
+
) -> Callable[..., Element]:
|
|
1374
|
+
"""Build an animated wrapper for ``element_type``."""
|
|
1375
|
+
from .component import Component
|
|
1376
|
+
|
|
1377
|
+
def _animated(*children: Any, **kwargs: Any) -> Element:
|
|
1378
|
+
from .components import Image as _Image
|
|
1379
|
+
from .components import Text as _Text
|
|
1380
|
+
from .components import View as _View
|
|
1381
|
+
|
|
1382
|
+
kids: List[Any] = list(children)
|
|
1383
|
+
style = kwargs.pop("style", None)
|
|
1384
|
+
plain_style, bindings = _resolve_style_with_values(style)
|
|
1385
|
+
|
|
1386
|
+
ref = use_ref(None)
|
|
1387
|
+
attached: Ref[List[Callable[[], None]]] = use_ref([])
|
|
1388
|
+
|
|
1389
|
+
def _attach_bindings() -> None:
|
|
1390
|
+
tag = ref._pn_tag
|
|
1391
|
+
if tag is None:
|
|
1392
|
+
return
|
|
1393
|
+
# Derived nodes can be rebuilt on every render. Install their new
|
|
1394
|
+
# bindings before releasing the old ones so a running native graph
|
|
1395
|
+
# never temporarily loses every owner.
|
|
1396
|
+
detachers = [value.attach(tag, _animated_prop_name(prop)) for prop, value in bindings.items()]
|
|
1397
|
+
previous = attached.current
|
|
1398
|
+
attached.current = detachers
|
|
1399
|
+
for detach in previous:
|
|
1400
|
+
detach()
|
|
1401
|
+
|
|
1402
|
+
def _unmount_bindings() -> Callable[[], None]:
|
|
1403
|
+
def _cleanup() -> None:
|
|
1404
|
+
for detach in attached.current:
|
|
1405
|
+
detach()
|
|
1406
|
+
attached.current = []
|
|
1407
|
+
|
|
1408
|
+
return _cleanup
|
|
1409
|
+
|
|
1410
|
+
# Re-attach whenever the binding set changes identity.
|
|
1411
|
+
use_effect(_attach_bindings, [tuple(sorted((k, id(v)) for k, v in bindings.items()))])
|
|
1412
|
+
use_effect(_unmount_bindings, [])
|
|
1413
|
+
|
|
1414
|
+
if element_type == "Text":
|
|
1415
|
+
text = children[0] if children else kwargs.pop("text", "")
|
|
1416
|
+
return _Text(text, style=plain_style, ref=ref, **kwargs)
|
|
1417
|
+
if element_type == "Image":
|
|
1418
|
+
source = kids[0] if kids else kwargs.pop("source", "")
|
|
1419
|
+
return _Image(source, style=plain_style, ref=ref, **kwargs)
|
|
1420
|
+
if not accept_children:
|
|
1421
|
+
kids = []
|
|
1422
|
+
return _View(*kids, style=plain_style, ref=ref, **kwargs)
|
|
1423
|
+
|
|
1424
|
+
return Component(_animated, display_name=f"Animated.{element_type}")
|
|
1425
|
+
|
|
1426
|
+
|
|
1427
|
+
def _animated_prop_name(prop: str) -> str:
|
|
1428
|
+
"""Map a style key to the name expected by ``set_animated_property``."""
|
|
1429
|
+
return prop
|
|
1430
|
+
|
|
1431
|
+
|
|
1432
|
+
# ======================================================================
|
|
1433
|
+
# Public API
|
|
1434
|
+
# ======================================================================
|
|
1435
|
+
|
|
1436
|
+
|
|
1437
|
+
class _AnimatedNamespace:
|
|
1438
|
+
"""Public ``Animated`` namespace.
|
|
1439
|
+
|
|
1440
|
+
Exposes the ``Value`` type, animation factories, composers,
|
|
1441
|
+
derived-node helpers (``event``, ``diff_clamp``), and component
|
|
1442
|
+
wrappers (``View``, ``Text``, ``Image``).
|
|
1443
|
+
"""
|
|
1444
|
+
|
|
1445
|
+
Value = AnimatedValue
|
|
1446
|
+
|
|
1447
|
+
@staticmethod
|
|
1448
|
+
def timing(
|
|
1449
|
+
value: AnimatedValue,
|
|
1450
|
+
*,
|
|
1451
|
+
to: float,
|
|
1452
|
+
duration: float = 300.0,
|
|
1453
|
+
easing: Any = "ease_in_out",
|
|
1454
|
+
) -> _AnimationHandle:
|
|
1455
|
+
"""Interpolate ``value`` to ``to`` over ``duration`` ms with ``easing``."""
|
|
1456
|
+
|
|
1457
|
+
def _spec() -> Dict[str, Any]:
|
|
1458
|
+
return {
|
|
1459
|
+
"kind": "timing",
|
|
1460
|
+
"from": value.value,
|
|
1461
|
+
"to": float(to),
|
|
1462
|
+
"duration_ms": float(duration),
|
|
1463
|
+
"easing": str(easing),
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
def _fallback() -> _RunningAnimation:
|
|
1467
|
+
return _TimingAnimation(value, to, duration, _resolve_easing(easing))
|
|
1468
|
+
|
|
1469
|
+
# Callable easings can't cross the bridge; tick them in Python.
|
|
1470
|
+
return _AnimationHandle(value, _spec, _fallback, native_eligible=not callable(easing))
|
|
1471
|
+
|
|
1472
|
+
@staticmethod
|
|
1473
|
+
def spring(
|
|
1474
|
+
value: AnimatedValue,
|
|
1475
|
+
*,
|
|
1476
|
+
to: float,
|
|
1477
|
+
stiffness: float = 100.0,
|
|
1478
|
+
damping: float = 10.0,
|
|
1479
|
+
mass: float = 1.0,
|
|
1480
|
+
initial_velocity: float = 0.0,
|
|
1481
|
+
) -> _AnimationHandle:
|
|
1482
|
+
"""Run a damped harmonic spring toward ``to``."""
|
|
1483
|
+
|
|
1484
|
+
def _spec() -> Dict[str, Any]:
|
|
1485
|
+
return {
|
|
1486
|
+
"kind": "spring",
|
|
1487
|
+
"from": value.value,
|
|
1488
|
+
"to": float(to),
|
|
1489
|
+
"stiffness": float(stiffness),
|
|
1490
|
+
"damping": float(damping),
|
|
1491
|
+
"mass": float(mass),
|
|
1492
|
+
"initial_velocity": float(initial_velocity),
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1495
|
+
def _fallback() -> _RunningAnimation:
|
|
1496
|
+
return _SpringAnimation(value, to, stiffness, damping, mass, initial_velocity)
|
|
1497
|
+
|
|
1498
|
+
return _AnimationHandle(value, _spec, _fallback)
|
|
1499
|
+
|
|
1500
|
+
@staticmethod
|
|
1501
|
+
def decay(
|
|
1502
|
+
value: AnimatedValue,
|
|
1503
|
+
*,
|
|
1504
|
+
velocity: float,
|
|
1505
|
+
deceleration: float = 0.997,
|
|
1506
|
+
) -> _AnimationHandle:
|
|
1507
|
+
"""Decelerate ``value`` from ``velocity`` (units/ms) until it rests."""
|
|
1508
|
+
|
|
1509
|
+
def _spec() -> Dict[str, Any]:
|
|
1510
|
+
return {
|
|
1511
|
+
"kind": "decay",
|
|
1512
|
+
"from": value.value,
|
|
1513
|
+
"velocity": float(velocity),
|
|
1514
|
+
"deceleration": float(deceleration),
|
|
1515
|
+
}
|
|
1516
|
+
|
|
1517
|
+
def _fallback() -> _RunningAnimation:
|
|
1518
|
+
return _DecayAnimation(value, velocity, deceleration)
|
|
1519
|
+
|
|
1520
|
+
return _AnimationHandle(value, _spec, _fallback)
|
|
1521
|
+
|
|
1522
|
+
@staticmethod
|
|
1523
|
+
def parallel(animations: List[Any]) -> _CompositeAnimation:
|
|
1524
|
+
"""Run all ``animations`` concurrently; complete when all finish."""
|
|
1525
|
+
return _CompositeAnimation(animations, "parallel")
|
|
1526
|
+
|
|
1527
|
+
@staticmethod
|
|
1528
|
+
def sequence(animations: List[Any]) -> _CompositeAnimation:
|
|
1529
|
+
"""Run ``animations`` one after another."""
|
|
1530
|
+
return _CompositeAnimation(animations, "sequence")
|
|
1531
|
+
|
|
1532
|
+
@staticmethod
|
|
1533
|
+
def stagger(delay: float, animations: List[Any]) -> _CompositeAnimation:
|
|
1534
|
+
"""Run ``animations`` in parallel, each starting ``delay`` ms after the previous.
|
|
1535
|
+
|
|
1536
|
+
Args:
|
|
1537
|
+
delay: Milliseconds between successive starts.
|
|
1538
|
+
animations: Animation handles (or awaitables) to run.
|
|
1539
|
+
|
|
1540
|
+
Example:
|
|
1541
|
+
```python
|
|
1542
|
+
pn.Animated.stagger(80, [
|
|
1543
|
+
pn.Animated.timing(v, to=1.0) for v in card_opacities
|
|
1544
|
+
]).start()
|
|
1545
|
+
```
|
|
1546
|
+
"""
|
|
1547
|
+
return _CompositeAnimation(animations, "stagger", stagger_ms=delay)
|
|
1548
|
+
|
|
1549
|
+
@staticmethod
|
|
1550
|
+
def loop(animation: Any, *, iterations: int = -1, reset: bool = True) -> _LoopAnimation:
|
|
1551
|
+
"""Repeat ``animation``, optionally forever.
|
|
1552
|
+
|
|
1553
|
+
Values driven by the animation are captured when the loop
|
|
1554
|
+
starts and restored before each iteration (matching React
|
|
1555
|
+
Native's ``resetBeforeIteration``), so ``timing`` loops replay
|
|
1556
|
+
the same motion instead of animating in place.
|
|
1557
|
+
|
|
1558
|
+
Args:
|
|
1559
|
+
animation: A handle from ``timing`` / ``spring`` / ``decay``
|
|
1560
|
+
or a ``sequence`` / ``parallel`` / ``stagger`` composite.
|
|
1561
|
+
iterations: Number of repetitions; ``-1`` (default) loops
|
|
1562
|
+
until [`stop`][pythonnative.animated._LoopAnimation.stop]
|
|
1563
|
+
is called or the awaiting task is cancelled.
|
|
1564
|
+
reset: When ``False``, values continue from wherever the
|
|
1565
|
+
previous iteration ended.
|
|
1566
|
+
|
|
1567
|
+
Example:
|
|
1568
|
+
```python
|
|
1569
|
+
pulse = pn.Animated.loop(
|
|
1570
|
+
pn.Animated.sequence([
|
|
1571
|
+
pn.Animated.timing(scale, to=1.15, duration=350),
|
|
1572
|
+
pn.Animated.timing(scale, to=1.0, duration=350),
|
|
1573
|
+
]),
|
|
1574
|
+
).start()
|
|
1575
|
+
# later: pulse.stop()
|
|
1576
|
+
```
|
|
1577
|
+
"""
|
|
1578
|
+
return _LoopAnimation(animation, iterations=iterations, reset=reset)
|
|
1579
|
+
|
|
1580
|
+
@staticmethod
|
|
1581
|
+
def delay(duration: float) -> _AnimationHandle:
|
|
1582
|
+
"""Wait ``duration`` ms before continuing in a sequence."""
|
|
1583
|
+
|
|
1584
|
+
def _spec() -> Dict[str, Any]:
|
|
1585
|
+
return {"kind": "delay", "duration_ms": float(duration)}
|
|
1586
|
+
|
|
1587
|
+
def _fallback() -> _RunningAnimation:
|
|
1588
|
+
return _DelayAnimation(duration)
|
|
1589
|
+
|
|
1590
|
+
return _AnimationHandle(None, _spec, _fallback)
|
|
1591
|
+
|
|
1592
|
+
@staticmethod
|
|
1593
|
+
def event(listener: Optional[Callable[..., None]] = None, **bindings: AnimatedValue) -> AnimatedEvent:
|
|
1594
|
+
"""Build a callback that copies event fields into animated values.
|
|
1595
|
+
|
|
1596
|
+
Pass the result to any event prop. Each keyword maps a payload
|
|
1597
|
+
field (dict key or dataclass attribute) onto an
|
|
1598
|
+
[`AnimatedValue`][pythonnative.AnimatedValue]:
|
|
1599
|
+
|
|
1600
|
+
```python
|
|
1601
|
+
scroll_y = pn.use_animated_value(0.0)
|
|
1602
|
+
pn.ScrollView(..., on_scroll=pn.Animated.event(y=scroll_y))
|
|
1603
|
+
|
|
1604
|
+
tx = pn.use_animated_value(0.0)
|
|
1605
|
+
gestures.Pan(on_change=pn.Animated.event(translation_x=tx))
|
|
1606
|
+
```
|
|
1607
|
+
|
|
1608
|
+
Args:
|
|
1609
|
+
listener: Optional plain callback invoked with the raw
|
|
1610
|
+
event after the values update.
|
|
1611
|
+
**bindings: ``field_name=animated_value`` pairs.
|
|
1612
|
+
|
|
1613
|
+
Returns:
|
|
1614
|
+
A callable [`AnimatedEvent`][pythonnative.animated.AnimatedEvent].
|
|
1615
|
+
"""
|
|
1616
|
+
return AnimatedEvent(listener, **bindings)
|
|
1617
|
+
|
|
1618
|
+
@staticmethod
|
|
1619
|
+
def diff_clamp(node: AnimatedNode, min_value: float, max_value: float) -> AnimatedNode:
|
|
1620
|
+
"""Accumulate ``node``'s deltas into ``[min_value, max_value]``.
|
|
1621
|
+
|
|
1622
|
+
The classic collapsing-header primitive: unlike ``interpolate``
|
|
1623
|
+
with ``"clamp"`` (which pins the *absolute* input), the output
|
|
1624
|
+
tracks input *movement*, so a small scroll upward immediately
|
|
1625
|
+
re-reveals the header no matter how far down the list is.
|
|
1626
|
+
|
|
1627
|
+
```python
|
|
1628
|
+
header_shift = pn.Animated.diff_clamp(scroll_y, 0, 56)
|
|
1629
|
+
```
|
|
1630
|
+
"""
|
|
1631
|
+
return _AnimatedDiffClamp(node, min_value, max_value)
|
|
1632
|
+
|
|
1633
|
+
View = staticmethod(_make_animated_factory("View", accept_children=True))
|
|
1634
|
+
Text = staticmethod(_make_animated_factory("Text", accept_children=False))
|
|
1635
|
+
Image = staticmethod(_make_animated_factory("Image", accept_children=False))
|
|
1636
|
+
|
|
1637
|
+
|
|
1638
|
+
Animated = _AnimatedNamespace()
|
|
1639
|
+
|
|
1640
|
+
|
|
1641
|
+
def use_animated_value(initial: float = 0.0) -> AnimatedValue:
|
|
1642
|
+
"""Return an [`AnimatedValue`][pythonnative.AnimatedValue] that is stable across renders.
|
|
1643
|
+
|
|
1644
|
+
Convenience wrapper for the common pattern
|
|
1645
|
+
``pn.use_memo(lambda: AnimatedValue(initial), [])``. The same
|
|
1646
|
+
instance is returned on every render of the same component, so
|
|
1647
|
+
you can drive it from event handlers without recreating it.
|
|
1648
|
+
|
|
1649
|
+
Args:
|
|
1650
|
+
initial: The starting numeric value.
|
|
1651
|
+
|
|
1652
|
+
Returns:
|
|
1653
|
+
A mount-stable [`AnimatedValue`][pythonnative.AnimatedValue].
|
|
1654
|
+
|
|
1655
|
+
Example:
|
|
1656
|
+
```python
|
|
1657
|
+
import pythonnative as pn
|
|
1658
|
+
|
|
1659
|
+
|
|
1660
|
+
@pn.component
|
|
1661
|
+
def FadeIn():
|
|
1662
|
+
opacity = pn.use_animated_value(0.0)
|
|
1663
|
+
|
|
1664
|
+
async def fade_in():
|
|
1665
|
+
await pn.Animated.timing(opacity, to=1.0, duration=300)
|
|
1666
|
+
|
|
1667
|
+
pn.use_effect(fade_in, [])
|
|
1668
|
+
return pn.Animated.View(
|
|
1669
|
+
pn.Text("Hello"),
|
|
1670
|
+
style=pn.style(opacity=opacity),
|
|
1671
|
+
)
|
|
1672
|
+
```
|
|
1673
|
+
"""
|
|
1674
|
+
from .hooks import use_memo
|
|
1675
|
+
|
|
1676
|
+
return use_memo(lambda: AnimatedValue(initial), [])
|
|
1677
|
+
|
|
1678
|
+
|
|
1679
|
+
__all__ = [
|
|
1680
|
+
"AnimatedNode",
|
|
1681
|
+
"AnimatedValue",
|
|
1682
|
+
"AnimatedInterpolation",
|
|
1683
|
+
"AnimatedEvent",
|
|
1684
|
+
"Animated",
|
|
1685
|
+
"use_animated_value",
|
|
1686
|
+
"native_animation_completed",
|
|
1687
|
+
]
|