@apollohg/react-native-prose-editor 0.4.0 → 0.4.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/README.md +21 -2
- package/android/build.gradle +23 -0
- package/android/src/main/java/com/apollohg/editor/EditorEditText.kt +502 -39
- package/android/src/main/java/com/apollohg/editor/NativeEditorExpoView.kt +56 -28
- package/android/src/main/java/com/apollohg/editor/NativeEditorModule.kt +6 -0
- package/android/src/main/java/com/apollohg/editor/PositionBridge.kt +57 -27
- package/android/src/main/java/com/apollohg/editor/RemoteSelectionOverlayView.kt +147 -78
- package/android/src/main/java/com/apollohg/editor/RenderBridge.kt +249 -71
- package/android/src/main/java/com/apollohg/editor/RichTextEditorView.kt +7 -6
- package/dist/NativeEditorBridge.d.ts +37 -1
- package/dist/NativeEditorBridge.js +192 -97
- package/dist/NativeRichTextEditor.d.ts +3 -2
- package/dist/NativeRichTextEditor.js +164 -56
- package/dist/YjsCollaboration.d.ts +2 -0
- package/dist/YjsCollaboration.js +142 -20
- package/dist/schemas.d.ts +2 -0
- package/dist/schemas.js +63 -0
- package/ios/EditorCore.xcframework/Info.plist +5 -5
- 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 +3 -3
- package/ios/Generated_editor_core.swift +41 -0
- package/ios/NativeEditorExpoView.swift +43 -11
- package/ios/NativeEditorModule.swift +6 -0
- package/ios/PositionBridge.swift +310 -75
- package/ios/RenderBridge.swift +362 -27
- package/ios/RichTextEditorView.swift +1983 -187
- package/ios/editor_coreFFI/editor_coreFFI.h +33 -0
- package/package.json +11 -2
- 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 +63 -0
|
@@ -65,6 +65,11 @@ class NativeEditorExpoView(
|
|
|
65
65
|
private var addons = NativeEditorAddons(null)
|
|
66
66
|
private var mentionQueryState: MentionQueryState? = null
|
|
67
67
|
private var lastMentionEventJson: String? = null
|
|
68
|
+
private var lastThemeJson: String? = null
|
|
69
|
+
private var lastAddonsJson: String? = null
|
|
70
|
+
private var lastRemoteSelectionsJson: String? = null
|
|
71
|
+
private var lastToolbarItemsJson: String? = null
|
|
72
|
+
private var lastToolbarFrameJson: String? = null
|
|
68
73
|
private var toolbarState = NativeToolbarState.empty
|
|
69
74
|
private var showsToolbar = true
|
|
70
75
|
private var toolbarPlacement = ToolbarPlacement.KEYBOARD
|
|
@@ -110,6 +115,8 @@ class NativeEditorExpoView(
|
|
|
110
115
|
}
|
|
111
116
|
|
|
112
117
|
fun setThemeJson(themeJson: String?) {
|
|
118
|
+
if (lastThemeJson == themeJson) return
|
|
119
|
+
lastThemeJson = themeJson
|
|
113
120
|
val theme = EditorTheme.fromJson(themeJson)
|
|
114
121
|
richTextView.applyTheme(theme)
|
|
115
122
|
keyboardToolbarView.applyTheme(theme?.toolbar)
|
|
@@ -141,12 +148,16 @@ class NativeEditorExpoView(
|
|
|
141
148
|
}
|
|
142
149
|
|
|
143
150
|
fun setAddonsJson(addonsJson: String?) {
|
|
151
|
+
if (lastAddonsJson == addonsJson) return
|
|
152
|
+
lastAddonsJson = addonsJson
|
|
144
153
|
addons = NativeEditorAddons.fromJson(addonsJson)
|
|
145
154
|
keyboardToolbarView.applyMentionTheme(richTextView.editorEditText.theme?.mentions ?: addons.mentions?.theme)
|
|
146
155
|
refreshMentionQuery()
|
|
147
156
|
}
|
|
148
157
|
|
|
149
158
|
fun setRemoteSelectionsJson(remoteSelectionsJson: String?) {
|
|
159
|
+
if (lastRemoteSelectionsJson == remoteSelectionsJson) return
|
|
160
|
+
lastRemoteSelectionsJson = remoteSelectionsJson
|
|
150
161
|
richTextView.setRemoteSelections(
|
|
151
162
|
RemoteSelectionDecoration.fromJson(context, remoteSelectionsJson)
|
|
152
163
|
)
|
|
@@ -175,10 +186,14 @@ class NativeEditorExpoView(
|
|
|
175
186
|
}
|
|
176
187
|
|
|
177
188
|
fun setToolbarItemsJson(toolbarItemsJson: String?) {
|
|
189
|
+
if (lastToolbarItemsJson == toolbarItemsJson) return
|
|
190
|
+
lastToolbarItemsJson = toolbarItemsJson
|
|
178
191
|
keyboardToolbarView.setItems(NativeToolbarItem.fromJson(toolbarItemsJson))
|
|
179
192
|
}
|
|
180
193
|
|
|
181
194
|
fun setToolbarFrameJson(toolbarFrameJson: String?) {
|
|
195
|
+
if (lastToolbarFrameJson == toolbarFrameJson) return
|
|
196
|
+
lastToolbarFrameJson = toolbarFrameJson
|
|
182
197
|
if (toolbarFrameJson.isNullOrBlank()) {
|
|
183
198
|
toolbarFrameInWindow = null
|
|
184
199
|
return
|
|
@@ -260,31 +275,44 @@ class NativeEditorExpoView(
|
|
|
260
275
|
if (heightBehavior != EditorHeightBehavior.AUTO_GROW) return
|
|
261
276
|
val editText = richTextView.editorEditText
|
|
262
277
|
val resolvedEditHeight = editText.resolveAutoGrowHeight()
|
|
278
|
+
val resolvedContainerHeight =
|
|
279
|
+
resolvedEditHeight +
|
|
280
|
+
richTextView.paddingTop +
|
|
281
|
+
richTextView.paddingBottom +
|
|
282
|
+
paddingTop +
|
|
283
|
+
paddingBottom
|
|
263
284
|
val contentHeight = (
|
|
264
285
|
when {
|
|
265
286
|
editText.isLaidOut && (editText.layout?.height ?: 0) > 0 -> {
|
|
266
|
-
(
|
|
267
|
-
editText.
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
287
|
+
maxOf(
|
|
288
|
+
(editText.layout?.height ?: 0) +
|
|
289
|
+
editText.compoundPaddingTop +
|
|
290
|
+
editText.compoundPaddingBottom +
|
|
291
|
+
richTextView.paddingTop +
|
|
292
|
+
richTextView.paddingBottom +
|
|
293
|
+
paddingTop +
|
|
294
|
+
paddingBottom,
|
|
295
|
+
resolvedContainerHeight
|
|
296
|
+
)
|
|
271
297
|
}
|
|
272
298
|
richTextView.measuredHeight > 0 -> {
|
|
273
|
-
|
|
299
|
+
maxOf(
|
|
300
|
+
richTextView.measuredHeight + paddingTop + paddingBottom,
|
|
301
|
+
resolvedContainerHeight
|
|
302
|
+
)
|
|
274
303
|
}
|
|
275
304
|
editText.measuredHeight > 0 -> {
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
305
|
+
maxOf(
|
|
306
|
+
editText.measuredHeight +
|
|
307
|
+
richTextView.paddingTop +
|
|
308
|
+
richTextView.paddingBottom +
|
|
309
|
+
paddingTop +
|
|
310
|
+
paddingBottom,
|
|
311
|
+
resolvedContainerHeight
|
|
312
|
+
)
|
|
281
313
|
}
|
|
282
314
|
else -> {
|
|
283
|
-
|
|
284
|
-
richTextView.paddingTop +
|
|
285
|
-
richTextView.paddingBottom +
|
|
286
|
-
paddingTop +
|
|
287
|
-
paddingBottom
|
|
315
|
+
resolvedContainerHeight
|
|
288
316
|
}
|
|
289
317
|
}
|
|
290
318
|
).coerceAtLeast(0)
|
|
@@ -304,20 +332,20 @@ class NativeEditorExpoView(
|
|
|
304
332
|
if (android.os.Looper.myLooper() == android.os.Looper.getMainLooper()) {
|
|
305
333
|
apply.run()
|
|
306
334
|
} else {
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
apply.run()
|
|
310
|
-
latch.countDown()
|
|
335
|
+
if (!post(apply)) {
|
|
336
|
+
richTextView.post(apply)
|
|
311
337
|
}
|
|
312
|
-
latch.await()
|
|
313
338
|
}
|
|
314
339
|
}
|
|
315
340
|
|
|
316
341
|
override fun onSelectionChanged(anchor: Int, head: Int) {
|
|
317
|
-
refreshToolbarStateFromEditorSelection()
|
|
342
|
+
val stateJson = refreshToolbarStateFromEditorSelection()
|
|
318
343
|
refreshMentionQuery()
|
|
319
344
|
richTextView.refreshRemoteSelections()
|
|
320
|
-
val event =
|
|
345
|
+
val event = mutableMapOf<String, Any>("anchor" to anchor, "head" to head)
|
|
346
|
+
if (stateJson != null) {
|
|
347
|
+
event["stateJson"] = stateJson
|
|
348
|
+
}
|
|
321
349
|
onSelectionChange(event)
|
|
322
350
|
}
|
|
323
351
|
|
|
@@ -541,13 +569,13 @@ class NativeEditorExpoView(
|
|
|
541
569
|
clearMentionQueryState()
|
|
542
570
|
}
|
|
543
571
|
|
|
544
|
-
private fun refreshToolbarStateFromEditorSelection() {
|
|
545
|
-
if (richTextView.editorId == 0L) return
|
|
546
|
-
val
|
|
547
|
-
|
|
548
|
-
) ?: return
|
|
572
|
+
private fun refreshToolbarStateFromEditorSelection(): String? {
|
|
573
|
+
if (richTextView.editorId == 0L) return null
|
|
574
|
+
val stateJson = editorGetSelectionState(richTextView.editorId.toULong())
|
|
575
|
+
val state = NativeToolbarState.fromUpdateJson(stateJson) ?: return null
|
|
549
576
|
toolbarState = state
|
|
550
577
|
keyboardToolbarView.applyState(state)
|
|
578
|
+
return stateJson
|
|
551
579
|
}
|
|
552
580
|
|
|
553
581
|
private fun ensureKeyboardToolbarAttached() {
|
|
@@ -63,6 +63,9 @@ class NativeEditorModule : Module() {
|
|
|
63
63
|
Function("editorGetJson") { id: Int ->
|
|
64
64
|
editorGetJson(id.toULong())
|
|
65
65
|
}
|
|
66
|
+
Function("editorGetContentSnapshot") { id: Int ->
|
|
67
|
+
editorGetContentSnapshot(id.toULong())
|
|
68
|
+
}
|
|
66
69
|
|
|
67
70
|
Function("editorInsertText") { id: Int, pos: Int, text: String ->
|
|
68
71
|
editorInsertText(id.toULong(), pos.toUInt(), text)
|
|
@@ -258,6 +261,9 @@ class NativeEditorModule : Module() {
|
|
|
258
261
|
Function("editorGetSelection") { id: Int ->
|
|
259
262
|
editorGetSelection(id.toULong())
|
|
260
263
|
}
|
|
264
|
+
Function("editorGetSelectionState") { id: Int ->
|
|
265
|
+
editorGetSelectionState(id.toULong())
|
|
266
|
+
}
|
|
261
267
|
Function("editorDocToScalar") { id: Int, docPos: Int ->
|
|
262
268
|
editorDocToScalar(id.toULong(), docPos.toUInt()).toInt()
|
|
263
269
|
}
|
|
@@ -8,25 +8,22 @@ import android.icu.text.BreakIterator
|
|
|
8
8
|
* cursor inside a composed character.
|
|
9
9
|
*/
|
|
10
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
|
|
11
19
|
|
|
12
20
|
/**
|
|
13
21
|
* Counts code points from the start of the string up to the given UTF-16 offset.
|
|
14
22
|
*/
|
|
15
23
|
fun utf16ToScalar(utf16Offset: Int, text: String): Int {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
var scalarCount = 0
|
|
20
|
-
var utf16Pos = 0
|
|
21
|
-
|
|
22
|
-
while (utf16Pos < endIndex) {
|
|
23
|
-
val codePoint = Character.codePointAt(text, utf16Pos)
|
|
24
|
-
val charCount = Character.charCount(codePoint)
|
|
25
|
-
utf16Pos += charCount
|
|
26
|
-
scalarCount++
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
return scalarCount
|
|
24
|
+
val conversionTable = conversionTableFor(text)
|
|
25
|
+
val endIndex = utf16Offset.coerceIn(0, conversionTable.utf16ToScalar.size - 1)
|
|
26
|
+
return conversionTable.utf16ToScalar[endIndex]
|
|
30
27
|
}
|
|
31
28
|
|
|
32
29
|
/**
|
|
@@ -35,19 +32,9 @@ object PositionBridge {
|
|
|
35
32
|
fun scalarToUtf16(scalarOffset: Int, text: String): Int {
|
|
36
33
|
if (scalarOffset <= 0) return 0
|
|
37
34
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
var i = 0
|
|
42
|
-
while (i < text.length && scalarsSeen < scalarOffset) {
|
|
43
|
-
val codePoint = Character.codePointAt(text, i)
|
|
44
|
-
val charCount = Character.charCount(codePoint)
|
|
45
|
-
utf16Len += charCount
|
|
46
|
-
scalarsSeen++
|
|
47
|
-
i += charCount
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
return utf16Len
|
|
35
|
+
val conversionTable = conversionTableFor(text)
|
|
36
|
+
val clampedScalar = scalarOffset.coerceIn(0, conversionTable.scalarToUtf16.size - 1)
|
|
37
|
+
return conversionTable.scalarToUtf16[clampedScalar]
|
|
51
38
|
}
|
|
52
39
|
|
|
53
40
|
/**
|
|
@@ -73,4 +60,47 @@ object PositionBridge {
|
|
|
73
60
|
val nextBoundary = breakIterator.following(clampedOffset)
|
|
74
61
|
return if (nextBoundary == BreakIterator.DONE) text.length else nextBoundary
|
|
75
62
|
}
|
|
63
|
+
|
|
64
|
+
private fun conversionTableFor(text: String): ConversionTable {
|
|
65
|
+
val lastText = cachedText
|
|
66
|
+
val lastTable = cachedTable
|
|
67
|
+
if (lastText == text && lastTable != null) {
|
|
68
|
+
return lastTable
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
synchronized(cacheLock) {
|
|
72
|
+
val synchronizedText = cachedText
|
|
73
|
+
val synchronizedTable = cachedTable
|
|
74
|
+
if (synchronizedText == text && synchronizedTable != null) {
|
|
75
|
+
return synchronizedTable
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
val scalarCount = text.codePointCount(0, text.length)
|
|
79
|
+
val utf16ToScalar = IntArray(text.length + 1)
|
|
80
|
+
val scalarToUtf16 = IntArray(scalarCount + 1)
|
|
81
|
+
|
|
82
|
+
var utf16Pos = 0
|
|
83
|
+
var scalarPos = 0
|
|
84
|
+
while (utf16Pos < text.length) {
|
|
85
|
+
val codePoint = Character.codePointAt(text, utf16Pos)
|
|
86
|
+
val charCount = Character.charCount(codePoint)
|
|
87
|
+
val nextUtf16Pos = utf16Pos + charCount
|
|
88
|
+
scalarPos += 1
|
|
89
|
+
|
|
90
|
+
for (offset in (utf16Pos + 1)..nextUtf16Pos) {
|
|
91
|
+
utf16ToScalar[offset] = scalarPos
|
|
92
|
+
}
|
|
93
|
+
scalarToUtf16[scalarPos] = nextUtf16Pos
|
|
94
|
+
utf16Pos = nextUtf16Pos
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return ConversionTable(
|
|
98
|
+
utf16ToScalar = utf16ToScalar,
|
|
99
|
+
scalarToUtf16 = scalarToUtf16,
|
|
100
|
+
).also {
|
|
101
|
+
cachedText = text
|
|
102
|
+
cachedTable = it
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
76
106
|
}
|
|
@@ -10,7 +10,6 @@ import android.util.AttributeSet
|
|
|
10
10
|
import android.util.TypedValue
|
|
11
11
|
import android.view.View
|
|
12
12
|
import androidx.appcompat.content.res.AppCompatResources
|
|
13
|
-
import com.google.android.material.R as MaterialR
|
|
14
13
|
import org.json.JSONArray
|
|
15
14
|
import uniffi.editor_core.editorDocToScalar
|
|
16
15
|
|
|
@@ -61,8 +60,8 @@ data class RemoteSelectionDecoration(
|
|
|
61
60
|
private fun resolveFallbackColor(context: Context): Int {
|
|
62
61
|
val typedValue = TypedValue()
|
|
63
62
|
val attrs = intArrayOf(
|
|
64
|
-
|
|
65
|
-
|
|
63
|
+
androidx.appcompat.R.attr.colorPrimary,
|
|
64
|
+
androidx.appcompat.R.attr.colorAccent,
|
|
66
65
|
android.R.attr.colorAccent,
|
|
67
66
|
android.R.attr.textColorPrimary
|
|
68
67
|
)
|
|
@@ -93,14 +92,40 @@ class RemoteSelectionOverlayView @JvmOverloads constructor(
|
|
|
93
92
|
attrs: AttributeSet? = null,
|
|
94
93
|
defStyleAttr: Int = 0,
|
|
95
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
|
+
)
|
|
96
120
|
|
|
97
121
|
private var editorView: RichTextEditorView? = null
|
|
98
122
|
private var remoteSelections: List<RemoteSelectionDecoration> = emptyList()
|
|
123
|
+
private var cachedSnapshot: GeometrySnapshot? = null
|
|
124
|
+
private var cachedGeometry: List<CachedSelectionGeometry> = emptyList()
|
|
99
125
|
internal var editorIdOverrideForTesting: Long? = null
|
|
100
126
|
internal var docToScalarResolver: (Long, Int) -> Int = { editorId, docPos ->
|
|
101
127
|
editorDocToScalar(editorId.toULong(), docPos.toUInt()).toInt()
|
|
102
128
|
}
|
|
103
|
-
private val selectionPath = Path()
|
|
104
129
|
private val selectionPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
|
105
130
|
style = Paint.Style.FILL
|
|
106
131
|
}
|
|
@@ -116,60 +141,76 @@ class RemoteSelectionOverlayView @JvmOverloads constructor(
|
|
|
116
141
|
|
|
117
142
|
fun bind(editorView: RichTextEditorView) {
|
|
118
143
|
this.editorView = editorView
|
|
144
|
+
invalidateGeometry()
|
|
119
145
|
}
|
|
120
146
|
|
|
121
147
|
fun setRemoteSelections(selections: List<RemoteSelectionDecoration>) {
|
|
148
|
+
if (remoteSelections == selections) {
|
|
149
|
+
return
|
|
150
|
+
}
|
|
122
151
|
remoteSelections = selections
|
|
123
|
-
|
|
152
|
+
invalidateGeometry()
|
|
153
|
+
refreshGeometry()
|
|
124
154
|
}
|
|
125
155
|
|
|
126
|
-
fun
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
156
|
+
fun refreshGeometry() {
|
|
157
|
+
ensureGeometry()
|
|
158
|
+
invalidate()
|
|
159
|
+
}
|
|
130
160
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
val baseX = (editorView.editorViewport.left + editorView.editorScrollView.left + editText.left).toFloat() +
|
|
135
|
-
editText.compoundPaddingLeft
|
|
136
|
-
val baseY = (editorView.editorViewport.top + editorView.editorScrollView.top + editText.top).toFloat() +
|
|
137
|
-
editText.compoundPaddingTop - editorView.editorScrollView.scrollY
|
|
138
|
-
val caretWidth = maxOf(2f, 2f * resources.displayMetrics.density / 2f)
|
|
161
|
+
fun hasSelectionsOrCachedGeometry(): Boolean {
|
|
162
|
+
return remoteSelections.isNotEmpty() || cachedGeometry.isNotEmpty()
|
|
163
|
+
}
|
|
139
164
|
|
|
140
|
-
|
|
165
|
+
fun debugSnapshotsForTesting(): List<RemoteSelectionDebugSnapshot> {
|
|
166
|
+
return ensureGeometry().map { geometry ->
|
|
141
167
|
RemoteSelectionDebugSnapshot(
|
|
142
|
-
clientId =
|
|
143
|
-
caretRect =
|
|
144
|
-
selection = selection,
|
|
145
|
-
editorId = editorId,
|
|
146
|
-
text = text,
|
|
147
|
-
layout = layout,
|
|
148
|
-
baseX = baseX,
|
|
149
|
-
baseY = baseY,
|
|
150
|
-
caretWidth = caretWidth,
|
|
151
|
-
),
|
|
168
|
+
clientId = geometry.clientId,
|
|
169
|
+
caretRect = geometry.caretRect?.let(::RectF),
|
|
152
170
|
)
|
|
153
171
|
}
|
|
154
172
|
}
|
|
155
173
|
|
|
156
174
|
override fun onDraw(canvas: Canvas) {
|
|
157
175
|
super.onDraw(canvas)
|
|
176
|
+
val geometry = ensureGeometry()
|
|
177
|
+
if (geometry.isEmpty()) return
|
|
158
178
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
179
|
+
for (entry in geometry) {
|
|
180
|
+
entry.selectionPath?.let { path ->
|
|
181
|
+
selectionPaint.color = entry.selectionColor
|
|
182
|
+
canvas.drawPath(path, selectionPaint)
|
|
183
|
+
}
|
|
162
184
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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
|
+
}
|
|
171
200
|
|
|
172
|
-
|
|
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 ->
|
|
173
214
|
val startDoc = minOf(selection.anchor, selection.head)
|
|
174
215
|
val endDoc = maxOf(selection.anchor, selection.head)
|
|
175
216
|
val startScalar = docToScalarResolver(editorId, startDoc)
|
|
@@ -177,59 +218,87 @@ class RemoteSelectionOverlayView @JvmOverloads constructor(
|
|
|
177
218
|
val startUtf16 = PositionBridge.scalarToUtf16(startScalar, text).coerceIn(0, text.length)
|
|
178
219
|
val endUtf16 = PositionBridge.scalarToUtf16(endScalar, text).coerceIn(0, text.length)
|
|
179
220
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
canvas.translate(baseX, baseY)
|
|
188
|
-
canvas.drawPath(selectionPath, selectionPaint)
|
|
189
|
-
canvas.restore()
|
|
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
|
|
190
228
|
}
|
|
191
229
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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
|
|
195
256
|
|
|
196
|
-
|
|
197
|
-
|
|
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(
|
|
198
268
|
editorId = editorId,
|
|
199
269
|
text = text,
|
|
200
|
-
|
|
270
|
+
layoutWidth = layout.width,
|
|
271
|
+
layoutHeight = layout.height,
|
|
201
272
|
baseX = baseX,
|
|
202
273
|
baseY = baseY,
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
}
|
|
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
|
|
215
285
|
}
|
|
216
286
|
|
|
217
|
-
private fun
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
text: String,
|
|
287
|
+
private fun caretRectForOffset(
|
|
288
|
+
endUtf16: Int,
|
|
289
|
+
textLength: Int,
|
|
221
290
|
layout: android.text.Layout,
|
|
222
291
|
baseX: Float,
|
|
223
292
|
baseY: Float,
|
|
224
293
|
caretWidth: Float,
|
|
294
|
+
isFocused: Boolean,
|
|
225
295
|
): RectF? {
|
|
226
|
-
if (!
|
|
296
|
+
if (!isFocused) return null
|
|
227
297
|
|
|
228
|
-
val
|
|
229
|
-
val
|
|
230
|
-
val
|
|
231
|
-
val
|
|
232
|
-
val horizontal = layout.getPrimaryHorizontal(endUtf16)
|
|
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)
|
|
233
302
|
val caretLeft = baseX + horizontal
|
|
234
303
|
val caretTop = baseY + layout.getLineTop(line)
|
|
235
304
|
val caretBottom = baseY + layout.getLineBottom(line)
|