@openeditor/react-native-prose-editor 0.0.2

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 (91) hide show
  1. package/LICENSE +160 -0
  2. package/README.md +180 -0
  3. package/android/build.gradle +69 -0
  4. package/android/consumer-rules.pro +18 -0
  5. package/android/src/androidTest/AndroidManifest.xml +8 -0
  6. package/android/src/androidTest/java/com/apollohg/editor/MeasureHeightInstrumentedTest.kt +53 -0
  7. package/android/src/androidTest/java/com/apollohg/editor/NativeDeviceCollaborationInitialSyncTest.kt +241 -0
  8. package/android/src/androidTest/java/com/apollohg/editor/NativeDeviceImeRegressionTest.kt +338 -0
  9. package/android/src/androidTest/java/com/apollohg/editor/NativeDeviceOutsideTapTest.kt +789 -0
  10. package/android/src/androidTest/java/com/apollohg/editor/NativeDevicePerformanceTest.kt +350 -0
  11. package/android/src/androidTest/java/com/apollohg/editor/NativeEditorOutsideTapActivity.kt +5 -0
  12. package/android/src/main/assets/editor-icons/MaterialIcons.json +2236 -0
  13. package/android/src/main/assets/editor-icons/MaterialIcons.ttf +0 -0
  14. package/android/src/main/java/com/apollohg/editor/CaretGeometry.kt +50 -0
  15. package/android/src/main/java/com/apollohg/editor/EditorAddons.kt +135 -0
  16. package/android/src/main/java/com/apollohg/editor/EditorEditText.kt +4527 -0
  17. package/android/src/main/java/com/apollohg/editor/EditorHeightBehavior.kt +14 -0
  18. package/android/src/main/java/com/apollohg/editor/EditorInputConnection.kt +999 -0
  19. package/android/src/main/java/com/apollohg/editor/EditorTheme.kt +474 -0
  20. package/android/src/main/java/com/apollohg/editor/ImageResizeOverlayView.kt +199 -0
  21. package/android/src/main/java/com/apollohg/editor/NativeEditorExpoView.kt +3137 -0
  22. package/android/src/main/java/com/apollohg/editor/NativeEditorModule.kt +561 -0
  23. package/android/src/main/java/com/apollohg/editor/NativeProseViewerExpoView.kt +315 -0
  24. package/android/src/main/java/com/apollohg/editor/NativeToolbar.kt +1413 -0
  25. package/android/src/main/java/com/apollohg/editor/PositionBridge.kt +133 -0
  26. package/android/src/main/java/com/apollohg/editor/RemoteSelectionOverlayView.kt +315 -0
  27. package/android/src/main/java/com/apollohg/editor/RenderBridge.kt +2172 -0
  28. package/android/src/main/java/com/apollohg/editor/RichTextEditorView.kt +399 -0
  29. package/android/src/sharedTest/java/com/apollohg/editor/NativePerformanceSupport.kt +482 -0
  30. package/android/src/test/java/com/apollohg/editor/CaretGeometryTest.kt +137 -0
  31. package/android/src/test/java/com/apollohg/editor/EditorEditTextHardwareKeyTest.kt +321 -0
  32. package/android/src/test/java/com/apollohg/editor/EditorInputConnectionTest.kt +4202 -0
  33. package/android/src/test/java/com/apollohg/editor/NativeEditorExpoViewTest.kt +2581 -0
  34. package/android/src/test/java/com/apollohg/editor/NativeEditorModuleTest.kt +27 -0
  35. package/android/src/test/java/com/apollohg/editor/NativePerformanceTest.kt +197 -0
  36. package/android/src/test/java/com/apollohg/editor/NativeProseViewerExpoViewTest.kt +84 -0
  37. package/android/src/test/java/com/apollohg/editor/NativeToolbarTest.kt +544 -0
  38. package/android/src/test/java/com/apollohg/editor/PositionBridgeTest.kt +461 -0
  39. package/android/src/test/java/com/apollohg/editor/RenderBridgeTest.kt +2054 -0
  40. package/android/src/test/java/com/apollohg/editor/RichTextEditorViewTest.kt +1367 -0
  41. package/app.plugin.js +62 -0
  42. package/dist/EditorTheme.d.ts +113 -0
  43. package/dist/EditorTheme.js +29 -0
  44. package/dist/EditorToolbar.d.ts +191 -0
  45. package/dist/EditorToolbar.js +1095 -0
  46. package/dist/NativeEditorBridge.d.ts +325 -0
  47. package/dist/NativeEditorBridge.js +933 -0
  48. package/dist/NativeProseViewer.d.ts +53 -0
  49. package/dist/NativeProseViewer.js +480 -0
  50. package/dist/NativeRichTextEditor.d.ts +183 -0
  51. package/dist/NativeRichTextEditor.js +2574 -0
  52. package/dist/YjsCollaboration.d.ts +90 -0
  53. package/dist/YjsCollaboration.js +743 -0
  54. package/dist/addons.d.ts +100 -0
  55. package/dist/addons.js +82 -0
  56. package/dist/heightCache.d.ts +6 -0
  57. package/dist/heightCache.js +45 -0
  58. package/dist/index.d.ts +10 -0
  59. package/dist/index.js +30 -0
  60. package/dist/schemas.d.ts +37 -0
  61. package/dist/schemas.js +263 -0
  62. package/dist/useNativeEditor.d.ts +42 -0
  63. package/dist/useNativeEditor.js +124 -0
  64. package/expo-module.config.json +9 -0
  65. package/ios/EditorAddons.swift +327 -0
  66. package/ios/EditorCore.xcframework/Info.plist +44 -0
  67. package/ios/EditorCore.xcframework/ios-arm64/libeditor_core.a +0 -0
  68. package/ios/EditorCore.xcframework/ios-arm64_x86_64-simulator/libeditor_core.a +0 -0
  69. package/ios/EditorLayoutManager.swift +774 -0
  70. package/ios/EditorTheme.swift +515 -0
  71. package/ios/Generated_editor_core.swift +1513 -0
  72. package/ios/NativeEditorExpoView.swift +3593 -0
  73. package/ios/NativeEditorModule.swift +624 -0
  74. package/ios/NativeProseViewerExpoView.swift +276 -0
  75. package/ios/PositionBridge.swift +558 -0
  76. package/ios/ReactNativeProseEditor.podspec +49 -0
  77. package/ios/RenderBridge.swift +1708 -0
  78. package/ios/RichTextEditorView.swift +6150 -0
  79. package/ios/Tests/NativePerformanceTests.swift +687 -0
  80. package/ios/Tests/PositionBridgeTests.swift +706 -0
  81. package/ios/Tests/RenderBridgeTests.swift +2236 -0
  82. package/ios/Tests/RichTextEditorViewTests.swift +6548 -0
  83. package/ios/editor_coreFFI/editor_coreFFI.h +1289 -0
  84. package/ios/editor_coreFFI/module.modulemap +7 -0
  85. package/ios/editor_coreFFI.h +904 -0
  86. package/ios/editor_coreFFI.modulemap +7 -0
  87. package/package.json +74 -0
  88. package/rust/android/arm64-v8a/libeditor_core.so +0 -0
  89. package/rust/android/armeabi-v7a/libeditor_core.so +0 -0
  90. package/rust/android/x86_64/libeditor_core.so +0 -0
  91. package/rust/bindings/kotlin/uniffi/editor_core/editor_core.kt +2563 -0
