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,131 @@
1
+ package com.pythonnative.runtime.components
2
+
3
+ import android.content.Context
4
+ import android.content.res.ColorStateList
5
+ import android.view.View
6
+ import com.google.android.material.bottomnavigation.BottomNavigationView
7
+ import com.google.android.material.navigation.NavigationBarView
8
+ import com.pythonnative.runtime.bridge.JsonUtil
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.JSONArray
13
+ import org.json.JSONObject
14
+
15
+ /**
16
+ * `TabBar` element backed by Material's `BottomNavigationView`.
17
+ *
18
+ * `items` is a list of `{name, title, icon}`; `active_tab` names the
19
+ * selected item (an integer `active_index` is also accepted). Selecting
20
+ * a tab fires `on_tab_select(name)` and `on_select(index)` when wired.
21
+ */
22
+ class TabBarManager : ComponentManager() {
23
+ override fun createView(context: Context, tag: Long, props: JSONObject): View {
24
+ val bnv = BottomNavigationView(context)
25
+ bnv.setBackgroundColor(0xFFFFFFFF.toInt())
26
+ bnv.labelVisibilityMode = NavigationBarView.LABEL_VISIBILITY_LABELED
27
+ bnv.setOnItemSelectedListener { item ->
28
+ if (stateOf(bnv)["suppress"] == true) return@setOnItemSelectedListener true
29
+ val items = propsOf(bnv).value("items") as? JSONArray
30
+ val index = item.itemId
31
+ val spec = items?.optJSONObject(index)
32
+ if (spec != null) {
33
+ fire(bnv, "on_tab_select", spec.str("name") ?: "")
34
+ if (hasEvent(bnv, "on_select")) fire(bnv, "on_select", index)
35
+ }
36
+ true
37
+ }
38
+ return bnv
39
+ }
40
+
41
+ override fun applyProps(view: View, props: JSONObject, initial: Boolean) {
42
+ val bnv = view as BottomNavigationView
43
+ val merged = propsOf(bnv)
44
+ if (props.has("items")) setMenu(bnv, merged.value("items") as? JSONArray)
45
+ if (props.has("active_tab") || props.has("active_index") || props.has("items")) {
46
+ setActive(bnv, merged)
47
+ }
48
+ val active = PNColor.parse(props.value("active_tint_color") ?: props.value("tint_color"))
49
+ val inactive = PNColor.parse(props.value("inactive_tint_color"))
50
+ if (active != null || inactive != null) {
51
+ val a = active ?: PNColor.parse(merged.value("active_tint_color")) ?: 0xFF1976D2.toInt()
52
+ val i = inactive ?: PNColor.parse(merged.value("inactive_tint_color")) ?: 0xFF757575.toInt()
53
+ val list = ColorStateList(arrayOf(intArrayOf(android.R.attr.state_checked), intArrayOf()), intArrayOf(a, i))
54
+ bnv.itemIconTintList = list
55
+ bnv.itemTextColor = list
56
+ }
57
+ PNColor.parse(props.value("background_color"))?.let { bnv.setBackgroundColor(it) }
58
+ if (props.has("shows_labels")) {
59
+ bnv.labelVisibilityMode = if (JsonUtil.truthy(props.value("shows_labels"))) {
60
+ NavigationBarView.LABEL_VISIBILITY_LABELED
61
+ } else {
62
+ NavigationBarView.LABEL_VISIBILITY_UNLABELED
63
+ }
64
+ }
65
+ ViewStyler.apply(bnv, JSONObject(props.toString()).apply { remove("background_color") })
66
+ }
67
+
68
+ private fun setMenu(bnv: BottomNavigationView, items: JSONArray?) {
69
+ val state = stateOf(bnv)
70
+ state["suppress"] = true
71
+ try {
72
+ val menu = bnv.menu
73
+ menu.clear()
74
+ if (items == null) return
75
+ for (i in 0 until items.length()) {
76
+ val item = items.optJSONObject(i) ?: continue
77
+ val title = item.str("title") ?: item.str("name") ?: ""
78
+ val menuItem = menu.add(0, i, i, title)
79
+ val icon = resolveIcon(bnv.context, item.value("icon"))
80
+ if (icon != 0) menuItem.setIcon(icon)
81
+ item.str("badge")?.let { badge ->
82
+ val b = bnv.getOrCreateBadge(i)
83
+ badge.toIntOrNull()?.let { b.number = it }
84
+ }
85
+ }
86
+ } catch (e: Exception) {
87
+ PNLog.swallowed("TabBarManager.setMenu", e)
88
+ } finally {
89
+ state["suppress"] = false
90
+ }
91
+ }
92
+
93
+ /** Resolve an icon spec (`"ic_menu_home"` or `{"android": ...}`) to a drawable id. */
94
+ private fun resolveIcon(context: Context, icon: Any?): Int {
95
+ val name = when (icon) {
96
+ is String -> icon
97
+ is JSONObject -> icon.str("android")
98
+ else -> null
99
+ } ?: return 0
100
+ return try {
101
+ val field = android.R.drawable::class.java.getField(name)
102
+ field.getInt(null)
103
+ } catch (e: Exception) {
104
+ context.resources.getIdentifier(name, "drawable", context.packageName)
105
+ }
106
+ }
107
+
108
+ private fun setActive(bnv: BottomNavigationView, merged: JSONObject) {
109
+ val items = merged.value("items") as? JSONArray ?: return
110
+ val active = merged.str("active_tab")
111
+ var target = -1
112
+ if (active != null) {
113
+ for (i in 0 until items.length()) {
114
+ if (items.optJSONObject(i)?.str("name") == active) {
115
+ target = i
116
+ break
117
+ }
118
+ }
119
+ }
120
+ if (target < 0) target = merged.value("active_index")?.let { JsonUtil.toInt(it, -1) } ?: -1
121
+ if (target < 0 || target >= items.length()) return
122
+ if (bnv.selectedItemId == target) return
123
+ val state = stateOf(bnv)
124
+ state["suppress"] = true
125
+ try {
126
+ bnv.selectedItemId = target
127
+ } finally {
128
+ state["suppress"] = false
129
+ }
130
+ }
131
+ }
@@ -0,0 +1,245 @@
1
+ package com.pythonnative.runtime.components
2
+
3
+ import android.content.Context
4
+ import android.os.Build
5
+ import android.text.Editable
6
+ import android.text.InputFilter
7
+ import android.text.InputType
8
+ import android.text.TextWatcher
9
+ import android.view.MotionEvent
10
+ import android.view.View
11
+ import android.view.inputmethod.EditorInfo
12
+ import android.view.inputmethod.InputMethodManager
13
+ import android.widget.EditText
14
+ import com.pythonnative.runtime.bridge.JsonUtil
15
+ import com.pythonnative.runtime.bridge.PNLog
16
+ import com.pythonnative.runtime.bridge.num
17
+ import com.pythonnative.runtime.bridge.str
18
+ import com.pythonnative.runtime.bridge.value
19
+ import com.pythonnative.runtime.views.PNEditText
20
+ import org.json.JSONObject
21
+ import kotlin.math.max
22
+ import kotlin.math.min
23
+
24
+ /**
25
+ * `TextInput` element: an `EditText` reporting `on_change`, `on_submit`,
26
+ * `on_focus`, `on_blur`, and `on_selection_change`, with keyboard type,
27
+ * secure entry, return key, autofill, and clear-button props.
28
+ */
29
+ class TextInputManager : ComponentManager() {
30
+ override fun createView(context: Context, tag: Long, props: JSONObject): View {
31
+ val et = PNEditText(context)
32
+ // Default to single-line so Enter triggers the IME action instead of a newline.
33
+ if (!JsonUtil.truthy(props.value("multiline"))) et.setSingleLine(true)
34
+ bindListeners(et)
35
+ return et
36
+ }
37
+
38
+ private fun bindListeners(et: PNEditText) {
39
+ et.addTextChangedListener(object : TextWatcher {
40
+ override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
41
+ override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
42
+ override fun afterTextChanged(s: Editable?) {
43
+ if (stateOf(et)["suppress"] == true) return
44
+ fire(et, "on_change", s?.toString() ?: "")
45
+ }
46
+ })
47
+ et.setOnFocusChangeListener { v, hasFocus -> fire(v, if (hasFocus) "on_focus" else "on_blur") }
48
+ et.onSelectionChangedListener = { start, end ->
49
+ if (hasEvent(et, "on_selection_change")) {
50
+ fire(et, "on_selection_change", mapOf("start" to start, "end" to end))
51
+ }
52
+ }
53
+ installEditorAction(et)
54
+ }
55
+
56
+ override fun applyProps(view: View, props: JSONObject, initial: Boolean) {
57
+ val et = view as EditText
58
+ val state = stateOf(et)
59
+ val merged = propsOf(et)
60
+ if (props.has("value")) {
61
+ val incoming = props.str("value") ?: ""
62
+ if (et.text.toString() != incoming) {
63
+ val selStart = et.selectionStart
64
+ val selEnd = et.selectionEnd
65
+ state["suppress"] = true
66
+ try {
67
+ et.setText(incoming)
68
+ val maxPos = incoming.length
69
+ val start = max(0, min(selStart, maxPos))
70
+ val end = max(0, min(selEnd, maxPos))
71
+ if (start == end) et.setSelection(start) else et.setSelection(start, end)
72
+ } catch (e: Exception) {
73
+ PNLog.swallowed("TextInputManager.value", e)
74
+ } finally {
75
+ state["suppress"] = false
76
+ }
77
+ }
78
+ }
79
+ if (props.has("placeholder")) et.hint = props.str("placeholder") ?: ""
80
+ PNColor.parse(props.value("placeholder_color"))?.let { et.setHintTextColor(it) }
81
+ props.num("font_size")?.let { et.textSize = it.toFloat() }
82
+ PNColor.parse(props.value("color"))?.let { et.setTextColor(it) }
83
+ if (listOf("font_family", "font_weight", "italic", "bold").any { props.has(it) }) {
84
+ TextStyle.applyTypeface(et, merged)
85
+ }
86
+ if (listOf("multiline", "secure", "secure_text_entry", "keyboard_type", "auto_capitalize", "auto_correct").any { props.has(it) }) {
87
+ applyInputType(et, merged)
88
+ }
89
+ if (props.has("max_length")) {
90
+ val limit = props.num("max_length")
91
+ et.filters = if (limit != null) arrayOf<InputFilter>(InputFilter.LengthFilter(limit.toInt())) else arrayOf()
92
+ }
93
+ if (JsonUtil.truthy(props.value("auto_focus"))) et.requestFocus()
94
+ if (props.has("editable")) {
95
+ // Only present when False (read-only); removal restores editing.
96
+ val editable = props.value("editable") != false
97
+ et.isFocusable = editable
98
+ et.isFocusableInTouchMode = editable
99
+ et.isCursorVisible = editable
100
+ et.isLongClickable = editable
101
+ }
102
+ PNColor.parse(props.value("selection_color"))?.let { et.highlightColor = it }
103
+ props.str("text_content_type")?.let { applyAutofill(et, it) }
104
+ if (props.has("clear_button")) applyClearButton(et, JsonUtil.truthy(props.value("clear_button")))
105
+ props.str("return_key_type")?.let { et.imeOptions = imeAction(it) }
106
+ if (props.has("text_align")) {
107
+ et.gravity = when (props.str("text_align")) {
108
+ "center" -> android.view.Gravity.CENTER_HORIZONTAL or (et.gravity and android.view.Gravity.VERTICAL_GRAVITY_MASK)
109
+ "right", "end" -> android.view.Gravity.END or (et.gravity and android.view.Gravity.VERTICAL_GRAVITY_MASK)
110
+ else -> android.view.Gravity.START or (et.gravity and android.view.Gravity.VERTICAL_GRAVITY_MASK)
111
+ }
112
+ }
113
+ ViewStyler.apply(et, props)
114
+ }
115
+
116
+ private fun applyInputType(et: EditText, merged: JSONObject) {
117
+ var base = InputType.TYPE_CLASS_TEXT
118
+ val secure = JsonUtil.truthy(merged.value("secure")) || JsonUtil.truthy(merged.value("secure_text_entry"))
119
+ if (secure) {
120
+ base = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD
121
+ } else {
122
+ when (merged.str("keyboard_type")) {
123
+ "email_address", "email" -> base = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS
124
+ "number_pad", "numeric" -> base = InputType.TYPE_CLASS_NUMBER
125
+ "decimal_pad", "decimal" -> base = InputType.TYPE_CLASS_NUMBER or InputType.TYPE_NUMBER_FLAG_DECIMAL
126
+ "phone_pad", "phone" -> base = InputType.TYPE_CLASS_PHONE
127
+ "url" -> base = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_URI
128
+ }
129
+ when (merged.str("auto_capitalize")) {
130
+ "sentences" -> base = base or InputType.TYPE_TEXT_FLAG_CAP_SENTENCES
131
+ "words" -> base = base or InputType.TYPE_TEXT_FLAG_CAP_WORDS
132
+ "characters" -> base = base or InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS
133
+ }
134
+ if (merged.value("auto_correct") == false) base = base or InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS
135
+ }
136
+ if (JsonUtil.truthy(merged.value("multiline"))) {
137
+ base = base or InputType.TYPE_TEXT_FLAG_MULTI_LINE
138
+ et.setSingleLine(false)
139
+ } else {
140
+ et.setSingleLine(true)
141
+ }
142
+ et.inputType = base
143
+ }
144
+
145
+ private fun imeAction(type: String): Int = when (type) {
146
+ "default" -> EditorInfo.IME_ACTION_UNSPECIFIED
147
+ "go" -> EditorInfo.IME_ACTION_GO
148
+ "next" -> EditorInfo.IME_ACTION_NEXT
149
+ "search" -> EditorInfo.IME_ACTION_SEARCH
150
+ "send" -> EditorInfo.IME_ACTION_SEND
151
+ "previous" -> EditorInfo.IME_ACTION_PREVIOUS
152
+ else -> EditorInfo.IME_ACTION_DONE // done, google, join, route, yahoo
153
+ }
154
+
155
+ /**
156
+ * Single-line inputs always dismiss the keyboard on the action key and
157
+ * fire `on_submit` first. Multi-line inputs only consume the action
158
+ * when an `on_submit` handler exists; otherwise Enter inserts a newline.
159
+ */
160
+ private fun installEditorAction(et: EditText) {
161
+ et.setOnEditorActionListener { v, _, _ ->
162
+ val multiline = JsonUtil.truthy(propsOf(v).value("multiline"))
163
+ val hasSubmit = hasEvent(v, "on_submit")
164
+ if (multiline && !hasSubmit) return@setOnEditorActionListener false
165
+ if (hasSubmit) fire(v, "on_submit", v.text.toString())
166
+ if (!multiline && propsOf(v).value("blur_on_submit") != false) blur(v)
167
+ true
168
+ }
169
+ }
170
+
171
+ private fun applyAutofill(et: EditText, contentType: String) {
172
+ if (Build.VERSION.SDK_INT < 26) return
173
+ val hint = when (contentType) {
174
+ "username" -> View.AUTOFILL_HINT_USERNAME
175
+ "password" -> View.AUTOFILL_HINT_PASSWORD
176
+ "new_password" -> "newPassword"
177
+ "email", "email_address" -> View.AUTOFILL_HINT_EMAIL_ADDRESS
178
+ "name" -> View.AUTOFILL_HINT_NAME
179
+ "given_name" -> "personGivenName"
180
+ "family_name" -> "personFamilyName"
181
+ "telephone", "phone", "phone_number" -> View.AUTOFILL_HINT_PHONE
182
+ "postal_code" -> View.AUTOFILL_HINT_POSTAL_CODE
183
+ "street_address" -> View.AUTOFILL_HINT_POSTAL_ADDRESS
184
+ "credit_card_number" -> View.AUTOFILL_HINT_CREDIT_CARD_NUMBER
185
+ "one_time_code" -> "smsOTPCode"
186
+ else -> null
187
+ } ?: return
188
+ et.setAutofillHints(hint)
189
+ }
190
+
191
+ private fun applyClearButton(et: EditText, enabled: Boolean) {
192
+ val state = stateOf(et)
193
+ if (!enabled) {
194
+ et.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0)
195
+ return
196
+ }
197
+ et.setCompoundDrawablesWithIntrinsicBounds(0, 0, android.R.drawable.ic_menu_close_clear_cancel, 0)
198
+ if (state["clear_bound"] == true) return
199
+ state["clear_bound"] = true
200
+ et.setOnTouchListener { v, event ->
201
+ if (event.action == MotionEvent.ACTION_UP) {
202
+ val right = (v as EditText).compoundDrawables.getOrNull(2)
203
+ if (right != null) {
204
+ val threshold = v.width - v.paddingRight - right.bounds.width()
205
+ if (event.x >= threshold) {
206
+ v.setText("")
207
+ v.performClick()
208
+ return@setOnTouchListener true
209
+ }
210
+ }
211
+ }
212
+ false
213
+ }
214
+ }
215
+
216
+ override fun command(view: View, name: String, args: JSONObject): Any? {
217
+ val et = view as? EditText ?: return null
218
+ when (name) {
219
+ "focus" -> {
220
+ et.requestFocus()
221
+ imm(et)?.showSoftInput(et, InputMethodManager.SHOW_IMPLICIT)
222
+ }
223
+ "blur" -> blur(et)
224
+ "clear" -> et.setText("")
225
+ "select_all" -> et.selectAll()
226
+ "set_selection" -> {
227
+ val start = JsonUtil.toInt(args.opt("start"))
228
+ val end = JsonUtil.toInt(args.opt("end"), start)
229
+ val len = et.text.length
230
+ et.setSelection(start.coerceIn(0, len), end.coerceIn(0, len))
231
+ }
232
+ "is_focused" -> return et.isFocused
233
+ "get_value" -> return et.text.toString()
234
+ }
235
+ return null
236
+ }
237
+
238
+ private fun blur(view: View) {
239
+ view.clearFocus()
240
+ imm(view)?.hideSoftInputFromWindow(view.windowToken, 0)
241
+ }
242
+
243
+ private fun imm(view: View): InputMethodManager? =
244
+ view.context.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager
245
+ }
@@ -0,0 +1,233 @@
1
+ package com.pythonnative.runtime.components
2
+
3
+ import android.content.Context
4
+ import android.graphics.Paint
5
+ import android.graphics.Typeface
6
+ import android.text.SpannableStringBuilder
7
+ import android.text.Spanned
8
+ import android.text.TextUtils
9
+ import android.text.style.AbsoluteSizeSpan
10
+ import android.text.style.BackgroundColorSpan
11
+ import android.text.style.ForegroundColorSpan
12
+ import android.text.style.StrikethroughSpan
13
+ import android.text.style.StyleSpan
14
+ import android.text.style.TypefaceSpan
15
+ import android.text.style.UnderlineSpan
16
+ import android.view.Gravity
17
+ import android.view.View
18
+ import android.widget.TextView
19
+ import com.pythonnative.runtime.bridge.JsonUtil
20
+ import com.pythonnative.runtime.bridge.PNLog
21
+ import com.pythonnative.runtime.bridge.num
22
+ import com.pythonnative.runtime.bridge.str
23
+ import com.pythonnative.runtime.bridge.value
24
+ import org.json.JSONArray
25
+ import org.json.JSONObject
26
+ import kotlin.math.max
27
+
28
+ /** Text helpers shared by `Text`, `Button`, and `TextInput`. */
29
+ object TextStyle {
30
+ private val boldWords = setOf("bold", "semibold", "black", "heavy", "extrabold", "extra_bold", "semi_bold")
31
+
32
+ /** Whether `weight` (a name or a numeric weight) implies bold. */
33
+ fun isBold(weight: Any?): Boolean {
34
+ return when (weight) {
35
+ is String -> weight.lowercase() in boldWords || (weight.toIntOrNull()?.let { it >= 600 } ?: false)
36
+ is Number -> weight.toDouble() >= 600
37
+ else -> false
38
+ }
39
+ }
40
+
41
+ /** `Typeface` style constant for `weight` / `italic`. */
42
+ fun typefaceStyle(weight: Any?, italic: Boolean): Int {
43
+ val bold = isBold(weight)
44
+ return when {
45
+ bold && italic -> Typeface.BOLD_ITALIC
46
+ bold -> Typeface.BOLD
47
+ italic -> Typeface.ITALIC
48
+ else -> Typeface.NORMAL
49
+ }
50
+ }
51
+
52
+ /** Apply `text_transform` to `text`; `capitalize` upper-cases each word's first letter. */
53
+ fun transform(text: String?, mode: String?): String {
54
+ val s = text ?: ""
55
+ return when (mode) {
56
+ "uppercase" -> s.uppercase()
57
+ "lowercase" -> s.lowercase()
58
+ "capitalize" -> {
59
+ val sb = StringBuilder(s.length)
60
+ var atWordStart = true
61
+ for (ch in s) {
62
+ if (ch.isWhitespace()) {
63
+ atWordStart = true
64
+ sb.append(ch)
65
+ } else {
66
+ sb.append(if (atWordStart) ch.uppercaseChar() else ch)
67
+ atWordStart = false
68
+ }
69
+ }
70
+ sb.toString()
71
+ }
72
+ else -> s
73
+ }
74
+ }
75
+
76
+ /** Coerce a `shadow_offset` value (`{width, height}` or `[dx, dy]`) to `(dx, dy)`. */
77
+ fun shadowOffset(value: Any?): Pair<Double, Double> {
78
+ return when (value) {
79
+ is JSONObject -> Pair(value.num("width") ?: 0.0, value.num("height") ?: 0.0)
80
+ is JSONArray -> if (value.length() >= 2) Pair(JsonUtil.toDouble(value.opt(0)), JsonUtil.toDouble(value.opt(1))) else Pair(0.0, 0.0)
81
+ else -> Pair(0.0, 0.0)
82
+ }
83
+ }
84
+
85
+ /** Build a spannable from a rich-text span list, applying `transform` per span. */
86
+ fun buildSpannable(spans: JSONArray, transform: String?): SpannableStringBuilder {
87
+ val builder = SpannableStringBuilder()
88
+ for (i in 0 until spans.length()) {
89
+ val span = spans.optJSONObject(i) ?: continue
90
+ val text = transform(span.str("text"), transform)
91
+ if (text.isEmpty()) continue
92
+ val start = builder.length
93
+ builder.append(text)
94
+ val end = builder.length
95
+ fun set(obj: Any) = builder.setSpan(obj, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
96
+ try {
97
+ PNColor.parse(span.value("color"))?.let { set(ForegroundColorSpan(it)) }
98
+ PNColor.parse(span.value("background_color"))?.let { set(BackgroundColorSpan(it)) }
99
+ span.num("font_size")?.let { set(AbsoluteSizeSpan(it.toInt(), true)) }
100
+ var bold = JsonUtil.truthy(span.value("bold"))
101
+ val weight = span.value("font_weight")
102
+ if (!bold && weight != null) bold = isBold(weight)
103
+ val italic = JsonUtil.truthy(span.value("italic"))
104
+ if (bold || italic) set(StyleSpan(typefaceStyle(if (bold) "bold" else null, italic)))
105
+ span.str("font_family")?.takeIf { it.isNotEmpty() }?.let { set(TypefaceSpan(it)) }
106
+ when (span.str("text_decoration")) {
107
+ "underline" -> set(UnderlineSpan())
108
+ "line_through" -> set(StrikethroughSpan())
109
+ }
110
+ } catch (e: Exception) {
111
+ PNLog.swallowed("TextStyle.buildSpannable", e)
112
+ }
113
+ }
114
+ return builder
115
+ }
116
+
117
+ /** Apply font props (`font_family`, `font_weight`, `bold`, `italic`) from merged props. */
118
+ fun applyTypeface(tv: TextView, merged: JSONObject) {
119
+ val family = merged.str("font_family")
120
+ val weight = merged.value("font_weight") ?: if (JsonUtil.truthy(merged.value("bold"))) "bold" else null
121
+ val italic = JsonUtil.truthy(merged.value("italic"))
122
+ val style = typefaceStyle(weight, italic)
123
+ if (!family.isNullOrEmpty()) {
124
+ tv.setTypeface(Typeface.create(family, style))
125
+ } else {
126
+ tv.setTypeface(tv.typeface, style)
127
+ }
128
+ }
129
+ }
130
+
131
+ /** `Text` element: a `TextView` with rich spans, transforms, shadows, and line limits. */
132
+ class TextManager : ComponentManager() {
133
+ private val shadowKeys = listOf("text_shadow_color", "text_shadow_offset", "text_shadow_radius")
134
+
135
+ override fun createView(context: Context, tag: Long, props: JSONObject): View = TextView(context)
136
+
137
+ override fun applyProps(view: View, props: JSONObject, initial: Boolean) {
138
+ val tv = view as TextView
139
+ val merged = propsOf(tv)
140
+ if (props.has("spans") || props.has("text") || props.has("text_transform")) {
141
+ val transform = merged.str("text_transform")
142
+ val spans = merged.value("spans") as? JSONArray
143
+ if (spans != null && spans.length() > 0) {
144
+ try {
145
+ tv.text = TextStyle.buildSpannable(spans, transform)
146
+ } catch (e: Exception) {
147
+ tv.text = TextStyle.transform(merged.str("text"), transform)
148
+ }
149
+ } else {
150
+ tv.text = TextStyle.transform(merged.str("text"), transform)
151
+ }
152
+ }
153
+ props.num("font_size")?.let { tv.textSize = it.toFloat() }
154
+ PNColor.parse(props.value("color"))?.let { tv.setTextColor(it) }
155
+ if (listOf("font_family", "font_weight", "italic", "bold").any { props.has(it) }) {
156
+ try {
157
+ TextStyle.applyTypeface(tv, merged)
158
+ } catch (e: Exception) {
159
+ PNLog.swallowed("TextManager.typeface", e)
160
+ }
161
+ }
162
+ if (props.has("max_lines") || props.has("number_of_lines")) {
163
+ val lines = merged.num("number_of_lines") ?: merged.num("max_lines")
164
+ if (lines != null && lines > 0) {
165
+ tv.maxLines = lines.toInt()
166
+ tv.ellipsize = ellipsizeMode(merged.str("ellipsize_mode") ?: merged.str("ellipsize"))
167
+ } else {
168
+ tv.maxLines = Int.MAX_VALUE
169
+ tv.ellipsize = null
170
+ }
171
+ }
172
+ if (props.has("ellipsize_mode") || props.has("ellipsize")) {
173
+ if (tv.maxLines != Int.MAX_VALUE) tv.ellipsize = ellipsizeMode(merged.str("ellipsize_mode") ?: merged.str("ellipsize"))
174
+ }
175
+ if (props.has("selectable")) tv.setTextIsSelectable(JsonUtil.truthy(props.value("selectable")))
176
+ if (props.has("text_align")) {
177
+ tv.gravity = when (props.str("text_align")) {
178
+ "center" -> Gravity.CENTER
179
+ "right", "end" -> Gravity.END
180
+ "justify" -> Gravity.START
181
+ else -> Gravity.START
182
+ }
183
+ }
184
+ props.num("letter_spacing")?.let {
185
+ // Android takes letter spacing in ems (a ratio of the font size).
186
+ val size = merged.num("font_size") ?: 16.0
187
+ tv.letterSpacing = (it / max(size, 1.0)).toFloat()
188
+ }
189
+ props.num("line_height")?.let {
190
+ val size = merged.num("font_size") ?: 16.0
191
+ tv.setLineSpacing(0f, (it / max(size, 1.0)).toFloat())
192
+ }
193
+ if (props.has("text_decoration")) {
194
+ var flags = tv.paintFlags and Paint.UNDERLINE_TEXT_FLAG.inv() and Paint.STRIKE_THRU_TEXT_FLAG.inv()
195
+ when (props.str("text_decoration")) {
196
+ "underline" -> flags = flags or Paint.UNDERLINE_TEXT_FLAG
197
+ "line_through" -> flags = flags or Paint.STRIKE_THRU_TEXT_FLAG
198
+ }
199
+ tv.paintFlags = flags
200
+ }
201
+ if (shadowKeys.any { props.has(it) }) applyTextShadow(tv, merged)
202
+ ViewStyler.apply(tv, props)
203
+ }
204
+
205
+ private fun ellipsizeMode(mode: String?): TextUtils.TruncateAt = when (mode) {
206
+ "head" -> TextUtils.TruncateAt.START
207
+ "middle" -> TextUtils.TruncateAt.MIDDLE
208
+ "clip" -> TextUtils.TruncateAt.END
209
+ "marquee" -> TextUtils.TruncateAt.MARQUEE
210
+ else -> TextUtils.TruncateAt.END
211
+ }
212
+
213
+ private fun applyTextShadow(tv: TextView, merged: JSONObject) {
214
+ if (shadowKeys.none { merged.value(it) != null }) {
215
+ tv.setShadowLayer(0f, 0f, 0f, 0)
216
+ return
217
+ }
218
+ val argb = PNColor.parseOr(merged.value("text_shadow_color") ?: "#000000", 0xFF000000.toInt())
219
+ val (dx, dy) = TextStyle.shadowOffset(merged.value("text_shadow_offset"))
220
+ var radiusPx = merged.num("text_shadow_radius")?.let { pxF(it) } ?: 0f
221
+ // A zero radius with an offset hides the shadow on some renderers; use a hairline blur.
222
+ if (radiusPx <= 0f && (dx != 0.0 || dy != 0.0)) radiusPx = 0.01f
223
+ tv.setShadowLayer(radiusPx, pxF(dx), pxF(dy), argb)
224
+ }
225
+
226
+ override fun setAnimatedProperty(view: View, prop: String, value: Any?) {
227
+ if (prop == "color" && view is TextView) {
228
+ PNColor.parse(value)?.let { view.setTextColor(it) }
229
+ return
230
+ }
231
+ super.setAnimatedProperty(view, prop, value)
232
+ }
233
+ }