pythonnative 0.35.0__py3-none-any.whl → 0.37.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (176) hide show
  1. pythonnative/__init__.py +6 -4
  2. pythonnative/_ios_log.py +1 -1
  3. pythonnative/alerts.py +34 -88
  4. pythonnative/bootstrap.py +90 -0
  5. pythonnative/bridge/__init__.py +306 -0
  6. pythonnative/bridge/android.py +78 -0
  7. pythonnative/bridge/codec.py +164 -0
  8. pythonnative/bridge/fake.py +236 -0
  9. pythonnative/bridge/ios.py +138 -0
  10. pythonnative/cli/pn.py +24 -6
  11. pythonnative/components/layout.py +65 -3
  12. pythonnative/gestures.py +13 -10
  13. pythonnative/hooks.py +41 -26
  14. pythonnative/hosts/__init__.py +13 -36
  15. pythonnative/hosts/native.py +348 -0
  16. pythonnative/images.py +6 -5
  17. pythonnative/mutations.py +21 -0
  18. pythonnative/native_modules/__init__.py +30 -29
  19. pythonnative/native_modules/app_state.py +10 -6
  20. pythonnative/native_modules/battery.py +20 -84
  21. pythonnative/native_modules/biometrics.py +11 -124
  22. pythonnative/native_modules/camera.py +15 -291
  23. pythonnative/native_modules/clipboard.py +14 -109
  24. pythonnative/native_modules/desktop.py +322 -0
  25. pythonnative/native_modules/file_system.py +15 -21
  26. pythonnative/native_modules/haptics.py +19 -129
  27. pythonnative/native_modules/linking.py +24 -137
  28. pythonnative/native_modules/location.py +28 -180
  29. pythonnative/native_modules/net_info.py +25 -135
  30. pythonnative/native_modules/notifications.py +36 -257
  31. pythonnative/native_modules/permissions.py +20 -184
  32. pythonnative/native_modules/registry.py +513 -0
  33. pythonnative/native_modules/secure_store.py +18 -155
  34. pythonnative/native_modules/share.py +8 -113
  35. pythonnative/native_views/__init__.py +67 -86
  36. pythonnative/native_views/bridge_backend.py +264 -0
  37. pythonnative/platform.py +19 -15
  38. pythonnative/platform_metrics.py +3 -3
  39. pythonnative/project/android.py +0 -3
  40. pythonnative/project/builder.py +38 -16
  41. pythonnative/project/config.py +12 -2
  42. pythonnative/project/plugins.py +366 -0
  43. pythonnative/py.typed +0 -0
  44. pythonnative/runtime.py +35 -203
  45. pythonnative/sdk/__init__.py +48 -33
  46. pythonnative/sdk/_components.py +57 -76
  47. pythonnative/storage.py +18 -243
  48. pythonnative/templates/android_template/app/build.gradle +6 -9
  49. pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/MainActivity.kt +34 -166
  50. pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/ScreenFragment.kt +13 -151
  51. pythonnative/templates/android_template/pythonnative/build.gradle +48 -0
  52. pythonnative/templates/android_template/pythonnative/src/main/AndroidManifest.xml +11 -0
  53. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/PNBridge.kt +214 -0
  54. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/PythonHost.kt +20 -0
  55. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/animation/AnimationSpecs.kt +141 -0
  56. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/animation/PNAnimator.kt +249 -0
  57. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/bridge/JsonUtil.kt +175 -0
  58. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/bridge/MainThread.kt +32 -0
  59. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/bridge/PNLog.kt +33 -0
  60. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/bridge/PNRegistry.kt +82 -0
  61. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/bridge/PNTransaction.kt +80 -0
  62. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/bridge/TransactionApplier.kt +115 -0
  63. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/bridge/ViewRegistry.kt +74 -0
  64. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/BuiltinComponents.kt +35 -0
  65. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ButtonManager.kt +33 -0
  66. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ComponentManager.kt +213 -0
  67. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ContainerManagers.kt +69 -0
  68. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ControlManagers.kt +186 -0
  69. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/DatePickerManager.kt +123 -0
  70. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ImageLoader.kt +141 -0
  71. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ImageManager.kt +156 -0
  72. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ModalManager.kt +134 -0
  73. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/PNColor.kt +201 -0
  74. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/PickerManager.kt +87 -0
  75. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/PortalManager.kt +55 -0
  76. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/PressableManager.kt +122 -0
  77. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ScrollViewManager.kt +214 -0
  78. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/SegmentedControlManager.kt +113 -0
  79. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/StatusBarManager.kt +52 -0
  80. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/TabBarManager.kt +131 -0
  81. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/TextInputManager.kt +245 -0
  82. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/TextManager.kt +233 -0
  83. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ViewStyler.kt +355 -0
  84. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/VirtualListManager.kt +179 -0
  85. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/WebViewManager.kt +149 -0
  86. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/gestures/GestureArbiter.kt +233 -0
  87. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/gestures/GestureCoordinator.kt +127 -0
  88. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/gestures/GestureRecognizers.kt +527 -0
  89. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/BuiltinModules.kt +73 -0
  90. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/DeviceModules.kt +316 -0
  91. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/HostModule.kt +97 -0
  92. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/MediaModules.kt +200 -0
  93. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/NativeModule.kt +135 -0
  94. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/NotificationsModule.kt +100 -0
  95. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/PermissionsModule.kt +88 -0
  96. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/StorageModules.kt +89 -0
  97. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/SystemModules.kt +178 -0
  98. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/plugins/GeneratedPlugins.kt +16 -0
  99. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/screens/Navigator.kt +116 -0
  100. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/screens/PNScreenFragment.kt +223 -0
  101. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/screens/ScreenRegistry.kt +80 -0
  102. pythonnative/templates/android_template/{app/src/main/java/com/pythonnative/android_template → pythonnative/src/main/java/com/pythonnative/runtime/views}/PNAccessibilityDelegate.kt +5 -4
  103. pythonnative/templates/android_template/{app/src/main/java/com/pythonnative/android_template → pythonnative/src/main/java/com/pythonnative/runtime/views}/PNBorderDrawable.kt +12 -3
  104. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/views/PNEditText.kt +18 -0
  105. pythonnative/templates/android_template/{app/src/main/java/com/pythonnative/android_template → pythonnative/src/main/java/com/pythonnative/runtime/views}/PNFrameLayout.kt +9 -10
  106. pythonnative/templates/android_template/{app/src/main/java/com/pythonnative/android_template → pythonnative/src/main/java/com/pythonnative/runtime/views}/PNVirtualListView.java +9 -12
  107. pythonnative/templates/android_template/pythonnative/src/test/java/com/pythonnative/runtime/AnimationSpecsTest.kt +75 -0
  108. pythonnative/templates/android_template/pythonnative/src/test/java/com/pythonnative/runtime/GestureArbiterTest.kt +197 -0
  109. pythonnative/templates/android_template/pythonnative/src/test/java/com/pythonnative/runtime/PNColorTest.kt +47 -0
  110. pythonnative/templates/android_template/pythonnative/src/test/java/com/pythonnative/runtime/PNTransactionTest.kt +78 -0
  111. pythonnative/templates/android_template/pythonnative/src/test/java/com/pythonnative/runtime/PromiseTest.kt +75 -0
  112. pythonnative/templates/android_template/settings.gradle +1 -0
  113. pythonnative/templates/ios_template/PythonNativeKit/Package.swift +21 -0
  114. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Animation/PNAnimator.swift +301 -0
  115. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Bridge/PNBridge.swift +191 -0
  116. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Bridge/PNRegistry.swift +140 -0
  117. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Bridge/PNTransaction.swift +165 -0
  118. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Bridge/PNViewRegistry.swift +53 -0
  119. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNButtonManagers.swift +206 -0
  120. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNColor.swift +173 -0
  121. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNComponentManager.swift +183 -0
  122. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNContainerView.swift +56 -0
  123. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNControlManagers.swift +281 -0
  124. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNImageManager.swift +221 -0
  125. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNOverlayManagers.swift +243 -0
  126. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNPressableManager.swift +114 -0
  127. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNScrollViewManager.swift +197 -0
  128. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNTabBarManager.swift +106 -0
  129. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNTextInputManager.swift +289 -0
  130. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNTextManager.swift +230 -0
  131. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNTransform.swift +82 -0
  132. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNViewManager.swift +86 -0
  133. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNViewState.swift +87 -0
  134. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNViewStyler.swift +340 -0
  135. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNVirtualListManager.swift +257 -0
  136. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNWebViewManager.swift +124 -0
  137. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Gestures/PNGestureCoordinator.swift +260 -0
  138. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Modules/AlertModule.swift +92 -0
  139. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Modules/HostModule.swift +158 -0
  140. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Modules/LifecycleModules.swift +174 -0
  141. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Modules/MediaModules.swift +177 -0
  142. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Modules/PNNativeModule.swift +125 -0
  143. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Modules/PermissionModules.swift +251 -0
  144. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Modules/SystemModules.swift +251 -0
  145. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Plugins/PNPluginRegistration.swift +8 -0
  146. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Screens/PNScreenRegistry.swift +46 -0
  147. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Screens/PNViewController.swift +251 -0
  148. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Support/PNCompat.swift +46 -0
  149. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Support/PNJSON.swift +173 -0
  150. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Support/PNLog.swift +47 -0
  151. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Support/PNWindow.swift +51 -0
  152. pythonnative/templates/ios_template/PythonNativeKit/Tests/PythonNativeKitTests/PNColorTests.swift +50 -0
  153. pythonnative/templates/ios_template/PythonNativeKit/Tests/PythonNativeKitTests/PNGestureTests.swift +101 -0
  154. pythonnative/templates/ios_template/PythonNativeKit/Tests/PythonNativeKitTests/PNManagerTests.swift +124 -0
  155. pythonnative/templates/ios_template/PythonNativeKit/Tests/PythonNativeKitTests/PNModuleTests.swift +98 -0
  156. pythonnative/templates/ios_template/PythonNativeKit/Tests/PythonNativeKitTests/PNTransactionTests.swift +91 -0
  157. pythonnative/templates/ios_template/ios_template/AppDelegate.swift +8 -50
  158. pythonnative/templates/ios_template/ios_template/Info.plist +4 -4
  159. pythonnative/templates/ios_template/ios_template/PythonRuntime.swift +38 -46
  160. pythonnative/templates/ios_template/ios_template/SceneDelegate.swift +11 -16
  161. pythonnative/templates/ios_template/ios_template/ViewController.swift +12 -142
  162. pythonnative/templates/ios_template/ios_template.xcodeproj/project.pbxproj +47 -1
  163. pythonnative/utils.py +4 -82
  164. pythonnative/virtual_rows.py +3 -3
  165. {pythonnative-0.35.0.dist-info → pythonnative-0.37.0.dist-info}/METADATA +3 -5
  166. pythonnative-0.37.0.dist-info/RECORD +263 -0
  167. pythonnative/hosts/android.py +0 -269
  168. pythonnative/hosts/ios.py +0 -404
  169. pythonnative/native_views/android.py +0 -3702
  170. pythonnative/native_views/ios.py +0 -5253
  171. pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/Navigator.kt +0 -43
  172. pythonnative-0.35.0.dist-info/RECORD +0 -155
  173. {pythonnative-0.35.0.dist-info → pythonnative-0.37.0.dist-info}/WHEEL +0 -0
  174. {pythonnative-0.35.0.dist-info → pythonnative-0.37.0.dist-info}/entry_points.txt +0 -0
  175. {pythonnative-0.35.0.dist-info → pythonnative-0.37.0.dist-info}/licenses/LICENSE +0 -0
  176. {pythonnative-0.35.0.dist-info → pythonnative-0.37.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,214 @@
1
+ package com.pythonnative.runtime
2
+
3
+ import android.app.Activity
4
+ import android.content.Context
5
+ import android.util.Log
6
+ import com.pythonnative.runtime.animation.PNAnimator
7
+ import com.pythonnative.runtime.bridge.JsonUtil
8
+ import com.pythonnative.runtime.bridge.MainThread
9
+ import com.pythonnative.runtime.bridge.PNRegistry
10
+ import com.pythonnative.runtime.bridge.TransactionApplier
11
+ import com.pythonnative.runtime.bridge.ViewRegistry
12
+ import com.pythonnative.runtime.modules.BuiltinModules
13
+ import com.pythonnative.runtime.modules.ModuleEnvelope
14
+ import com.pythonnative.runtime.modules.Promise
15
+ import org.json.JSONObject
16
+ import java.util.Locale
17
+
18
+ /**
19
+ * The single Java class Python touches.
20
+ *
21
+ * Every entry point mirrors one C symbol of the iOS bridge and is
22
+ * expected to run on the main thread (Python's asyncio loop lives
23
+ * there). Calls from other threads are logged but still executed so a
24
+ * misbehaving caller fails loudly in logcat instead of silently.
25
+ */
26
+ object PNBridge {
27
+ /** Logcat tag used by the whole runtime. */
28
+ const val TAG = "PythonNative"
29
+
30
+ /** Protocol version compiled into this library. */
31
+ const val PROTOCOL_VERSION = 1
32
+
33
+ private var host: PythonHost? = null
34
+ private var activity: Activity? = null
35
+ private val hostReadyListeners = ArrayList<Runnable>()
36
+
37
+ /** Tag to view bookkeeping shared by the applier and managers. */
38
+ val registry = ViewRegistry()
39
+
40
+ private val applier by lazy { TransactionApplier(registry) }
41
+
42
+ /** The protocol version Python must match before starting. */
43
+ @JvmStatic
44
+ fun protocolVersion(): Int = PROTOCOL_VERSION
45
+
46
+ /** Install the Python callback target; flushes callbacks queued before it existed. */
47
+ @JvmStatic
48
+ fun setHost(host: PythonHost) {
49
+ this.host = host
50
+ val pending = ArrayList(hostReadyListeners)
51
+ hostReadyListeners.clear()
52
+ for (listener in pending) {
53
+ try {
54
+ listener.run()
55
+ } catch (e: Exception) {
56
+ Log.e(TAG, "host-ready listener failed", e)
57
+ }
58
+ }
59
+ }
60
+
61
+ /** Whether a [PythonHost] has been installed. */
62
+ fun hasHost(): Boolean = host != null
63
+
64
+ /** Run `runnable` now if a host is installed, otherwise once [setHost] is called. */
65
+ fun runWhenHostReady(runnable: Runnable) {
66
+ if (host != null) runnable.run() else hostReadyListeners.add(runnable)
67
+ }
68
+
69
+ /** Record the current activity; modules and managers derive their `Context` from it. */
70
+ @JvmStatic
71
+ fun setContext(activity: Activity) {
72
+ this.activity = activity
73
+ PNRegistry.ensureBuiltins()
74
+ BuiltinModules.onContextAttached(activity)
75
+ }
76
+
77
+ /** Forget `activity` if it is the current one (called from `Activity.onDestroy`). */
78
+ @JvmStatic
79
+ fun clearContext(activity: Activity) {
80
+ if (this.activity === activity) this.activity = null
81
+ }
82
+
83
+ /** The current activity, or `null` before [setContext]. */
84
+ fun activity(): Activity? = activity
85
+
86
+ /** The context used to create views; throws before [setContext]. */
87
+ fun context(): Context =
88
+ activity ?: throw IllegalStateException("PNBridge.setContext(activity) has not been called")
89
+
90
+ /** Display density of the current context (1.0 when no context is attached). */
91
+ fun density(): Float {
92
+ val ctx = activity ?: return 1f
93
+ val d = ctx.resources.displayMetrics.density
94
+ return if (d > 0f) d else 1f
95
+ }
96
+
97
+ // ------------------------------------------------------------------
98
+ // Python -> native
99
+ // ------------------------------------------------------------------
100
+
101
+ /** Apply one serialized transaction (a JSON array of ops). */
102
+ @JvmStatic
103
+ fun apply(transactionJson: String) {
104
+ assertMain("apply")
105
+ applier.apply(transactionJson)
106
+ }
107
+
108
+ /**
109
+ * Measure the view with `tag` under the given constraints (dp; `1e6`
110
+ * means unconstrained) and return `"w,h"` in dp.
111
+ */
112
+ @JvmStatic
113
+ fun measure(tag: Long, maxWidth: Double, maxHeight: Double): String {
114
+ assertMain("measure")
115
+ val record = registry.get(tag)
116
+ if (record == null) {
117
+ Log.w(TAG, "measure: unknown tag $tag")
118
+ return "0,0"
119
+ }
120
+ val size = try {
121
+ record.manager.measure(record.view, maxWidth, maxHeight)
122
+ } catch (e: Exception) {
123
+ Log.e(TAG, "measure failed for ${record.typeName}#$tag", e)
124
+ floatArrayOf(0f, 0f)
125
+ }
126
+ return String.format(Locale.US, "%.3f,%.3f", size[0], size[1])
127
+ }
128
+
129
+ /** Run an imperative command on one view and return its JSON result. */
130
+ @JvmStatic
131
+ fun command(tag: Long, name: String, argsJson: String): String? {
132
+ assertMain("command")
133
+ val record = registry.get(tag) ?: return null
134
+ return try {
135
+ val args = if (argsJson.isBlank()) JSONObject() else JSONObject(argsJson)
136
+ JsonUtil.encode(record.manager.command(record.view, name, args))
137
+ } catch (e: Exception) {
138
+ Log.e(TAG, "command '$name' failed for ${record.typeName}#$tag", e)
139
+ null
140
+ }
141
+ }
142
+
143
+ /** Handle an animation request (`set`, `start`, or `cancel`) for one view. */
144
+ @JvmStatic
145
+ fun animate(tag: Long, requestJson: String): String? {
146
+ assertMain("animate")
147
+ val record = registry.get(tag) ?: return null
148
+ return try {
149
+ PNAnimator.handle(record, JSONObject(requestJson))
150
+ } catch (e: Exception) {
151
+ Log.e(TAG, "animate failed for ${record.typeName}#$tag: $requestJson", e)
152
+ null
153
+ }
154
+ }
155
+
156
+ /** Call a native module method; `argsJson` is `{"call_id": n, "args": {...}}`. */
157
+ @JvmStatic
158
+ fun call(module: String, method: String, argsJson: String): String? {
159
+ assertMain("call")
160
+ val (callId, args) = try {
161
+ ModuleEnvelope.decodeCall(argsJson)
162
+ } catch (e: Exception) {
163
+ return ModuleEnvelope.error("malformed call envelope: ${e.message}", "bad_request")
164
+ }
165
+ val target = PNRegistry.module(module)
166
+ ?: return ModuleEnvelope.error("unknown module '$module'", "unknown_module")
167
+ val promise = Promise(callId, module)
168
+ try {
169
+ target.call(method, args, promise)
170
+ } catch (e: Exception) {
171
+ Log.e(TAG, "module $module.$method threw", e)
172
+ if (!promise.isSettled) promise.reject(e.message ?: e.toString(), "exception")
173
+ }
174
+ return promise.markReturned()
175
+ }
176
+
177
+ // ------------------------------------------------------------------
178
+ // native -> Python
179
+ // ------------------------------------------------------------------
180
+
181
+ /**
182
+ * Deliver one callback to Python. Returns `null` when no host is
183
+ * installed or the Python handler raised (the error is logged).
184
+ */
185
+ fun callPython(kind: String, tag: Long, name: String, payloadJson: String): String? {
186
+ val target = host
187
+ if (target == null) {
188
+ Log.w(TAG, "callPython($kind, $tag, $name) dropped: no PythonHost installed")
189
+ return null
190
+ }
191
+ return try {
192
+ target.callback(kind, tag, name, payloadJson)
193
+ } catch (e: Exception) {
194
+ Log.e(TAG, "Python callback $kind/$name (tag $tag) raised", e)
195
+ null
196
+ }
197
+ }
198
+
199
+ // ------------------------------------------------------------------
200
+ // Threading helpers
201
+ // ------------------------------------------------------------------
202
+
203
+ /** Whether the caller is on the main thread. */
204
+ fun isMainThread(): Boolean = MainThread.isMain()
205
+
206
+ /** Post `runnable` to the main thread. */
207
+ fun post(runnable: Runnable) = MainThread.post(runnable)
208
+
209
+ private fun assertMain(entry: String) {
210
+ if (!MainThread.isMain()) {
211
+ Log.w(TAG, "PNBridge.$entry called off the main thread (${Thread.currentThread().name})")
212
+ }
213
+ }
214
+ }
@@ -0,0 +1,20 @@
1
+ package com.pythonnative.runtime
2
+
3
+ /**
4
+ * The reverse direction of the bridge: native to Python.
5
+ *
6
+ * The app template installs an implementation that forwards to
7
+ * `pythonnative.bridge.native_callback` through Chaquopy. The runtime
8
+ * library itself never touches Chaquopy, which keeps it unit-testable
9
+ * with plain JUnit.
10
+ *
11
+ * `kind` is one of `event`, `module`, `host`, `animation`, or `pump`;
12
+ * see the bridge protocol documentation for the payload of each.
13
+ */
14
+ interface PythonHost {
15
+ /**
16
+ * Deliver one callback to Python and return its (JSON-encoded)
17
+ * result, or `null` when the handler produced nothing.
18
+ */
19
+ fun callback(kind: String, tag: Long, name: String, payloadJson: String): String?
20
+ }
@@ -0,0 +1,141 @@
1
+ package com.pythonnative.runtime.animation
2
+
3
+ import org.json.JSONArray
4
+ import org.json.JSONObject
5
+ import kotlin.math.max
6
+ import kotlin.math.sqrt
7
+
8
+ /**
9
+ * Pure-Kotlin decoding of animation specs (`pn.Animated.timing`,
10
+ * `spring`, `decay`) and their mapping to Android animator parameters.
11
+ * Kept free of Android types so the math is unit-testable.
12
+ */
13
+ object AnimationSpecs {
14
+ /** `FlingAnimation` scales `friction` by this constant internally (`DragForce`). */
15
+ const val FLING_FRICTION_SCALE = 4.2
16
+
17
+ /** Easing curves identical to the Python ticker's `_EASINGS`. */
18
+ val EASINGS: Map<String, (Float) -> Float> = mapOf(
19
+ "linear" to { t -> t },
20
+ "ease" to { t -> 3f * t * t - 2f * t * t * t },
21
+ "ease_in" to { t -> t * t },
22
+ "ease_out" to { t -> 1f - (1f - t) * (1f - t) },
23
+ "ease_in_out" to { t -> 3f * t * t - 2f * t * t * t },
24
+ "ease_in_quad" to { t -> t * t },
25
+ "ease_out_quad" to { t -> 1f - (1f - t) * (1f - t) },
26
+ "bounce" to { t -> bounceOut(t) },
27
+ )
28
+
29
+ /** Decoded `timing` parameters. */
30
+ data class Timing(val from: Double, val to: Double, val durationMs: Long, val easing: Any?)
31
+
32
+ /** Decoded `spring` parameters mapped to `SpringForce` terms. */
33
+ data class Spring(
34
+ val from: Double,
35
+ val to: Double,
36
+ /** `stiffness / mass`, so the natural frequency matches the Python integrator. */
37
+ val stiffness: Double,
38
+ /** `damping / (2 * sqrt(stiffness * mass))`. */
39
+ val dampingRatio: Double,
40
+ /** Units per second. */
41
+ val initialVelocity: Double,
42
+ )
43
+
44
+ /** Decoded `decay` parameters mapped to `FlingAnimation` terms. */
45
+ data class Decay(
46
+ val from: Double,
47
+ /** Units per second (Python specs use units per millisecond). */
48
+ val startVelocity: Double,
49
+ /** `FlingAnimation.setFriction` value reproducing `v(t) = v0 * exp(-k * 1000 * t)`. */
50
+ val friction: Double,
51
+ /** Where the value settles: `from + v0 / (k * 1000)`. */
52
+ val projectedFinal: Double,
53
+ )
54
+
55
+ fun kind(spec: JSONObject): String = spec.optString("kind", "")
56
+
57
+ fun timing(spec: JSONObject): Timing = Timing(
58
+ from = spec.optDouble("from", 0.0),
59
+ to = spec.optDouble("to", 0.0),
60
+ durationMs = max(0L, spec.optDouble("duration_ms", 300.0).toLong()),
61
+ easing = spec.opt("easing"),
62
+ )
63
+
64
+ fun spring(spec: JSONObject): Spring {
65
+ val stiffness = max(1e-6, spec.optDouble("stiffness", 100.0))
66
+ val damping = max(0.0, spec.optDouble("damping", 10.0))
67
+ val mass = max(1e-6, spec.optDouble("mass", 1.0))
68
+ return Spring(
69
+ from = spec.optDouble("from", 0.0),
70
+ to = spec.optDouble("to", 0.0),
71
+ stiffness = stiffness / mass,
72
+ dampingRatio = dampingRatio(stiffness, damping, mass),
73
+ initialVelocity = spec.optDouble("initial_velocity", 0.0),
74
+ )
75
+ }
76
+
77
+ /** `damping / (2 * sqrt(stiffness * mass))`. */
78
+ fun dampingRatio(stiffness: Double, damping: Double, mass: Double): Double =
79
+ damping / (2.0 * sqrt(max(1e-12, stiffness * mass)))
80
+
81
+ fun decay(spec: JSONObject): Decay {
82
+ val v0PerMs = spec.optDouble("velocity", 0.0)
83
+ val k = max(1e-6, spec.optDouble("deceleration", 0.997))
84
+ val from = spec.optDouble("from", 0.0)
85
+ return Decay(
86
+ from = from,
87
+ startVelocity = v0PerMs * 1000.0,
88
+ friction = decayFriction(k),
89
+ projectedFinal = from + v0PerMs / (k * 1000.0),
90
+ )
91
+ }
92
+
93
+ /**
94
+ * The Python ticker decays velocity as `exp(-k * dt_ms)`, that is
95
+ * `exp(-k * 1000 * t_s)`. `FlingAnimation` decays as
96
+ * `exp(-4.2 * friction * t_s)`, so `friction = k * 1000 / 4.2`.
97
+ */
98
+ fun decayFriction(deceleration: Double): Double = deceleration * 1000.0 / FLING_FRICTION_SCALE
99
+
100
+ /**
101
+ * Resolve an easing value. Names map to [EASINGS]; a four-number
102
+ * array is returned as cubic-bezier control points (`DoubleArray`)
103
+ * for the caller to build a `PathInterpolator`. Unknown names fall
104
+ * back to `ease_in_out` like the Python ticker.
105
+ */
106
+ fun resolveEasing(easing: Any?): Any {
107
+ if (easing is JSONArray && easing.length() == 4) {
108
+ return DoubleArray(4) { easing.optDouble(it, 0.0) }
109
+ }
110
+ if (easing is String) {
111
+ val trimmed = easing.trim()
112
+ if (trimmed.startsWith("cubic-bezier(") || trimmed.startsWith("cubic_bezier(")) {
113
+ val inner = trimmed.substringAfter('(').substringBeforeLast(')')
114
+ val parts = inner.split(',').mapNotNull { it.trim().toDoubleOrNull() }
115
+ if (parts.size == 4) return parts.toDoubleArray()
116
+ }
117
+ return EASINGS[trimmed] ?: EASINGS.getValue("ease_in_out")
118
+ }
119
+ return EASINGS.getValue("ease_in_out")
120
+ }
121
+
122
+ private fun bounceOut(t: Float): Float {
123
+ val n1 = 7.5625f
124
+ val d1 = 2.75f
125
+ return when {
126
+ t < 1f / d1 -> n1 * t * t
127
+ t < 2f / d1 -> {
128
+ val u = t - 1.5f / d1
129
+ n1 * u * u + 0.75f
130
+ }
131
+ t < 2.5f / d1 -> {
132
+ val u = t - 2.25f / d1
133
+ n1 * u * u + 0.9375f
134
+ }
135
+ else -> {
136
+ val u = t - 2.625f / d1
137
+ n1 * u * u + 0.984375f
138
+ }
139
+ }
140
+ }
141
+ }
@@ -0,0 +1,249 @@
1
+ package com.pythonnative.runtime.animation
2
+
3
+ import android.animation.Animator
4
+ import android.animation.AnimatorListenerAdapter
5
+ import android.animation.ArgbEvaluator
6
+ import android.animation.TimeInterpolator
7
+ import android.animation.ValueAnimator
8
+ import android.view.View
9
+ import android.view.animation.PathInterpolator
10
+ import android.widget.TextView
11
+ import androidx.dynamicanimation.animation.DynamicAnimation
12
+ import androidx.dynamicanimation.animation.FlingAnimation
13
+ import androidx.dynamicanimation.animation.FloatPropertyCompat
14
+ import androidx.dynamicanimation.animation.SpringAnimation
15
+ import androidx.dynamicanimation.animation.SpringForce
16
+ import com.pythonnative.runtime.PNBridge
17
+ import com.pythonnative.runtime.bridge.JsonUtil
18
+ import com.pythonnative.runtime.bridge.PNLog
19
+ import com.pythonnative.runtime.bridge.ViewRecord
20
+ import com.pythonnative.runtime.components.PNColor
21
+ import com.pythonnative.runtime.components.ViewStyler
22
+ import org.json.JSONObject
23
+
24
+ /**
25
+ * Native driver for `animate` requests on the animatable props
26
+ * (`opacity`, `background_color`, `translate_x`, `translate_y`,
27
+ * `scale`, `scale_x`, `scale_y`, `rotate`, plus `color` for text).
28
+ *
29
+ * `timing` runs on a `ValueAnimator`, `spring` on a `SpringAnimation`,
30
+ * and `decay` on a `FlingAnimation`. Completion is reported with
31
+ * `callback("animation", 0, "", {"id": n, "finished": bool})`.
32
+ */
33
+ object PNAnimator {
34
+ private class Running(val view: View, val prop: String, val cancel: () -> Unit)
35
+
36
+ private val running = HashMap<Long, Running>()
37
+
38
+ /** Handle one request object; returns the JSON reply string per the spec. */
39
+ fun handle(record: ViewRecord, request: JSONObject): String? {
40
+ val view = record.view
41
+ return when (request.optString("op")) {
42
+ "set" -> {
43
+ record.manager.setAnimatedProperty(view, request.optString("prop"), request.opt("value"))
44
+ null
45
+ }
46
+ "start" -> {
47
+ val id = request.optLong("id")
48
+ val spec = request.optJSONObject("spec") ?: JSONObject()
49
+ val ok = record.manager.startAnimation(view, id, request.optString("prop"), spec)
50
+ JSONObject().put("ok", ok).toString()
51
+ }
52
+ "cancel" -> {
53
+ val value = record.manager.cancelAnimation(view, request.optLong("id"))
54
+ if (value == null) null else JSONObject().put("value", JsonUtil.wrap(value)).toString()
55
+ }
56
+ else -> null
57
+ }
58
+ }
59
+
60
+ // ------------------------------------------------------------------
61
+ // Property access
62
+ // ------------------------------------------------------------------
63
+
64
+ /** Apply one animation frame to `view`. Unknown props are ignored. */
65
+ fun setProperty(view: View, prop: String, value: Any?) {
66
+ if (JsonUtil.isNull(value)) return
67
+ try {
68
+ when (prop) {
69
+ "opacity" -> view.alpha = JsonUtil.toDouble(value).toFloat()
70
+ "translate_x" -> view.translationX = px(JsonUtil.toDouble(value))
71
+ "translate_y" -> view.translationY = px(JsonUtil.toDouble(value))
72
+ "scale" -> {
73
+ val s = JsonUtil.toDouble(value).toFloat()
74
+ view.scaleX = s
75
+ view.scaleY = s
76
+ }
77
+ "scale_x" -> view.scaleX = JsonUtil.toDouble(value).toFloat()
78
+ "scale_y" -> view.scaleY = JsonUtil.toDouble(value).toFloat()
79
+ "rotate" -> view.rotation = ViewStyler.angleDegrees(value)
80
+ "background_color" -> ViewStyler.setAnimatedBackground(view, value)
81
+ "color" -> (view as? TextView)?.setTextColor(PNColor.parseOr(value, 0xFF000000.toInt()))
82
+ }
83
+ } catch (e: Exception) {
84
+ PNLog.swallowed("PNAnimator.setProperty", e)
85
+ }
86
+ }
87
+
88
+ /** Read the current (presentation) value of an animatable prop, or `null`. */
89
+ fun readProperty(view: View, prop: String): Any? = when (prop) {
90
+ "opacity" -> view.alpha.toDouble()
91
+ "translate_x" -> view.translationX / PNBridge.density().toDouble()
92
+ "translate_y" -> view.translationY / PNBridge.density().toDouble()
93
+ "scale", "scale_x" -> view.scaleX.toDouble()
94
+ "scale_y" -> view.scaleY.toDouble()
95
+ "rotate" -> view.rotation.toDouble()
96
+ else -> null
97
+ }
98
+
99
+ // ------------------------------------------------------------------
100
+ // Start / cancel
101
+ // ------------------------------------------------------------------
102
+
103
+ /** Start a native animation; `false` means Python should tick it instead. */
104
+ fun start(view: View, id: Long, prop: String, spec: JSONObject): Boolean {
105
+ cancelQuietly(id)
106
+ return try {
107
+ when (AnimationSpecs.kind(spec)) {
108
+ "timing" -> startTiming(view, id, prop, spec)
109
+ "spring" -> startSpring(view, id, prop, spec)
110
+ "decay" -> startDecay(view, id, prop, spec)
111
+ else -> false
112
+ }
113
+ } catch (e: Exception) {
114
+ PNLog.rateLimited("anim-start", "native animation failed for '$prop'", e)
115
+ running.remove(id)
116
+ false
117
+ }
118
+ }
119
+
120
+ /** Cancel `id` and return the presentation value of its prop, if readable. */
121
+ fun cancel(id: Long): Any? {
122
+ val entry = running.remove(id) ?: return null
123
+ try {
124
+ entry.cancel()
125
+ } catch (e: Exception) {
126
+ PNLog.swallowed("PNAnimator.cancel", e)
127
+ }
128
+ return readProperty(entry.view, entry.prop)
129
+ }
130
+
131
+ private fun cancelQuietly(id: Long) {
132
+ running.remove(id)?.let { runCatching { it.cancel() } }
133
+ }
134
+
135
+ private fun complete(id: Long, finished: Boolean) {
136
+ if (running.remove(id) == null) return
137
+ PNBridge.callPython("animation", 0, "", JSONObject().put("id", id).put("finished", finished).toString())
138
+ }
139
+
140
+ private fun startTiming(view: View, id: Long, prop: String, spec: JSONObject): Boolean {
141
+ val timing = AnimationSpecs.timing(spec)
142
+ val animator: ValueAnimator
143
+ if (prop == "background_color") {
144
+ val from = PNColor.parse(spec.opt("from")) ?: return false
145
+ val to = PNColor.parse(spec.opt("to")) ?: return false
146
+ animator = ValueAnimator.ofObject(ArgbEvaluator(), from, to)
147
+ animator.addUpdateListener { ViewStyler.setAnimatedBackground(view, it.animatedValue as Int) }
148
+ } else if (prop == "color") {
149
+ val from = PNColor.parse(spec.opt("from")) ?: return false
150
+ val to = PNColor.parse(spec.opt("to")) ?: return false
151
+ animator = ValueAnimator.ofObject(ArgbEvaluator(), from, to)
152
+ animator.addUpdateListener { (view as? TextView)?.setTextColor(it.animatedValue as Int) }
153
+ } else {
154
+ if (!isFloatProp(prop)) return false
155
+ animator = ValueAnimator.ofFloat(timing.from.toFloat(), timing.to.toFloat())
156
+ animator.addUpdateListener { setProperty(view, prop, it.animatedValue as Float) }
157
+ }
158
+ animator.duration = timing.durationMs
159
+ animator.interpolator = interpolator(timing.easing)
160
+ var cancelled = false
161
+ animator.addListener(object : AnimatorListenerAdapter() {
162
+ override fun onAnimationCancel(animation: Animator) {
163
+ cancelled = true
164
+ }
165
+
166
+ override fun onAnimationEnd(animation: Animator) {
167
+ complete(id, !cancelled)
168
+ }
169
+ })
170
+ running[id] = Running(view, prop) { animator.cancel() }
171
+ animator.start()
172
+ return true
173
+ }
174
+
175
+ private fun startSpring(view: View, id: Long, prop: String, spec: JSONObject): Boolean {
176
+ val params = AnimationSpecs.spring(spec)
177
+ val property = floatProperty(prop) ?: return false
178
+ val scale = unitScale(prop)
179
+ val anim = SpringAnimation(view, property, (params.to * scale).toFloat())
180
+ anim.spring = SpringForce((params.to * scale).toFloat())
181
+ .setStiffness(params.stiffness.toFloat())
182
+ .setDampingRatio(params.dampingRatio.toFloat())
183
+ anim.setStartValue((params.from * scale).toFloat())
184
+ anim.setStartVelocity((params.initialVelocity * scale).toFloat())
185
+ anim.addEndListener { _, canceled, _, _ -> complete(id, !canceled) }
186
+ running[id] = Running(view, prop) { anim.cancel() }
187
+ anim.start()
188
+ return true
189
+ }
190
+
191
+ private fun startDecay(view: View, id: Long, prop: String, spec: JSONObject): Boolean {
192
+ val params = AnimationSpecs.decay(spec)
193
+ val property = floatProperty(prop) ?: return false
194
+ val scale = unitScale(prop)
195
+ val anim = FlingAnimation(view, property)
196
+ anim.setStartValue((params.from * scale).toFloat())
197
+ anim.setStartVelocity((params.startVelocity * scale).toFloat())
198
+ anim.friction = params.friction.toFloat()
199
+ anim.addEndListener { _, canceled, _, _ -> complete(id, !canceled) }
200
+ running[id] = Running(view, prop) { anim.cancel() }
201
+ anim.start()
202
+ return true
203
+ }
204
+
205
+ // ------------------------------------------------------------------
206
+ // Helpers
207
+ // ------------------------------------------------------------------
208
+
209
+ private fun px(dp: Double): Float = (dp * PNBridge.density()).toFloat()
210
+
211
+ private fun isFloatProp(prop: String): Boolean =
212
+ prop in setOf("opacity", "translate_x", "translate_y", "scale", "scale_x", "scale_y", "rotate")
213
+
214
+ /** Multiplier from spec units to native units (dp to px for translations). */
215
+ private fun unitScale(prop: String): Double =
216
+ if (prop == "translate_x" || prop == "translate_y") PNBridge.density().toDouble() else 1.0
217
+
218
+ /** A `FloatPropertyCompat` for physics animations; `null` for unsupported props. */
219
+ private fun floatProperty(prop: String): FloatPropertyCompat<View>? = when (prop) {
220
+ "opacity" -> DynamicAnimation.ALPHA
221
+ "translate_x" -> DynamicAnimation.TRANSLATION_X
222
+ "translate_y" -> DynamicAnimation.TRANSLATION_Y
223
+ "scale_x" -> DynamicAnimation.SCALE_X
224
+ "scale_y" -> DynamicAnimation.SCALE_Y
225
+ "rotate" -> DynamicAnimation.ROTATION
226
+ "scale" -> object : FloatPropertyCompat<View>("pnScale") {
227
+ override fun getValue(v: View): Float = v.scaleX
228
+ override fun setValue(v: View, value: Float) {
229
+ v.scaleX = value
230
+ v.scaleY = value
231
+ }
232
+ }
233
+ // Color props are not floats; physics specs on them fall back to the Python ticker.
234
+ else -> null
235
+ }
236
+
237
+ private fun interpolator(easing: Any?): TimeInterpolator {
238
+ return when (val resolved = AnimationSpecs.resolveEasing(easing)) {
239
+ is DoubleArray -> PathInterpolator(
240
+ resolved[0].toFloat(), resolved[1].toFloat(), resolved[2].toFloat(), resolved[3].toFloat(),
241
+ )
242
+ else -> {
243
+ @Suppress("UNCHECKED_CAST")
244
+ val fn = resolved as (Float) -> Float
245
+ TimeInterpolator { t -> fn(t) }
246
+ }
247
+ }
248
+ }
249
+ }