@openeditor/react-native-prose-editor 0.0.7 → 0.0.8
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/android/build.gradle +35 -0
- package/android/src/main/java/com/apollohg/editor/CaretGeometry.kt +2 -2
- package/android/src/main/java/com/apollohg/editor/EditorEditText.kt +138 -597
- package/android/src/main/java/com/apollohg/editor/EditorInputConnection.kt +13 -3
- package/android/src/main/java/com/apollohg/editor/InputSnapshotSupport.kt +6 -0
- package/android/src/main/java/com/apollohg/editor/NativeBlockEditorSurface.kt +1086 -0
- package/android/src/main/java/com/apollohg/editor/NativeEditorExpoView.kt +1 -1
- package/android/src/main/java/com/apollohg/editor/NativeEditorModule.kt +26 -50
- package/android/src/main/java/com/apollohg/editor/NativeImageLoader.kt +89 -0
- package/android/src/main/java/com/apollohg/editor/RemoteSelectionOverlayView.kt +31 -66
- package/android/src/main/java/com/apollohg/editor/RichTextEditorView.kt +98 -33
- package/dist/EditorToolbar.d.ts +1 -1
- package/dist/EditorToolbar.js +19 -0
- package/dist/NativeEditorBridge.d.ts +62 -35
- package/dist/NativeEditorBridge.js +122 -79
- package/dist/NativeProseViewer.d.ts +13 -34
- package/dist/NativeProseViewer.js +40 -465
- package/dist/NativeRichTextEditor.d.ts +10 -1
- package/dist/NativeRichTextEditor.js +72 -34
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -1
- package/dist/schemas.d.ts +11 -0
- package/dist/schemas.js +149 -0
- package/dist/useNativeEditor.d.ts +3 -3
- package/dist/useNativeEditor.js +3 -3
- 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/Generated_editor_core.swift +97 -2
- package/ios/NativeBlockEditorSurface.swift +1393 -0
- package/ios/NativeEditorExpoView.swift +3 -3
- package/ios/NativeEditorModule.swift +30 -45
- package/ios/NativeInputSupport.swift +396 -0
- package/ios/PositionBridge.swift +4 -4
- package/ios/RichTextEditorView.swift +333 -786
- package/ios/editor_coreFFI/editor_coreFFI.h +147 -70
- package/package.json +1 -1
- 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 +2087 -1399
- package/android/src/androidTest/AndroidManifest.xml +0 -8
- package/android/src/androidTest/java/com/apollohg/editor/MeasureHeightInstrumentedTest.kt +0 -53
- package/android/src/androidTest/java/com/apollohg/editor/NativeDeviceCollaborationInitialSyncTest.kt +0 -241
- package/android/src/androidTest/java/com/apollohg/editor/NativeDeviceImeRegressionTest.kt +0 -338
- package/android/src/androidTest/java/com/apollohg/editor/NativeDeviceOutsideTapTest.kt +0 -789
- package/android/src/androidTest/java/com/apollohg/editor/NativeDevicePerformanceTest.kt +0 -350
- package/android/src/androidTest/java/com/apollohg/editor/NativeEditorOutsideTapActivity.kt +0 -5
- package/android/src/main/java/com/apollohg/editor/NativeProseViewerExpoView.kt +0 -315
- package/android/src/main/java/com/apollohg/editor/RenderBridge.kt +0 -2172
- package/android/src/sharedTest/java/com/apollohg/editor/NativePerformanceSupport.kt +0 -482
- package/android/src/test/java/com/apollohg/editor/CaretGeometryTest.kt +0 -137
- package/android/src/test/java/com/apollohg/editor/EditorEditTextHardwareKeyTest.kt +0 -321
- package/android/src/test/java/com/apollohg/editor/EditorInputConnectionTest.kt +0 -4202
- package/android/src/test/java/com/apollohg/editor/NativeEditorExpoViewTest.kt +0 -2581
- package/android/src/test/java/com/apollohg/editor/NativeEditorModuleTest.kt +0 -27
- package/android/src/test/java/com/apollohg/editor/NativePerformanceTest.kt +0 -197
- package/android/src/test/java/com/apollohg/editor/NativeProseViewerExpoViewTest.kt +0 -84
- package/android/src/test/java/com/apollohg/editor/NativeToolbarTest.kt +0 -544
- package/android/src/test/java/com/apollohg/editor/PositionBridgeTest.kt +0 -461
- package/android/src/test/java/com/apollohg/editor/RenderBridgeTest.kt +0 -2054
- package/android/src/test/java/com/apollohg/editor/RichTextEditorViewTest.kt +0 -1367
- package/ios/EditorLayoutManager.swift +0 -774
- package/ios/NativeProseViewerExpoView.swift +0 -276
- package/ios/RenderBridge.swift +0 -1708
- package/ios/Tests/NativePerformanceTests.swift +0 -687
- package/ios/Tests/PositionBridgeTests.swift +0 -706
- package/ios/Tests/RenderBridgeTests.swift +0 -2236
- package/ios/Tests/RichTextEditorViewTests.swift +0 -6548
|
@@ -48,11 +48,11 @@ import uniffi.editor_core.* // UniFFI-generated bindings
|
|
|
48
48
|
* Instead of letting Android's EditText internal text storage handle insertions
|
|
49
49
|
* and deletions, this class captures the user's intent (typing, deleting,
|
|
50
50
|
* pasting, autocorrect) and sends it to the Rust editor. The Rust editor
|
|
51
|
-
* returns
|
|
52
|
-
*
|
|
51
|
+
* returns a canonical plain-text input snapshot, which is authorized and
|
|
52
|
+
* applied back to this hidden input adapter.
|
|
53
53
|
*
|
|
54
54
|
* This is the "input interception" pattern: the EditText is effectively
|
|
55
|
-
*
|
|
55
|
+
* an IME/selection surface, not the visible block renderer.
|
|
56
56
|
*
|
|
57
57
|
* ## Composition Handling
|
|
58
58
|
*
|
|
@@ -99,6 +99,7 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
99
99
|
val rect: RectF
|
|
100
100
|
)
|
|
101
101
|
|
|
102
|
+
|
|
102
103
|
data class MentionHit(
|
|
103
104
|
val docPos: Int,
|
|
104
105
|
val label: String
|
|
@@ -120,29 +121,11 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
120
121
|
val text: String
|
|
121
122
|
)
|
|
122
123
|
|
|
123
|
-
private data class ParsedRenderPatch(
|
|
124
|
-
val startIndex: Int,
|
|
125
|
-
val deleteCount: Int,
|
|
126
|
-
val renderBlocks: org.json.JSONArray
|
|
127
|
-
)
|
|
128
|
-
|
|
129
124
|
private data class RenderReplaceRange(
|
|
130
125
|
val start: Int,
|
|
131
126
|
val endExclusive: Int
|
|
132
127
|
)
|
|
133
128
|
|
|
134
|
-
private data class PatchApplyTrace(
|
|
135
|
-
val applied: Boolean,
|
|
136
|
-
val eligibilityNanos: Long,
|
|
137
|
-
val buildRenderNanos: Long,
|
|
138
|
-
val applyRenderNanos: Long
|
|
139
|
-
)
|
|
140
|
-
|
|
141
|
-
private data class ImageSelectionRange(
|
|
142
|
-
val start: Int,
|
|
143
|
-
val end: Int
|
|
144
|
-
)
|
|
145
|
-
|
|
146
129
|
private data class NativeTextMutation(
|
|
147
130
|
val scalarFrom: Int,
|
|
148
131
|
val scalarTo: Int,
|
|
@@ -226,6 +209,7 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
226
209
|
/** Listener for editor events. */
|
|
227
210
|
var editorListener: EditorListener? = null
|
|
228
211
|
var onSelectionOrContentMayChange: (() -> Unit)? = null
|
|
212
|
+
var onUpdateJSONApplied: ((String) -> Unit)? = null
|
|
229
213
|
|
|
230
214
|
/** The base font size in pixels used for unstyled text. */
|
|
231
215
|
private var baseFontSize: Float = textSize
|
|
@@ -285,14 +269,12 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
285
269
|
private var nativeTextMutationAdoptionSuppression: NativeTextMutationAdoptionSuppression? = null
|
|
286
270
|
private var lastAuthorizedTextRevision: Long = 0L
|
|
287
271
|
private var lastAuthorizedRenderedText: CharSequence? = null
|
|
288
|
-
private var explicitSelectedImageRange: ImageSelectionRange? = null
|
|
289
272
|
private var lastRenderAppliedPatchForTesting: Boolean = false
|
|
290
273
|
internal var captureApplyUpdateTraceForTesting: Boolean = false
|
|
291
274
|
private var lastApplyUpdateTraceForTesting: ApplyUpdateTrace? = null
|
|
292
275
|
private val imeTraceForTesting = java.util.ArrayDeque<String>()
|
|
293
276
|
private var imeTraceSequence: Long = 0L
|
|
294
277
|
private var lastImeTraceUptimeMs: Long = 0L
|
|
295
|
-
private var currentRenderBlocksJson: org.json.JSONArray? = null
|
|
296
278
|
private var renderAppearanceRevision: Long = 1L
|
|
297
279
|
private var lastAppliedRenderAppearanceRevision: Long = 0L
|
|
298
280
|
private var pendingOptimisticRenderText: String? = null
|
|
@@ -401,8 +383,8 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
401
383
|
linksClickable = false
|
|
402
384
|
|
|
403
385
|
// Suppress the platform caret and draw our own. Android's Editor anchors
|
|
404
|
-
// the native caret to getLineBottom(line), which
|
|
405
|
-
//
|
|
386
|
+
// the native caret to getLineBottom(line), which line-height spans can
|
|
387
|
+
// inflate. Our caret is
|
|
406
388
|
// clipped to the glyph height via [CaretGeometry]. See [drawCustomCaret].
|
|
407
389
|
isCursorVisible = false
|
|
408
390
|
|
|
@@ -642,7 +624,7 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
642
624
|
'\\' -> builder.append("\\\\")
|
|
643
625
|
'"' -> builder.append("\\\"")
|
|
644
626
|
else -> {
|
|
645
|
-
if (ch.code < 0x20 || ch ==
|
|
627
|
+
if (ch.code < 0x20 || ch == InputSnapshotConstants.SYNTHETIC_PLACEHOLDER_CHARACTER[0]) {
|
|
646
628
|
builder.append("\\u")
|
|
647
629
|
builder.append(ch.code.toString(16).padStart(4, '0'))
|
|
648
630
|
} else {
|
|
@@ -746,15 +728,6 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
746
728
|
}
|
|
747
729
|
|
|
748
730
|
override fun onTouchEvent(event: MotionEvent): Boolean {
|
|
749
|
-
if (event.actionMasked == MotionEvent.ACTION_DOWN && imageSpanHitAt(event.x, event.y) == null) {
|
|
750
|
-
clearExplicitSelectedImageRange()
|
|
751
|
-
}
|
|
752
|
-
if (handleTaskListMarkerTap(event)) {
|
|
753
|
-
return true
|
|
754
|
-
}
|
|
755
|
-
if (handleImageTap(event)) {
|
|
756
|
-
return true
|
|
757
|
-
}
|
|
758
731
|
if (heightBehavior == EditorHeightBehavior.FIXED) {
|
|
759
732
|
val canScroll = canScrollVertically(-1) || canScrollVertically(1)
|
|
760
733
|
if (canScroll) {
|
|
@@ -962,9 +935,7 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
962
935
|
fun setImageResizingEnabled(enabled: Boolean) {
|
|
963
936
|
if (imageResizingEnabled == enabled) return
|
|
964
937
|
imageResizingEnabled = enabled
|
|
965
|
-
if (
|
|
966
|
-
clearExplicitSelectedImageRange()
|
|
967
|
-
} else {
|
|
938
|
+
if (enabled) {
|
|
968
939
|
onSelectionOrContentMayChange?.invoke()
|
|
969
940
|
}
|
|
970
941
|
}
|
|
@@ -1043,8 +1014,8 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
1043
1014
|
val selectionOffset = selectionEnd.takeIf { it >= 0 } ?: return null
|
|
1044
1015
|
val clampedOffset = selectionOffset.coerceIn(0, textLayout.text.length)
|
|
1045
1016
|
val caretLeft = textLayout.getPrimaryHorizontal(clampedOffset)
|
|
1046
|
-
// Clip the caret to the rendered glyph height so
|
|
1047
|
-
//
|
|
1017
|
+
// Clip the caret to the rendered glyph height so inflated descent does
|
|
1018
|
+
// not stretch it into extra line space.
|
|
1048
1019
|
val bounds = CaretGeometry.verticalBounds(textLayout, clampedOffset, paint)
|
|
1049
1020
|
val left = totalPaddingLeft + caretLeft - scrollX
|
|
1050
1021
|
val top = totalPaddingTop + bounds.top - scrollY
|
|
@@ -1169,8 +1140,6 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
1169
1140
|
if (currentText != lastAuthorizedText) return false
|
|
1170
1141
|
if (startUtf16 < 0 || endUtf16 < startUtf16 || endUtf16 > editable.length) return false
|
|
1171
1142
|
val spanned = editable as? Spanned
|
|
1172
|
-
if (spanned != null && spannedRangeContainsImageSpan(spanned, startUtf16, endUtf16)) return false
|
|
1173
|
-
|
|
1174
1143
|
val inlineSpans = spanned?.let {
|
|
1175
1144
|
optimisticInlineSpansForInsertion(it, startUtf16)
|
|
1176
1145
|
}.orEmpty()
|
|
@@ -1290,6 +1259,32 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
1290
1259
|
setCompositionReplacementRange(start, end)
|
|
1291
1260
|
}
|
|
1292
1261
|
|
|
1262
|
+
/** Select the synthetic empty-block scalar so BaseInputConnection replaces
|
|
1263
|
+
* it with transient composing text instead of appending beside it. The
|
|
1264
|
+
* Rust replacement range remains the collapsed 0-width document range. */
|
|
1265
|
+
internal fun selectSyntheticPlaceholderForTransientComposition(): Boolean {
|
|
1266
|
+
val editable = text ?: return false
|
|
1267
|
+
val start = selectionStart
|
|
1268
|
+
val end = selectionEnd
|
|
1269
|
+
if (start < 0 || start != end || start >= editable.length) return false
|
|
1270
|
+
if (editable[start] != EMPTY_BLOCK_PLACEHOLDER) return false
|
|
1271
|
+
Selection.setSelection(editable, start, start + 1)
|
|
1272
|
+
return true
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
internal fun collapseSelectionToComposingEndForEditor() {
|
|
1276
|
+
val editable = text ?: return
|
|
1277
|
+
val composingEnd = BaseInputConnection.getComposingSpanEnd(editable)
|
|
1278
|
+
val replacementStart = composingReplacementStartUtf16
|
|
1279
|
+
val trackedEnd = if (replacementStart != null) {
|
|
1280
|
+
replacementStart + (composingText?.length ?: 0)
|
|
1281
|
+
} else {
|
|
1282
|
+
-1
|
|
1283
|
+
}
|
|
1284
|
+
val end = if (composingEnd >= 0) composingEnd else trackedEnd
|
|
1285
|
+
if (end >= 0) Selection.setSelection(editable, end.coerceIn(0, editable.length))
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1293
1288
|
internal fun setCompositionReplacementRange(start: Int, end: Int) {
|
|
1294
1289
|
if (didInvalidateCompositionReplacementRange) return
|
|
1295
1290
|
val replacementRange = authorizedUtf16Range(start, end)
|
|
@@ -1425,8 +1420,22 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
1425
1420
|
val currentText = text?.toString() ?: return null
|
|
1426
1421
|
if (start < 0 || end < start || end > authorizedText.length) return null
|
|
1427
1422
|
|
|
1423
|
+
// The input snapshot represents an otherwise empty text block with a
|
|
1424
|
+
// native-only zero-width scalar. Composition at that document position
|
|
1425
|
+
// replaces the scalar visually, while its Rust replacement range stays
|
|
1426
|
+
// collapsed because the scalar is not document content. Exclude it from
|
|
1427
|
+
// the authorized suffix when recovering the transient replacement.
|
|
1428
|
+
val authorizedEnd = if (
|
|
1429
|
+
start == end &&
|
|
1430
|
+
end < authorizedText.length &&
|
|
1431
|
+
authorizedText[end] == EMPTY_BLOCK_PLACEHOLDER
|
|
1432
|
+
) {
|
|
1433
|
+
end + 1
|
|
1434
|
+
} else {
|
|
1435
|
+
end
|
|
1436
|
+
}
|
|
1428
1437
|
val authorizedPrefix = authorizedText.substring(0, start)
|
|
1429
|
-
val authorizedSuffix = authorizedText.substring(
|
|
1438
|
+
val authorizedSuffix = authorizedText.substring(authorizedEnd)
|
|
1430
1439
|
if (!currentText.startsWith(authorizedPrefix)) return null
|
|
1431
1440
|
if (!currentText.endsWith(authorizedSuffix)) return null
|
|
1432
1441
|
|
|
@@ -2620,10 +2629,6 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
2620
2629
|
override fun onSelectionChanged(selStart: Int, selEnd: Int) {
|
|
2621
2630
|
super.onSelectionChanged(selStart, selEnd)
|
|
2622
2631
|
if (isApplyingRustState) return
|
|
2623
|
-
val spannable = text as? Spanned
|
|
2624
|
-
if (spannable != null && isExactImageSpanRange(spannable, selStart, selEnd)) {
|
|
2625
|
-
explicitSelectedImageRange = ImageSelectionRange(selStart, selEnd)
|
|
2626
|
-
}
|
|
2627
2632
|
ensureSelectionVisible()
|
|
2628
2633
|
onSelectionOrContentMayChange?.invoke()
|
|
2629
2634
|
// Keep the custom caret solid at its new position, then resume blinking.
|
|
@@ -2708,7 +2713,6 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
2708
2713
|
lastAuthorizedText = text?.toString().orEmpty()
|
|
2709
2714
|
lastAuthorizedRenderedText = text?.let { SpannableStringBuilder(it) }
|
|
2710
2715
|
lastAuthorizedTextRevision += 1L
|
|
2711
|
-
currentRenderBlocksJson = null
|
|
2712
2716
|
clearNativeTextMutationAdoptionSuppression()
|
|
2713
2717
|
clearNativeTextMutationAfterBlurWindow()
|
|
2714
2718
|
}
|
|
@@ -3371,7 +3375,7 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
3371
3375
|
var effectiveCursor = cursor
|
|
3372
3376
|
while (
|
|
3373
3377
|
effectiveCursor > 0 &&
|
|
3374
|
-
content[effectiveCursor - 1] ==
|
|
3378
|
+
content[effectiveCursor - 1] == InputSnapshotConstants.SYNTHETIC_PLACEHOLDER_CHARACTER[0]
|
|
3375
3379
|
) {
|
|
3376
3380
|
effectiveCursor -= 1
|
|
3377
3381
|
}
|
|
@@ -3386,7 +3390,7 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
3386
3390
|
|
|
3387
3391
|
internal fun initialSurroundingTextForImeForEditor(): ImeInitialSurroundingText? {
|
|
3388
3392
|
val rawText = text?.toString() ?: return null
|
|
3389
|
-
val placeholder =
|
|
3393
|
+
val placeholder = InputSnapshotConstants.SYNTHETIC_PLACEHOLDER_CHARACTER[0]
|
|
3390
3394
|
if (rawText.indexOf(placeholder) < 0) return null
|
|
3391
3395
|
val start = selectionStart
|
|
3392
3396
|
val end = selectionEnd
|
|
@@ -3471,14 +3475,14 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
3471
3475
|
ch == ' ' ||
|
|
3472
3476
|
ch == '\t' ||
|
|
3473
3477
|
ch == '\u00A0' ||
|
|
3474
|
-
ch ==
|
|
3478
|
+
ch == InputSnapshotConstants.SYNTHETIC_PLACEHOLDER_CHARACTER[0]
|
|
3475
3479
|
|
|
3476
3480
|
private fun renderedListMarkerEnd(text: String, start: Int, endExclusive: Int): Int? {
|
|
3477
3481
|
if (start >= endExclusive) return null
|
|
3478
3482
|
if (renderedTaskListMarkerEnd(text, start, endExclusive) != null) {
|
|
3479
3483
|
return start + 2
|
|
3480
3484
|
}
|
|
3481
|
-
if (text[start] ==
|
|
3485
|
+
if (text[start] == InputSnapshotConstants.UNORDERED_LIST_BULLET[0]) {
|
|
3482
3486
|
return start + 1
|
|
3483
3487
|
}
|
|
3484
3488
|
|
|
@@ -3556,48 +3560,7 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
3556
3560
|
PositionBridge.utf16ToScalar(snappedHead, currentText)
|
|
3557
3561
|
}
|
|
3558
3562
|
|
|
3559
|
-
fun selectedImageGeometry(): SelectedImageGeometry? {
|
|
3560
|
-
if (!imageResizingEnabled) return null
|
|
3561
|
-
val spannable = text as? Spanned ?: return null
|
|
3562
|
-
val selection = resolvedSelectedImageRange(spannable) ?: return null
|
|
3563
|
-
val start = selection.start
|
|
3564
|
-
val end = selection.end
|
|
3565
|
-
val imageSpan = spannable
|
|
3566
|
-
.getSpans(start, end, BlockImageSpan::class.java)
|
|
3567
|
-
.firstOrNull() ?: return null
|
|
3568
|
-
val spanStart = spannable.getSpanStart(imageSpan)
|
|
3569
|
-
val spanEnd = spannable.getSpanEnd(imageSpan)
|
|
3570
|
-
if (spanStart != start || spanEnd != end) return null
|
|
3571
|
-
|
|
3572
|
-
val textLayout = layout ?: return null
|
|
3573
|
-
val currentText = text?.toString() ?: return null
|
|
3574
|
-
val scalarPos = PositionBridge.utf16ToScalar(spanStart, currentText)
|
|
3575
|
-
val docPos = if (hasLiveEditor()) {
|
|
3576
|
-
editorScalarToDoc(editorId.toULong(), scalarPos.toUInt()).toInt()
|
|
3577
|
-
} else {
|
|
3578
|
-
0
|
|
3579
|
-
}
|
|
3580
|
-
val line = textLayout.getLineForOffset(spanStart.coerceAtMost(maxOf(spannable.length - 1, 0)))
|
|
3581
|
-
val rect = resolvedImageRect(textLayout, imageSpan, spanStart, spanEnd)
|
|
3582
|
-
return SelectedImageGeometry(
|
|
3583
|
-
docPos = docPos,
|
|
3584
|
-
rect = rect
|
|
3585
|
-
)
|
|
3586
|
-
}
|
|
3587
3563
|
|
|
3588
|
-
fun resizeImageAtDocPos(docPos: Int, widthPx: Float, heightPx: Float) {
|
|
3589
|
-
if (!hasLiveEditor()) return
|
|
3590
|
-
val density = resources.displayMetrics.density
|
|
3591
|
-
val widthDp = maxOf(48, (widthPx / density).roundToInt())
|
|
3592
|
-
val heightDp = maxOf(48, (heightPx / density).roundToInt())
|
|
3593
|
-
val updateJSON = editorResizeImageAtDocPos(
|
|
3594
|
-
editorId.toULong(),
|
|
3595
|
-
docPos.toUInt(),
|
|
3596
|
-
widthDp.toUInt(),
|
|
3597
|
-
heightDp.toUInt()
|
|
3598
|
-
)
|
|
3599
|
-
applyUpdateJSON(updateJSON)
|
|
3600
|
-
}
|
|
3601
3564
|
|
|
3602
3565
|
private fun isSelectionInsideList(): Boolean {
|
|
3603
3566
|
if (!hasLiveEditor()) return false
|
|
@@ -3636,66 +3599,6 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
3636
3599
|
|
|
3637
3600
|
// ── Applying Rust State ─────────────────────────────────────────────
|
|
3638
3601
|
|
|
3639
|
-
private fun parseRenderPatch(raw: org.json.JSONObject?): ParsedRenderPatch? {
|
|
3640
|
-
if (raw == null) return null
|
|
3641
|
-
val renderBlocks = raw.optJSONArray("renderBlocks") ?: return null
|
|
3642
|
-
return ParsedRenderPatch(
|
|
3643
|
-
startIndex = raw.optInt("startIndex", -1),
|
|
3644
|
-
deleteCount = raw.optInt("deleteCount", -1),
|
|
3645
|
-
renderBlocks = renderBlocks
|
|
3646
|
-
).takeIf { it.startIndex >= 0 && it.deleteCount >= 0 }
|
|
3647
|
-
}
|
|
3648
|
-
|
|
3649
|
-
private fun hasTopLevelChildMetadata(content: Spanned): Boolean =
|
|
3650
|
-
content.getSpans(0, content.length, Annotation::class.java).any {
|
|
3651
|
-
it.key == RenderBridge.NATIVE_TOP_LEVEL_CHILD_INDEX_ANNOTATION
|
|
3652
|
-
}
|
|
3653
|
-
|
|
3654
|
-
private fun firstCharacterOffsetForTopLevelChildIndex(content: Spanned, index: Int): Int? {
|
|
3655
|
-
val targetValue = index.toString()
|
|
3656
|
-
return content
|
|
3657
|
-
.getSpans(0, content.length, Annotation::class.java)
|
|
3658
|
-
.asSequence()
|
|
3659
|
-
.filter { it.key == RenderBridge.NATIVE_TOP_LEVEL_CHILD_INDEX_ANNOTATION && it.value == targetValue }
|
|
3660
|
-
.mapNotNull { span ->
|
|
3661
|
-
val spanStart = content.getSpanStart(span)
|
|
3662
|
-
val spanEnd = content.getSpanEnd(span)
|
|
3663
|
-
if (spanStart < 0 || spanEnd <= spanStart) {
|
|
3664
|
-
null
|
|
3665
|
-
} else {
|
|
3666
|
-
var candidate = spanStart
|
|
3667
|
-
while (candidate < spanEnd && candidate < content.length) {
|
|
3668
|
-
when (content[candidate]) {
|
|
3669
|
-
'\n', '\r' -> candidate += 1
|
|
3670
|
-
else -> return@mapNotNull candidate
|
|
3671
|
-
}
|
|
3672
|
-
}
|
|
3673
|
-
null
|
|
3674
|
-
}
|
|
3675
|
-
}
|
|
3676
|
-
.minOrNull()
|
|
3677
|
-
}
|
|
3678
|
-
|
|
3679
|
-
private fun replacementRangeForRenderPatch(
|
|
3680
|
-
content: Spanned,
|
|
3681
|
-
startIndex: Int,
|
|
3682
|
-
deleteCount: Int
|
|
3683
|
-
): RenderReplaceRange? {
|
|
3684
|
-
val start = firstCharacterOffsetForTopLevelChildIndex(content, startIndex)
|
|
3685
|
-
?: if (deleteCount == 0) content.length else return null
|
|
3686
|
-
val endExclusive = firstCharacterOffsetForTopLevelChildIndex(content, startIndex + deleteCount)
|
|
3687
|
-
?: content.length
|
|
3688
|
-
if (start > endExclusive) return null
|
|
3689
|
-
return RenderReplaceRange(start = start, endExclusive = endExclusive)
|
|
3690
|
-
}
|
|
3691
|
-
|
|
3692
|
-
private fun spannedRangeContainsImageSpan(content: Spanned, start: Int, endExclusive: Int): Boolean {
|
|
3693
|
-
if (start >= endExclusive) return false
|
|
3694
|
-
return content.getSpans(start, endExclusive, BlockImageSpan::class.java).isNotEmpty()
|
|
3695
|
-
}
|
|
3696
|
-
|
|
3697
|
-
private fun spannedContainsImageSpan(content: Spanned): Boolean =
|
|
3698
|
-
spannedRangeContainsImageSpan(content, 0, content.length)
|
|
3699
3602
|
|
|
3700
3603
|
private fun applyRenderedSpannable(
|
|
3701
3604
|
spannable: CharSequence,
|
|
@@ -3766,167 +3669,11 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
3766
3669
|
)
|
|
3767
3670
|
}
|
|
3768
3671
|
|
|
3769
|
-
private fun buildPatchedSpannable(patch: ParsedRenderPatch): android.text.SpannableStringBuilder =
|
|
3770
|
-
RenderBridge.buildSpannableFromBlocks(
|
|
3771
|
-
patch.renderBlocks,
|
|
3772
|
-
startIndex = patch.startIndex,
|
|
3773
|
-
baseFontSize = baseFontSize,
|
|
3774
|
-
textColor = baseTextColor,
|
|
3775
|
-
theme = theme,
|
|
3776
|
-
density = resources.displayMetrics.density,
|
|
3777
|
-
hostView = this
|
|
3778
|
-
)
|
|
3779
|
-
|
|
3780
|
-
private fun cloneJsonArray(array: org.json.JSONArray): org.json.JSONArray =
|
|
3781
|
-
org.json.JSONArray().also { clone ->
|
|
3782
|
-
for (index in 0 until array.length()) {
|
|
3783
|
-
clone.put(array.opt(index))
|
|
3784
|
-
}
|
|
3785
|
-
}
|
|
3786
|
-
|
|
3787
|
-
private fun normalizedJsonValue(value: Any?): Any? =
|
|
3788
|
-
if (value === org.json.JSONObject.NULL) null else value
|
|
3789
|
-
|
|
3790
|
-
private fun jsonValuesEqual(left: Any?, right: Any?): Boolean {
|
|
3791
|
-
val normalizedLeft = normalizedJsonValue(left)
|
|
3792
|
-
val normalizedRight = normalizedJsonValue(right)
|
|
3793
|
-
if (normalizedLeft === normalizedRight) return true
|
|
3794
|
-
if (normalizedLeft == null || normalizedRight == null) return false
|
|
3795
|
-
|
|
3796
|
-
if (normalizedLeft is org.json.JSONArray && normalizedRight is org.json.JSONArray) {
|
|
3797
|
-
if (normalizedLeft.length() != normalizedRight.length()) return false
|
|
3798
|
-
for (index in 0 until normalizedLeft.length()) {
|
|
3799
|
-
if (!jsonValuesEqual(normalizedLeft.opt(index), normalizedRight.opt(index))) {
|
|
3800
|
-
return false
|
|
3801
|
-
}
|
|
3802
|
-
}
|
|
3803
|
-
return true
|
|
3804
|
-
}
|
|
3805
|
-
|
|
3806
|
-
if (normalizedLeft is org.json.JSONObject && normalizedRight is org.json.JSONObject) {
|
|
3807
|
-
if (normalizedLeft.length() != normalizedRight.length()) return false
|
|
3808
|
-
val keys = normalizedLeft.keys()
|
|
3809
|
-
while (keys.hasNext()) {
|
|
3810
|
-
val key = keys.next()
|
|
3811
|
-
if (!normalizedRight.has(key)) return false
|
|
3812
|
-
if (!jsonValuesEqual(normalizedLeft.opt(key), normalizedRight.opt(key))) {
|
|
3813
|
-
return false
|
|
3814
|
-
}
|
|
3815
|
-
}
|
|
3816
|
-
return true
|
|
3817
|
-
}
|
|
3818
|
-
|
|
3819
|
-
if (normalizedLeft is Number && normalizedRight is Number) {
|
|
3820
|
-
return normalizedLeft.toDouble() == normalizedRight.toDouble()
|
|
3821
|
-
}
|
|
3822
|
-
|
|
3823
|
-
return normalizedLeft == normalizedRight
|
|
3824
|
-
}
|
|
3825
|
-
|
|
3826
|
-
private fun renderBlocksEqual(
|
|
3827
|
-
current: org.json.JSONArray,
|
|
3828
|
-
updated: org.json.JSONArray
|
|
3829
|
-
): Boolean {
|
|
3830
|
-
if (current.length() != updated.length()) return false
|
|
3831
|
-
for (index in 0 until current.length()) {
|
|
3832
|
-
if (!jsonValuesEqual(current.opt(index), updated.opt(index))) {
|
|
3833
|
-
return false
|
|
3834
|
-
}
|
|
3835
|
-
}
|
|
3836
|
-
return true
|
|
3837
|
-
}
|
|
3838
|
-
|
|
3839
|
-
private fun mergeRenderBlocks(
|
|
3840
|
-
current: org.json.JSONArray,
|
|
3841
|
-
patch: ParsedRenderPatch
|
|
3842
|
-
): org.json.JSONArray? {
|
|
3843
|
-
if (
|
|
3844
|
-
patch.startIndex < 0 ||
|
|
3845
|
-
patch.deleteCount < 0 ||
|
|
3846
|
-
patch.startIndex > current.length() ||
|
|
3847
|
-
patch.startIndex + patch.deleteCount > current.length()
|
|
3848
|
-
) {
|
|
3849
|
-
return null
|
|
3850
|
-
}
|
|
3851
|
-
|
|
3852
|
-
return org.json.JSONArray().also { merged ->
|
|
3853
|
-
for (index in 0 until patch.startIndex) {
|
|
3854
|
-
merged.put(current.opt(index))
|
|
3855
|
-
}
|
|
3856
|
-
for (index in 0 until patch.renderBlocks.length()) {
|
|
3857
|
-
merged.put(patch.renderBlocks.opt(index))
|
|
3858
|
-
}
|
|
3859
|
-
for (index in (patch.startIndex + patch.deleteCount) until current.length()) {
|
|
3860
|
-
merged.put(current.opt(index))
|
|
3861
|
-
}
|
|
3862
|
-
}
|
|
3863
|
-
}
|
|
3864
|
-
|
|
3865
|
-
private fun applyRenderPatchIfPossible(patch: ParsedRenderPatch): PatchApplyTrace {
|
|
3866
|
-
val eligibilityStartedAt = System.nanoTime()
|
|
3867
|
-
val content = text as? Spanned ?: return PatchApplyTrace(
|
|
3868
|
-
applied = false,
|
|
3869
|
-
eligibilityNanos = System.nanoTime() - eligibilityStartedAt,
|
|
3870
|
-
buildRenderNanos = 0L,
|
|
3871
|
-
applyRenderNanos = 0L
|
|
3872
|
-
)
|
|
3873
|
-
if (!hasTopLevelChildMetadata(content)) {
|
|
3874
|
-
return PatchApplyTrace(
|
|
3875
|
-
applied = false,
|
|
3876
|
-
eligibilityNanos = System.nanoTime() - eligibilityStartedAt,
|
|
3877
|
-
buildRenderNanos = 0L,
|
|
3878
|
-
applyRenderNanos = 0L
|
|
3879
|
-
)
|
|
3880
|
-
}
|
|
3881
|
-
|
|
3882
|
-
val replaceRange = replacementRangeForRenderPatch(content, patch.startIndex, patch.deleteCount)
|
|
3883
|
-
?: return PatchApplyTrace(
|
|
3884
|
-
applied = false,
|
|
3885
|
-
eligibilityNanos = System.nanoTime() - eligibilityStartedAt,
|
|
3886
|
-
buildRenderNanos = 0L,
|
|
3887
|
-
applyRenderNanos = 0L
|
|
3888
|
-
)
|
|
3889
|
-
if (spannedRangeContainsImageSpan(content, replaceRange.start, replaceRange.endExclusive)) {
|
|
3890
|
-
return PatchApplyTrace(
|
|
3891
|
-
applied = false,
|
|
3892
|
-
eligibilityNanos = System.nanoTime() - eligibilityStartedAt,
|
|
3893
|
-
buildRenderNanos = 0L,
|
|
3894
|
-
applyRenderNanos = 0L
|
|
3895
|
-
)
|
|
3896
|
-
}
|
|
3897
|
-
val eligibilityNanos = System.nanoTime() - eligibilityStartedAt
|
|
3898
|
-
|
|
3899
|
-
val buildStartedAt = System.nanoTime()
|
|
3900
|
-
val patchedSpannable = buildPatchedSpannable(patch)
|
|
3901
|
-
val buildRenderNanos = System.nanoTime() - buildStartedAt
|
|
3902
|
-
if (spannedContainsImageSpan(patchedSpannable)) {
|
|
3903
|
-
return PatchApplyTrace(
|
|
3904
|
-
applied = false,
|
|
3905
|
-
eligibilityNanos = eligibilityNanos,
|
|
3906
|
-
buildRenderNanos = buildRenderNanos,
|
|
3907
|
-
applyRenderNanos = 0L
|
|
3908
|
-
)
|
|
3909
|
-
}
|
|
3910
|
-
|
|
3911
|
-
val applyStartedAt = System.nanoTime()
|
|
3912
|
-
applyRenderedSpannable(
|
|
3913
|
-
spannable = patchedSpannable,
|
|
3914
|
-
replaceRange = replaceRange,
|
|
3915
|
-
usedPatch = true
|
|
3916
|
-
)
|
|
3917
|
-
return PatchApplyTrace(
|
|
3918
|
-
applied = true,
|
|
3919
|
-
eligibilityNanos = eligibilityNanos,
|
|
3920
|
-
buildRenderNanos = buildRenderNanos,
|
|
3921
|
-
applyRenderNanos = System.nanoTime() - applyStartedAt
|
|
3922
|
-
)
|
|
3923
|
-
}
|
|
3924
|
-
|
|
3925
3672
|
/**
|
|
3926
|
-
* Apply a
|
|
3673
|
+
* Apply a structural editor update from Rust to the hidden input EditText.
|
|
3927
3674
|
*
|
|
3928
|
-
*
|
|
3929
|
-
*
|
|
3675
|
+
* The visible editor surface renders the block tree. This EditText keeps a
|
|
3676
|
+
* plain text snapshot for input, selection, and IME routing only.
|
|
3930
3677
|
*
|
|
3931
3678
|
* @param updateJSON The JSON string from editor_insert_text, etc.
|
|
3932
3679
|
*/
|
|
@@ -3950,76 +3697,57 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
3950
3697
|
cancelDeferredRustUpdateApplication()
|
|
3951
3698
|
val parseNanos = System.nanoTime() - parseStartedAt
|
|
3952
3699
|
|
|
3953
|
-
val
|
|
3954
|
-
val
|
|
3955
|
-
|
|
3956
|
-
|
|
3957
|
-
|
|
3958
|
-
|
|
3959
|
-
|
|
3700
|
+
val resolveRenderBlocksNanos = 0L
|
|
3701
|
+
val inputSnapshot = if (update.has("inputText")) update.optString("inputText") else null
|
|
3702
|
+
if (inputSnapshot == null && update.has("selection")) {
|
|
3703
|
+
val selectionStartedAt = System.nanoTime()
|
|
3704
|
+
update.optJSONObject("selection")?.let { applySelectionFromJSON(it) }
|
|
3705
|
+
val selectionNanos = System.nanoTime() - selectionStartedAt
|
|
3706
|
+
if (notifyListener) {
|
|
3707
|
+
editorListener?.onEditorUpdate(updateJSON)
|
|
3960
3708
|
}
|
|
3961
|
-
|
|
3709
|
+
onUpdateJSONApplied?.invoke(updateJSON)
|
|
3710
|
+
onSelectionOrContentMayChange?.invoke()
|
|
3711
|
+
recordImeTraceForTesting(
|
|
3712
|
+
"applyUpdateJSON",
|
|
3713
|
+
"notify=$notifyListener selectionOnly=true jsonLength=${updateJSON.length} selectionUs=${nanosToMicros(selectionNanos)}"
|
|
3714
|
+
)
|
|
3715
|
+
return
|
|
3716
|
+
}
|
|
3962
3717
|
val shouldSkipRender = !refreshInputConnectionForExternalUpdate &&
|
|
3963
|
-
|
|
3964
|
-
|
|
3965
|
-
renderBlocksEqual(current, resolvedRenderBlocks)
|
|
3966
|
-
} == true &&
|
|
3718
|
+
inputSnapshot != null &&
|
|
3719
|
+
text?.toString() == inputSnapshot &&
|
|
3967
3720
|
text?.toString() == lastAuthorizedText &&
|
|
3968
3721
|
lastAppliedRenderAppearanceRevision == renderAppearanceRevision
|
|
3969
3722
|
val previousScrollX = scrollX
|
|
3970
3723
|
val previousScrollY = scrollY
|
|
3971
3724
|
|
|
3972
|
-
explicitSelectedImageRange = null
|
|
3973
3725
|
val buildRenderNanos: Long
|
|
3974
3726
|
val applyRenderNanos: Long
|
|
3975
3727
|
if (shouldSkipRender) {
|
|
3976
3728
|
pendingOptimisticRenderText = null
|
|
3977
3729
|
lastRenderAppliedPatchForTesting = false
|
|
3978
|
-
currentRenderBlocksJson = resolvedRenderBlocks?.let(::cloneJsonArray)
|
|
3979
3730
|
clearNativeTextMutationAdoptionSuppression()
|
|
3980
3731
|
clearNativeTextMutationAfterBlurWindow()
|
|
3981
3732
|
buildRenderNanos = 0L
|
|
3982
3733
|
applyRenderNanos = 0L
|
|
3983
3734
|
} else {
|
|
3984
|
-
// Android's Editable.replace(...) path benchmarks substantially slower than
|
|
3985
|
-
// rebuilding from merged render blocks, so patch payloads are treated as a
|
|
3986
|
-
// transport optimization only. We still resolve the merged block state above,
|
|
3987
|
-
// then apply it through the faster full-text path here.
|
|
3988
3735
|
val buildStartedAt = System.nanoTime()
|
|
3989
|
-
|
|
3990
|
-
RenderBridge.buildSpannableFromBlocks(
|
|
3991
|
-
resolvedRenderBlocks,
|
|
3992
|
-
baseFontSize = baseFontSize,
|
|
3993
|
-
textColor = baseTextColor,
|
|
3994
|
-
theme = theme,
|
|
3995
|
-
density = resources.displayMetrics.density,
|
|
3996
|
-
hostView = this
|
|
3997
|
-
)
|
|
3998
|
-
} else if (renderElements != null) {
|
|
3999
|
-
RenderBridge.buildSpannableFromArray(
|
|
4000
|
-
renderElements,
|
|
4001
|
-
baseFontSize,
|
|
4002
|
-
baseTextColor,
|
|
4003
|
-
theme,
|
|
4004
|
-
resources.displayMetrics.density,
|
|
4005
|
-
this
|
|
4006
|
-
)
|
|
4007
|
-
} else {
|
|
3736
|
+
if (inputSnapshot == null) {
|
|
4008
3737
|
recordImeTraceForTesting(
|
|
4009
3738
|
"applyUpdateJSONNoop",
|
|
4010
|
-
"reason=
|
|
3739
|
+
"reason=noRenderTree jsonLength=${updateJSON.length}"
|
|
4011
3740
|
)
|
|
4012
3741
|
return
|
|
4013
3742
|
}
|
|
3743
|
+
val fullSpannable = android.text.SpannableStringBuilder(inputSnapshot)
|
|
4014
3744
|
buildRenderNanos = System.nanoTime() - buildStartedAt
|
|
4015
|
-
currentRenderBlocksJson = resolvedRenderBlocks?.let(::cloneJsonArray)
|
|
4016
3745
|
val applyStartedAt = System.nanoTime()
|
|
4017
3746
|
val optimisticText = pendingOptimisticRenderText
|
|
4018
3747
|
val canReuseOptimisticVisibleText =
|
|
4019
3748
|
optimisticText != null &&
|
|
4020
3749
|
text?.toString() == optimisticText &&
|
|
4021
|
-
fullSpannable.toString() == optimisticText
|
|
4022
|
-
!spannedContainsImageSpan(fullSpannable)
|
|
3750
|
+
fullSpannable.toString() == optimisticText
|
|
4023
3751
|
if (canReuseOptimisticVisibleText) {
|
|
4024
3752
|
authorizeVisibleTextForMatchedOptimisticRender(fullSpannable)
|
|
4025
3753
|
} else {
|
|
@@ -4046,6 +3774,7 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
4046
3774
|
if (notifyListener) {
|
|
4047
3775
|
editorListener?.onEditorUpdate(updateJSON)
|
|
4048
3776
|
}
|
|
3777
|
+
onUpdateJSONApplied?.invoke(updateJSON)
|
|
4049
3778
|
onSelectionOrContentMayChange?.invoke()
|
|
4050
3779
|
if (heightBehavior == EditorHeightBehavior.AUTO_GROW) {
|
|
4051
3780
|
requestLayout()
|
|
@@ -4061,12 +3790,12 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
4061
3790
|
val totalNanos = System.nanoTime() - totalStartedAt
|
|
4062
3791
|
recordImeTraceForTesting(
|
|
4063
3792
|
"applyUpdateJSON",
|
|
4064
|
-
"notify=$notifyListener skippedRender=$shouldSkipRender
|
|
3793
|
+
"notify=$notifyListener skippedRender=$shouldSkipRender structuralInput=true jsonLength=${updateJSON.length} parseUs=${nanosToMicros(parseNanos)} buildUs=${nanosToMicros(buildRenderNanos)} applyUs=${nanosToMicros(applyRenderNanos)} selectionUs=${nanosToMicros(selectionNanos)} postUs=${nanosToMicros(postApplyNanos)} totalUs=${nanosToMicros(totalNanos)}"
|
|
4065
3794
|
)
|
|
4066
3795
|
|
|
4067
3796
|
if (captureApplyUpdateTraceForTesting) {
|
|
4068
3797
|
lastApplyUpdateTraceForTesting = ApplyUpdateTrace(
|
|
4069
|
-
attemptedPatch =
|
|
3798
|
+
attemptedPatch = false,
|
|
4070
3799
|
usedPatch = false,
|
|
4071
3800
|
skippedRender = shouldSkipRender,
|
|
4072
3801
|
parseNanos = parseNanos,
|
|
@@ -4081,6 +3810,60 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
4081
3810
|
}
|
|
4082
3811
|
}
|
|
4083
3812
|
|
|
3813
|
+
private fun plainTextSnapshotFromRenderTree(tree: org.json.JSONObject?): String? {
|
|
3814
|
+
val root = tree?.optJSONObject("root") ?: return null
|
|
3815
|
+
val builder = StringBuilder()
|
|
3816
|
+
appendInputSnapshotNode(root, builder, isRoot = true)
|
|
3817
|
+
return builder.toString()
|
|
3818
|
+
}
|
|
3819
|
+
|
|
3820
|
+
private fun appendInputSnapshotNode(
|
|
3821
|
+
node: org.json.JSONObject,
|
|
3822
|
+
builder: StringBuilder,
|
|
3823
|
+
isRoot: Boolean
|
|
3824
|
+
) {
|
|
3825
|
+
val kind = node.optString("kind", "block")
|
|
3826
|
+
val nodeType = node.optString("nodeType", "unknown")
|
|
3827
|
+
when (kind) {
|
|
3828
|
+
"text" -> {
|
|
3829
|
+
builder.append(node.optString("text", ""))
|
|
3830
|
+
return
|
|
3831
|
+
}
|
|
3832
|
+
"inlineAtom" -> {
|
|
3833
|
+
if (nodeType == "hardBreak" || nodeType == "hard_break") {
|
|
3834
|
+
builder.append('\n')
|
|
3835
|
+
} else {
|
|
3836
|
+
builder.append(node.optString("label", "\uFFFC"))
|
|
3837
|
+
}
|
|
3838
|
+
return
|
|
3839
|
+
}
|
|
3840
|
+
"blockAtom" -> {
|
|
3841
|
+
if (builder.isNotEmpty() && builder.last() != '\n') builder.append('\n')
|
|
3842
|
+
builder.append(node.optString("label", "\uFFFC"))
|
|
3843
|
+
builder.append('\n')
|
|
3844
|
+
return
|
|
3845
|
+
}
|
|
3846
|
+
}
|
|
3847
|
+
|
|
3848
|
+
val children = node.optJSONArray("children") ?: org.json.JSONArray()
|
|
3849
|
+
val startsBlock = !isRoot && isInputSnapshotBlockBoundary(kind)
|
|
3850
|
+
if (startsBlock && builder.isNotEmpty() && builder.last() != '\n') builder.append('\n')
|
|
3851
|
+
for (index in 0 until children.length()) {
|
|
3852
|
+
children.optJSONObject(index)?.let {
|
|
3853
|
+
appendInputSnapshotNode(
|
|
3854
|
+
it,
|
|
3855
|
+
builder,
|
|
3856
|
+
isRoot = false
|
|
3857
|
+
)
|
|
3858
|
+
}
|
|
3859
|
+
}
|
|
3860
|
+
if (startsBlock && builder.isNotEmpty() && builder.last() != '\n') builder.append('\n')
|
|
3861
|
+
}
|
|
3862
|
+
|
|
3863
|
+
private fun isInputSnapshotBlockBoundary(kind: String): Boolean =
|
|
3864
|
+
kind == "textBlock" || kind == "listItem" || kind == "block" ||
|
|
3865
|
+
kind == "tableCell" || kind == "tableRow" || kind == "table"
|
|
3866
|
+
|
|
4084
3867
|
private fun refreshInputConnectionAfterExternalTextReplacementIfNeeded(
|
|
4085
3868
|
enabled: Boolean,
|
|
4086
3869
|
previousVisibleText: String
|
|
@@ -4099,44 +3882,6 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
4099
3882
|
clearNativeComposingSpans()
|
|
4100
3883
|
}
|
|
4101
3884
|
|
|
4102
|
-
/**
|
|
4103
|
-
* Apply a render JSON string (just render elements, no update wrapper).
|
|
4104
|
-
*
|
|
4105
|
-
* Used for initial content loading (set_html / set_json return render
|
|
4106
|
-
* elements directly, not wrapped in an EditorUpdate).
|
|
4107
|
-
*
|
|
4108
|
-
* @param renderJSON The JSON array string of render elements.
|
|
4109
|
-
*/
|
|
4110
|
-
fun applyRenderJSON(renderJSON: String) {
|
|
4111
|
-
val startedAt = System.nanoTime()
|
|
4112
|
-
val spannable = RenderBridge.buildSpannable(
|
|
4113
|
-
renderJSON,
|
|
4114
|
-
baseFontSize,
|
|
4115
|
-
baseTextColor,
|
|
4116
|
-
theme,
|
|
4117
|
-
resources.displayMetrics.density,
|
|
4118
|
-
this
|
|
4119
|
-
)
|
|
4120
|
-
|
|
4121
|
-
val previousScrollX = scrollX
|
|
4122
|
-
val previousScrollY = scrollY
|
|
4123
|
-
|
|
4124
|
-
explicitSelectedImageRange = null
|
|
4125
|
-
currentRenderBlocksJson = null
|
|
4126
|
-
pendingOptimisticRenderText = null
|
|
4127
|
-
applyRenderedSpannable(spannable, usedPatch = false)
|
|
4128
|
-
onSelectionOrContentMayChange?.invoke()
|
|
4129
|
-
if (heightBehavior == EditorHeightBehavior.AUTO_GROW) {
|
|
4130
|
-
requestLayout()
|
|
4131
|
-
} else {
|
|
4132
|
-
preserveScrollPosition(previousScrollX, previousScrollY)
|
|
4133
|
-
}
|
|
4134
|
-
recordImeTraceForTesting(
|
|
4135
|
-
"applyRenderJSON",
|
|
4136
|
-
"jsonLength=${renderJSON.length} totalUs=${nanosToMicros(System.nanoTime() - startedAt)}"
|
|
4137
|
-
)
|
|
4138
|
-
}
|
|
4139
|
-
|
|
4140
3885
|
private fun textOffsetHitAt(x: Float, y: Float): Pair<Spanned, Int>? {
|
|
4141
3886
|
val spannable = text as? Spanned ?: return null
|
|
4142
3887
|
val layout = layout ?: return null
|
|
@@ -4160,134 +3905,12 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
4160
3905
|
return spannable to offset
|
|
4161
3906
|
}
|
|
4162
3907
|
|
|
4163
|
-
fun mentionHitAt(x: Float, y: Float): MentionHit? {
|
|
4164
|
-
val (spannable, offset) = textOffsetHitAt(x, y) ?: return null
|
|
4165
|
-
val annotations = spannable.getSpans(
|
|
4166
|
-
offset,
|
|
4167
|
-
(offset + 1).coerceAtMost(spannable.length),
|
|
4168
|
-
Annotation::class.java
|
|
4169
|
-
)
|
|
4170
|
-
val mentionAnnotation = annotations.firstOrNull {
|
|
4171
|
-
it.key == "nativeVoidNodeType" && it.value == "mention"
|
|
4172
|
-
} ?: return null
|
|
4173
|
-
val docPos = annotations.firstOrNull { it.key == "nativeDocPos" }
|
|
4174
|
-
?.value
|
|
4175
|
-
?.toIntOrNull() ?: return null
|
|
4176
|
-
val start = spannable.getSpanStart(mentionAnnotation)
|
|
4177
|
-
val end = spannable.getSpanEnd(mentionAnnotation)
|
|
4178
|
-
if (start < 0 || end <= start) {
|
|
4179
|
-
return null
|
|
4180
|
-
}
|
|
4181
3908
|
|
|
4182
|
-
return MentionHit(
|
|
4183
|
-
docPos = docPos,
|
|
4184
|
-
label = spannable.subSequence(start, end).toString()
|
|
4185
|
-
)
|
|
4186
|
-
}
|
|
4187
3909
|
|
|
4188
|
-
fun linkHitAt(x: Float, y: Float): LinkHit? {
|
|
4189
|
-
val (spannable, offset) = textOffsetHitAt(x, y) ?: return null
|
|
4190
|
-
val annotations = spannable.getSpans(
|
|
4191
|
-
offset,
|
|
4192
|
-
(offset + 1).coerceAtMost(spannable.length),
|
|
4193
|
-
Annotation::class.java
|
|
4194
|
-
)
|
|
4195
|
-
val linkAnnotation = annotations.firstOrNull {
|
|
4196
|
-
it.key == RenderBridge.NATIVE_LINK_HREF_ANNOTATION && it.value.isNotBlank()
|
|
4197
|
-
} ?: return null
|
|
4198
|
-
val start = spannable.getSpanStart(linkAnnotation)
|
|
4199
|
-
val end = spannable.getSpanEnd(linkAnnotation)
|
|
4200
|
-
if (start < 0 || end <= start) {
|
|
4201
|
-
return null
|
|
4202
|
-
}
|
|
4203
|
-
|
|
4204
|
-
return LinkHit(
|
|
4205
|
-
href = linkAnnotation.value,
|
|
4206
|
-
text = spannable.subSequence(start, end).toString()
|
|
4207
|
-
)
|
|
4208
|
-
}
|
|
4209
|
-
|
|
4210
|
-
private fun handleImageTap(event: MotionEvent): Boolean {
|
|
4211
|
-
if (!imageResizingEnabled) {
|
|
4212
|
-
return false
|
|
4213
|
-
}
|
|
4214
|
-
if (event.actionMasked != MotionEvent.ACTION_DOWN && event.actionMasked != MotionEvent.ACTION_UP) {
|
|
4215
|
-
return false
|
|
4216
|
-
}
|
|
4217
|
-
val hit = imageSpanHitAt(event.x, event.y) ?: return false
|
|
4218
|
-
requestFocus()
|
|
4219
|
-
selectExplicitImageRange(hit.first, hit.second)
|
|
4220
|
-
if (event.actionMasked == MotionEvent.ACTION_UP) {
|
|
4221
|
-
performClick()
|
|
4222
|
-
}
|
|
4223
|
-
return true
|
|
4224
|
-
}
|
|
4225
|
-
|
|
4226
|
-
private fun handleTaskListMarkerTap(event: MotionEvent): Boolean {
|
|
4227
|
-
if (event.actionMasked != MotionEvent.ACTION_DOWN && event.actionMasked != MotionEvent.ACTION_UP) {
|
|
4228
|
-
return false
|
|
4229
|
-
}
|
|
4230
|
-
val scalarHit = taskListMarkerScalarHitAt(event.x, event.y) ?: return false
|
|
4231
|
-
requestFocus()
|
|
4232
|
-
if (event.actionMasked == MotionEvent.ACTION_UP) {
|
|
4233
|
-
toggleTaskItemCheckedAtSelectionScalarInRust(scalarHit, scalarHit)
|
|
4234
|
-
performClick()
|
|
4235
|
-
}
|
|
4236
|
-
return true
|
|
4237
|
-
}
|
|
4238
3910
|
|
|
4239
|
-
private fun taskListMarkerScalarHitAt(x: Float, y: Float): Int? {
|
|
4240
|
-
val currentText = text?.toString() ?: return null
|
|
4241
|
-
val textLayout = layout ?: return null
|
|
4242
|
-
if (currentText.isEmpty()) return null
|
|
4243
3911
|
|
|
4244
|
-
val localX = x + scrollX - totalPaddingLeft
|
|
4245
|
-
val localY = y + scrollY - totalPaddingTop
|
|
4246
|
-
if (localY < 0) return null
|
|
4247
|
-
|
|
4248
|
-
val line = textLayout.getLineForVertical(localY.toInt().coerceAtLeast(0))
|
|
4249
|
-
val lineStart = textLayout.getLineStart(line).coerceIn(0, currentText.length)
|
|
4250
|
-
val lineEnd = textLayout.getLineEnd(line).coerceIn(lineStart, currentText.length)
|
|
4251
|
-
val markerEnd = renderedTaskListMarkerEnd(currentText, lineStart, lineEnd) ?: return null
|
|
4252
|
-
val markerRight = textLayout.getPrimaryHorizontal(markerEnd).coerceAtLeast(
|
|
4253
|
-
textLayout.getPrimaryHorizontal(lineStart)
|
|
4254
|
-
) + (8f * resources.displayMetrics.density)
|
|
4255
|
-
if (localX > markerRight) {
|
|
4256
|
-
return null
|
|
4257
|
-
}
|
|
4258
|
-
val snappedUtf16 = PositionBridge.snapToScalarBoundary(
|
|
4259
|
-
lineStart,
|
|
4260
|
-
currentText,
|
|
4261
|
-
biasForward = true
|
|
4262
|
-
)
|
|
4263
|
-
return PositionBridge.utf16ToScalar(snappedUtf16, currentText)
|
|
4264
|
-
}
|
|
4265
3912
|
|
|
4266
|
-
private fun imageSpanHitAt(x: Float, y: Float): Pair<Int, Int>? {
|
|
4267
|
-
val spannable = text as? Spanned ?: return null
|
|
4268
|
-
imageSpanRangeNearTouchOffset(spannable, x, y)?.let { return it }
|
|
4269
|
-
val textLayout = layout ?: return null
|
|
4270
|
-
return imageSpanRectHit(spannable, textLayout, x, y)
|
|
4271
|
-
}
|
|
4272
3913
|
|
|
4273
|
-
private fun imageSpanRectHit(
|
|
4274
|
-
spannable: Spanned,
|
|
4275
|
-
textLayout: Layout,
|
|
4276
|
-
x: Float,
|
|
4277
|
-
y: Float
|
|
4278
|
-
): Pair<Int, Int>? {
|
|
4279
|
-
val candidateSpans = spannable.getSpans(0, spannable.length, BlockImageSpan::class.java)
|
|
4280
|
-
for (span in candidateSpans) {
|
|
4281
|
-
val spanStart = spannable.getSpanStart(span)
|
|
4282
|
-
val spanEnd = spannable.getSpanEnd(span)
|
|
4283
|
-
if (spanStart < 0 || spanEnd <= spanStart) continue
|
|
4284
|
-
val rect = resolvedImageRect(textLayout, span, spanStart, spanEnd)
|
|
4285
|
-
if (rect.contains(x, y)) {
|
|
4286
|
-
return spanStart to spanEnd
|
|
4287
|
-
}
|
|
4288
|
-
}
|
|
4289
|
-
return null
|
|
4290
|
-
}
|
|
4291
3914
|
|
|
4292
3915
|
override fun onFocusChanged(focused: Boolean, direction: Int, previouslyFocusedRect: Rect?) {
|
|
4293
3916
|
super.onFocusChanged(focused, direction, previouslyFocusedRect)
|
|
@@ -4296,7 +3919,6 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
4296
3919
|
restartCaretBlink()
|
|
4297
3920
|
} else {
|
|
4298
3921
|
beginNativeTextMutationAfterBlurWindow()
|
|
4299
|
-
clearExplicitSelectedImageRange()
|
|
4300
3922
|
stopCaretBlink()
|
|
4301
3923
|
}
|
|
4302
3924
|
}
|
|
@@ -4316,92 +3938,11 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
4316
3938
|
super.onDetachedFromWindow()
|
|
4317
3939
|
}
|
|
4318
3940
|
|
|
4319
|
-
private fun selectExplicitImageRange(start: Int, end: Int) {
|
|
4320
|
-
explicitSelectedImageRange = ImageSelectionRange(start, end)
|
|
4321
|
-
if (selectionStart == start && selectionEnd == end) {
|
|
4322
|
-
onSelectionOrContentMayChange?.invoke()
|
|
4323
|
-
return
|
|
4324
|
-
}
|
|
4325
|
-
setSelection(start, end)
|
|
4326
|
-
}
|
|
4327
|
-
|
|
4328
|
-
private fun clearExplicitSelectedImageRange() {
|
|
4329
|
-
if (explicitSelectedImageRange == null) return
|
|
4330
|
-
explicitSelectedImageRange = null
|
|
4331
|
-
onSelectionOrContentMayChange?.invoke()
|
|
4332
|
-
}
|
|
4333
|
-
|
|
4334
|
-
private fun resolvedSelectedImageRange(spannable: Spanned): ImageSelectionRange? {
|
|
4335
|
-
explicitSelectedImageRange?.let { explicit ->
|
|
4336
|
-
if (isExactImageSpanRange(spannable, explicit.start, explicit.end)) {
|
|
4337
|
-
return explicit
|
|
4338
|
-
}
|
|
4339
|
-
explicitSelectedImageRange = null
|
|
4340
|
-
}
|
|
4341
3941
|
|
|
4342
|
-
val start = selectionStart
|
|
4343
|
-
val end = selectionEnd
|
|
4344
|
-
if (!isExactImageSpanRange(spannable, start, end)) return null
|
|
4345
|
-
return ImageSelectionRange(start, end)
|
|
4346
|
-
}
|
|
4347
3942
|
|
|
4348
|
-
private fun isExactImageSpanRange(spannable: Spanned, start: Int, end: Int): Boolean {
|
|
4349
|
-
if (start < 0 || end != start + 1) return false
|
|
4350
|
-
val imageSpan = spannable
|
|
4351
|
-
.getSpans(start, end, BlockImageSpan::class.java)
|
|
4352
|
-
.firstOrNull() ?: return false
|
|
4353
|
-
return spannable.getSpanStart(imageSpan) == start && spannable.getSpanEnd(imageSpan) == end
|
|
4354
|
-
}
|
|
4355
3943
|
|
|
4356
|
-
private fun imageSpanRangeNearTouchOffset(
|
|
4357
|
-
spannable: Spanned,
|
|
4358
|
-
x: Float,
|
|
4359
|
-
y: Float
|
|
4360
|
-
): Pair<Int, Int>? {
|
|
4361
|
-
val safeOffset = runCatching { getOffsetForPosition(x, y) }.getOrNull() ?: return null
|
|
4362
|
-
val nearbyOffsets = linkedSetOf(
|
|
4363
|
-
safeOffset,
|
|
4364
|
-
(safeOffset - 1).coerceAtLeast(0),
|
|
4365
|
-
(safeOffset + 1).coerceAtMost(spannable.length)
|
|
4366
|
-
)
|
|
4367
|
-
for (offset in nearbyOffsets) {
|
|
4368
|
-
val searchStart = (offset - 1).coerceAtLeast(0)
|
|
4369
|
-
val searchEnd = (offset + 1).coerceAtMost(spannable.length)
|
|
4370
|
-
val imageSpan = spannable
|
|
4371
|
-
.getSpans(searchStart, searchEnd, BlockImageSpan::class.java)
|
|
4372
|
-
.firstOrNull() ?: continue
|
|
4373
|
-
val spanStart = spannable.getSpanStart(imageSpan)
|
|
4374
|
-
val spanEnd = spannable.getSpanEnd(imageSpan)
|
|
4375
|
-
if (spanStart >= 0 && spanEnd > spanStart) {
|
|
4376
|
-
return spanStart to spanEnd
|
|
4377
|
-
}
|
|
4378
|
-
}
|
|
4379
|
-
return null
|
|
4380
|
-
}
|
|
4381
3944
|
|
|
4382
|
-
private fun resolvedImageRect(
|
|
4383
|
-
textLayout: Layout,
|
|
4384
|
-
imageSpan: BlockImageSpan,
|
|
4385
|
-
spanStart: Int,
|
|
4386
|
-
spanEnd: Int
|
|
4387
|
-
): RectF {
|
|
4388
|
-
imageSpan.currentDrawRect()?.let { drawnRect ->
|
|
4389
|
-
return drawnRect
|
|
4390
|
-
}
|
|
4391
3945
|
|
|
4392
|
-
val safeOffset = spanStart.coerceAtMost(maxOf((text?.length ?: 0) - 1, 0))
|
|
4393
|
-
val line = textLayout.getLineForOffset(safeOffset)
|
|
4394
|
-
val startHorizontal = textLayout.getPrimaryHorizontal(spanStart)
|
|
4395
|
-
val endHorizontal = textLayout.getPrimaryHorizontal(spanEnd)
|
|
4396
|
-
val (widthPx, heightPx) = imageSpan.currentSizePx()
|
|
4397
|
-
val left = compoundPaddingLeft + minOf(startHorizontal, endHorizontal)
|
|
4398
|
-
val right = compoundPaddingLeft + maxOf(
|
|
4399
|
-
maxOf(startHorizontal, endHorizontal),
|
|
4400
|
-
minOf(startHorizontal, endHorizontal) + widthPx
|
|
4401
|
-
)
|
|
4402
|
-
val top = extendedPaddingTop + textLayout.getLineBottom(line) - heightPx
|
|
4403
|
-
return RectF(left, top.toFloat(), right, top + heightPx.toFloat())
|
|
4404
|
-
}
|
|
4405
3946
|
|
|
4406
3947
|
/**
|
|
4407
3948
|
* Apply a selection from a parsed JSON selection object.
|