@@ -0,0 +1,133 @@
1
+ package com.openeditor.editor
2
+
3
+ import android.icu.text.BreakIterator
4
+
5
+ /**
6
+ * Converts between Android UTF-16 offsets and Rust editor-core scalar offsets,
7
+ * then snaps UTF-16 positions to grapheme boundaries when Android reports a
8
+ * cursor inside a composed character.
9
+ */
10
+ object PositionBridge {
11
+ private data class ConversionTable(
12
+ val utf16ToScalar: IntArray,
13
+ val scalarToUtf16: IntArray,
14
+ )
15
+
16
+ private val cacheLock = Any()
17
+ @Volatile private var cachedText: String? = null
18
+ @Volatile private var cachedTable: ConversionTable? = null
19
+
20
+ /**
21
+ * Counts code points from the start of the string up to the given UTF-16 offset.
22
+ */
23
+ fun utf16ToScalar(utf16Offset: Int, text: String): Int {
24
+ val conversionTable = conversionTableFor(text)
25
+ val endIndex = utf16Offset.coerceIn(0, conversionTable.utf16ToScalar.size - 1)
26
+ return conversionTable.utf16ToScalar[endIndex]
27
+ }
28
+
29
+ /**
30
+ * Counts UTF-16 code units from the start of the string up to the given scalar offset.
31
+ */
32
+ fun scalarToUtf16(scalarOffset: Int, text: String): Int {
33
+ if (scalarOffset <= 0) return 0
34
+
35
+ val conversionTable = conversionTableFor(text)
36
+ val clampedScalar = scalarOffset.coerceIn(0, conversionTable.scalarToUtf16.size - 1)
37
+ return conversionTable.scalarToUtf16[clampedScalar]
38
+ }
39
+
40
+ /**
41
+ * Biases forward to the next grapheme boundary when Android reports an
42
+ * offset inside a composed character sequence.
43
+ */
44
+ fun snapToGraphemeBoundary(utf16Offset: Int, text: String): Int {
45
+ if (text.isEmpty()) return 0
46
+
47
+ val clampedOffset = utf16Offset.coerceIn(0, text.length)
48
+
49
+ if (clampedOffset == 0 || clampedOffset == text.length) {
50
+ return clampedOffset
51
+ }
52
+
53
+ val breakIterator = BreakIterator.getCharacterInstance()
54
+ breakIterator.setText(text)
55
+
56
+ if (breakIterator.isBoundary(clampedOffset)) {
57
+ return clampedOffset
58
+ }
59
+
60
+ val nextBoundary = breakIterator.following(clampedOffset)
61
+ return if (nextBoundary == BreakIterator.DONE) text.length else nextBoundary
62
+ }
63
+
64
+ /**
65
+ * Snaps a UTF-16 offset out of the middle of a surrogate pair without
66
+ * applying full grapheme-cluster expansion.
67
+ */
68
+ fun snapToScalarBoundary(
69
+ utf16Offset: Int,
70
+ text: String,
71
+ biasForward: Boolean
72
+ ): Int {
73
+ val clampedOffset = utf16Offset.coerceIn(0, text.length)
74
+ if (clampedOffset <= 0 || clampedOffset >= text.length) return clampedOffset
75
+
76
+ val previous = text[clampedOffset - 1]
77
+ val current = text[clampedOffset]
78
+ if (Character.isHighSurrogate(previous) && Character.isLowSurrogate(current)) {
79
+ return if (biasForward) clampedOffset + 1 else clampedOffset - 1
80
+ }
81
+ return clampedOffset
82
+ }
83
+
84
+ fun snapRangeToScalarBoundaries(start: Int, end: Int, text: String): Pair<Int, Int> {
85
+ val lower = minOf(start, end).coerceIn(0, text.length)
86
+ val upper = maxOf(start, end).coerceIn(0, text.length)
87
+ return snapToScalarBoundary(lower, text, biasForward = false) to
88
+ snapToScalarBoundary(upper, text, biasForward = true)
89
+ }
90
+
91
+ private fun conversionTableFor(text: String): ConversionTable {
92
+ val lastText = cachedText
93
+ val lastTable = cachedTable
94
+ if (lastText == text && lastTable != null) {
95
+ return lastTable
96
+ }
97
+
98
+ synchronized(cacheLock) {
99
+ val synchronizedText = cachedText
100
+ val synchronizedTable = cachedTable
101
+ if (synchronizedText == text && synchronizedTable != null) {
102
+ return synchronizedTable
103
+ }
104
+
105
+ val scalarCount = text.codePointCount(0, text.length)
106
+ val utf16ToScalar = IntArray(text.length + 1)
107
+ val scalarToUtf16 = IntArray(scalarCount + 1)
108
+
109
+ var utf16Pos = 0
110
+ var scalarPos = 0
111
+ while (utf16Pos < text.length) {
112
+ val codePoint = Character.codePointAt(text, utf16Pos)
113
+ val charCount = Character.charCount(codePoint)
114
+ val nextUtf16Pos = utf16Pos + charCount
115
+ scalarPos += 1
116
+
117
+ for (offset in (utf16Pos + 1)..nextUtf16Pos) {
118
+ utf16ToScalar[offset] = scalarPos
119
+ }
120
+ scalarToUtf16[scalarPos] = nextUtf16Pos
121
+ utf16Pos = nextUtf16Pos
122
+ }
123
+
124
+ return ConversionTable(
125
+ utf16ToScalar = utf16ToScalar,
126
+ scalarToUtf16 = scalarToUtf16,
127
+ ).also {
128
+ cachedText = text
129
+ cachedTable = it
130
+ }
131
+ }
132
+ }
133
+ }
@@ -0,0 +1,315 @@
1
+ package com.openeditor.editor
2
+
3
+ import android.content.Context
4
+ import android.graphics.Canvas
5
+ import android.graphics.Color
6
+ import android.graphics.Paint
7
+ import android.graphics.Path
8
+ import android.graphics.RectF
9
+ import android.util.AttributeSet
10
+ import android.util.TypedValue
11
+ import android.view.View
12
+ import androidx.appcompat.content.res.AppCompatResources
13
+ import org.json.JSONArray
14
+ import uniffi.editor_core.editorDocToScalar
15
+
16
+ data class RemoteSelectionDecoration(
17
+ val clientId: Int,
18
+ val anchor: Int,
19
+ val head: Int,
20
+ val color: Int,
21
+ val name: String?,
22
+ val isFocused: Boolean,
23
+ ) {
24
+ companion object {
25
+ fun fromJson(context: Context, json: String?): List<RemoteSelectionDecoration> {
26
+ if (json.isNullOrBlank()) return emptyList()
27
+ val array = try {
28
+ JSONArray(json)
29
+ } catch (_: Throwable) {
30
+ return emptyList()
31
+ }
32
+ val fallbackColor = resolveFallbackColor(context)
33
+
34
+ return buildList {
35
+ for (index in 0 until array.length()) {
36
+ val item = array.optJSONObject(index) ?: continue
37
+ val color = parseColor(item.optString("color", ""), fallbackColor)
38
+ add(
39
+ RemoteSelectionDecoration(
40
+ clientId = item.optInt("clientId", 0),
41
+ anchor = item.optInt("anchor", 0),
42
+ head = item.optInt("head", 0),
43
+ color = color,
44
+ name = item.optString("name").takeIf { it.isNotBlank() },
45
+ isFocused = item.optBoolean("isFocused", false),
46
+ )
47
+ )
48
+ }
49
+ }
50
+ }
51
+
52
+ private fun parseColor(raw: String, fallbackColor: Int): Int {
53
+ return try {
54
+ Color.parseColor(raw)
55
+ } catch (_: Throwable) {
56
+ fallbackColor
57
+ }
58
+ }
59
+
60
+ private fun resolveFallbackColor(context: Context): Int {
61
+ val typedValue = TypedValue()
62
+ val attrs = intArrayOf(
63
+ androidx.appcompat.R.attr.colorPrimary,
64
+ androidx.appcompat.R.attr.colorAccent,
65
+ android.R.attr.colorAccent,
66
+ android.R.attr.textColorPrimary
67
+ )
68
+ for (attr in attrs) {
69
+ if (!context.theme.resolveAttribute(attr, typedValue, true)) {
70
+ continue
71
+ }
72
+ if (typedValue.resourceId != 0) {
73
+ AppCompatResources.getColorStateList(context, typedValue.resourceId)
74
+ ?.defaultColor
75
+ ?.let { return it }
76
+ } else if (typedValue.type in TypedValue.TYPE_FIRST_COLOR_INT..TypedValue.TYPE_LAST_COLOR_INT) {
77
+ return typedValue.data
78
+ }
79
+ }
80
+ return Color.TRANSPARENT
81
+ }
82
+ }
83
+ }
84
+
85
+ data class RemoteSelectionDebugSnapshot(
86
+ val clientId: Int,
87
+ val caretRect: RectF?,
88
+ )
89
+
90
+ class RemoteSelectionOverlayView @JvmOverloads constructor(
91
+ context: Context,
92
+ attrs: AttributeSet? = null,
93
+ defStyleAttr: Int = 0,
94
+ ) : View(context, attrs, defStyleAttr) {
95
+ private data class CachedSelectionGeometry(
96
+ val clientId: Int,
97
+ val selectionPath: Path?,
98
+ val selectionColor: Int,
99
+ val caretRect: RectF?,
100
+ val caretColor: Int,
101
+ )
102
+
103
+ private data class GeometrySnapshot(
104
+ val editorId: Long,
105
+ val text: String,
106
+ val layoutWidth: Int,
107
+ val layoutHeight: Int,
108
+ val baseX: Int,
109
+ val baseY: Int,
110
+ val width: Int,
111
+ val height: Int,
112
+ val selections: List<RemoteSelectionDecoration>,
113
+ )
114
+
115
+ private data class GeometryContext(
116
+ val snapshot: GeometrySnapshot,
117
+ val layout: android.text.Layout,
118
+ val caretWidth: Float,
119
+ )
120
+
121
+ private var editorView: RichTextEditorView? = null
122
+ private var remoteSelections: List<RemoteSelectionDecoration> = emptyList()
123
+ private var cachedSnapshot: GeometrySnapshot? = null
124
+ private var cachedGeometry: List<CachedSelectionGeometry> = emptyList()
125
+ internal var editorIdOverrideForTesting: Long? = null
126
+ internal var docToScalarResolver: (Long, Int) -> Int = { editorId, docPos ->
127
+ editorDocToScalar(editorId.toULong(), docPos.toUInt()).toInt()
128
+ }
129
+ private val selectionPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
130
+ style = Paint.Style.FILL
131
+ }
132
+ private val caretPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
133
+ style = Paint.Style.FILL
134
+ }
135
+
136
+ init {
137
+ setWillNotDraw(false)
138
+ isClickable = false
139
+ isFocusable = false
140
+ }
141
+
142
+ fun bind(editorView: RichTextEditorView) {
143
+ this.editorView = editorView
144
+ invalidateGeometry()
145
+ }
146
+
147
+ fun setRemoteSelections(selections: List<RemoteSelectionDecoration>) {
148
+ if (remoteSelections == selections) {
149
+ return
150
+ }
151
+ remoteSelections = selections
152
+ invalidateGeometry()
153
+ refreshGeometry()
154
+ }
155
+
156
+ fun refreshGeometry() {
157
+ ensureGeometry()
158
+ invalidate()
159
+ }
160
+
161
+ fun hasSelectionsOrCachedGeometry(): Boolean {
162
+ return remoteSelections.isNotEmpty() || cachedGeometry.isNotEmpty()
163
+ }
164
+
165
+ fun debugSnapshotsForTesting(): List<RemoteSelectionDebugSnapshot> {
166
+ return ensureGeometry().map { geometry ->
167
+ RemoteSelectionDebugSnapshot(
168
+ clientId = geometry.clientId,
169
+ caretRect = geometry.caretRect?.let(::RectF),
170
+ )
171
+ }
172
+ }
173
+
174
+ override fun onDraw(canvas: Canvas) {
175
+ super.onDraw(canvas)
176
+ val geometry = ensureGeometry()
177
+ if (geometry.isEmpty()) return
178
+
179
+ for (entry in geometry) {
180
+ entry.selectionPath?.let { path ->
181
+ selectionPaint.color = entry.selectionColor
182
+ canvas.drawPath(path, selectionPaint)
183
+ }
184
+
185
+ entry.caretRect?.let { caretRect ->
186
+ caretPaint.color = entry.caretColor
187
+ val cornerRadius = maxOf(1f, caretRect.width() / 2f)
188
+ canvas.drawRoundRect(
189
+ caretRect.left,
190
+ caretRect.top,
191
+ caretRect.right,
192
+ caretRect.bottom,
193
+ cornerRadius,
194
+ cornerRadius,
195
+ caretPaint
196
+ )
197
+ }
198
+ }
199
+ }
200
+
201
+ private fun ensureGeometry(): List<CachedSelectionGeometry> {
202
+ val context = buildGeometryContext() ?: run {
203
+ cachedSnapshot = null
204
+ cachedGeometry = emptyList()
205
+ return emptyList()
206
+ }
207
+ if (cachedSnapshot == context.snapshot) {
208
+ return cachedGeometry
209
+ }
210
+
211
+ val text = context.snapshot.text
212
+ val editorId = context.snapshot.editorId
213
+ val geometry = remoteSelections.map { selection ->
214
+ val startDoc = minOf(selection.anchor, selection.head)
215
+ val endDoc = maxOf(selection.anchor, selection.head)
216
+ val startScalar = docToScalarResolver(editorId, startDoc)
217
+ val endScalar = docToScalarResolver(editorId, endDoc)
218
+ val startUtf16 = PositionBridge.scalarToUtf16(startScalar, text).coerceIn(0, text.length)
219
+ val endUtf16 = PositionBridge.scalarToUtf16(endScalar, text).coerceIn(0, text.length)
220
+
221
+ val selectionPath = if (startUtf16 != endUtf16) {
222
+ Path().apply {
223
+ context.layout.getSelectionPath(startUtf16, endUtf16, this)
224
+ offset(context.snapshot.baseX.toFloat(), context.snapshot.baseY.toFloat())
225
+ }
226
+ } else {
227
+ null
228
+ }
229
+
230
+ CachedSelectionGeometry(
231
+ clientId = selection.clientId,
232
+ selectionPath = selectionPath,
233
+ selectionColor = withAlpha(selection.color, 0.18f),
234
+ caretRect = caretRectForOffset(
235
+ endUtf16 = endUtf16,
236
+ textLength = text.length,
237
+ layout = context.layout,
238
+ baseX = context.snapshot.baseX.toFloat(),
239
+ baseY = context.snapshot.baseY.toFloat(),
240
+ caretWidth = context.caretWidth,
241
+ isFocused = selection.isFocused,
242
+ ),
243
+ caretColor = selection.color,
244
+ )
245
+ }
246
+
247
+ cachedSnapshot = context.snapshot
248
+ cachedGeometry = geometry
249
+ return geometry
250
+ }
251
+
252
+ private fun buildGeometryContext(): GeometryContext? {
253
+ val editorView = editorView ?: return null
254
+ val editorId = resolvedEditorId(editorView)
255
+ if (editorId == 0L || remoteSelections.isEmpty()) return null
256
+
257
+ val editText = editorView.editorEditText
258
+ val layout = editText.layout ?: return null
259
+ val text = editText.text?.toString() ?: return null
260
+ val baseX = editorView.editorViewport.left + editorView.editorScrollView.left + editText.left +
261
+ editText.compoundPaddingLeft
262
+ val baseY = editorView.editorViewport.top + editorView.editorScrollView.top + editText.top +
263
+ editText.compoundPaddingTop - editorView.editorScrollView.scrollY
264
+ val caretWidth = maxOf(2f, resources.displayMetrics.density)
265
+
266
+ return GeometryContext(
267
+ snapshot = GeometrySnapshot(
268
+ editorId = editorId,
269
+ text = text,
270
+ layoutWidth = layout.width,
271
+ layoutHeight = layout.height,
272
+ baseX = baseX,
273
+ baseY = baseY,
274
+ width = width,
275
+ height = height,
276
+ selections = remoteSelections,
277
+ ),
278
+ layout = layout,
279
+ caretWidth = caretWidth,
280
+ )
281
+ }
282
+
283
+ private fun invalidateGeometry() {
284
+ cachedSnapshot = null
285
+ }
286
+
287
+ private fun caretRectForOffset(
288
+ endUtf16: Int,
289
+ textLength: Int,
290
+ layout: android.text.Layout,
291
+ baseX: Float,
292
+ baseY: Float,
293
+ caretWidth: Float,
294
+ isFocused: Boolean,
295
+ ): RectF? {
296
+ if (!isFocused) return null
297
+
298
+ val clampedOffset = endUtf16.coerceIn(0, textLength)
299
+ val lineLookupOffset = clampedOffset.coerceAtMost(maxOf(textLength - 1, 0))
300
+ val line = layout.getLineForOffset(lineLookupOffset)
301
+ val horizontal = layout.getPrimaryHorizontal(clampedOffset)
302
+ val caretLeft = baseX + horizontal
303
+ val caretTop = baseY + layout.getLineTop(line)
304
+ val caretBottom = baseY + layout.getLineBottom(line)
305
+ return RectF(caretLeft, caretTop, caretLeft + caretWidth, caretBottom)
306
+ }
307
+
308
+ private fun withAlpha(color: Int, alphaFraction: Float): Int {
309
+ val alpha = (255f * alphaFraction).toInt().coerceIn(0, 255)
310
+ return Color.argb(alpha, Color.red(color), Color.green(color), Color.blue(color))
311
+ }
312
+
313
+ private fun resolvedEditorId(editorView: RichTextEditorView): Long =
314
+ editorIdOverrideForTesting ?: editorView.editorId
315
+ }