@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.
- package/LICENSE +160 -0
- package/README.md +180 -0
- package/android/build.gradle +69 -0
- package/android/consumer-rules.pro +18 -0
- package/android/src/androidTest/AndroidManifest.xml +8 -0
- package/android/src/androidTest/java/com/apollohg/editor/MeasureHeightInstrumentedTest.kt +53 -0
- package/android/src/androidTest/java/com/apollohg/editor/NativeDeviceCollaborationInitialSyncTest.kt +241 -0
- package/android/src/androidTest/java/com/apollohg/editor/NativeDeviceImeRegressionTest.kt +338 -0
- package/android/src/androidTest/java/com/apollohg/editor/NativeDeviceOutsideTapTest.kt +789 -0
- package/android/src/androidTest/java/com/apollohg/editor/NativeDevicePerformanceTest.kt +350 -0
- package/android/src/androidTest/java/com/apollohg/editor/NativeEditorOutsideTapActivity.kt +5 -0
- package/android/src/main/assets/editor-icons/MaterialIcons.json +2236 -0
- package/android/src/main/assets/editor-icons/MaterialIcons.ttf +0 -0
- package/android/src/main/java/com/apollohg/editor/CaretGeometry.kt +50 -0
- package/android/src/main/java/com/apollohg/editor/EditorAddons.kt +135 -0
- package/android/src/main/java/com/apollohg/editor/EditorEditText.kt +4527 -0
- package/android/src/main/java/com/apollohg/editor/EditorHeightBehavior.kt +14 -0
- package/android/src/main/java/com/apollohg/editor/EditorInputConnection.kt +999 -0
- package/android/src/main/java/com/apollohg/editor/EditorTheme.kt +474 -0
- package/android/src/main/java/com/apollohg/editor/ImageResizeOverlayView.kt +199 -0
- package/android/src/main/java/com/apollohg/editor/NativeEditorExpoView.kt +3137 -0
- package/android/src/main/java/com/apollohg/editor/NativeEditorModule.kt +561 -0
- package/android/src/main/java/com/apollohg/editor/NativeProseViewerExpoView.kt +315 -0
- package/android/src/main/java/com/apollohg/editor/NativeToolbar.kt +1413 -0
- package/android/src/main/java/com/apollohg/editor/PositionBridge.kt +133 -0
- package/android/src/main/java/com/apollohg/editor/RemoteSelectionOverlayView.kt +315 -0
- package/android/src/main/java/com/apollohg/editor/RenderBridge.kt +2172 -0
- package/android/src/main/java/com/apollohg/editor/RichTextEditorView.kt +399 -0
- package/android/src/sharedTest/java/com/apollohg/editor/NativePerformanceSupport.kt +482 -0
- package/android/src/test/java/com/apollohg/editor/CaretGeometryTest.kt +137 -0
- package/android/src/test/java/com/apollohg/editor/EditorEditTextHardwareKeyTest.kt +321 -0
- package/android/src/test/java/com/apollohg/editor/EditorInputConnectionTest.kt +4202 -0
- package/android/src/test/java/com/apollohg/editor/NativeEditorExpoViewTest.kt +2581 -0
- package/android/src/test/java/com/apollohg/editor/NativeEditorModuleTest.kt +27 -0
- package/android/src/test/java/com/apollohg/editor/NativePerformanceTest.kt +197 -0
- package/android/src/test/java/com/apollohg/editor/NativeProseViewerExpoViewTest.kt +84 -0
- package/android/src/test/java/com/apollohg/editor/NativeToolbarTest.kt +544 -0
- package/android/src/test/java/com/apollohg/editor/PositionBridgeTest.kt +461 -0
- package/android/src/test/java/com/apollohg/editor/RenderBridgeTest.kt +2054 -0
- package/android/src/test/java/com/apollohg/editor/RichTextEditorViewTest.kt +1367 -0
- package/app.plugin.js +62 -0
- package/dist/EditorTheme.d.ts +113 -0
- package/dist/EditorTheme.js +29 -0
- package/dist/EditorToolbar.d.ts +191 -0
- package/dist/EditorToolbar.js +1095 -0
- package/dist/NativeEditorBridge.d.ts +325 -0
- package/dist/NativeEditorBridge.js +933 -0
- package/dist/NativeProseViewer.d.ts +53 -0
- package/dist/NativeProseViewer.js +480 -0
- package/dist/NativeRichTextEditor.d.ts +183 -0
- package/dist/NativeRichTextEditor.js +2574 -0
- package/dist/YjsCollaboration.d.ts +90 -0
- package/dist/YjsCollaboration.js +743 -0
- package/dist/addons.d.ts +100 -0
- package/dist/addons.js +82 -0
- package/dist/heightCache.d.ts +6 -0
- package/dist/heightCache.js +45 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +30 -0
- package/dist/schemas.d.ts +37 -0
- package/dist/schemas.js +263 -0
- package/dist/useNativeEditor.d.ts +42 -0
- package/dist/useNativeEditor.js +124 -0
- package/expo-module.config.json +9 -0
- package/ios/EditorAddons.swift +327 -0
- package/ios/EditorCore.xcframework/Info.plist +44 -0
- package/ios/EditorCore.xcframework/ios-arm64/libeditor_core.a +0 -0
- package/ios/EditorCore.xcframework/ios-arm64_x86_64-simulator/libeditor_core.a +0 -0
- package/ios/EditorLayoutManager.swift +774 -0
- package/ios/EditorTheme.swift +515 -0
- package/ios/Generated_editor_core.swift +1513 -0
- package/ios/NativeEditorExpoView.swift +3593 -0
- package/ios/NativeEditorModule.swift +624 -0
- package/ios/NativeProseViewerExpoView.swift +276 -0
- package/ios/PositionBridge.swift +558 -0
- package/ios/ReactNativeProseEditor.podspec +49 -0
- package/ios/RenderBridge.swift +1708 -0
- package/ios/RichTextEditorView.swift +6150 -0
- package/ios/Tests/NativePerformanceTests.swift +687 -0
- package/ios/Tests/PositionBridgeTests.swift +706 -0
- package/ios/Tests/RenderBridgeTests.swift +2236 -0
- package/ios/Tests/RichTextEditorViewTests.swift +6548 -0
- package/ios/editor_coreFFI/editor_coreFFI.h +1289 -0
- package/ios/editor_coreFFI/module.modulemap +7 -0
- package/ios/editor_coreFFI.h +904 -0
- package/ios/editor_coreFFI.modulemap +7 -0
- package/package.json +74 -0
- package/rust/android/arm64-v8a/libeditor_core.so +0 -0
- package/rust/android/armeabi-v7a/libeditor_core.so +0 -0
- package/rust/android/x86_64/libeditor_core.so +0 -0
- 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
|
+
}
|