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,141 @@
1
+ package com.pythonnative.runtime.components
2
+
3
+ import android.content.Context
4
+ import android.graphics.Bitmap
5
+ import android.graphics.BitmapFactory
6
+ import android.util.LruCache
7
+ import com.pythonnative.runtime.bridge.MainThread
8
+ import java.io.File
9
+ import java.io.FileOutputStream
10
+ import java.net.HttpURLConnection
11
+ import java.net.URL
12
+ import java.security.MessageDigest
13
+ import java.util.concurrent.Executors
14
+ import kotlin.math.max
15
+
16
+ /**
17
+ * Image fetching and decoding for the `Image` manager.
18
+ *
19
+ * Remote images are downloaded on a small background pool into a disk
20
+ * cache under `cacheDir/pn_images` (keyed by the SHA-256 of the URL),
21
+ * decoded with `inSampleSize` downsampling to the target size, and kept
22
+ * in a memory `LruCache`. Callbacks run on the main thread.
23
+ */
24
+ object ImageLoader {
25
+ private val executor = Executors.newFixedThreadPool(3) { runnable ->
26
+ Thread(runnable, "pn-image").apply { isDaemon = true }
27
+ }
28
+
29
+ private val memory: LruCache<String, Bitmap> by lazy {
30
+ val maxKb = (Runtime.getRuntime().maxMemory() / 1024L).toInt()
31
+ object : LruCache<String, Bitmap>(max(4 * 1024, maxKb / 8)) {
32
+ override fun sizeOf(key: String, value: Bitmap): Int = max(1, value.byteCount / 1024)
33
+ }
34
+ }
35
+
36
+ /** Result callback: exactly one of `bitmap` or `error` is non-null. */
37
+ fun interface Callback {
38
+ fun onResult(bitmap: Bitmap?, error: String?)
39
+ }
40
+
41
+ /** Load `url` (http/https) into a bitmap sized for `targetW` x `targetH` pixels. */
42
+ fun loadRemote(context: Context, url: String, targetW: Int, targetH: Int, callback: Callback) {
43
+ val key = "$url@$targetW"
44
+ memory.get(key)?.let {
45
+ callback.onResult(it, null)
46
+ return
47
+ }
48
+ val cacheDir = File(context.cacheDir, "pn_images")
49
+ executor.execute {
50
+ try {
51
+ val file = cachedFile(cacheDir, url)
52
+ if (!file.exists() || file.length() == 0L) download(url, file)
53
+ val bitmap = decodeDownsampled(file.absolutePath, targetW, targetH)
54
+ if (bitmap == null) {
55
+ file.delete()
56
+ deliver(callback, null, "decode failed")
57
+ } else {
58
+ memory.put(key, bitmap)
59
+ deliver(callback, bitmap, null)
60
+ }
61
+ } catch (e: Exception) {
62
+ deliver(callback, null, e.message ?: e.javaClass.simpleName)
63
+ }
64
+ }
65
+ }
66
+
67
+ /** Decode a local file on a background thread. */
68
+ fun loadFile(path: String, targetW: Int, targetH: Int, callback: Callback) {
69
+ executor.execute {
70
+ val bitmap = try {
71
+ decodeDownsampled(path, targetW, targetH)
72
+ } catch (e: Exception) {
73
+ null
74
+ }
75
+ deliver(callback, bitmap, if (bitmap == null) "decode failed" else null)
76
+ }
77
+ }
78
+
79
+ /** Decode `path` with `inSampleSize` chosen so the result still covers the target. */
80
+ fun decodeDownsampled(path: String, targetW: Int, targetH: Int): Bitmap? {
81
+ val bounds = BitmapFactory.Options().apply { inJustDecodeBounds = true }
82
+ BitmapFactory.decodeFile(path, bounds)
83
+ val srcW = bounds.outWidth
84
+ val srcH = bounds.outHeight
85
+ if (srcW <= 0 || srcH <= 0) return null
86
+ var tw = if (targetW > 0) targetW else srcW
87
+ var th = if (targetH > 0) targetH else tw
88
+ if (tw <= 0) tw = srcW
89
+ if (th <= 0) th = srcH
90
+ var sample = 1
91
+ while (srcW / (sample * 2) >= tw && srcH / (sample * 2) >= th) sample *= 2
92
+ val opts = BitmapFactory.Options().apply { inSampleSize = sample }
93
+ return BitmapFactory.decodeFile(path, opts)
94
+ }
95
+
96
+ private fun cachedFile(dir: File, url: String): File {
97
+ if (!dir.exists()) dir.mkdirs()
98
+ val digest = MessageDigest.getInstance("SHA-256").digest(url.toByteArray(Charsets.UTF_8))
99
+ val hex = digest.joinToString("") { String.format("%02x", it) }
100
+ val path = url.substringBefore('?').substringBefore('#')
101
+ val ext = path.substringAfterLast('.', "").takeIf { it.length in 1..5 && it.all { c -> c.isLetterOrDigit() } }
102
+ return File(dir, if (ext != null) "$hex.$ext" else hex)
103
+ }
104
+
105
+ private fun download(url: String, target: File) {
106
+ var current = url
107
+ var redirects = 0
108
+ while (true) {
109
+ val connection = URL(current).openConnection() as HttpURLConnection
110
+ connection.connectTimeout = 15_000
111
+ connection.readTimeout = 30_000
112
+ connection.instanceFollowRedirects = true
113
+ connection.setRequestProperty("User-Agent", "PythonNative/Android")
114
+ try {
115
+ val code = connection.responseCode
116
+ if (code in 300..399 && redirects < 5) {
117
+ val location = connection.getHeaderField("Location") ?: throw IllegalStateException("redirect without Location")
118
+ current = URL(URL(current), location).toString()
119
+ redirects++
120
+ continue
121
+ }
122
+ if (code !in 200..299) throw IllegalStateException("HTTP $code")
123
+ val tmp = File(target.parentFile, target.name + ".part")
124
+ connection.inputStream.use { input ->
125
+ FileOutputStream(tmp).use { output -> input.copyTo(output) }
126
+ }
127
+ if (!tmp.renameTo(target)) {
128
+ tmp.copyTo(target, overwrite = true)
129
+ tmp.delete()
130
+ }
131
+ return
132
+ } finally {
133
+ connection.disconnect()
134
+ }
135
+ }
136
+ }
137
+
138
+ private fun deliver(callback: Callback, bitmap: Bitmap?, error: String?) {
139
+ MainThread.post { callback.onResult(bitmap, error) }
140
+ }
141
+ }
@@ -0,0 +1,156 @@
1
+ package com.pythonnative.runtime.components
2
+
3
+ import android.content.Context
4
+ import android.content.res.ColorStateList
5
+ import android.graphics.BitmapFactory
6
+ import android.util.Base64
7
+ import android.view.View
8
+ import android.widget.ImageView
9
+ import com.pythonnative.runtime.bridge.PNLog
10
+ import com.pythonnative.runtime.bridge.str
11
+ import com.pythonnative.runtime.bridge.value
12
+ import org.json.JSONObject
13
+
14
+ /**
15
+ * `Image` element. Sources: `http(s)` URLs (downloaded and cached by
16
+ * [ImageLoader]), absolute file paths, drawable resource names, and
17
+ * base64 `data:` URIs. Fires `on_load` / `on_error`.
18
+ */
19
+ class ImageManager : ComponentManager() {
20
+ override fun createView(context: Context, tag: Long, props: JSONObject): View = ImageView(context)
21
+
22
+ override fun applyProps(view: View, props: JSONObject, initial: Boolean) {
23
+ val iv = view as ImageView
24
+ PNColor.parse(props.value("tint_color"))?.let { iv.imageTintList = ColorStateList.valueOf(it) }
25
+ if (props.has("tint_color") && props.value("tint_color") == null) iv.imageTintList = null
26
+ PNColor.parse(props.value("placeholder_color"))?.let { iv.setBackgroundColor(it) }
27
+ if (props.has("source")) {
28
+ val source = sourceString(props.value("source"))
29
+ if (source != null) loadSource(iv, source) else iv.setImageDrawable(null)
30
+ }
31
+ val mode = props.str("resize_mode") ?: props.str("scale_type")
32
+ if (mode != null) {
33
+ iv.scaleType = when (mode) {
34
+ "cover" -> ImageView.ScaleType.CENTER_CROP
35
+ "contain" -> ImageView.ScaleType.FIT_CENTER
36
+ "stretch" -> ImageView.ScaleType.FIT_XY
37
+ "center", "repeat" -> ImageView.ScaleType.CENTER
38
+ else -> iv.scaleType
39
+ }
40
+ }
41
+ ViewStyler.apply(iv, props)
42
+ }
43
+
44
+ private fun sourceString(value: Any?): String? {
45
+ return when (value) {
46
+ null -> null
47
+ is JSONObject -> value.str("uri") ?: value.str("url") ?: value.str("path")
48
+ else -> value.toString().takeIf { it.isNotEmpty() }
49
+ }
50
+ }
51
+
52
+ private fun loadSource(iv: ImageView, source: String) {
53
+ val state = stateOf(iv)
54
+ state["pending_uri"] = source
55
+ try {
56
+ when {
57
+ source.startsWith("data:") -> loadDataUri(iv, source)
58
+ source.startsWith("http://") || source.startsWith("https://") -> {
59
+ val (tw, th) = targetSize(iv)
60
+ ImageLoader.loadRemote(iv.context, source, tw, th) { bitmap, error ->
61
+ if (stateOf(iv)["pending_uri"] != source) return@loadRemote
62
+ if (bitmap != null) {
63
+ iv.setImageBitmap(bitmap)
64
+ fire(iv, "on_load")
65
+ } else {
66
+ fire(iv, "on_error", error ?: "load failed")
67
+ }
68
+ }
69
+ }
70
+ source.startsWith("/") || source.startsWith("file://") -> {
71
+ val path = source.removePrefix("file://")
72
+ val (tw, th) = targetSize(iv)
73
+ ImageLoader.loadFile(path, tw, th) { bitmap, error ->
74
+ if (stateOf(iv)["pending_uri"] != source) return@loadFile
75
+ if (bitmap != null) {
76
+ iv.setImageBitmap(bitmap)
77
+ fire(iv, "on_load")
78
+ } else {
79
+ fire(iv, "on_error", error ?: "decode failed")
80
+ }
81
+ }
82
+ }
83
+ else -> {
84
+ val ctx = iv.context
85
+ val name = source.substringBeforeLast('.', source)
86
+ val resId = ctx.resources.getIdentifier(name, "drawable", ctx.packageName)
87
+ .takeIf { it != 0 }
88
+ ?: ctx.resources.getIdentifier(name, "mipmap", ctx.packageName)
89
+ if (resId != 0) {
90
+ iv.setImageResource(resId)
91
+ fire(iv, "on_load")
92
+ } else {
93
+ fire(iv, "on_error", "drawable '$name' not found")
94
+ }
95
+ }
96
+ }
97
+ } catch (e: Exception) {
98
+ PNLog.swallowed("ImageManager.loadSource", e)
99
+ fire(iv, "on_error", e.message ?: "load failed")
100
+ }
101
+ }
102
+
103
+ private fun loadDataUri(iv: ImageView, source: String) {
104
+ try {
105
+ val payload = source.substringAfter(',', "")
106
+ val raw = Base64.decode(payload, Base64.DEFAULT)
107
+ val bitmap = BitmapFactory.decodeByteArray(raw, 0, raw.size)
108
+ if (bitmap != null) {
109
+ iv.setImageBitmap(bitmap)
110
+ fire(iv, "on_load")
111
+ } else {
112
+ fire(iv, "on_error", "data URI decode failed")
113
+ }
114
+ } catch (e: Exception) {
115
+ fire(iv, "on_error", "data URI decode failed")
116
+ }
117
+ }
118
+
119
+ private fun targetSize(iv: ImageView): Pair<Int, Int> {
120
+ var w = iv.width
121
+ var h = iv.height
122
+ val frame = recordOf(iv)?.frame
123
+ if (w <= 0 && frame != null) {
124
+ w = px(frame[2])
125
+ h = px(frame[3])
126
+ }
127
+ if (w <= 0) {
128
+ val metrics = iv.context.resources.displayMetrics
129
+ w = metrics.widthPixels
130
+ h = metrics.heightPixels
131
+ }
132
+ if (h <= 0) h = w
133
+ return Pair(w, h)
134
+ }
135
+
136
+ override fun measure(view: View, maxWidth: Double, maxHeight: Double): FloatArray {
137
+ val iv = view as ImageView
138
+ val drawable = iv.drawable ?: return floatArrayOf(0f, 0f)
139
+ val density = view.context.resources.displayMetrics.density
140
+ // Bitmaps decode at device density, so their pixel size is already a dp-ish measure.
141
+ var w = drawable.intrinsicWidth / density
142
+ var h = drawable.intrinsicHeight / density
143
+ if (w <= 0f || h <= 0f) return floatArrayOf(0f, 0f)
144
+ val maxW = if (maxWidth.isFinite() && maxWidth < 1e6) maxWidth.toFloat() else Float.MAX_VALUE
145
+ val maxH = if (maxHeight.isFinite() && maxHeight < 1e6) maxHeight.toFloat() else Float.MAX_VALUE
146
+ if (w > maxW) {
147
+ h *= maxW / w
148
+ w = maxW
149
+ }
150
+ if (h > maxH) {
151
+ w *= maxH / h
152
+ h = maxH
153
+ }
154
+ return floatArrayOf(w, h)
155
+ }
156
+ }
@@ -0,0 +1,134 @@
1
+ package com.pythonnative.runtime.components
2
+
3
+ import android.app.Dialog
4
+ import android.content.Context
5
+ import android.graphics.drawable.ColorDrawable
6
+ import android.view.View
7
+ import android.view.ViewGroup
8
+ import android.view.WindowManager
9
+ import android.widget.FrameLayout
10
+ import com.pythonnative.runtime.bridge.JsonUtil
11
+ import com.pythonnative.runtime.bridge.str
12
+ import com.pythonnative.runtime.bridge.value
13
+ import org.json.JSONObject
14
+
15
+ /**
16
+ * `Modal` element: real modal presentation backed by a `Dialog`.
17
+ *
18
+ * The on-tree placeholder is a hidden `View`. When `visible` flips to
19
+ * `true`, a `Dialog` hosting a `FrameLayout` is shown and the
20
+ * reconciler's child inserts are forwarded into that content view.
21
+ */
22
+ class ModalManager : ComponentManager() {
23
+ override fun createView(context: Context, tag: Long, props: JSONObject): View {
24
+ val placeholder = View(context)
25
+ placeholder.visibility = View.GONE
26
+ return placeholder
27
+ }
28
+
29
+ override fun applyProps(view: View, props: JSONObject, initial: Boolean) {
30
+ val state = stateOf(view)
31
+ // Only react when `visible` itself changed; a re-render while the
32
+ // dialog is open must not tear it down.
33
+ if (props.has("visible")) {
34
+ val visible = JsonUtil.truthy(props.value("visible"))
35
+ if (visible && state["dialog"] == null) present(view)
36
+ else if (!visible && state["dialog"] != null) dismiss(view, fromUser = false)
37
+ }
38
+ val dialog = state["dialog"] as? Dialog
39
+ if (dialog != null && props.has("dismiss_on_backdrop")) {
40
+ dialog.setCanceledOnTouchOutside(props.value("dismiss_on_backdrop") != false)
41
+ }
42
+ }
43
+
44
+ override fun insertChild(parent: View, child: View, index: Int) {
45
+ val state = stateOf(parent)
46
+ val content = state["content_view"] as? FrameLayout
47
+ if (content != null) {
48
+ ViewChildren.insert(content, child, index)
49
+ } else {
50
+ @Suppress("UNCHECKED_CAST")
51
+ val pending = state.getOrPut("pending") { ArrayList<View>() } as ArrayList<View>
52
+ pending.remove(child)
53
+ pending.add(index.coerceIn(0, pending.size), child)
54
+ }
55
+ }
56
+
57
+ override fun removeChild(parent: View, child: View) {
58
+ val state = stateOf(parent)
59
+ val content = state["content_view"] as? FrameLayout
60
+ if (content != null) {
61
+ content.removeView(child)
62
+ } else {
63
+ @Suppress("UNCHECKED_CAST")
64
+ (state["pending"] as? ArrayList<View>)?.remove(child)
65
+ }
66
+ }
67
+
68
+ override fun setFrame(view: View, x: Double, y: Double, width: Double, height: Double) {
69
+ // The dialog owns its own window; the layout engine frames only the children.
70
+ }
71
+
72
+ override fun teardown(view: View) {
73
+ if (stateOf(view)["dialog"] != null) dismiss(view, fromUser = false)
74
+ }
75
+
76
+ private fun present(placeholder: View) {
77
+ val state = stateOf(placeholder)
78
+ val props = propsOf(placeholder)
79
+ val ctx = placeholder.context
80
+ val dialog = Dialog(ctx)
81
+ val content = FrameLayout(ctx)
82
+ val presentation = props.str("presentation_style") ?: "page_sheet"
83
+ val isOverlay = presentation == "overlay" || JsonUtil.truthy(props.value("transparent"))
84
+ content.setBackgroundColor(if (isOverlay) 0x00FFFFFF else 0xFFFFFFFF.toInt())
85
+ dialog.setContentView(content, ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT))
86
+ dialog.window?.let { window ->
87
+ if (isOverlay) {
88
+ window.setBackgroundDrawable(ColorDrawable(0x00000000))
89
+ window.setDimAmount(0.5f)
90
+ window.setLayout(WindowManager.LayoutParams.MATCH_PARENT, WindowManager.LayoutParams.MATCH_PARENT)
91
+ } else {
92
+ window.setLayout(WindowManager.LayoutParams.MATCH_PARENT, WindowManager.LayoutParams.MATCH_PARENT)
93
+ }
94
+ val animation = props.str("animation_type") ?: props.str("animation")
95
+ when (animation) {
96
+ "none" -> window.setWindowAnimations(0)
97
+ "fade" -> window.setWindowAnimations(android.R.style.Animation_Toast)
98
+ "slide" -> window.setWindowAnimations(android.R.style.Animation_InputMethod)
99
+ }
100
+ PNColor.parse(props.value("status_bar_color"))?.let { window.statusBarColor = it }
101
+ }
102
+ dialog.setCanceledOnTouchOutside(props.value("dismiss_on_backdrop") != false)
103
+ dialog.setCancelable(props.value("dismissable") != false)
104
+ state["dialog"] = dialog
105
+ state["content_view"] = content
106
+ @Suppress("UNCHECKED_CAST")
107
+ val pending = state.remove("pending") as? ArrayList<View>
108
+ pending?.forEach { child ->
109
+ (child.parent as? ViewGroup)?.removeView(child)
110
+ content.addView(child)
111
+ }
112
+ dialog.setOnShowListener { fire(placeholder, "on_show") }
113
+ dialog.setOnDismissListener {
114
+ // Programmatic dismiss (visible=false) also lands here; the
115
+ // Python side expects on_dismiss only for user-driven closes,
116
+ // but android.py fired it unconditionally, so keep that.
117
+ if (stateOf(placeholder)["dialog"] === dialog) {
118
+ stateOf(placeholder).remove("dialog")
119
+ stateOf(placeholder).remove("content_view")
120
+ }
121
+ fire(placeholder, "on_dismiss")
122
+ if (hasEvent(placeholder, "on_request_close")) fire(placeholder, "on_request_close")
123
+ }
124
+ dialog.show()
125
+ }
126
+
127
+ private fun dismiss(placeholder: View, fromUser: Boolean) {
128
+ val state = stateOf(placeholder)
129
+ val dialog = state.remove("dialog") as? Dialog
130
+ state.remove("content_view")
131
+ if (!fromUser) dialog?.setOnDismissListener(null)
132
+ dialog?.dismiss()
133
+ }
134
+ }
@@ -0,0 +1,201 @@
1
+ package com.pythonnative.runtime.components
2
+
3
+ import org.json.JSONArray
4
+ import org.json.JSONObject
5
+
6
+ /**
7
+ * Color parsing shared by every manager.
8
+ *
9
+ * Accepts `#RGB`, `#RGBA`, `#RRGGBB`, `#AARRGGBB` (the Android/Python
10
+ * convention used by `parse_color_int`), `rgb(r, g, b)`,
11
+ * `rgba(r, g, b, a)` with `a` in `0..1`, CSS named colors, packed
12
+ * integers (`0xRRGGBB` gains full alpha), and `[r, g, b(, a)]` arrays.
13
+ * Results are signed 32-bit ARGB ints suitable for Android APIs.
14
+ */
15
+ object PNColor {
16
+ private val named: Map<String, Int> = mapOf(
17
+ "transparent" to 0x00000000,
18
+ "clear" to 0x00000000,
19
+ "black" to 0xFF000000.toInt(),
20
+ "white" to 0xFFFFFFFF.toInt(),
21
+ "red" to 0xFFFF0000.toInt(),
22
+ "green" to 0xFF008000.toInt(),
23
+ "lime" to 0xFF00FF00.toInt(),
24
+ "blue" to 0xFF0000FF.toInt(),
25
+ "yellow" to 0xFFFFFF00.toInt(),
26
+ "cyan" to 0xFF00FFFF.toInt(),
27
+ "aqua" to 0xFF00FFFF.toInt(),
28
+ "magenta" to 0xFFFF00FF.toInt(),
29
+ "fuchsia" to 0xFFFF00FF.toInt(),
30
+ "gray" to 0xFF808080.toInt(),
31
+ "grey" to 0xFF808080.toInt(),
32
+ "darkgray" to 0xFFA9A9A9.toInt(),
33
+ "darkgrey" to 0xFFA9A9A9.toInt(),
34
+ "lightgray" to 0xFFD3D3D3.toInt(),
35
+ "lightgrey" to 0xFFD3D3D3.toInt(),
36
+ "dimgray" to 0xFF696969.toInt(),
37
+ "dimgrey" to 0xFF696969.toInt(),
38
+ "silver" to 0xFFC0C0C0.toInt(),
39
+ "maroon" to 0xFF800000.toInt(),
40
+ "olive" to 0xFF808000.toInt(),
41
+ "navy" to 0xFF000080.toInt(),
42
+ "teal" to 0xFF008080.toInt(),
43
+ "purple" to 0xFF800080.toInt(),
44
+ "orange" to 0xFFFFA500.toInt(),
45
+ "pink" to 0xFFFFC0CB.toInt(),
46
+ "brown" to 0xFFA52A2A.toInt(),
47
+ "gold" to 0xFFFFD700.toInt(),
48
+ "indigo" to 0xFF4B0082.toInt(),
49
+ "violet" to 0xFFEE82EE.toInt(),
50
+ "coral" to 0xFFFF7F50.toInt(),
51
+ "salmon" to 0xFFFA8072.toInt(),
52
+ "tomato" to 0xFFFF6347.toInt(),
53
+ "crimson" to 0xFFDC143C.toInt(),
54
+ "khaki" to 0xFFF0E68C.toInt(),
55
+ "beige" to 0xFFF5F5DC.toInt(),
56
+ "ivory" to 0xFFFFFFF0.toInt(),
57
+ "tan" to 0xFFD2B48C.toInt(),
58
+ "turquoise" to 0xFF40E0D0.toInt(),
59
+ "skyblue" to 0xFF87CEEB.toInt(),
60
+ "steelblue" to 0xFF4682B4.toInt(),
61
+ "royalblue" to 0xFF4169E1.toInt(),
62
+ "dodgerblue" to 0xFF1E90FF.toInt(),
63
+ "slategray" to 0xFF708090.toInt(),
64
+ "slategrey" to 0xFF708090.toInt(),
65
+ "whitesmoke" to 0xFFF5F5F5.toInt(),
66
+ "snow" to 0xFFFFFAFA.toInt(),
67
+ "seagreen" to 0xFF2E8B57.toInt(),
68
+ "forestgreen" to 0xFF228B22.toInt(),
69
+ "darkgreen" to 0xFF006400.toInt(),
70
+ "lightblue" to 0xFFADD8E6.toInt(),
71
+ "lightgreen" to 0xFF90EE90.toInt(),
72
+ "darkblue" to 0xFF00008B.toInt(),
73
+ "darkred" to 0xFF8B0000.toInt(),
74
+ "orangered" to 0xFFFF4500.toInt(),
75
+ "hotpink" to 0xFFFF69B4.toInt(),
76
+ "deeppink" to 0xFFFF1493.toInt(),
77
+ "chocolate" to 0xFFD2691E.toInt(),
78
+ "plum" to 0xFFDDA0DD.toInt(),
79
+ "lavender" to 0xFFE6E6FA.toInt(),
80
+ "mintcream" to 0xFFF5FFFA.toInt(),
81
+ "gainsboro" to 0xFFDCDCDC.toInt(),
82
+ )
83
+
84
+ /** Parse `value` into a signed ARGB int, or `null` when unparseable. */
85
+ fun parse(value: Any?): Int? {
86
+ return when (value) {
87
+ null, JSONObject.NULL -> null
88
+ is Boolean -> null
89
+ is Int -> if (value ushr 24 == 0) value or 0xFF000000.toInt() else value
90
+ is Long -> parseLong(value)
91
+ is Number -> {
92
+ val d = value.toDouble()
93
+ if (d.isNaN() || d.isInfinite()) null else parseLong(d.toLong())
94
+ }
95
+ is String -> parseString(value)
96
+ is JSONArray -> parseArray((0 until value.length()).map { value.opt(it) })
97
+ is List<*> -> parseArray(value)
98
+ else -> null
99
+ }
100
+ }
101
+
102
+ /** Parse `value`, falling back to `default` when unparseable. */
103
+ fun parseOr(value: Any?, default: Int): Int = parse(value) ?: default
104
+
105
+ /** Replace the alpha channel of `color` with `alpha` in `0..1`. */
106
+ fun withAlpha(color: Int, alpha: Double): Int {
107
+ val a = (alpha.coerceIn(0.0, 1.0) * 255.0).toInt()
108
+ return (color and 0x00FFFFFF) or (a shl 24)
109
+ }
110
+
111
+ private fun parseLong(raw: Long): Int {
112
+ val masked = raw and 0xFFFFFFFFL
113
+ val withAlpha = if (masked ushr 24 == 0L) masked or 0xFF000000L else masked
114
+ return withAlpha.toInt()
115
+ }
116
+
117
+ private fun parseArray(items: List<Any?>): Int? {
118
+ if (items.size < 3) return null
119
+ val r = channel(items[0]) ?: return null
120
+ val g = channel(items[1]) ?: return null
121
+ val b = channel(items[2]) ?: return null
122
+ val a = if (items.size >= 4) alphaChannel(items[3]) ?: return null else 255
123
+ return argb(a, r, g, b)
124
+ }
125
+
126
+ private fun parseString(raw: String): Int? {
127
+ val s = raw.trim()
128
+ if (s.isEmpty()) return null
129
+ if (s.startsWith("#")) return parseHex(s.substring(1))
130
+ val lower = s.lowercase()
131
+ named[lower]?.let { return it }
132
+ if (lower.startsWith("rgb")) return parseFunctional(lower)
133
+ if (lower.startsWith("0x")) return s.substring(2).toLongOrNull(16)?.let { parseLong(it) }
134
+ return s.toLongOrNull(16)?.takeIf { s.length == 6 || s.length == 8 }?.let {
135
+ parseHexDigits(s)
136
+ }
137
+ }
138
+
139
+ private fun parseHex(digits: String): Int? {
140
+ if (digits.isEmpty() || !digits.all { it.isLetterOrDigit() }) return null
141
+ val expanded = when (digits.length) {
142
+ 3 -> "FF" + digits.map { "$it$it" }.joinToString("")
143
+ 4 -> {
144
+ // CSS #RGBA: alpha is the last digit.
145
+ val rgb = digits.substring(0, 3).map { "$it$it" }.joinToString("")
146
+ val a = "${digits[3]}${digits[3]}"
147
+ a + rgb
148
+ }
149
+ 6 -> "FF$digits"
150
+ 8 -> digits
151
+ else -> return null
152
+ }
153
+ return expanded.toLongOrNull(16)?.toInt()
154
+ }
155
+
156
+ private fun parseHexDigits(digits: String): Int? = parseHex(digits)
157
+
158
+ private fun parseFunctional(lower: String): Int? {
159
+ val open = lower.indexOf('(')
160
+ val close = lower.lastIndexOf(')')
161
+ if (open < 0 || close < open) return null
162
+ val parts = lower.substring(open + 1, close).split(',', '/', ' ').map { it.trim() }.filter { it.isNotEmpty() }
163
+ if (parts.size < 3) return null
164
+ val r = channel(parts[0]) ?: return null
165
+ val g = channel(parts[1]) ?: return null
166
+ val b = channel(parts[2]) ?: return null
167
+ val a = if (parts.size >= 4) alphaChannel(parts[3]) ?: return null else 255
168
+ return argb(a, r, g, b)
169
+ }
170
+
171
+ private fun channel(value: Any?): Int? {
172
+ val d = when (value) {
173
+ is Number -> value.toDouble()
174
+ is String -> {
175
+ val t = value.trim()
176
+ if (t.endsWith("%")) (t.dropLast(1).toDoubleOrNull() ?: return null) * 2.55
177
+ else t.toDoubleOrNull() ?: return null
178
+ }
179
+ else -> return null
180
+ }
181
+ return d.toInt().coerceIn(0, 255)
182
+ }
183
+
184
+ private fun alphaChannel(value: Any?): Int? {
185
+ val d = when (value) {
186
+ is Number -> value.toDouble()
187
+ is String -> {
188
+ val t = value.trim()
189
+ if (t.endsWith("%")) (t.dropLast(1).toDoubleOrNull() ?: return null) / 100.0
190
+ else t.toDoubleOrNull() ?: return null
191
+ }
192
+ else -> return null
193
+ }
194
+ // Values above 1 are taken as 0..255 bytes.
195
+ val a = if (d > 1.0) d / 255.0 else d
196
+ return (a.coerceIn(0.0, 1.0) * 255.0 + 0.5).toInt()
197
+ }
198
+
199
+ private fun argb(a: Int, r: Int, g: Int, b: Int): Int =
200
+ (a shl 24) or (r shl 16) or (g shl 8) or b
201
+ }