@openeditor/react-native-prose-editor 0.0.9 → 0.0.11
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 +5 -9
- package/android/src/main/java/com/apollohg/editor/EditorTheme.kt +23 -0
- package/android/src/main/java/com/apollohg/editor/ImageResizeOverlayView.kt +2 -2
- package/android/src/main/java/com/apollohg/editor/NativeBlockEditorSurface.kt +476 -53
- package/android/src/main/java/com/apollohg/editor/NativeEditorExpoView.kt +110 -169
- package/android/src/main/java/com/apollohg/editor/NativeEditorModule.kt +1 -1
- package/android/src/main/java/com/apollohg/editor/RichTextEditorView.kt +124 -337
- package/dist/EditorTheme.d.ts +8 -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/Generated_editor_core.swift +15 -0
- package/ios/NativeBlockEditorSurface.swift +371 -60
- package/ios/NativeEditorExpoView.swift +114 -117
- package/ios/NativeInputSupport.swift +0 -302
- package/ios/PositionBridge.swift +22 -548
- package/ios/RichTextEditorView.swift +217 -4671
- package/ios/editor_coreFFI/editor_coreFFI.h +11 -0
- 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 +31 -0
- package/android/src/main/java/com/apollohg/editor/CaretGeometry.kt +0 -50
- package/android/src/main/java/com/apollohg/editor/EditorEditText.kt +0 -4068
- package/android/src/main/java/com/apollohg/editor/EditorInputConnection.kt +0 -1009
- package/android/src/main/java/com/apollohg/editor/InputSnapshotSupport.kt +0 -6
|
@@ -1,4068 +0,0 @@
|
|
|
1
|
-
package com.openeditor.editor
|
|
2
|
-
|
|
3
|
-
import android.content.ClipData
|
|
4
|
-
import android.content.ClipboardManager
|
|
5
|
-
import android.content.Context
|
|
6
|
-
import android.graphics.Typeface
|
|
7
|
-
import android.graphics.Rect
|
|
8
|
-
import android.graphics.RectF
|
|
9
|
-
import android.os.Build
|
|
10
|
-
import android.os.Handler
|
|
11
|
-
import android.os.Looper
|
|
12
|
-
import android.os.SystemClock
|
|
13
|
-
import android.provider.Settings
|
|
14
|
-
import android.text.Annotation
|
|
15
|
-
import android.text.Editable
|
|
16
|
-
import android.text.InputType
|
|
17
|
-
import android.text.Layout
|
|
18
|
-
import android.text.Selection
|
|
19
|
-
import android.text.Spanned
|
|
20
|
-
import android.text.SpannableStringBuilder
|
|
21
|
-
import android.text.StaticLayout
|
|
22
|
-
import android.text.TextPaint
|
|
23
|
-
import android.text.TextWatcher
|
|
24
|
-
import android.text.style.AbsoluteSizeSpan
|
|
25
|
-
import android.text.style.BackgroundColorSpan
|
|
26
|
-
import android.text.style.ForegroundColorSpan
|
|
27
|
-
import android.text.style.StrikethroughSpan
|
|
28
|
-
import android.text.style.StyleSpan
|
|
29
|
-
import android.text.style.TypefaceSpan
|
|
30
|
-
import android.text.style.UnderlineSpan
|
|
31
|
-
import android.util.AttributeSet
|
|
32
|
-
import android.util.Log
|
|
33
|
-
import android.util.TypedValue
|
|
34
|
-
import android.view.KeyEvent
|
|
35
|
-
import android.view.MotionEvent
|
|
36
|
-
import android.view.inputmethod.BaseInputConnection
|
|
37
|
-
import android.view.inputmethod.EditorInfo
|
|
38
|
-
import android.view.inputmethod.InputConnection
|
|
39
|
-
import android.view.inputmethod.InputMethodManager
|
|
40
|
-
import androidx.appcompat.widget.AppCompatEditText
|
|
41
|
-
import kotlin.math.roundToInt
|
|
42
|
-
import uniffi.editor_core.* // UniFFI-generated bindings
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Custom [AppCompatEditText] subclass that intercepts all text input and routes it
|
|
46
|
-
* through the Rust editor-core engine via UniFFI bindings.
|
|
47
|
-
*
|
|
48
|
-
* Instead of letting Android's EditText internal text storage handle insertions
|
|
49
|
-
* and deletions, this class captures the user's intent (typing, deleting,
|
|
50
|
-
* pasting, autocorrect) and sends it to the Rust editor. The Rust editor
|
|
51
|
-
* returns a canonical plain-text input snapshot, which is authorized and
|
|
52
|
-
* applied back to this hidden input adapter.
|
|
53
|
-
*
|
|
54
|
-
* This is the "input interception" pattern: the EditText is effectively
|
|
55
|
-
* an IME/selection surface, not the visible block renderer.
|
|
56
|
-
*
|
|
57
|
-
* ## Composition Handling
|
|
58
|
-
*
|
|
59
|
-
* For CJK input methods, swipe keyboards, and some autocorrect flows, composing
|
|
60
|
-
* text is rendered transiently by the base [InputConnection]. The final commit
|
|
61
|
-
* is routed through Rust against the original authorized selection.
|
|
62
|
-
*
|
|
63
|
-
* ## Thread Safety
|
|
64
|
-
*
|
|
65
|
-
* All EditText methods are called on the main thread. The UniFFI calls
|
|
66
|
-
* (`editor_insert_text`, `editor_delete_range`, etc.) are synchronous and
|
|
67
|
-
* fast enough for main-thread use.
|
|
68
|
-
*/
|
|
69
|
-
class EditorEditText @JvmOverloads constructor(
|
|
70
|
-
context: Context,
|
|
71
|
-
attrs: AttributeSet? = null,
|
|
72
|
-
defStyleAttr: Int = android.R.attr.editTextStyle
|
|
73
|
-
) : AppCompatEditText(context, attrs, defStyleAttr) {
|
|
74
|
-
data class ApplyUpdateTrace(
|
|
75
|
-
val attemptedPatch: Boolean,
|
|
76
|
-
val usedPatch: Boolean,
|
|
77
|
-
val skippedRender: Boolean,
|
|
78
|
-
val parseNanos: Long,
|
|
79
|
-
val resolveRenderBlocksNanos: Long,
|
|
80
|
-
val patchEligibilityNanos: Long,
|
|
81
|
-
val buildRenderNanos: Long,
|
|
82
|
-
val applyRenderNanos: Long,
|
|
83
|
-
val selectionNanos: Long,
|
|
84
|
-
val postApplyNanos: Long,
|
|
85
|
-
val totalNanos: Long
|
|
86
|
-
)
|
|
87
|
-
|
|
88
|
-
internal data class ImeInitialSurroundingText(
|
|
89
|
-
val text: String,
|
|
90
|
-
val selectionStart: Int,
|
|
91
|
-
val selectionEnd: Int,
|
|
92
|
-
val originalSelectionStart: Int,
|
|
93
|
-
val originalSelectionEnd: Int,
|
|
94
|
-
val removedPlaceholderCount: Int
|
|
95
|
-
)
|
|
96
|
-
|
|
97
|
-
data class SelectedImageGeometry(
|
|
98
|
-
val docPos: Int,
|
|
99
|
-
val rect: RectF
|
|
100
|
-
)
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
data class MentionHit(
|
|
104
|
-
val docPos: Int,
|
|
105
|
-
val label: String
|
|
106
|
-
)
|
|
107
|
-
|
|
108
|
-
data class CommandPreparation(
|
|
109
|
-
val ready: Boolean,
|
|
110
|
-
val updateJSON: String?
|
|
111
|
-
)
|
|
112
|
-
|
|
113
|
-
private data class HardwareKeyEventSignature(
|
|
114
|
-
val keyCode: Int,
|
|
115
|
-
val downTime: Long,
|
|
116
|
-
val repeatCount: Int
|
|
117
|
-
)
|
|
118
|
-
|
|
119
|
-
data class LinkHit(
|
|
120
|
-
val href: String,
|
|
121
|
-
val text: String
|
|
122
|
-
)
|
|
123
|
-
|
|
124
|
-
private data class RenderReplaceRange(
|
|
125
|
-
val start: Int,
|
|
126
|
-
val endExclusive: Int
|
|
127
|
-
)
|
|
128
|
-
|
|
129
|
-
private data class NativeTextMutation(
|
|
130
|
-
val scalarFrom: Int,
|
|
131
|
-
val scalarTo: Int,
|
|
132
|
-
val replacementText: String,
|
|
133
|
-
val resultingText: String,
|
|
134
|
-
val replacementStartUtf16: Int,
|
|
135
|
-
val replacementEndUtf16: Int,
|
|
136
|
-
val selectionScalarAnchor: Int?,
|
|
137
|
-
val selectionScalarHead: Int?
|
|
138
|
-
)
|
|
139
|
-
|
|
140
|
-
private data class NativeTextMutationAfterBlurWindow(
|
|
141
|
-
val editorId: Long,
|
|
142
|
-
val authorizedTextRevision: Long,
|
|
143
|
-
val deadlineMs: Long,
|
|
144
|
-
var didAdoptMutation: Boolean = false
|
|
145
|
-
)
|
|
146
|
-
|
|
147
|
-
private data class NativeTextMutationAdoptionSuppression(
|
|
148
|
-
val editorId: Long,
|
|
149
|
-
val authorizedTextRevision: Long
|
|
150
|
-
)
|
|
151
|
-
|
|
152
|
-
private interface TransientComposingTextStyleSpan
|
|
153
|
-
|
|
154
|
-
private class TransientComposingSizeSpan(sizePx: Int) :
|
|
155
|
-
AbsoluteSizeSpan(sizePx, false),
|
|
156
|
-
TransientComposingTextStyleSpan
|
|
157
|
-
|
|
158
|
-
private class TransientComposingColorSpan(color: Int) :
|
|
159
|
-
ForegroundColorSpan(color),
|
|
160
|
-
TransientComposingTextStyleSpan
|
|
161
|
-
|
|
162
|
-
private class TransientComposingTypefaceSpan(family: String) :
|
|
163
|
-
TypefaceSpan(family),
|
|
164
|
-
TransientComposingTextStyleSpan
|
|
165
|
-
|
|
166
|
-
private class TransientComposingStyleSpan(style: Int) :
|
|
167
|
-
StyleSpan(style),
|
|
168
|
-
TransientComposingTextStyleSpan
|
|
169
|
-
|
|
170
|
-
/**
|
|
171
|
-
* Listener interface for editor events, parallel to iOS's EditorTextViewDelegate.
|
|
172
|
-
*/
|
|
173
|
-
interface EditorListener {
|
|
174
|
-
/** Called when the editor's selection changes (anchor and head as scalar offsets). */
|
|
175
|
-
fun onSelectionChanged(anchor: Int, head: Int)
|
|
176
|
-
|
|
177
|
-
/** Called when the editor content is updated after a Rust operation. */
|
|
178
|
-
fun onEditorUpdate(updateJSON: String)
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
/** The Rust editor instance ID (from editor_create / editor_create_with_max_length). */
|
|
182
|
-
var editorId: Long = 0
|
|
183
|
-
|
|
184
|
-
/**
|
|
185
|
-
* Controls whether user input is accepted.
|
|
186
|
-
*
|
|
187
|
-
* When false, all user-input mutation entry points (typing, deletion,
|
|
188
|
-
* paste, composition) are blocked. Unlike [isEnabled], this preserves
|
|
189
|
-
* focus, text selection, and copy capability.
|
|
190
|
-
*/
|
|
191
|
-
var isEditable: Boolean = true
|
|
192
|
-
set(value) {
|
|
193
|
-
if (field == value) return
|
|
194
|
-
if (!value) {
|
|
195
|
-
discardTransientNativeInputForReadOnly()
|
|
196
|
-
}
|
|
197
|
-
field = value
|
|
198
|
-
if (value) {
|
|
199
|
-
restartInputForEditorIfFocused("editable")
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
/**
|
|
204
|
-
* Guard flag to prevent re-entrant input interception while we're
|
|
205
|
-
* applying state from Rust (calling [setText] or modifying text storage).
|
|
206
|
-
*/
|
|
207
|
-
var isApplyingRustState = false
|
|
208
|
-
|
|
209
|
-
/** Listener for editor events. */
|
|
210
|
-
var editorListener: EditorListener? = null
|
|
211
|
-
var onSelectionOrContentMayChange: (() -> Unit)? = null
|
|
212
|
-
var onUpdateJSONApplied: ((String) -> Unit)? = null
|
|
213
|
-
|
|
214
|
-
/** The base font size in pixels used for unstyled text. */
|
|
215
|
-
private var baseFontSize: Float = textSize
|
|
216
|
-
|
|
217
|
-
/** The base text color as an ARGB int. */
|
|
218
|
-
private var baseTextColor: Int = currentTextColor
|
|
219
|
-
|
|
220
|
-
/** The base background color before theme overrides. */
|
|
221
|
-
private var baseBackgroundColor: Int = android.graphics.Color.WHITE
|
|
222
|
-
|
|
223
|
-
/** Optional render theme supplied by React. */
|
|
224
|
-
var theme: EditorTheme? = null
|
|
225
|
-
private set
|
|
226
|
-
|
|
227
|
-
var placeholderText: String = ""
|
|
228
|
-
set(value) {
|
|
229
|
-
if (field == value) return
|
|
230
|
-
field = value
|
|
231
|
-
requestLayout()
|
|
232
|
-
invalidate()
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
var heightBehavior: EditorHeightBehavior = EditorHeightBehavior.FIXED
|
|
236
|
-
private set
|
|
237
|
-
private var imageResizingEnabled = true
|
|
238
|
-
private var nativeAutoCapitalize = DEFAULT_AUTO_CAPITALIZE
|
|
239
|
-
private var nativeAutoCorrect = DEFAULT_AUTO_CORRECT
|
|
240
|
-
private var nativeKeyboardType = DEFAULT_KEYBOARD_TYPE
|
|
241
|
-
|
|
242
|
-
private var contentInsets: EditorContentInsets? = null
|
|
243
|
-
private var viewportBottomInsetPx: Int = 0
|
|
244
|
-
|
|
245
|
-
/**
|
|
246
|
-
* The plain text from the last Rust-authorized render.
|
|
247
|
-
* Used by [ReconciliationWatcher] to detect unauthorized divergence.
|
|
248
|
-
*/
|
|
249
|
-
private var lastAuthorizedText: String = ""
|
|
250
|
-
|
|
251
|
-
/**
|
|
252
|
-
* Number of reconciliation events triggered during this EditText's lifetime.
|
|
253
|
-
* Useful for monitoring and kill-condition analysis.
|
|
254
|
-
*/
|
|
255
|
-
var reconciliationCount: Int = 0
|
|
256
|
-
private set
|
|
257
|
-
|
|
258
|
-
private var lastHandledHardwareKeySignature: HardwareKeyEventSignature? = null
|
|
259
|
-
private var recentHandledHardwareKeyDownSignature: HardwareKeyEventSignature? = null
|
|
260
|
-
private var recentHandledHardwareKeyDownUptimeMs: Long = 0L
|
|
261
|
-
private var activeInputConnection: EditorInputConnection? = null
|
|
262
|
-
private var inputConnectionGeneration: Long = 0L
|
|
263
|
-
private var composingText: String? = null
|
|
264
|
-
private var composingReplacementStartUtf16: Int? = null
|
|
265
|
-
private var composingReplacementEndUtf16: Int? = null
|
|
266
|
-
private var composingReplacementAuthorizedTextRevision: Long? = null
|
|
267
|
-
private var didInvalidateCompositionReplacementRange = false
|
|
268
|
-
private var nativeTextMutationAfterBlurWindow: NativeTextMutationAfterBlurWindow? = null
|
|
269
|
-
private var nativeTextMutationAdoptionSuppression: NativeTextMutationAdoptionSuppression? = null
|
|
270
|
-
private var lastAuthorizedTextRevision: Long = 0L
|
|
271
|
-
private var lastAuthorizedRenderedText: CharSequence? = null
|
|
272
|
-
private var lastRenderAppliedPatchForTesting: Boolean = false
|
|
273
|
-
internal var captureApplyUpdateTraceForTesting: Boolean = false
|
|
274
|
-
private var lastApplyUpdateTraceForTesting: ApplyUpdateTrace? = null
|
|
275
|
-
private val imeTraceForTesting = java.util.ArrayDeque<String>()
|
|
276
|
-
private var imeTraceSequence: Long = 0L
|
|
277
|
-
private var lastImeTraceUptimeMs: Long = 0L
|
|
278
|
-
private var renderAppearanceRevision: Long = 1L
|
|
279
|
-
private var lastAppliedRenderAppearanceRevision: Long = 0L
|
|
280
|
-
private var pendingOptimisticRenderText: String? = null
|
|
281
|
-
private var deferredRustUpdateApplicationDepth: Int = 0
|
|
282
|
-
private var deferredRustUpdateJSON: String? = null
|
|
283
|
-
private var deferredRustUpdateGeneration: Long = 0L
|
|
284
|
-
private var lineBoundaryInputRefreshGeneration: Long = 0L
|
|
285
|
-
private var restartInputSelectionUpdateGeneration: Long = 0L
|
|
286
|
-
internal var onDeleteRangeInRustForTesting: ((Int, Int) -> Unit)? = null
|
|
287
|
-
internal var onDeleteBackwardAtSelectionScalarInRustForTesting: ((Int, Int) -> Unit)? = null
|
|
288
|
-
internal var onToggleTaskItemCheckedAtSelectionScalarInRustForTesting: ((Int, Int) -> Unit)? = null
|
|
289
|
-
internal var onInsertTextInRustForTesting: ((String, Int) -> Unit)? = null
|
|
290
|
-
internal var onReplaceTextInRustForTesting: ((Int, Int, String) -> Unit)? = null
|
|
291
|
-
internal var onSetSelectionScalarInRustForTesting: ((Int, Int) -> Unit)? = null
|
|
292
|
-
internal var onDeleteAndSplitScalarInRustForTesting: ((Int, Int) -> Unit)? = null
|
|
293
|
-
internal var onInsertContentHtmlInRustForTesting: ((String) -> Unit)? = null
|
|
294
|
-
internal var onInsertContentJsonAtSelectionScalarForTesting: ((Int, Int, String) -> Unit)? = null
|
|
295
|
-
internal var blockExternalEditorUpdatePreparationForTesting = false
|
|
296
|
-
internal var blockExternalEditorCommandPreparationForTesting = false
|
|
297
|
-
|
|
298
|
-
fun lastRenderAppliedPatch(): Boolean = lastRenderAppliedPatchForTesting
|
|
299
|
-
fun lastApplyUpdateTrace(): ApplyUpdateTrace? = lastApplyUpdateTraceForTesting
|
|
300
|
-
internal fun hasDeferredRustUpdateApplicationForTesting(): Boolean = deferredRustUpdateJSON != null
|
|
301
|
-
|
|
302
|
-
internal fun applyRustUpdateJSONForTesting(updateJSON: String) {
|
|
303
|
-
applyRustUpdateJSON(updateJSON)
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
internal fun recordImeTraceForTesting(event: String, details: String = "") {
|
|
307
|
-
if (imeTraceForTesting.size >= IME_TRACE_LIMIT_FOR_TESTING) {
|
|
308
|
-
imeTraceForTesting.removeFirst()
|
|
309
|
-
}
|
|
310
|
-
imeTraceForTesting.addLast(
|
|
311
|
-
if (details.isEmpty()) event else "$event:$details"
|
|
312
|
-
)
|
|
313
|
-
if (Log.isLoggable(IME_TRACE_LOG_TAG, Log.VERBOSE)) {
|
|
314
|
-
val now = SystemClock.uptimeMillis()
|
|
315
|
-
val deltaMs = if (lastImeTraceUptimeMs == 0L) 0L else now - lastImeTraceUptimeMs
|
|
316
|
-
lastImeTraceUptimeMs = now
|
|
317
|
-
imeTraceSequence += 1L
|
|
318
|
-
val textLength = text?.length ?: -1
|
|
319
|
-
val selection = "${selectionStart}..${selectionEnd}"
|
|
320
|
-
val composingRange = "${composingReplacementStartUtf16 ?: -1}.." +
|
|
321
|
-
"${composingReplacementEndUtf16 ?: -1}"
|
|
322
|
-
val composingRevisionMatches =
|
|
323
|
-
composingReplacementAuthorizedTextRevision == lastAuthorizedTextRevision
|
|
324
|
-
val message = buildString {
|
|
325
|
-
append("#").append(imeTraceSequence)
|
|
326
|
-
append(" +").append(deltaMs).append("ms ")
|
|
327
|
-
append(event)
|
|
328
|
-
if (details.isNotEmpty()) {
|
|
329
|
-
append(" ").append(details)
|
|
330
|
-
}
|
|
331
|
-
append(" editor=").append(editorId)
|
|
332
|
-
append(" gen=").append(inputConnectionGeneration)
|
|
333
|
-
append(" activeIc=").append(activeInputConnection != null)
|
|
334
|
-
append(" focus=").append(hasFocus())
|
|
335
|
-
append(" applying=").append(isApplyingRustState)
|
|
336
|
-
append(" editable=").append(isEditable)
|
|
337
|
-
append(" textLen=").append(textLength)
|
|
338
|
-
append(" authLen=").append(lastAuthorizedText.length)
|
|
339
|
-
append(" sel=").append(selection)
|
|
340
|
-
append(" composingTextLen=").append(composingText?.length ?: -1)
|
|
341
|
-
append(" composingRange=").append(composingRange)
|
|
342
|
-
append(" composingRevOk=").append(composingRevisionMatches)
|
|
343
|
-
append(" invalidComp=").append(didInvalidateCompositionReplacementRange)
|
|
344
|
-
append(" deferredRustUpdate=").append(deferredRustUpdateJSON != null)
|
|
345
|
-
append(" scroll=").append(scrollX).append(",").append(scrollY)
|
|
346
|
-
}
|
|
347
|
-
Log.v(IME_TRACE_LOG_TAG, message)
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
internal fun clearImeTraceForTesting() {
|
|
352
|
-
imeTraceForTesting.clear()
|
|
353
|
-
imeTraceSequence = 0L
|
|
354
|
-
lastImeTraceUptimeMs = 0L
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
internal fun imeTraceSnapshotForTesting(): List<String> =
|
|
358
|
-
imeTraceForTesting.toList()
|
|
359
|
-
|
|
360
|
-
private fun nanosToMicros(nanos: Long): Long = nanos / 1_000L
|
|
361
|
-
|
|
362
|
-
init {
|
|
363
|
-
// Configure for rich text editing.
|
|
364
|
-
inputType = resolvedInputType()
|
|
365
|
-
|
|
366
|
-
// Disable built-in spell checking to avoid conflicts with Rust state.
|
|
367
|
-
// The Rust editor is the source of truth for text content.
|
|
368
|
-
isSaveEnabled = false
|
|
369
|
-
|
|
370
|
-
// Watch for unauthorized text mutations (IME, accessibility, etc.)
|
|
371
|
-
// and reconcile back to Rust's authoritative state.
|
|
372
|
-
addTextChangedListener(ReconciliationWatcher())
|
|
373
|
-
baseBackgroundColor = android.graphics.Color.WHITE
|
|
374
|
-
isVerticalScrollBarEnabled = true
|
|
375
|
-
overScrollMode = OVER_SCROLL_IF_CONTENT_SCROLLS
|
|
376
|
-
|
|
377
|
-
// Pin content to top-start to prevent theme-dependent vertical centering.
|
|
378
|
-
gravity = android.view.Gravity.TOP or android.view.Gravity.START
|
|
379
|
-
|
|
380
|
-
// Strip the default EditText theme drawable which carries implicit padding.
|
|
381
|
-
// Background color is applied in setBaseStyle() / applyTheme().
|
|
382
|
-
background = null
|
|
383
|
-
linksClickable = false
|
|
384
|
-
|
|
385
|
-
// Suppress the platform caret and draw our own. Android's Editor anchors
|
|
386
|
-
// the native caret to getLineBottom(line), which line-height spans can
|
|
387
|
-
// inflate. Our caret is
|
|
388
|
-
// clipped to the glyph height via [CaretGeometry]. See [drawCustomCaret].
|
|
389
|
-
isCursorVisible = false
|
|
390
|
-
|
|
391
|
-
updateEffectivePadding()
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
// ── Custom caret ────────────────────────────────────────────────────────
|
|
395
|
-
|
|
396
|
-
private var caretBlinkVisible = true
|
|
397
|
-
private val caretPaint = android.graphics.Paint(android.graphics.Paint.ANTI_ALIAS_FLAG)
|
|
398
|
-
private val caretWidthPx: Float by lazy { maxOf(MIN_CARET_WIDTH_PX, resources.displayMetrics.density) }
|
|
399
|
-
private val caretColor: Int by lazy { resolveCaretColor() }
|
|
400
|
-
private val caretBlinkRunnable = object : Runnable {
|
|
401
|
-
override fun run() {
|
|
402
|
-
caretBlinkVisible = !caretBlinkVisible
|
|
403
|
-
invalidate()
|
|
404
|
-
postDelayed(this, CARET_BLINK_INTERVAL_MS)
|
|
405
|
-
}
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
/**
|
|
409
|
-
* Reset the caret to solid-on and (re)schedule the blink. Called whenever the
|
|
410
|
-
* caret could appear or move (focus, window focus, selection change) so it
|
|
411
|
-
* behaves like the platform caret: solid immediately after a move, then blinks.
|
|
412
|
-
*/
|
|
413
|
-
private fun restartCaretBlink() {
|
|
414
|
-
removeCallbacks(caretBlinkRunnable)
|
|
415
|
-
caretBlinkVisible = true
|
|
416
|
-
if (CaretGeometry.shouldRender(isFocused, hasWindowFocus(), selectionStart, selectionEnd)) {
|
|
417
|
-
postDelayed(caretBlinkRunnable, CARET_BLINK_INTERVAL_MS)
|
|
418
|
-
}
|
|
419
|
-
invalidate()
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
private fun stopCaretBlink() {
|
|
423
|
-
removeCallbacks(caretBlinkRunnable)
|
|
424
|
-
caretBlinkVisible = false
|
|
425
|
-
invalidate()
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
private fun drawCustomCaret(canvas: android.graphics.Canvas) {
|
|
429
|
-
if (!caretBlinkVisible) return
|
|
430
|
-
if (!CaretGeometry.shouldRender(isFocused, hasWindowFocus(), selectionStart, selectionEnd)) return
|
|
431
|
-
val rect = customCaretDrawRect() ?: return
|
|
432
|
-
caretPaint.color = caretColor
|
|
433
|
-
canvas.drawRect(rect.left, rect.top, rect.right, rect.bottom, caretPaint)
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
/**
|
|
437
|
-
* The caret rectangle in canvas (content) coordinates — clipped to the glyph
|
|
438
|
-
* height via [CaretGeometry]. No scroll offset is applied: at onDraw time the
|
|
439
|
-
* framework canvas is already in scrolled content space, exactly like the text
|
|
440
|
-
* Layout it paints. (This differs from [caretRect], which reports view-relative
|
|
441
|
-
* coordinates to JS.)
|
|
442
|
-
*/
|
|
443
|
-
internal fun customCaretDrawRect(): RectF? {
|
|
444
|
-
val textLayout = layout ?: return null
|
|
445
|
-
val offset = selectionEnd.coerceIn(0, textLayout.text.length)
|
|
446
|
-
val bounds = CaretGeometry.verticalBounds(textLayout, offset, paint)
|
|
447
|
-
val left = totalPaddingLeft + textLayout.getPrimaryHorizontal(offset)
|
|
448
|
-
return RectF(left, totalPaddingTop + bounds.top, left + caretWidthPx, totalPaddingTop + bounds.bottom)
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
/**
|
|
452
|
-
* The native caret is tinted by the theme's `colorControlActivated`; resolve
|
|
453
|
-
* the same value so the replacement keeps the platform appearance, falling
|
|
454
|
-
* back to the text color when the attribute is not a color.
|
|
455
|
-
*/
|
|
456
|
-
private fun resolveCaretColor(): Int {
|
|
457
|
-
val resolved = TypedValue()
|
|
458
|
-
val found = context.theme.resolveAttribute(
|
|
459
|
-
android.R.attr.colorControlActivated,
|
|
460
|
-
resolved,
|
|
461
|
-
true
|
|
462
|
-
)
|
|
463
|
-
val isColor = resolved.type >= TypedValue.TYPE_FIRST_COLOR_INT &&
|
|
464
|
-
resolved.type <= TypedValue.TYPE_LAST_COLOR_INT
|
|
465
|
-
return if (found && isColor) resolved.data else currentTextColor
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
fun setAutoCapitalize(autoCapitalize: String?) {
|
|
469
|
-
val next = when (autoCapitalize) {
|
|
470
|
-
"none",
|
|
471
|
-
"sentences",
|
|
472
|
-
"words",
|
|
473
|
-
"characters" -> autoCapitalize
|
|
474
|
-
else -> DEFAULT_AUTO_CAPITALIZE
|
|
475
|
-
}
|
|
476
|
-
if (nativeAutoCapitalize == next) return
|
|
477
|
-
nativeAutoCapitalize = next
|
|
478
|
-
applyInputTraits()
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
fun setAutoCorrect(autoCorrect: Boolean?) {
|
|
482
|
-
val next = autoCorrect ?: DEFAULT_AUTO_CORRECT
|
|
483
|
-
if (nativeAutoCorrect == next) return
|
|
484
|
-
nativeAutoCorrect = next
|
|
485
|
-
applyInputTraits()
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
fun setKeyboardType(keyboardType: String?) {
|
|
489
|
-
val next = when (keyboardType) {
|
|
490
|
-
"default",
|
|
491
|
-
"email-address",
|
|
492
|
-
"numeric",
|
|
493
|
-
"phone-pad",
|
|
494
|
-
"ascii-capable",
|
|
495
|
-
"numbers-and-punctuation",
|
|
496
|
-
"url",
|
|
497
|
-
"number-pad",
|
|
498
|
-
"name-phone-pad",
|
|
499
|
-
"decimal-pad",
|
|
500
|
-
"twitter",
|
|
501
|
-
"web-search",
|
|
502
|
-
"visible-password",
|
|
503
|
-
"ascii-capable-number-pad" -> keyboardType
|
|
504
|
-
else -> DEFAULT_KEYBOARD_TYPE
|
|
505
|
-
}
|
|
506
|
-
if (nativeKeyboardType == next) return
|
|
507
|
-
nativeKeyboardType = next
|
|
508
|
-
applyInputTraits()
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
private fun applyInputTraits() {
|
|
512
|
-
val nextInputType = resolvedInputType()
|
|
513
|
-
if (inputType == nextInputType) return
|
|
514
|
-
|
|
515
|
-
val currentStart = selectionStart
|
|
516
|
-
val currentEnd = selectionEnd
|
|
517
|
-
val authorizedSelection = authorizedSelectionForTransientInputRestore(
|
|
518
|
-
currentStart,
|
|
519
|
-
currentEnd
|
|
520
|
-
)
|
|
521
|
-
discardTransientInputAndRestoreAuthorizedTextForEditor()
|
|
522
|
-
setRawInputType(nextInputType)
|
|
523
|
-
|
|
524
|
-
val editable = text
|
|
525
|
-
if (editable != null && authorizedSelection != null) {
|
|
526
|
-
setSelection(
|
|
527
|
-
authorizedSelection.first.coerceIn(0, editable.length),
|
|
528
|
-
authorizedSelection.second.coerceIn(0, editable.length)
|
|
529
|
-
)
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
if (hasFocus()) {
|
|
533
|
-
restartInputForEditor()
|
|
534
|
-
}
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
private fun resolvedInputType(): Int {
|
|
538
|
-
var nextInputType = when (nativeKeyboardType) {
|
|
539
|
-
"email-address" -> InputType.TYPE_CLASS_TEXT or
|
|
540
|
-
InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS
|
|
541
|
-
"url" -> InputType.TYPE_CLASS_TEXT or
|
|
542
|
-
InputType.TYPE_TEXT_VARIATION_URI
|
|
543
|
-
"phone-pad" -> InputType.TYPE_CLASS_PHONE
|
|
544
|
-
"number-pad" -> InputType.TYPE_CLASS_NUMBER
|
|
545
|
-
"decimal-pad" -> InputType.TYPE_CLASS_NUMBER or
|
|
546
|
-
InputType.TYPE_NUMBER_FLAG_DECIMAL
|
|
547
|
-
"numeric" -> InputType.TYPE_CLASS_NUMBER or
|
|
548
|
-
InputType.TYPE_NUMBER_FLAG_DECIMAL or
|
|
549
|
-
InputType.TYPE_NUMBER_FLAG_SIGNED
|
|
550
|
-
"visible-password" -> InputType.TYPE_CLASS_TEXT or
|
|
551
|
-
InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD
|
|
552
|
-
else -> InputType.TYPE_CLASS_TEXT
|
|
553
|
-
}
|
|
554
|
-
|
|
555
|
-
if ((nextInputType and InputType.TYPE_MASK_CLASS) == InputType.TYPE_CLASS_TEXT) {
|
|
556
|
-
nextInputType = nextInputType or InputType.TYPE_TEXT_FLAG_MULTI_LINE
|
|
557
|
-
nextInputType = nextInputType or when (nativeAutoCapitalize) {
|
|
558
|
-
"none" -> 0
|
|
559
|
-
"words" -> InputType.TYPE_TEXT_FLAG_CAP_WORDS
|
|
560
|
-
"characters" -> InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS
|
|
561
|
-
else -> InputType.TYPE_TEXT_FLAG_CAP_SENTENCES
|
|
562
|
-
}
|
|
563
|
-
nextInputType = nextInputType or if (nativeAutoCorrect) {
|
|
564
|
-
InputType.TYPE_TEXT_FLAG_AUTO_CORRECT
|
|
565
|
-
} else {
|
|
566
|
-
InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS
|
|
567
|
-
}
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
return nextInputType
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
// ── InputConnection Override ────────────────────────────────────────
|
|
574
|
-
|
|
575
|
-
/**
|
|
576
|
-
* Create a custom [EditorInputConnection] that intercepts all input
|
|
577
|
-
* from the soft keyboard.
|
|
578
|
-
*/
|
|
579
|
-
override fun onCreateInputConnection(outAttrs: EditorInfo): InputConnection? {
|
|
580
|
-
val baseConnection = super.onCreateInputConnection(outAttrs) ?: return null
|
|
581
|
-
val originalInitialCapsMode = outAttrs.initialCapsMode
|
|
582
|
-
outAttrs.initialCapsMode = cursorCapsModeForEditor(
|
|
583
|
-
reqModes = outAttrs.inputType,
|
|
584
|
-
baseCapsMode = outAttrs.initialCapsMode
|
|
585
|
-
)
|
|
586
|
-
val initialSurroundingText = applyInitialSurroundingTextForIme(outAttrs)
|
|
587
|
-
val generation = nextInputConnectionGenerationForEditor()
|
|
588
|
-
recordImeTraceForTesting(
|
|
589
|
-
"createInputConnection",
|
|
590
|
-
"boundEditor=$editorId boundGen=$generation inputType=$inputType initialCaps=$originalInitialCapsMode->${outAttrs.initialCapsMode} " +
|
|
591
|
-
"imeContextPlaceholdersRemoved=${initialSurroundingText?.removedPlaceholderCount ?: 0} " +
|
|
592
|
-
"imeContextSel=${initialSurroundingText?.selectionStart ?: outAttrs.initialSelStart}..${initialSurroundingText?.selectionEnd ?: outAttrs.initialSelEnd} " +
|
|
593
|
-
"imeContextRawSel=${initialSurroundingText?.originalSelectionStart ?: selectionStart}..${initialSurroundingText?.originalSelectionEnd ?: selectionEnd} " +
|
|
594
|
-
"imeContextBeforeTail=\"${initialSurroundingText?.textBeforeSelectionTailForImeLog() ?: ""}\""
|
|
595
|
-
)
|
|
596
|
-
return EditorInputConnection(this, baseConnection, editorId, generation).also {
|
|
597
|
-
activeInputConnection = it
|
|
598
|
-
}
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
private fun applyInitialSurroundingTextForIme(outAttrs: EditorInfo): ImeInitialSurroundingText? {
|
|
602
|
-
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) return null
|
|
603
|
-
val initialText = initialSurroundingTextForImeForEditor() ?: return null
|
|
604
|
-
|
|
605
|
-
outAttrs.initialSelStart = initialText.selectionStart
|
|
606
|
-
outAttrs.initialSelEnd = initialText.selectionEnd
|
|
607
|
-
outAttrs.setInitialSurroundingText(initialText.text)
|
|
608
|
-
return initialText
|
|
609
|
-
}
|
|
610
|
-
|
|
611
|
-
private fun ImeInitialSurroundingText.textBeforeSelectionTailForImeLog(limit: Int = 24): String {
|
|
612
|
-
val end = selectionStart.coerceIn(0, text.length)
|
|
613
|
-
val start = maxOf(0, end - limit)
|
|
614
|
-
return text.substring(start, end).toImeTraceSnippet()
|
|
615
|
-
}
|
|
616
|
-
|
|
617
|
-
private fun String.toImeTraceSnippet(): String {
|
|
618
|
-
val builder = StringBuilder(length)
|
|
619
|
-
forEach { ch ->
|
|
620
|
-
when (ch) {
|
|
621
|
-
'\n' -> builder.append("\\n")
|
|
622
|
-
'\r' -> builder.append("\\r")
|
|
623
|
-
'\t' -> builder.append("\\t")
|
|
624
|
-
'\\' -> builder.append("\\\\")
|
|
625
|
-
'"' -> builder.append("\\\"")
|
|
626
|
-
else -> {
|
|
627
|
-
if (ch.code < 0x20 || ch == InputSnapshotConstants.SYNTHETIC_PLACEHOLDER_CHARACTER[0]) {
|
|
628
|
-
builder.append("\\u")
|
|
629
|
-
builder.append(ch.code.toString(16).padStart(4, '0'))
|
|
630
|
-
} else {
|
|
631
|
-
builder.append(ch)
|
|
632
|
-
}
|
|
633
|
-
}
|
|
634
|
-
}
|
|
635
|
-
}
|
|
636
|
-
return builder.toString()
|
|
637
|
-
}
|
|
638
|
-
|
|
639
|
-
override fun dispatchKeyEvent(event: KeyEvent): Boolean {
|
|
640
|
-
if (!isEditable && isReadOnlyTextMutationKeyEvent(event)) {
|
|
641
|
-
return true
|
|
642
|
-
}
|
|
643
|
-
if (handleCompositionKeyEvent(event) { super.dispatchKeyEvent(event) }) {
|
|
644
|
-
return true
|
|
645
|
-
}
|
|
646
|
-
if (handleHardwareKeyEvent(event)) {
|
|
647
|
-
return true
|
|
648
|
-
}
|
|
649
|
-
if (handlePrintableHardwareKeyEvent(event) { super.dispatchKeyEvent(event) }) {
|
|
650
|
-
return true
|
|
651
|
-
}
|
|
652
|
-
return super.dispatchKeyEvent(event)
|
|
653
|
-
}
|
|
654
|
-
|
|
655
|
-
internal fun handleCompositionKeyEvent(event: KeyEvent, applyBaseEvent: () -> Boolean): Boolean {
|
|
656
|
-
val inputConnection = activeInputConnection ?: return false
|
|
657
|
-
if (!inputConnection.hasPendingComposition()) return false
|
|
658
|
-
if (!isCompositionKeyCode(event.keyCode)) return false
|
|
659
|
-
if (event.action == KeyEvent.ACTION_DOWN) {
|
|
660
|
-
val signature = hardwareKeyEventSignature(event)
|
|
661
|
-
if (
|
|
662
|
-
lastHandledHardwareKeySignature == signature ||
|
|
663
|
-
didRecentlyHandleHardwareKeyDown(signature)
|
|
664
|
-
) {
|
|
665
|
-
return true
|
|
666
|
-
}
|
|
667
|
-
markHandledHardwareKeyDown(signature)
|
|
668
|
-
runWithTransientInputMutationGuard {
|
|
669
|
-
when (event.keyCode) {
|
|
670
|
-
KeyEvent.KEYCODE_DEL,
|
|
671
|
-
KeyEvent.KEYCODE_FORWARD_DEL -> inputConnection.deleteTransientTextForHardwareKeyEvent(event)
|
|
672
|
-
else -> applyBaseEvent()
|
|
673
|
-
}
|
|
674
|
-
}
|
|
675
|
-
inputConnection.refreshComposingTextFromEditableForEditor()
|
|
676
|
-
return true
|
|
677
|
-
}
|
|
678
|
-
if (event.action == KeyEvent.ACTION_UP) {
|
|
679
|
-
if (lastHandledHardwareKeySignature?.let {
|
|
680
|
-
it.keyCode == event.keyCode && it.downTime == event.downTime
|
|
681
|
-
} == true) {
|
|
682
|
-
lastHandledHardwareKeySignature = null
|
|
683
|
-
}
|
|
684
|
-
return true
|
|
685
|
-
}
|
|
686
|
-
return false
|
|
687
|
-
}
|
|
688
|
-
|
|
689
|
-
private fun isCompositionKeyCode(keyCode: Int): Boolean =
|
|
690
|
-
when (keyCode) {
|
|
691
|
-
KeyEvent.KEYCODE_DEL,
|
|
692
|
-
KeyEvent.KEYCODE_FORWARD_DEL,
|
|
693
|
-
KeyEvent.KEYCODE_ENTER,
|
|
694
|
-
KeyEvent.KEYCODE_NUMPAD_ENTER,
|
|
695
|
-
KeyEvent.KEYCODE_TAB -> true
|
|
696
|
-
else -> false
|
|
697
|
-
}
|
|
698
|
-
|
|
699
|
-
override fun onDraw(canvas: android.graphics.Canvas) {
|
|
700
|
-
super.onDraw(canvas)
|
|
701
|
-
drawCustomCaret(canvas)
|
|
702
|
-
|
|
703
|
-
val placeholderLayout =
|
|
704
|
-
buildPlaceholderLayout(width - compoundPaddingLeft - compoundPaddingRight) ?: return
|
|
705
|
-
|
|
706
|
-
val previousColor = paint.color
|
|
707
|
-
val saveCount = canvas.save()
|
|
708
|
-
canvas.translate(compoundPaddingLeft.toFloat(), extendedPaddingTop.toFloat())
|
|
709
|
-
placeholderLayout.draw(canvas)
|
|
710
|
-
canvas.restoreToCount(saveCount)
|
|
711
|
-
paint.color = previousColor
|
|
712
|
-
}
|
|
713
|
-
|
|
714
|
-
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
|
715
|
-
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
|
|
716
|
-
|
|
717
|
-
val placeholderHeight = resolvePlaceholderHeightForMeasuredWidth(measuredWidth) ?: return
|
|
718
|
-
val desiredHeight = maxOf(measuredHeight, placeholderHeight)
|
|
719
|
-
val resolvedHeight = when (MeasureSpec.getMode(heightMeasureSpec)) {
|
|
720
|
-
MeasureSpec.EXACTLY -> measuredHeight
|
|
721
|
-
MeasureSpec.AT_MOST -> desiredHeight.coerceAtMost(MeasureSpec.getSize(heightMeasureSpec))
|
|
722
|
-
else -> desiredHeight
|
|
723
|
-
}
|
|
724
|
-
|
|
725
|
-
if (resolvedHeight != measuredHeight) {
|
|
726
|
-
setMeasuredDimension(measuredWidth, resolvedHeight)
|
|
727
|
-
}
|
|
728
|
-
}
|
|
729
|
-
|
|
730
|
-
override fun onTouchEvent(event: MotionEvent): Boolean {
|
|
731
|
-
if (heightBehavior == EditorHeightBehavior.FIXED) {
|
|
732
|
-
val canScroll = canScrollVertically(-1) || canScrollVertically(1)
|
|
733
|
-
if (canScroll) {
|
|
734
|
-
when (event.actionMasked) {
|
|
735
|
-
MotionEvent.ACTION_DOWN,
|
|
736
|
-
MotionEvent.ACTION_MOVE -> parent?.requestDisallowInterceptTouchEvent(true)
|
|
737
|
-
MotionEvent.ACTION_UP,
|
|
738
|
-
MotionEvent.ACTION_CANCEL -> parent?.requestDisallowInterceptTouchEvent(false)
|
|
739
|
-
}
|
|
740
|
-
}
|
|
741
|
-
}
|
|
742
|
-
return super.onTouchEvent(event)
|
|
743
|
-
}
|
|
744
|
-
|
|
745
|
-
override fun performClick(): Boolean {
|
|
746
|
-
return super.performClick()
|
|
747
|
-
}
|
|
748
|
-
|
|
749
|
-
private fun isRenderedContentEmpty(content: CharSequence? = text): Boolean {
|
|
750
|
-
val renderedContent = content ?: return true
|
|
751
|
-
if (renderedContent.isEmpty()) return true
|
|
752
|
-
|
|
753
|
-
for (index in 0 until renderedContent.length) {
|
|
754
|
-
when (renderedContent[index]) {
|
|
755
|
-
EMPTY_BLOCK_PLACEHOLDER, '\n', '\r' -> continue
|
|
756
|
-
else -> return false
|
|
757
|
-
}
|
|
758
|
-
}
|
|
759
|
-
|
|
760
|
-
return true
|
|
761
|
-
}
|
|
762
|
-
|
|
763
|
-
private fun shouldDisplayPlaceholder(): Boolean {
|
|
764
|
-
return placeholderText.isNotEmpty() && isRenderedContentEmpty()
|
|
765
|
-
}
|
|
766
|
-
|
|
767
|
-
fun shouldDisplayPlaceholderForTesting(): Boolean = shouldDisplayPlaceholder()
|
|
768
|
-
|
|
769
|
-
private fun buildPlaceholderLayout(availableWidth: Int): StaticLayout? {
|
|
770
|
-
if (!shouldDisplayPlaceholder()) return null
|
|
771
|
-
if (availableWidth <= 0) return null
|
|
772
|
-
|
|
773
|
-
val placeholderPaint = resolvedPlaceholderPaint()
|
|
774
|
-
return StaticLayout.Builder
|
|
775
|
-
.obtain(
|
|
776
|
-
placeholderText,
|
|
777
|
-
0,
|
|
778
|
-
placeholderText.length,
|
|
779
|
-
placeholderPaint,
|
|
780
|
-
availableWidth
|
|
781
|
-
)
|
|
782
|
-
.setAlignment(Layout.Alignment.ALIGN_NORMAL)
|
|
783
|
-
.setIncludePad(includeFontPadding)
|
|
784
|
-
.build()
|
|
785
|
-
}
|
|
786
|
-
|
|
787
|
-
private fun resolvedPlaceholderPaint(): TextPaint {
|
|
788
|
-
val textStyle = theme?.effectiveTextStyle("paragraph")
|
|
789
|
-
val resolvedTextSize = textStyle?.fontSize?.times(resources.displayMetrics.density) ?: baseFontSize
|
|
790
|
-
val resolvedTypeface = resolvePlaceholderTypeface(textStyle)
|
|
791
|
-
|
|
792
|
-
return TextPaint(paint).apply {
|
|
793
|
-
color = theme?.placeholderColor ?: currentHintTextColor
|
|
794
|
-
textSize = resolvedTextSize
|
|
795
|
-
typeface = resolvedTypeface
|
|
796
|
-
}
|
|
797
|
-
}
|
|
798
|
-
|
|
799
|
-
private fun resolvePlaceholderTypeface(textStyle: EditorTextStyle?): Typeface {
|
|
800
|
-
val baseTypeface = typeface ?: Typeface.DEFAULT
|
|
801
|
-
val requestedStyle = textStyle?.typefaceStyle() ?: Typeface.NORMAL
|
|
802
|
-
val family = textStyle?.fontFamily?.takeIf { it.isNotBlank() }
|
|
803
|
-
|
|
804
|
-
return when {
|
|
805
|
-
family != null -> Typeface.create(family, requestedStyle)
|
|
806
|
-
requestedStyle != Typeface.NORMAL -> Typeface.create(baseTypeface, requestedStyle)
|
|
807
|
-
else -> baseTypeface
|
|
808
|
-
}
|
|
809
|
-
}
|
|
810
|
-
|
|
811
|
-
private fun resolvePlaceholderHeightForMeasuredWidth(widthPx: Int): Int? {
|
|
812
|
-
val availableWidth = (widthPx - compoundPaddingLeft - compoundPaddingRight).coerceAtLeast(0)
|
|
813
|
-
return resolvePlaceholderHeightForAvailableWidth(availableWidth)
|
|
814
|
-
}
|
|
815
|
-
|
|
816
|
-
private fun resolvePlaceholderHeightForAvailableWidth(availableWidth: Int): Int? {
|
|
817
|
-
val placeholderLayout = buildPlaceholderLayout(availableWidth) ?: return null
|
|
818
|
-
val placeholderHeight = placeholderLayout.height.takeIf { it > 0 } ?: lineHeight
|
|
819
|
-
return placeholderHeight + compoundPaddingTop + compoundPaddingBottom
|
|
820
|
-
}
|
|
821
|
-
|
|
822
|
-
// ── Editor Binding ──────────────────────────────────────────────────
|
|
823
|
-
|
|
824
|
-
/**
|
|
825
|
-
* Bind this EditText to a Rust editor instance and optionally apply initial content.
|
|
826
|
-
*
|
|
827
|
-
* @param id The editor ID from `editor_create()`.
|
|
828
|
-
* @param initialHTML Optional HTML to set as initial content.
|
|
829
|
-
*/
|
|
830
|
-
fun bindEditor(id: Long, initialHTML: String? = null, notifyListener: Boolean = true) {
|
|
831
|
-
if (id != 0L && NativeEditorViewRegistry.isDestroyed(id)) {
|
|
832
|
-
discardTransientNativeInputForEditorRebind()
|
|
833
|
-
editorId = 0L
|
|
834
|
-
return
|
|
835
|
-
}
|
|
836
|
-
if (editorId != id) {
|
|
837
|
-
discardTransientNativeInputForEditorRebind()
|
|
838
|
-
}
|
|
839
|
-
editorId = id
|
|
840
|
-
|
|
841
|
-
if (!initialHTML.isNullOrEmpty()) {
|
|
842
|
-
editorSetHtml(editorId.toULong(), initialHTML)
|
|
843
|
-
val stateJSON = editorGetCurrentState(editorId.toULong())
|
|
844
|
-
applyUpdateJSON(stateJSON, notifyListener = false)
|
|
845
|
-
} else {
|
|
846
|
-
// Pull current state from Rust (content may already be loaded via bridge).
|
|
847
|
-
val stateJSON = editorGetCurrentState(editorId.toULong())
|
|
848
|
-
applyUpdateJSON(stateJSON, notifyListener = notifyListener)
|
|
849
|
-
}
|
|
850
|
-
}
|
|
851
|
-
|
|
852
|
-
/**
|
|
853
|
-
* Unbind from the current editor instance.
|
|
854
|
-
*/
|
|
855
|
-
fun unbindEditor() {
|
|
856
|
-
if (editorId != 0L) {
|
|
857
|
-
discardTransientNativeInputForEditorRebind()
|
|
858
|
-
}
|
|
859
|
-
editorId = 0
|
|
860
|
-
}
|
|
861
|
-
|
|
862
|
-
fun setBaseStyle(fontSizePx: Float, textColor: Int, backgroundColor: Int) {
|
|
863
|
-
if (baseFontSize != fontSizePx || baseTextColor != textColor) {
|
|
864
|
-
renderAppearanceRevision += 1L
|
|
865
|
-
}
|
|
866
|
-
baseFontSize = fontSizePx
|
|
867
|
-
baseTextColor = textColor
|
|
868
|
-
baseBackgroundColor = backgroundColor
|
|
869
|
-
setTextSize(TypedValue.COMPLEX_UNIT_PX, fontSizePx)
|
|
870
|
-
setTextColor(textColor)
|
|
871
|
-
setBackgroundColor(theme?.backgroundColor ?: backgroundColor)
|
|
872
|
-
}
|
|
873
|
-
|
|
874
|
-
fun applyTheme(theme: EditorTheme?) {
|
|
875
|
-
this.theme = theme
|
|
876
|
-
renderAppearanceRevision += 1L
|
|
877
|
-
setBackgroundColor(theme?.backgroundColor ?: baseBackgroundColor)
|
|
878
|
-
applyContentInsets(theme?.contentInsets)
|
|
879
|
-
if (hasLiveEditor()) {
|
|
880
|
-
val previousScrollX = scrollX
|
|
881
|
-
val previousScrollY = scrollY
|
|
882
|
-
val stateJSON = editorGetCurrentState(editorId.toULong())
|
|
883
|
-
applyUpdateJSON(stateJSON, notifyListener = false)
|
|
884
|
-
if (heightBehavior == EditorHeightBehavior.FIXED) {
|
|
885
|
-
preserveScrollPosition(previousScrollX, previousScrollY)
|
|
886
|
-
} else {
|
|
887
|
-
requestLayout()
|
|
888
|
-
}
|
|
889
|
-
}
|
|
890
|
-
}
|
|
891
|
-
|
|
892
|
-
fun setHeightBehavior(heightBehavior: EditorHeightBehavior) {
|
|
893
|
-
if (this.heightBehavior == heightBehavior) return
|
|
894
|
-
this.heightBehavior = heightBehavior
|
|
895
|
-
isVerticalScrollBarEnabled = heightBehavior == EditorHeightBehavior.FIXED
|
|
896
|
-
overScrollMode = if (heightBehavior == EditorHeightBehavior.FIXED) {
|
|
897
|
-
OVER_SCROLL_IF_CONTENT_SCROLLS
|
|
898
|
-
} else {
|
|
899
|
-
OVER_SCROLL_NEVER
|
|
900
|
-
}
|
|
901
|
-
updateEffectivePadding()
|
|
902
|
-
ensureSelectionVisible()
|
|
903
|
-
requestLayout()
|
|
904
|
-
}
|
|
905
|
-
|
|
906
|
-
private fun applyContentInsets(contentInsets: EditorContentInsets?) {
|
|
907
|
-
this.contentInsets = contentInsets
|
|
908
|
-
updateEffectivePadding()
|
|
909
|
-
}
|
|
910
|
-
|
|
911
|
-
fun setViewportBottomInsetPx(bottomInsetPx: Int) {
|
|
912
|
-
val clampedInset = bottomInsetPx.coerceAtLeast(0)
|
|
913
|
-
if (viewportBottomInsetPx == clampedInset) return
|
|
914
|
-
viewportBottomInsetPx = clampedInset
|
|
915
|
-
updateEffectivePadding()
|
|
916
|
-
ensureSelectionVisible()
|
|
917
|
-
}
|
|
918
|
-
|
|
919
|
-
private fun updateEffectivePadding() {
|
|
920
|
-
val density = resources.displayMetrics.density
|
|
921
|
-
val left = ((contentInsets?.left ?: 0f) * density).toInt()
|
|
922
|
-
val top = ((contentInsets?.top ?: 0f) * density).toInt()
|
|
923
|
-
val right = ((contentInsets?.right ?: 0f) * density).toInt()
|
|
924
|
-
val bottom = ((contentInsets?.bottom ?: 0f) * density).toInt()
|
|
925
|
-
|
|
926
|
-
if (heightBehavior == EditorHeightBehavior.FIXED) {
|
|
927
|
-
setPadding(left, 0, right, 0)
|
|
928
|
-
setCompoundDrawablesRelativeWithIntrinsicBounds(null, null, null, null)
|
|
929
|
-
} else {
|
|
930
|
-
setPadding(left, top, right, bottom)
|
|
931
|
-
setCompoundDrawablesRelativeWithIntrinsicBounds(null, null, null, null)
|
|
932
|
-
}
|
|
933
|
-
}
|
|
934
|
-
|
|
935
|
-
fun setImageResizingEnabled(enabled: Boolean) {
|
|
936
|
-
if (imageResizingEnabled == enabled) return
|
|
937
|
-
imageResizingEnabled = enabled
|
|
938
|
-
if (enabled) {
|
|
939
|
-
onSelectionOrContentMayChange?.invoke()
|
|
940
|
-
}
|
|
941
|
-
}
|
|
942
|
-
|
|
943
|
-
fun resolveAutoGrowHeight(): Int {
|
|
944
|
-
val availableWidth = (measuredWidth - compoundPaddingLeft - compoundPaddingRight).coerceAtLeast(0)
|
|
945
|
-
val placeholderHeight = resolvePlaceholderHeightForAvailableWidth(availableWidth)
|
|
946
|
-
val laidOutTextHeight = if (isLaidOut) layout?.height else null
|
|
947
|
-
if (laidOutTextHeight != null && laidOutTextHeight > 0) {
|
|
948
|
-
return maxOf(
|
|
949
|
-
laidOutTextHeight + compoundPaddingTop + compoundPaddingBottom,
|
|
950
|
-
placeholderHeight ?: 0
|
|
951
|
-
)
|
|
952
|
-
}
|
|
953
|
-
|
|
954
|
-
val currentText = text
|
|
955
|
-
if (availableWidth > 0 && currentText != null) {
|
|
956
|
-
val staticLayout = StaticLayout.Builder
|
|
957
|
-
.obtain(currentText, 0, currentText.length, paint, availableWidth)
|
|
958
|
-
.setAlignment(Layout.Alignment.ALIGN_NORMAL)
|
|
959
|
-
.setIncludePad(includeFontPadding)
|
|
960
|
-
.build()
|
|
961
|
-
val textHeight = staticLayout.height.takeIf { it > 0 } ?: lineHeight
|
|
962
|
-
return maxOf(
|
|
963
|
-
textHeight + compoundPaddingTop + compoundPaddingBottom,
|
|
964
|
-
placeholderHeight ?: 0
|
|
965
|
-
)
|
|
966
|
-
}
|
|
967
|
-
|
|
968
|
-
val minimumHeight = suggestedMinimumHeight.coerceAtLeast(minHeight)
|
|
969
|
-
return maxOf(
|
|
970
|
-
placeholderHeight ?: 0,
|
|
971
|
-
(lineHeight + compoundPaddingTop + compoundPaddingBottom).coerceAtLeast(minimumHeight)
|
|
972
|
-
)
|
|
973
|
-
}
|
|
974
|
-
|
|
975
|
-
private fun preserveScrollPosition(previousScrollX: Int, previousScrollY: Int) {
|
|
976
|
-
val restore = {
|
|
977
|
-
val maxScrollX = maxOf(0, computeHorizontalScrollRange() - width)
|
|
978
|
-
val maxScrollY = maxOf(0, computeVerticalScrollRange() - height)
|
|
979
|
-
scrollTo(
|
|
980
|
-
previousScrollX.coerceIn(0, maxScrollX),
|
|
981
|
-
previousScrollY.coerceIn(0, maxScrollY)
|
|
982
|
-
)
|
|
983
|
-
}
|
|
984
|
-
|
|
985
|
-
restore()
|
|
986
|
-
post { restore() }
|
|
987
|
-
}
|
|
988
|
-
|
|
989
|
-
private fun ensureSelectionVisible() {
|
|
990
|
-
if (heightBehavior != EditorHeightBehavior.FIXED) return
|
|
991
|
-
if (!isLaidOut || width <= 0 || height <= 0) return
|
|
992
|
-
val selectionOffset = selectionEnd.takeIf { it >= 0 } ?: return
|
|
993
|
-
|
|
994
|
-
post {
|
|
995
|
-
if (!isLaidOut || layout == null) return@post
|
|
996
|
-
bringPointIntoView(selectionOffset)
|
|
997
|
-
|
|
998
|
-
val textLayout = layout ?: return@post
|
|
999
|
-
val clampedOffset = selectionOffset.coerceAtMost(textLayout.text.length)
|
|
1000
|
-
val line = textLayout.getLineForOffset(clampedOffset)
|
|
1001
|
-
val caretLeft = textLayout.getPrimaryHorizontal(clampedOffset).toInt()
|
|
1002
|
-
val rect = Rect(
|
|
1003
|
-
caretLeft + totalPaddingLeft,
|
|
1004
|
-
textLayout.getLineTop(line) + totalPaddingTop,
|
|
1005
|
-
caretLeft + totalPaddingLeft + 1,
|
|
1006
|
-
textLayout.getLineBottom(line) + totalPaddingTop
|
|
1007
|
-
)
|
|
1008
|
-
requestRectangleOnScreen(rect)
|
|
1009
|
-
}
|
|
1010
|
-
}
|
|
1011
|
-
|
|
1012
|
-
internal fun caretRect(): RectF? {
|
|
1013
|
-
val textLayout = layout ?: return null
|
|
1014
|
-
val selectionOffset = selectionEnd.takeIf { it >= 0 } ?: return null
|
|
1015
|
-
val clampedOffset = selectionOffset.coerceIn(0, textLayout.text.length)
|
|
1016
|
-
val caretLeft = textLayout.getPrimaryHorizontal(clampedOffset)
|
|
1017
|
-
// Clip the caret to the rendered glyph height so inflated descent does
|
|
1018
|
-
// not stretch it into extra line space.
|
|
1019
|
-
val bounds = CaretGeometry.verticalBounds(textLayout, clampedOffset, paint)
|
|
1020
|
-
val left = totalPaddingLeft + caretLeft - scrollX
|
|
1021
|
-
val top = totalPaddingTop + bounds.top - scrollY
|
|
1022
|
-
val bottom = totalPaddingTop + bounds.bottom - scrollY
|
|
1023
|
-
return RectF(left, top, left + 1f, bottom)
|
|
1024
|
-
}
|
|
1025
|
-
|
|
1026
|
-
// ── Input Handling: Text Commit ─────────────────────────────────────
|
|
1027
|
-
|
|
1028
|
-
/**
|
|
1029
|
-
* Handle committed text from the IME (typed characters, autocomplete).
|
|
1030
|
-
*
|
|
1031
|
-
* Called by [EditorInputConnection.commitText]. Routes the text through
|
|
1032
|
-
* the Rust editor instead of directly inserting into the EditText.
|
|
1033
|
-
*/
|
|
1034
|
-
fun handleTextCommit(text: String, newCursorPosition: Int = 1) {
|
|
1035
|
-
val startedAt = System.nanoTime()
|
|
1036
|
-
if (!isEditable) {
|
|
1037
|
-
recordImeTraceForTesting("handleTextCommitNoop", "reason=notEditable textLength=${text.length}")
|
|
1038
|
-
return
|
|
1039
|
-
}
|
|
1040
|
-
if (isApplyingRustState) {
|
|
1041
|
-
recordImeTraceForTesting("handleTextCommitNoop", "reason=applyingRust textLength=${text.length}")
|
|
1042
|
-
return
|
|
1043
|
-
}
|
|
1044
|
-
val selectionRange = normalizedUtf16SelectionRange()
|
|
1045
|
-
if (selectionRange == null) {
|
|
1046
|
-
recordImeTraceForTesting("handleTextCommitNoop", "reason=noSelection textLength=${text.length}")
|
|
1047
|
-
return
|
|
1048
|
-
}
|
|
1049
|
-
if (editorId == 0L) {
|
|
1050
|
-
// No Rust editor bound — fall through to direct editing (dev mode).
|
|
1051
|
-
val editable = this.text ?: return
|
|
1052
|
-
val (start, end) = selectionRange
|
|
1053
|
-
editable.replace(start, end, text)
|
|
1054
|
-
recordImeTraceForTesting(
|
|
1055
|
-
"handleTextCommitDirect",
|
|
1056
|
-
"textLength=${text.length} utf16Sel=$start..$end totalUs=${nanosToMicros(System.nanoTime() - startedAt)}"
|
|
1057
|
-
)
|
|
1058
|
-
return
|
|
1059
|
-
}
|
|
1060
|
-
if (discardTransientInputForDestroyedEditorIfNeeded()) {
|
|
1061
|
-
recordImeTraceForTesting("handleTextCommitNoop", "reason=destroyedEditor textLength=${text.length}")
|
|
1062
|
-
return
|
|
1063
|
-
}
|
|
1064
|
-
|
|
1065
|
-
// Handle Enter/Return as a block split operation.
|
|
1066
|
-
if (text == "\n") {
|
|
1067
|
-
recordImeTraceForTesting(
|
|
1068
|
-
"handleTextCommit",
|
|
1069
|
-
"route=return utf16Sel=${selectionRange.first}..${selectionRange.second}"
|
|
1070
|
-
)
|
|
1071
|
-
handleReturnKey()
|
|
1072
|
-
recordImeTraceForTesting(
|
|
1073
|
-
"handleTextCommitDone",
|
|
1074
|
-
"route=return totalUs=${nanosToMicros(System.nanoTime() - startedAt)}"
|
|
1075
|
-
)
|
|
1076
|
-
return
|
|
1077
|
-
}
|
|
1078
|
-
|
|
1079
|
-
val currentText = this.text?.toString() ?: ""
|
|
1080
|
-
val scalarSelectionRange = normalizedScalarSelectionRange(currentText)
|
|
1081
|
-
if (scalarSelectionRange == null) {
|
|
1082
|
-
recordImeTraceForTesting("handleTextCommitNoop", "reason=noScalarSelection textLength=${text.length}")
|
|
1083
|
-
return
|
|
1084
|
-
}
|
|
1085
|
-
val (scalarStart, scalarEnd) = scalarSelectionRange
|
|
1086
|
-
val requestedCursor = requestedCursorScalar(
|
|
1087
|
-
scalarStart,
|
|
1088
|
-
scalarEnd,
|
|
1089
|
-
currentText,
|
|
1090
|
-
text,
|
|
1091
|
-
newCursorPosition
|
|
1092
|
-
)
|
|
1093
|
-
recordImeTraceForTesting(
|
|
1094
|
-
"handleTextCommit",
|
|
1095
|
-
"textLength=${text.length} cursor=$newCursorPosition utf16Sel=${selectionRange.first}..${selectionRange.second} scalarSel=$scalarStart..$scalarEnd requestedCursor=$requestedCursor"
|
|
1096
|
-
)
|
|
1097
|
-
val didApplyOptimisticVisibleText = applyOptimisticPlainTextCommitIfPossible(
|
|
1098
|
-
startUtf16 = selectionRange.first,
|
|
1099
|
-
endUtf16 = selectionRange.second,
|
|
1100
|
-
committedText = text,
|
|
1101
|
-
newCursorPosition = newCursorPosition
|
|
1102
|
-
)
|
|
1103
|
-
if (didApplyOptimisticVisibleText) {
|
|
1104
|
-
recordImeTraceForTesting(
|
|
1105
|
-
"optimisticVisibleTextCommit",
|
|
1106
|
-
"textLength=${text.length} utf16Sel=${selectionRange.first}..${selectionRange.second}"
|
|
1107
|
-
)
|
|
1108
|
-
}
|
|
1109
|
-
insertPlainTextRangeInRust(
|
|
1110
|
-
scalarStart,
|
|
1111
|
-
scalarEnd,
|
|
1112
|
-
text,
|
|
1113
|
-
requestedCursorScalar = requestedCursor
|
|
1114
|
-
)
|
|
1115
|
-
recordImeTraceForTesting(
|
|
1116
|
-
"handleTextCommitDone",
|
|
1117
|
-
"textLength=${text.length} totalUs=${nanosToMicros(System.nanoTime() - startedAt)}"
|
|
1118
|
-
)
|
|
1119
|
-
}
|
|
1120
|
-
|
|
1121
|
-
private data class OptimisticInlineSpan(
|
|
1122
|
-
val span: Any,
|
|
1123
|
-
val flags: Int
|
|
1124
|
-
)
|
|
1125
|
-
|
|
1126
|
-
private fun applyOptimisticPlainTextCommitIfPossible(
|
|
1127
|
-
startUtf16: Int,
|
|
1128
|
-
endUtf16: Int,
|
|
1129
|
-
committedText: String,
|
|
1130
|
-
newCursorPosition: Int
|
|
1131
|
-
): Boolean {
|
|
1132
|
-
if (newCursorPosition != 1) return false
|
|
1133
|
-
if (startUtf16 != endUtf16) return false
|
|
1134
|
-
if (committedText.isEmpty()) return false
|
|
1135
|
-
if (committedText.codePointCount(0, committedText.length) != 1) return false
|
|
1136
|
-
if (committedText.indexOf('\n') >= 0 || committedText.indexOf('\r') >= 0) return false
|
|
1137
|
-
if (hasCompositionTrackingForEditor()) return false
|
|
1138
|
-
val editable = text ?: return false
|
|
1139
|
-
val currentText = editable.toString()
|
|
1140
|
-
if (currentText != lastAuthorizedText) return false
|
|
1141
|
-
if (startUtf16 < 0 || endUtf16 < startUtf16 || endUtf16 > editable.length) return false
|
|
1142
|
-
val spanned = editable as? Spanned
|
|
1143
|
-
val inlineSpans = spanned?.let {
|
|
1144
|
-
optimisticInlineSpansForInsertion(it, startUtf16)
|
|
1145
|
-
}.orEmpty()
|
|
1146
|
-
var didApply = false
|
|
1147
|
-
runWithTransientInputMutationGuard {
|
|
1148
|
-
editable.replace(startUtf16, endUtf16, committedText)
|
|
1149
|
-
val insertedEnd = startUtf16 + committedText.length
|
|
1150
|
-
applyOptimisticInlineSpans(editable, startUtf16, insertedEnd, inlineSpans)
|
|
1151
|
-
Selection.setSelection(editable, insertedEnd, insertedEnd)
|
|
1152
|
-
didApply = true
|
|
1153
|
-
true
|
|
1154
|
-
}
|
|
1155
|
-
if (didApply) {
|
|
1156
|
-
pendingOptimisticRenderText = editable.toString()
|
|
1157
|
-
}
|
|
1158
|
-
return didApply
|
|
1159
|
-
}
|
|
1160
|
-
|
|
1161
|
-
private fun optimisticInlineSpansForInsertion(
|
|
1162
|
-
spanned: Spanned,
|
|
1163
|
-
insertionStart: Int
|
|
1164
|
-
): List<OptimisticInlineSpan> {
|
|
1165
|
-
if (spanned.isEmpty()) return emptyList()
|
|
1166
|
-
val sourceIndex = when {
|
|
1167
|
-
insertionStart > 0 -> insertionStart - 1
|
|
1168
|
-
insertionStart < spanned.length -> insertionStart
|
|
1169
|
-
else -> return emptyList()
|
|
1170
|
-
}
|
|
1171
|
-
val queryStart = sourceIndex.coerceIn(0, spanned.length - 1)
|
|
1172
|
-
val queryEnd = (queryStart + 1).coerceAtMost(spanned.length)
|
|
1173
|
-
val spans = mutableListOf<OptimisticInlineSpan>()
|
|
1174
|
-
spanned.getSpans(queryStart, queryEnd, Any::class.java).forEach { span ->
|
|
1175
|
-
if (spanned.getSpanStart(span) > queryStart || spanned.getSpanEnd(span) <= queryStart) {
|
|
1176
|
-
return@forEach
|
|
1177
|
-
}
|
|
1178
|
-
cloneOptimisticInlineSpan(span)?.let { clone ->
|
|
1179
|
-
spans.add(OptimisticInlineSpan(clone, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE))
|
|
1180
|
-
}
|
|
1181
|
-
}
|
|
1182
|
-
return spans
|
|
1183
|
-
}
|
|
1184
|
-
|
|
1185
|
-
private fun cloneOptimisticInlineSpan(span: Any): Any? =
|
|
1186
|
-
when (span) {
|
|
1187
|
-
is ForegroundColorSpan -> ForegroundColorSpan(span.foregroundColor)
|
|
1188
|
-
is BackgroundColorSpan -> BackgroundColorSpan(span.backgroundColor)
|
|
1189
|
-
is AbsoluteSizeSpan -> AbsoluteSizeSpan(span.size, span.dip)
|
|
1190
|
-
is StyleSpan -> StyleSpan(span.style)
|
|
1191
|
-
is UnderlineSpan -> UnderlineSpan()
|
|
1192
|
-
is StrikethroughSpan -> StrikethroughSpan()
|
|
1193
|
-
else -> null
|
|
1194
|
-
}
|
|
1195
|
-
|
|
1196
|
-
private fun applyOptimisticInlineSpans(
|
|
1197
|
-
editable: Editable,
|
|
1198
|
-
start: Int,
|
|
1199
|
-
end: Int,
|
|
1200
|
-
inlineSpans: List<OptimisticInlineSpan>
|
|
1201
|
-
) {
|
|
1202
|
-
if (start >= end || end > editable.length) return
|
|
1203
|
-
var hasColor = false
|
|
1204
|
-
var hasSize = false
|
|
1205
|
-
inlineSpans.forEach { spec ->
|
|
1206
|
-
hasColor = hasColor || spec.span is ForegroundColorSpan
|
|
1207
|
-
hasSize = hasSize || spec.span is AbsoluteSizeSpan
|
|
1208
|
-
editable.setSpan(spec.span, start, end, spec.flags)
|
|
1209
|
-
}
|
|
1210
|
-
val textStyle = theme?.effectiveTextStyle("paragraph")
|
|
1211
|
-
if (!hasColor) {
|
|
1212
|
-
editable.setSpan(
|
|
1213
|
-
ForegroundColorSpan(textStyle?.color ?: baseTextColor),
|
|
1214
|
-
start,
|
|
1215
|
-
end,
|
|
1216
|
-
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
|
|
1217
|
-
)
|
|
1218
|
-
}
|
|
1219
|
-
if (!hasSize) {
|
|
1220
|
-
val resolvedTextSize = textStyle?.fontSize?.times(resources.displayMetrics.density) ?: baseFontSize
|
|
1221
|
-
editable.setSpan(
|
|
1222
|
-
AbsoluteSizeSpan(resolvedTextSize.toInt(), false),
|
|
1223
|
-
start,
|
|
1224
|
-
end,
|
|
1225
|
-
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
|
|
1226
|
-
)
|
|
1227
|
-
}
|
|
1228
|
-
}
|
|
1229
|
-
|
|
1230
|
-
internal fun runWithTransientInputMutationGuard(block: () -> Boolean): Boolean {
|
|
1231
|
-
val wasApplyingRustState = isApplyingRustState
|
|
1232
|
-
isApplyingRustState = true
|
|
1233
|
-
return try {
|
|
1234
|
-
block()
|
|
1235
|
-
} finally {
|
|
1236
|
-
isApplyingRustState = wasApplyingRustState
|
|
1237
|
-
}
|
|
1238
|
-
}
|
|
1239
|
-
|
|
1240
|
-
internal fun authorizedUtf16Range(start: Int, end: Int): Pair<Int, Int> {
|
|
1241
|
-
if (start == end) {
|
|
1242
|
-
val snapped = PositionBridge.snapToScalarBoundary(
|
|
1243
|
-
start,
|
|
1244
|
-
lastAuthorizedText,
|
|
1245
|
-
biasForward = true
|
|
1246
|
-
)
|
|
1247
|
-
return snapped to snapped
|
|
1248
|
-
}
|
|
1249
|
-
return PositionBridge.snapRangeToScalarBoundaries(start, end, lastAuthorizedText)
|
|
1250
|
-
}
|
|
1251
|
-
|
|
1252
|
-
internal fun isCurrentTextAuthorizedForEditor(): Boolean =
|
|
1253
|
-
(text?.toString() ?: "") == lastAuthorizedText
|
|
1254
|
-
|
|
1255
|
-
internal fun captureCompositionReplacementRangeIfNeeded() {
|
|
1256
|
-
if (didInvalidateCompositionReplacementRange) return
|
|
1257
|
-
if (compositionReplacementRange() != null) return
|
|
1258
|
-
val (start, end) = normalizedUtf16SelectionRange() ?: return
|
|
1259
|
-
setCompositionReplacementRange(start, end)
|
|
1260
|
-
}
|
|
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
|
-
|
|
1288
|
-
internal fun setCompositionReplacementRange(start: Int, end: Int) {
|
|
1289
|
-
if (didInvalidateCompositionReplacementRange) return
|
|
1290
|
-
val replacementRange = authorizedUtf16Range(start, end)
|
|
1291
|
-
composingReplacementStartUtf16 = replacementRange.first
|
|
1292
|
-
composingReplacementEndUtf16 = replacementRange.second
|
|
1293
|
-
composingReplacementAuthorizedTextRevision = lastAuthorizedTextRevision
|
|
1294
|
-
didInvalidateCompositionReplacementRange = false
|
|
1295
|
-
}
|
|
1296
|
-
|
|
1297
|
-
internal fun compositionReplacementRange(): Pair<Int, Int>? {
|
|
1298
|
-
val start = composingReplacementStartUtf16 ?: return null
|
|
1299
|
-
val end = composingReplacementEndUtf16 ?: return null
|
|
1300
|
-
if (composingReplacementAuthorizedTextRevision != lastAuthorizedTextRevision) {
|
|
1301
|
-
clearCompositionTrackingForEditor()
|
|
1302
|
-
didInvalidateCompositionReplacementRange = true
|
|
1303
|
-
return null
|
|
1304
|
-
}
|
|
1305
|
-
return start to end
|
|
1306
|
-
}
|
|
1307
|
-
|
|
1308
|
-
private fun authorizedSelectionForTransientInputRestore(
|
|
1309
|
-
currentStart: Int,
|
|
1310
|
-
currentEnd: Int
|
|
1311
|
-
): Pair<Int, Int>? {
|
|
1312
|
-
compositionReplacementRange()?.let { return it }
|
|
1313
|
-
return if (
|
|
1314
|
-
currentStart >= 0 &&
|
|
1315
|
-
currentEnd >= 0 &&
|
|
1316
|
-
currentStart <= lastAuthorizedText.length &&
|
|
1317
|
-
currentEnd <= lastAuthorizedText.length
|
|
1318
|
-
) {
|
|
1319
|
-
currentStart to currentEnd
|
|
1320
|
-
} else {
|
|
1321
|
-
null
|
|
1322
|
-
}
|
|
1323
|
-
}
|
|
1324
|
-
|
|
1325
|
-
internal fun consumeInvalidatedCompositionReplacementRangeForEditor(): Boolean {
|
|
1326
|
-
val invalidated = didInvalidateCompositionReplacementRange
|
|
1327
|
-
didInvalidateCompositionReplacementRange = false
|
|
1328
|
-
return invalidated
|
|
1329
|
-
}
|
|
1330
|
-
|
|
1331
|
-
internal fun hasInvalidatedCompositionReplacementRangeForEditor(): Boolean =
|
|
1332
|
-
didInvalidateCompositionReplacementRange
|
|
1333
|
-
|
|
1334
|
-
internal fun setComposingTextForEditor(text: String?) {
|
|
1335
|
-
composingText = text
|
|
1336
|
-
}
|
|
1337
|
-
|
|
1338
|
-
internal fun composingTextForEditor(): String? = composingText
|
|
1339
|
-
|
|
1340
|
-
internal fun samsungSentenceCapsComposingTextForEditor(composingText: String?): String? {
|
|
1341
|
-
if (composingText.isNullOrEmpty()) return composingText
|
|
1342
|
-
if (!isSamsungKeyboardActiveForEditor()) return composingText
|
|
1343
|
-
if ((inputType and InputType.TYPE_TEXT_FLAG_CAP_SENTENCES) != InputType.TYPE_TEXT_FLAG_CAP_SENTENCES) {
|
|
1344
|
-
return composingText
|
|
1345
|
-
}
|
|
1346
|
-
val (replacementStart, replacementEnd) = compositionReplacementRange() ?: return composingText
|
|
1347
|
-
if (replacementStart != replacementEnd) return composingText
|
|
1348
|
-
if (!isRenderedLineStartForSentenceCaps(lastAuthorizedText, replacementStart)) {
|
|
1349
|
-
return composingText
|
|
1350
|
-
}
|
|
1351
|
-
|
|
1352
|
-
val firstCodePoint = Character.codePointAt(composingText, 0)
|
|
1353
|
-
if (!Character.isLowerCase(firstCodePoint)) return composingText
|
|
1354
|
-
val adjusted = buildString(composingText.length) {
|
|
1355
|
-
appendCodePoint(Character.toTitleCase(firstCodePoint))
|
|
1356
|
-
append(composingText.substring(Character.charCount(firstCodePoint)))
|
|
1357
|
-
}
|
|
1358
|
-
recordImeTraceForTesting(
|
|
1359
|
-
"samsungSentenceCapsFallback",
|
|
1360
|
-
"range=$replacementStart..$replacementEnd textLength=${composingText.length}"
|
|
1361
|
-
)
|
|
1362
|
-
return adjusted
|
|
1363
|
-
}
|
|
1364
|
-
|
|
1365
|
-
internal fun applyTransientComposingTextStyleForEditor() {
|
|
1366
|
-
val editable = text ?: return
|
|
1367
|
-
removeTransientComposingTextStyleSpans(editable)
|
|
1368
|
-
|
|
1369
|
-
val start = BaseInputConnection.getComposingSpanStart(editable)
|
|
1370
|
-
val end = BaseInputConnection.getComposingSpanEnd(editable)
|
|
1371
|
-
if (start < 0 || end < 0 || start >= end || end > editable.length) return
|
|
1372
|
-
|
|
1373
|
-
val textStyle = theme?.effectiveTextStyle("paragraph")
|
|
1374
|
-
val resolvedTextSize = textStyle?.fontSize?.times(resources.displayMetrics.density) ?: baseFontSize
|
|
1375
|
-
val resolvedTextColor = textStyle?.color ?: baseTextColor
|
|
1376
|
-
|
|
1377
|
-
editable.setSpan(
|
|
1378
|
-
TransientComposingSizeSpan(resolvedTextSize.toInt()),
|
|
1379
|
-
start,
|
|
1380
|
-
end,
|
|
1381
|
-
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
|
|
1382
|
-
)
|
|
1383
|
-
editable.setSpan(
|
|
1384
|
-
TransientComposingColorSpan(resolvedTextColor),
|
|
1385
|
-
start,
|
|
1386
|
-
end,
|
|
1387
|
-
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
|
|
1388
|
-
)
|
|
1389
|
-
|
|
1390
|
-
val typefaceStyle = textStyle?.typefaceStyle() ?: Typeface.NORMAL
|
|
1391
|
-
if (typefaceStyle != Typeface.NORMAL) {
|
|
1392
|
-
editable.setSpan(
|
|
1393
|
-
TransientComposingStyleSpan(typefaceStyle),
|
|
1394
|
-
start,
|
|
1395
|
-
end,
|
|
1396
|
-
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
|
|
1397
|
-
)
|
|
1398
|
-
}
|
|
1399
|
-
|
|
1400
|
-
val fontFamily = textStyle?.fontFamily?.takeIf { it.isNotBlank() }
|
|
1401
|
-
if (fontFamily != null) {
|
|
1402
|
-
editable.setSpan(
|
|
1403
|
-
TransientComposingTypefaceSpan(fontFamily),
|
|
1404
|
-
start,
|
|
1405
|
-
end,
|
|
1406
|
-
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
|
|
1407
|
-
)
|
|
1408
|
-
}
|
|
1409
|
-
}
|
|
1410
|
-
|
|
1411
|
-
private fun removeTransientComposingTextStyleSpans(editable: Editable) {
|
|
1412
|
-
editable
|
|
1413
|
-
.getSpans(0, editable.length, TransientComposingTextStyleSpan::class.java)
|
|
1414
|
-
.forEach(editable::removeSpan)
|
|
1415
|
-
}
|
|
1416
|
-
|
|
1417
|
-
internal fun composingTextFromVisibleReplacementForEditor(): String? {
|
|
1418
|
-
val (start, end) = compositionReplacementRange() ?: return null
|
|
1419
|
-
val authorizedText = lastAuthorizedText
|
|
1420
|
-
val currentText = text?.toString() ?: return null
|
|
1421
|
-
if (start < 0 || end < start || end > authorizedText.length) return null
|
|
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
|
-
}
|
|
1437
|
-
val authorizedPrefix = authorizedText.substring(0, start)
|
|
1438
|
-
val authorizedSuffix = authorizedText.substring(authorizedEnd)
|
|
1439
|
-
if (!currentText.startsWith(authorizedPrefix)) return null
|
|
1440
|
-
if (!currentText.endsWith(authorizedSuffix)) return null
|
|
1441
|
-
|
|
1442
|
-
val replacementEnd = currentText.length - authorizedSuffix.length
|
|
1443
|
-
if (replacementEnd < authorizedPrefix.length) return null
|
|
1444
|
-
return currentText.substring(authorizedPrefix.length, replacementEnd)
|
|
1445
|
-
}
|
|
1446
|
-
|
|
1447
|
-
internal fun clearCompositionTrackingForEditor() {
|
|
1448
|
-
composingText = null
|
|
1449
|
-
composingReplacementStartUtf16 = null
|
|
1450
|
-
composingReplacementEndUtf16 = null
|
|
1451
|
-
composingReplacementAuthorizedTextRevision = null
|
|
1452
|
-
}
|
|
1453
|
-
|
|
1454
|
-
private fun hasCompositionTrackingForEditor(): Boolean =
|
|
1455
|
-
composingText != null ||
|
|
1456
|
-
composingReplacementStartUtf16 != null ||
|
|
1457
|
-
composingReplacementEndUtf16 != null ||
|
|
1458
|
-
composingReplacementAuthorizedTextRevision != null
|
|
1459
|
-
|
|
1460
|
-
private fun retireInputConnectionForEditor() {
|
|
1461
|
-
recordImeTraceForTesting("retireInputConnection")
|
|
1462
|
-
activeInputConnection?.clearCompositionTrackingForEditor()
|
|
1463
|
-
invalidateInputConnectionsForEditor()
|
|
1464
|
-
clearCompositionTrackingForEditor()
|
|
1465
|
-
clearCompositionInvalidationForEditor()
|
|
1466
|
-
clearNativeComposingSpans()
|
|
1467
|
-
}
|
|
1468
|
-
|
|
1469
|
-
internal fun isEditorDestroyedForInput(): Boolean =
|
|
1470
|
-
editorId != 0L && NativeEditorViewRegistry.isDestroyed(editorId)
|
|
1471
|
-
|
|
1472
|
-
private fun hasLiveEditor(): Boolean =
|
|
1473
|
-
editorId != 0L && !isEditorDestroyedForInput()
|
|
1474
|
-
|
|
1475
|
-
private fun discardTransientInputForDestroyedEditorIfNeeded(): Boolean {
|
|
1476
|
-
if (!isEditorDestroyedForInput()) return false
|
|
1477
|
-
retireInputConnectionForEditor()
|
|
1478
|
-
clearNativeTextMutationAfterBlurWindow()
|
|
1479
|
-
clearNativeTextMutationAdoptionSuppression()
|
|
1480
|
-
return true
|
|
1481
|
-
}
|
|
1482
|
-
|
|
1483
|
-
private fun discardTransientInputAndRestoreAuthorizedTextForEditor() {
|
|
1484
|
-
retireInputConnectionForEditor()
|
|
1485
|
-
clearNativeTextMutationAfterBlurWindow()
|
|
1486
|
-
restoreAuthorizedTextSnapshotForEditor()
|
|
1487
|
-
suppressNativeTextMutationAdoptionForCurrentRevision()
|
|
1488
|
-
}
|
|
1489
|
-
|
|
1490
|
-
private fun restoreAuthorizedTextSnapshotForEditor() {
|
|
1491
|
-
if ((text?.toString() ?: "") == lastAuthorizedText) return
|
|
1492
|
-
val authorizedSnapshot = lastAuthorizedRenderedText ?: lastAuthorizedText
|
|
1493
|
-
val wasApplyingRustState = isApplyingRustState
|
|
1494
|
-
isApplyingRustState = true
|
|
1495
|
-
beginBatchEdit()
|
|
1496
|
-
try {
|
|
1497
|
-
setText(authorizedSnapshot)
|
|
1498
|
-
} finally {
|
|
1499
|
-
endBatchEdit()
|
|
1500
|
-
isApplyingRustState = wasApplyingRustState
|
|
1501
|
-
}
|
|
1502
|
-
}
|
|
1503
|
-
|
|
1504
|
-
private fun restartInputAfterCompositionInvalidationIfNeeded(shouldRestart: Boolean) {
|
|
1505
|
-
if (!shouldRestart) return
|
|
1506
|
-
restartInputForEditorIfFocused("focused")
|
|
1507
|
-
}
|
|
1508
|
-
|
|
1509
|
-
private fun restartInputForEditorIfFocused(source: String) {
|
|
1510
|
-
if (!hasFocus()) return
|
|
1511
|
-
restartInputForEditor(source)
|
|
1512
|
-
}
|
|
1513
|
-
|
|
1514
|
-
private fun restartInputForEditor(source: String = "explicit") {
|
|
1515
|
-
recordImeTraceForTesting("restartInput", "source=$source")
|
|
1516
|
-
val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager
|
|
1517
|
-
imm?.restartInput(this)
|
|
1518
|
-
scheduleSelectionUpdateAfterRestartInput(source)
|
|
1519
|
-
}
|
|
1520
|
-
|
|
1521
|
-
private fun scheduleSelectionUpdateAfterRestartInput(source: String) {
|
|
1522
|
-
val generation = ++restartInputSelectionUpdateGeneration
|
|
1523
|
-
post {
|
|
1524
|
-
if (generation != restartInputSelectionUpdateGeneration) return@post
|
|
1525
|
-
if (!hasFocus()) return@post
|
|
1526
|
-
val start = selectionStart
|
|
1527
|
-
val end = selectionEnd
|
|
1528
|
-
if (start < 0 || end < 0) {
|
|
1529
|
-
recordImeTraceForTesting(
|
|
1530
|
-
"updateSelectionAfterRestartSkipped",
|
|
1531
|
-
"source=$source reason=selection start=$start end=$end"
|
|
1532
|
-
)
|
|
1533
|
-
return@post
|
|
1534
|
-
}
|
|
1535
|
-
val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager
|
|
1536
|
-
imm?.updateSelection(this, start, end, -1, -1)
|
|
1537
|
-
recordImeTraceForTesting(
|
|
1538
|
-
"updateSelectionAfterRestart",
|
|
1539
|
-
"source=$source sel=$start..$end"
|
|
1540
|
-
)
|
|
1541
|
-
}
|
|
1542
|
-
}
|
|
1543
|
-
|
|
1544
|
-
private fun scheduleLineBoundaryInputRefreshForEditor(source: String) {
|
|
1545
|
-
if (!hasFocus()) return
|
|
1546
|
-
val generation = ++lineBoundaryInputRefreshGeneration
|
|
1547
|
-
recordImeTraceForTesting(
|
|
1548
|
-
"lineBoundaryInputRefreshScheduled",
|
|
1549
|
-
"source=$source generation=$generation"
|
|
1550
|
-
)
|
|
1551
|
-
post {
|
|
1552
|
-
if (generation != lineBoundaryInputRefreshGeneration) return@post
|
|
1553
|
-
if (!hasFocus()) return@post
|
|
1554
|
-
if (!isCursorAtRenderedLineStartForSentenceCaps()) {
|
|
1555
|
-
recordImeTraceForTesting(
|
|
1556
|
-
"lineBoundaryInputRefreshSkipped",
|
|
1557
|
-
"source=$source reason=cursor"
|
|
1558
|
-
)
|
|
1559
|
-
return@post
|
|
1560
|
-
}
|
|
1561
|
-
restartInputForEditor("lineBoundary:$source")
|
|
1562
|
-
}
|
|
1563
|
-
}
|
|
1564
|
-
|
|
1565
|
-
private fun clearCompositionInvalidationForEditor() {
|
|
1566
|
-
didInvalidateCompositionReplacementRange = false
|
|
1567
|
-
}
|
|
1568
|
-
|
|
1569
|
-
private fun nextInputConnectionGenerationForEditor(): Long {
|
|
1570
|
-
return inputConnectionGeneration
|
|
1571
|
-
}
|
|
1572
|
-
|
|
1573
|
-
internal fun isInputConnectionCurrentForEditor(
|
|
1574
|
-
boundEditorId: Long,
|
|
1575
|
-
boundGeneration: Long
|
|
1576
|
-
): Boolean =
|
|
1577
|
-
editorId == boundEditorId &&
|
|
1578
|
-
inputConnectionGeneration == boundGeneration &&
|
|
1579
|
-
!isEditorDestroyedForInput()
|
|
1580
|
-
|
|
1581
|
-
private fun invalidateInputConnectionsForEditor() {
|
|
1582
|
-
inputConnectionGeneration += 1L
|
|
1583
|
-
recordImeTraceForTesting("invalidateInputConnections", "nextGen=$inputConnectionGeneration")
|
|
1584
|
-
activeInputConnection = null
|
|
1585
|
-
}
|
|
1586
|
-
|
|
1587
|
-
private fun clearNativeComposingSpans() {
|
|
1588
|
-
val editable = text ?: return
|
|
1589
|
-
BaseInputConnection.removeComposingSpans(editable)
|
|
1590
|
-
removeTransientComposingTextStyleSpans(editable)
|
|
1591
|
-
}
|
|
1592
|
-
|
|
1593
|
-
internal fun restoreAuthorizedTextIfNeeded() {
|
|
1594
|
-
if (!hasLiveEditor()) return
|
|
1595
|
-
if ((text?.toString() ?: "") == lastAuthorizedText) return
|
|
1596
|
-
recordImeTraceForTesting(
|
|
1597
|
-
"restoreAuthorizedText",
|
|
1598
|
-
"authorizedLength=${lastAuthorizedText.length}"
|
|
1599
|
-
)
|
|
1600
|
-
val stateJSON = editorGetCurrentState(editorId.toULong())
|
|
1601
|
-
applyUpdateJSON(stateJSON)
|
|
1602
|
-
}
|
|
1603
|
-
|
|
1604
|
-
fun discardTransientNativeInputForEditorRebind() {
|
|
1605
|
-
retireInputConnectionForEditor()
|
|
1606
|
-
nativeTextMutationAfterBlurWindow = null
|
|
1607
|
-
clearNativeTextMutationAdoptionSuppression()
|
|
1608
|
-
clearImeTraceForTesting()
|
|
1609
|
-
}
|
|
1610
|
-
|
|
1611
|
-
internal fun discardTransientNativeInputForExternalRecovery() {
|
|
1612
|
-
retireInputConnectionForEditor()
|
|
1613
|
-
nativeTextMutationAfterBlurWindow = null
|
|
1614
|
-
restoreAuthorizedTextIfNeeded()
|
|
1615
|
-
suppressNativeTextMutationAdoptionForCurrentRevision()
|
|
1616
|
-
}
|
|
1617
|
-
|
|
1618
|
-
private fun discardTransientNativeInputForReadOnly() {
|
|
1619
|
-
discardTransientNativeInputForExternalRecovery()
|
|
1620
|
-
}
|
|
1621
|
-
|
|
1622
|
-
fun prepareForExternalEditorUpdate(): Boolean {
|
|
1623
|
-
if (blockExternalEditorUpdatePreparationForTesting) return false
|
|
1624
|
-
if (discardTransientInputForDestroyedEditorIfNeeded()) return false
|
|
1625
|
-
val inputConnection = activeInputConnection
|
|
1626
|
-
if (inputConnection?.flushPendingCompositionForExternalMutation() == false) {
|
|
1627
|
-
return false
|
|
1628
|
-
}
|
|
1629
|
-
return drainNativeTextMutationIfNeeded(
|
|
1630
|
-
allowAfterBlur = true,
|
|
1631
|
-
preserveInputConnectionForExternalUpdate = true
|
|
1632
|
-
)
|
|
1633
|
-
}
|
|
1634
|
-
|
|
1635
|
-
fun prepareForExternalEditorCommand(): CommandPreparation {
|
|
1636
|
-
if (blockExternalEditorCommandPreparationForTesting) {
|
|
1637
|
-
return CommandPreparation(ready = false, updateJSON = null)
|
|
1638
|
-
}
|
|
1639
|
-
val previousAuthorizedText = lastAuthorizedText
|
|
1640
|
-
if (!prepareForExternalEditorUpdate()) {
|
|
1641
|
-
return CommandPreparation(ready = false, updateJSON = null)
|
|
1642
|
-
}
|
|
1643
|
-
if (!hasLiveEditor() || lastAuthorizedText == previousAuthorizedText) {
|
|
1644
|
-
return CommandPreparation(ready = true, updateJSON = null)
|
|
1645
|
-
}
|
|
1646
|
-
return CommandPreparation(
|
|
1647
|
-
ready = true,
|
|
1648
|
-
updateJSON = editorGetCurrentState(editorId.toULong())
|
|
1649
|
-
)
|
|
1650
|
-
}
|
|
1651
|
-
|
|
1652
|
-
fun handleCompositionCommit(
|
|
1653
|
-
text: String,
|
|
1654
|
-
replacementStartUtf16: Int,
|
|
1655
|
-
replacementEndUtf16: Int,
|
|
1656
|
-
newCursorPosition: Int = 1
|
|
1657
|
-
) {
|
|
1658
|
-
val startedAt = System.nanoTime()
|
|
1659
|
-
if (!isEditable) {
|
|
1660
|
-
recordImeTraceForTesting("handleCompositionCommitNoop", "reason=notEditable textLength=${text.length}")
|
|
1661
|
-
return
|
|
1662
|
-
}
|
|
1663
|
-
if (isApplyingRustState) {
|
|
1664
|
-
recordImeTraceForTesting("handleCompositionCommitNoop", "reason=applyingRust textLength=${text.length}")
|
|
1665
|
-
return
|
|
1666
|
-
}
|
|
1667
|
-
if (!hasLiveEditor()) {
|
|
1668
|
-
recordImeTraceForTesting("handleCompositionCommitNoop", "reason=noLiveEditor textLength=${text.length}")
|
|
1669
|
-
return
|
|
1670
|
-
}
|
|
1671
|
-
|
|
1672
|
-
val authorizedText = lastAuthorizedText
|
|
1673
|
-
val (startUtf16, endUtf16) = PositionBridge.snapRangeToScalarBoundaries(
|
|
1674
|
-
replacementStartUtf16,
|
|
1675
|
-
replacementEndUtf16,
|
|
1676
|
-
authorizedText
|
|
1677
|
-
)
|
|
1678
|
-
val scalarStart = PositionBridge.utf16ToScalar(startUtf16, authorizedText)
|
|
1679
|
-
val scalarEnd = PositionBridge.utf16ToScalar(endUtf16, authorizedText)
|
|
1680
|
-
|
|
1681
|
-
if (
|
|
1682
|
-
startUtf16 <= endUtf16 &&
|
|
1683
|
-
endUtf16 <= authorizedText.length &&
|
|
1684
|
-
authorizedText.substring(startUtf16, endUtf16) == text
|
|
1685
|
-
) {
|
|
1686
|
-
val requestedCursor = requestedCursorScalar(
|
|
1687
|
-
scalarStart,
|
|
1688
|
-
scalarEnd,
|
|
1689
|
-
authorizedText,
|
|
1690
|
-
text,
|
|
1691
|
-
newCursorPosition
|
|
1692
|
-
) ?: scalarEnd
|
|
1693
|
-
recordImeTraceForTesting(
|
|
1694
|
-
"handleCompositionCommitNoop",
|
|
1695
|
-
"reason=alreadyAuthorized textLength=${text.length} requestedCursor=$requestedCursor range=$startUtf16..$endUtf16"
|
|
1696
|
-
)
|
|
1697
|
-
restoreAuthorizedTextIfNeeded()
|
|
1698
|
-
applyRequestedCursorScalar(requestedCursor)
|
|
1699
|
-
return
|
|
1700
|
-
}
|
|
1701
|
-
|
|
1702
|
-
if (text == "\n") {
|
|
1703
|
-
recordImeTraceForTesting(
|
|
1704
|
-
"handleCompositionCommit",
|
|
1705
|
-
"route=return textLength=${text.length} utf16Range=$startUtf16..$endUtf16 scalarRange=$scalarStart..$scalarEnd"
|
|
1706
|
-
)
|
|
1707
|
-
if (scalarStart != scalarEnd) {
|
|
1708
|
-
deleteAndSplitInRust(scalarStart, scalarEnd)
|
|
1709
|
-
} else {
|
|
1710
|
-
splitBlockInRust(scalarStart)
|
|
1711
|
-
}
|
|
1712
|
-
recordImeTraceForTesting(
|
|
1713
|
-
"handleCompositionCommitDone",
|
|
1714
|
-
"route=return totalUs=${nanosToMicros(System.nanoTime() - startedAt)}"
|
|
1715
|
-
)
|
|
1716
|
-
return
|
|
1717
|
-
}
|
|
1718
|
-
|
|
1719
|
-
val requestedCursor = requestedCursorScalar(
|
|
1720
|
-
scalarStart,
|
|
1721
|
-
scalarEnd,
|
|
1722
|
-
authorizedText,
|
|
1723
|
-
text,
|
|
1724
|
-
newCursorPosition
|
|
1725
|
-
)
|
|
1726
|
-
recordImeTraceForTesting(
|
|
1727
|
-
"handleCompositionCommit",
|
|
1728
|
-
"textLength=${text.length} cursor=$newCursorPosition utf16Range=$startUtf16..$endUtf16 scalarRange=$scalarStart..$scalarEnd requestedCursor=$requestedCursor"
|
|
1729
|
-
)
|
|
1730
|
-
insertPlainTextRangeInRust(
|
|
1731
|
-
scalarStart,
|
|
1732
|
-
scalarEnd,
|
|
1733
|
-
text,
|
|
1734
|
-
requestedCursorScalar = requestedCursor
|
|
1735
|
-
)
|
|
1736
|
-
recordImeTraceForTesting(
|
|
1737
|
-
"handleCompositionCommitDone",
|
|
1738
|
-
"textLength=${text.length} totalUs=${nanosToMicros(System.nanoTime() - startedAt)}"
|
|
1739
|
-
)
|
|
1740
|
-
}
|
|
1741
|
-
|
|
1742
|
-
fun handleCorrectionCommit(
|
|
1743
|
-
offsetUtf16: Int,
|
|
1744
|
-
oldText: String,
|
|
1745
|
-
newText: String
|
|
1746
|
-
): Boolean {
|
|
1747
|
-
if (!isEditable) return true
|
|
1748
|
-
if (isApplyingRustState) return true
|
|
1749
|
-
if (!hasLiveEditor()) return false
|
|
1750
|
-
|
|
1751
|
-
val authorizedText = lastAuthorizedText
|
|
1752
|
-
if (offsetUtf16 < 0) {
|
|
1753
|
-
recordImeTraceForTesting(
|
|
1754
|
-
"correctionExplicitNoop",
|
|
1755
|
-
"reason=invalidOffset offset=$offsetUtf16 oldLength=${oldText.length} newLength=${newText.length}"
|
|
1756
|
-
)
|
|
1757
|
-
return false
|
|
1758
|
-
}
|
|
1759
|
-
val endUtf16 = offsetUtf16 + oldText.length
|
|
1760
|
-
if (endUtf16 < offsetUtf16 || endUtf16 > authorizedText.length) {
|
|
1761
|
-
recordImeTraceForTesting(
|
|
1762
|
-
"correctionExplicitNoop",
|
|
1763
|
-
"reason=outOfBounds offset=$offsetUtf16 oldLength=${oldText.length} authorizedLength=${authorizedText.length}"
|
|
1764
|
-
)
|
|
1765
|
-
return false
|
|
1766
|
-
}
|
|
1767
|
-
if (authorizedText.substring(offsetUtf16, endUtf16) != oldText) {
|
|
1768
|
-
recordImeTraceForTesting(
|
|
1769
|
-
"correctionExplicitNoop",
|
|
1770
|
-
"reason=staleText offset=$offsetUtf16 oldLength=${oldText.length} newLength=${newText.length}"
|
|
1771
|
-
)
|
|
1772
|
-
return false
|
|
1773
|
-
}
|
|
1774
|
-
|
|
1775
|
-
val (startUtf16, snappedEndUtf16) = PositionBridge.snapRangeToScalarBoundaries(
|
|
1776
|
-
offsetUtf16,
|
|
1777
|
-
endUtf16,
|
|
1778
|
-
authorizedText
|
|
1779
|
-
)
|
|
1780
|
-
if (
|
|
1781
|
-
startUtf16 != offsetUtf16 ||
|
|
1782
|
-
snappedEndUtf16 != endUtf16 ||
|
|
1783
|
-
startUtf16 > snappedEndUtf16
|
|
1784
|
-
) {
|
|
1785
|
-
recordImeTraceForTesting(
|
|
1786
|
-
"correctionExplicitNoop",
|
|
1787
|
-
"reason=unsnappedScalarBoundary range=$offsetUtf16..$endUtf16 snapped=$startUtf16..$snappedEndUtf16"
|
|
1788
|
-
)
|
|
1789
|
-
return false
|
|
1790
|
-
}
|
|
1791
|
-
|
|
1792
|
-
val scalarStart = PositionBridge.utf16ToScalar(startUtf16, authorizedText)
|
|
1793
|
-
val scalarEnd = PositionBridge.utf16ToScalar(snappedEndUtf16, authorizedText)
|
|
1794
|
-
recordImeTraceForTesting(
|
|
1795
|
-
"correctionExplicitApply",
|
|
1796
|
-
"range=$scalarStart..$scalarEnd newLength=${newText.length}"
|
|
1797
|
-
)
|
|
1798
|
-
insertPlainTextRangeInRust(scalarStart, scalarEnd, newText)
|
|
1799
|
-
return true
|
|
1800
|
-
}
|
|
1801
|
-
|
|
1802
|
-
fun handleMissingOldTextCorrectionCommit(
|
|
1803
|
-
offsetUtf16: Int,
|
|
1804
|
-
newText: String
|
|
1805
|
-
): Boolean {
|
|
1806
|
-
if (!isEditable) return true
|
|
1807
|
-
if (isApplyingRustState) return true
|
|
1808
|
-
if (!hasLiveEditor()) return false
|
|
1809
|
-
|
|
1810
|
-
val authorizedText = lastAuthorizedText
|
|
1811
|
-
val tokenRange = missingOldTextCorrectionTokenRange(authorizedText, offsetUtf16)
|
|
1812
|
-
?: run {
|
|
1813
|
-
recordImeTraceForTesting(
|
|
1814
|
-
"correctionInferredNoop",
|
|
1815
|
-
"reason=noToken offset=$offsetUtf16 newLength=${newText.length}"
|
|
1816
|
-
)
|
|
1817
|
-
return false
|
|
1818
|
-
}
|
|
1819
|
-
val (startUtf16, endUtf16) = tokenRange
|
|
1820
|
-
|
|
1821
|
-
val (snappedStartUtf16, snappedEndUtf16) = PositionBridge.snapRangeToScalarBoundaries(
|
|
1822
|
-
startUtf16,
|
|
1823
|
-
endUtf16,
|
|
1824
|
-
authorizedText
|
|
1825
|
-
)
|
|
1826
|
-
if (snappedStartUtf16 >= snappedEndUtf16) {
|
|
1827
|
-
recordImeTraceForTesting(
|
|
1828
|
-
"correctionInferredNoop",
|
|
1829
|
-
"reason=emptySnappedRange token=$startUtf16..$endUtf16 snapped=$snappedStartUtf16..$snappedEndUtf16"
|
|
1830
|
-
)
|
|
1831
|
-
return false
|
|
1832
|
-
}
|
|
1833
|
-
|
|
1834
|
-
val scalarStart = PositionBridge.utf16ToScalar(snappedStartUtf16, authorizedText)
|
|
1835
|
-
val scalarEnd = PositionBridge.utf16ToScalar(snappedEndUtf16, authorizedText)
|
|
1836
|
-
recordImeTraceForTesting(
|
|
1837
|
-
"correctionInferredApply",
|
|
1838
|
-
"range=$scalarStart..$scalarEnd utf16=$snappedStartUtf16..$snappedEndUtf16 newLength=${newText.length}"
|
|
1839
|
-
)
|
|
1840
|
-
insertPlainTextRangeInRust(scalarStart, scalarEnd, newText)
|
|
1841
|
-
return true
|
|
1842
|
-
}
|
|
1843
|
-
|
|
1844
|
-
private fun missingOldTextCorrectionTokenRange(
|
|
1845
|
-
text: String,
|
|
1846
|
-
offsetUtf16: Int
|
|
1847
|
-
): Pair<Int, Int>? {
|
|
1848
|
-
if (offsetUtf16 < 0 || offsetUtf16 >= text.length) return null
|
|
1849
|
-
|
|
1850
|
-
val tokenOffset = PositionBridge.snapToScalarBoundary(
|
|
1851
|
-
offsetUtf16,
|
|
1852
|
-
text,
|
|
1853
|
-
biasForward = false
|
|
1854
|
-
)
|
|
1855
|
-
if (tokenOffset < 0 || tokenOffset >= text.length) return null
|
|
1856
|
-
if (!isMissingOldTextCorrectionTokenCodePointAt(text, tokenOffset)) return null
|
|
1857
|
-
|
|
1858
|
-
var startUtf16 = tokenOffset
|
|
1859
|
-
while (startUtf16 > 0) {
|
|
1860
|
-
val previousUtf16 = Character.offsetByCodePoints(text, startUtf16, -1)
|
|
1861
|
-
if (!isMissingOldTextCorrectionTokenCodePointAt(text, previousUtf16)) break
|
|
1862
|
-
startUtf16 = previousUtf16
|
|
1863
|
-
}
|
|
1864
|
-
|
|
1865
|
-
var endUtf16 = tokenOffset + Character.charCount(Character.codePointAt(text, tokenOffset))
|
|
1866
|
-
while (endUtf16 < text.length) {
|
|
1867
|
-
if (!isMissingOldTextCorrectionTokenCodePointAt(text, endUtf16)) break
|
|
1868
|
-
endUtf16 += Character.charCount(Character.codePointAt(text, endUtf16))
|
|
1869
|
-
}
|
|
1870
|
-
|
|
1871
|
-
return if (startUtf16 < endUtf16) startUtf16 to endUtf16 else null
|
|
1872
|
-
}
|
|
1873
|
-
|
|
1874
|
-
private fun isMissingOldTextCorrectionTokenCodePointAt(text: String, utf16Offset: Int): Boolean {
|
|
1875
|
-
if (utf16Offset < 0 || utf16Offset >= text.length) return false
|
|
1876
|
-
val codePoint = Character.codePointAt(text, utf16Offset)
|
|
1877
|
-
if (isMissingOldTextCorrectionCoreTokenCodePoint(codePoint)) return true
|
|
1878
|
-
if (!isMissingOldTextCorrectionJoinerCodePoint(codePoint)) return false
|
|
1879
|
-
|
|
1880
|
-
val previousCodePoint = previousCodePointBefore(text, utf16Offset) ?: return false
|
|
1881
|
-
val nextUtf16Offset = utf16Offset + Character.charCount(codePoint)
|
|
1882
|
-
val nextCodePoint = nextCodePointAt(text, nextUtf16Offset) ?: return false
|
|
1883
|
-
return isMissingOldTextCorrectionCoreTokenCodePoint(previousCodePoint) &&
|
|
1884
|
-
isMissingOldTextCorrectionCoreTokenCodePoint(nextCodePoint)
|
|
1885
|
-
}
|
|
1886
|
-
|
|
1887
|
-
private fun isMissingOldTextCorrectionCoreTokenCodePoint(codePoint: Int): Boolean {
|
|
1888
|
-
if (Character.isLetterOrDigit(codePoint)) return true
|
|
1889
|
-
return when (Character.getType(codePoint)) {
|
|
1890
|
-
Character.NON_SPACING_MARK.toInt(),
|
|
1891
|
-
Character.COMBINING_SPACING_MARK.toInt(),
|
|
1892
|
-
Character.ENCLOSING_MARK.toInt(),
|
|
1893
|
-
Character.CONNECTOR_PUNCTUATION.toInt(),
|
|
1894
|
-
Character.MATH_SYMBOL.toInt(),
|
|
1895
|
-
Character.CURRENCY_SYMBOL.toInt(),
|
|
1896
|
-
Character.MODIFIER_SYMBOL.toInt(),
|
|
1897
|
-
Character.OTHER_SYMBOL.toInt(),
|
|
1898
|
-
Character.SURROGATE.toInt() -> true
|
|
1899
|
-
else -> false
|
|
1900
|
-
}
|
|
1901
|
-
}
|
|
1902
|
-
|
|
1903
|
-
private fun isMissingOldTextCorrectionJoinerCodePoint(codePoint: Int): Boolean =
|
|
1904
|
-
codePoint == '\''.code ||
|
|
1905
|
-
codePoint == 0x2018 ||
|
|
1906
|
-
codePoint == 0x2019 ||
|
|
1907
|
-
codePoint == 0x201B ||
|
|
1908
|
-
codePoint == 0xFF07 ||
|
|
1909
|
-
codePoint == '-'.code ||
|
|
1910
|
-
codePoint == 0x2010 ||
|
|
1911
|
-
codePoint == 0x2011 ||
|
|
1912
|
-
codePoint == 0x2012 ||
|
|
1913
|
-
codePoint == 0x2013 ||
|
|
1914
|
-
codePoint == 0x2014 ||
|
|
1915
|
-
codePoint == 0x2212 ||
|
|
1916
|
-
codePoint == 0x200D
|
|
1917
|
-
|
|
1918
|
-
private fun previousCodePointBefore(text: String, utf16Offset: Int): Int? {
|
|
1919
|
-
if (utf16Offset <= 0 || utf16Offset > text.length) return null
|
|
1920
|
-
val previousUtf16 = Character.offsetByCodePoints(text, utf16Offset, -1)
|
|
1921
|
-
return Character.codePointAt(text, previousUtf16)
|
|
1922
|
-
}
|
|
1923
|
-
|
|
1924
|
-
private fun nextCodePointAt(text: String, utf16Offset: Int): Int? {
|
|
1925
|
-
if (utf16Offset < 0 || utf16Offset >= text.length) return null
|
|
1926
|
-
return Character.codePointAt(text, utf16Offset)
|
|
1927
|
-
}
|
|
1928
|
-
|
|
1929
|
-
// ── Input Handling: Deletion ────────────────────────────────────────
|
|
1930
|
-
|
|
1931
|
-
/**
|
|
1932
|
-
* Handle surrounding text deletion from the IME.
|
|
1933
|
-
*
|
|
1934
|
-
* Called by [EditorInputConnection.deleteSurroundingText].
|
|
1935
|
-
*
|
|
1936
|
-
* @param beforeLength Number of UTF-16 code units to delete before the cursor.
|
|
1937
|
-
* @param afterLength Number of UTF-16 code units to delete after the cursor.
|
|
1938
|
-
*/
|
|
1939
|
-
fun handleDelete(beforeLength: Int, afterLength: Int) {
|
|
1940
|
-
if (!isEditable) return
|
|
1941
|
-
if (isApplyingRustState) return
|
|
1942
|
-
val selectionRange = normalizedUtf16SelectionRange()
|
|
1943
|
-
if (editorId == 0L) {
|
|
1944
|
-
// Dev mode: direct editing.
|
|
1945
|
-
val editable = this.text ?: return
|
|
1946
|
-
val (selectionStart, selectionEnd) = selectionRange ?: return
|
|
1947
|
-
val delStart: Int
|
|
1948
|
-
val delEnd: Int
|
|
1949
|
-
if (selectionStart != selectionEnd) {
|
|
1950
|
-
delStart = selectionStart
|
|
1951
|
-
delEnd = selectionEnd
|
|
1952
|
-
} else {
|
|
1953
|
-
delStart = maxOf(0, selectionStart - beforeLength.coerceAtLeast(0))
|
|
1954
|
-
delEnd = minOf(editable.length, selectionStart + afterLength.coerceAtLeast(0))
|
|
1955
|
-
}
|
|
1956
|
-
editable.delete(delStart, delEnd)
|
|
1957
|
-
return
|
|
1958
|
-
}
|
|
1959
|
-
if (discardTransientInputForDestroyedEditorIfNeeded()) return
|
|
1960
|
-
|
|
1961
|
-
val currentText = text?.toString() ?: ""
|
|
1962
|
-
val (selectionStart, selectionEnd) = selectionRange ?: return
|
|
1963
|
-
if (selectionStart != selectionEnd) {
|
|
1964
|
-
val (scalarStart, scalarEnd) = normalizedScalarSelectionRange(currentText) ?: return
|
|
1965
|
-
deleteRangeInRust(scalarStart, scalarEnd)
|
|
1966
|
-
return
|
|
1967
|
-
}
|
|
1968
|
-
val cursor = selectionStart
|
|
1969
|
-
if (beforeLength > 0 &&
|
|
1970
|
-
afterLength == 0 &&
|
|
1971
|
-
cursor > 0 &&
|
|
1972
|
-
currentText.getOrNull(cursor - 1) == EMPTY_BLOCK_PLACEHOLDER
|
|
1973
|
-
) {
|
|
1974
|
-
val scalarCursor = PositionBridge.utf16ToScalar(cursor, currentText)
|
|
1975
|
-
deleteBackwardAtSelectionScalarInRust(scalarCursor, scalarCursor)
|
|
1976
|
-
return
|
|
1977
|
-
}
|
|
1978
|
-
val rawDelStart = maxOf(0, cursor - beforeLength.coerceAtLeast(0))
|
|
1979
|
-
val rawDelEnd = minOf(currentText.length, cursor + afterLength.coerceAtLeast(0))
|
|
1980
|
-
val (delStart, delEnd) = PositionBridge.snapRangeToScalarBoundaries(
|
|
1981
|
-
rawDelStart,
|
|
1982
|
-
rawDelEnd,
|
|
1983
|
-
currentText
|
|
1984
|
-
)
|
|
1985
|
-
|
|
1986
|
-
val scalarStart = PositionBridge.utf16ToScalar(delStart, currentText)
|
|
1987
|
-
val scalarEnd = PositionBridge.utf16ToScalar(delEnd, currentText)
|
|
1988
|
-
|
|
1989
|
-
if (scalarStart < scalarEnd) {
|
|
1990
|
-
deleteRangeInRust(scalarStart, scalarEnd)
|
|
1991
|
-
} else if (beforeLength > 0 && afterLength == 0) {
|
|
1992
|
-
deleteBackwardAtSelectionScalarInRust(scalarEnd, scalarEnd)
|
|
1993
|
-
}
|
|
1994
|
-
}
|
|
1995
|
-
|
|
1996
|
-
/**
|
|
1997
|
-
* Handle backspace key press (hardware keyboard or key event).
|
|
1998
|
-
*
|
|
1999
|
-
* If there's a range selection, deletes the range. Otherwise deletes
|
|
2000
|
-
* the grapheme cluster before the cursor.
|
|
2001
|
-
*/
|
|
2002
|
-
fun handleBackspace() {
|
|
2003
|
-
if (!isEditable) return
|
|
2004
|
-
if (isApplyingRustState) return
|
|
2005
|
-
val selectionRange = normalizedUtf16SelectionRange() ?: return
|
|
2006
|
-
if (editorId == 0L) {
|
|
2007
|
-
// Dev mode: direct editing.
|
|
2008
|
-
val editable = this.text ?: return
|
|
2009
|
-
val (start, end) = selectionRange
|
|
2010
|
-
if (start != end) {
|
|
2011
|
-
editable.delete(start, end)
|
|
2012
|
-
} else if (start > 0) {
|
|
2013
|
-
// Delete one grapheme cluster backward.
|
|
2014
|
-
val prevBoundary = PositionBridge.snapToGraphemeBoundary(start - 1, text?.toString() ?: "")
|
|
2015
|
-
val adjustedPrev = if (prevBoundary >= start) maxOf(0, start - 1) else prevBoundary
|
|
2016
|
-
editable.delete(adjustedPrev, start)
|
|
2017
|
-
}
|
|
2018
|
-
return
|
|
2019
|
-
}
|
|
2020
|
-
if (discardTransientInputForDestroyedEditorIfNeeded()) return
|
|
2021
|
-
|
|
2022
|
-
val currentText = text?.toString() ?: ""
|
|
2023
|
-
val (start, end) = selectionRange
|
|
2024
|
-
|
|
2025
|
-
if (start != end) {
|
|
2026
|
-
// Range selection: delete the range.
|
|
2027
|
-
val (scalarStart, scalarEnd) = normalizedScalarSelectionRange(currentText) ?: return
|
|
2028
|
-
deleteRangeInRust(scalarStart, scalarEnd)
|
|
2029
|
-
} else if (start > 0) {
|
|
2030
|
-
if (currentText.getOrNull(start - 1) == EMPTY_BLOCK_PLACEHOLDER) {
|
|
2031
|
-
val scalarCursor = PositionBridge.utf16ToScalar(start, currentText)
|
|
2032
|
-
deleteBackwardAtSelectionScalarInRust(scalarCursor, scalarCursor)
|
|
2033
|
-
return
|
|
2034
|
-
}
|
|
2035
|
-
// Cursor: delete one grapheme cluster backward.
|
|
2036
|
-
// Find the previous grapheme boundary by snapping (start - 1).
|
|
2037
|
-
val breakIter = java.text.BreakIterator.getCharacterInstance()
|
|
2038
|
-
breakIter.setText(currentText)
|
|
2039
|
-
val prevBoundary = breakIter.preceding(start)
|
|
2040
|
-
val prevUtf16 = if (prevBoundary == java.text.BreakIterator.DONE) 0 else prevBoundary
|
|
2041
|
-
|
|
2042
|
-
val scalarStart = PositionBridge.utf16ToScalar(prevUtf16, currentText)
|
|
2043
|
-
val scalarEnd = PositionBridge.utf16ToScalar(start, currentText)
|
|
2044
|
-
if (scalarStart < scalarEnd) {
|
|
2045
|
-
deleteRangeInRust(scalarStart, scalarEnd)
|
|
2046
|
-
} else {
|
|
2047
|
-
deleteBackwardAtSelectionScalarInRust(scalarEnd, scalarEnd)
|
|
2048
|
-
}
|
|
2049
|
-
} else {
|
|
2050
|
-
deleteBackwardAtSelectionScalarInRust(0, 0)
|
|
2051
|
-
}
|
|
2052
|
-
}
|
|
2053
|
-
|
|
2054
|
-
fun handleForwardDelete() {
|
|
2055
|
-
if (!isEditable) return
|
|
2056
|
-
if (isApplyingRustState) return
|
|
2057
|
-
val selectionRange = normalizedUtf16SelectionRange() ?: return
|
|
2058
|
-
if (editorId == 0L) {
|
|
2059
|
-
val editable = this.text ?: return
|
|
2060
|
-
val (start, end) = selectionRange
|
|
2061
|
-
if (start != end) {
|
|
2062
|
-
editable.delete(start, end)
|
|
2063
|
-
} else if (start < editable.length) {
|
|
2064
|
-
val breakIter = java.text.BreakIterator.getCharacterInstance()
|
|
2065
|
-
breakIter.setText(editable.toString())
|
|
2066
|
-
val nextBoundary = breakIter.following(start)
|
|
2067
|
-
val nextUtf16 = if (nextBoundary == java.text.BreakIterator.DONE) {
|
|
2068
|
-
editable.length
|
|
2069
|
-
} else {
|
|
2070
|
-
nextBoundary
|
|
2071
|
-
}
|
|
2072
|
-
editable.delete(start, nextUtf16.coerceIn(start, editable.length))
|
|
2073
|
-
}
|
|
2074
|
-
return
|
|
2075
|
-
}
|
|
2076
|
-
if (discardTransientInputForDestroyedEditorIfNeeded()) return
|
|
2077
|
-
|
|
2078
|
-
val currentText = text?.toString() ?: ""
|
|
2079
|
-
val (start, end) = selectionRange
|
|
2080
|
-
if (start != end) {
|
|
2081
|
-
val (scalarStart, scalarEnd) = normalizedScalarSelectionRange(currentText) ?: return
|
|
2082
|
-
deleteRangeInRust(scalarStart, scalarEnd)
|
|
2083
|
-
} else if (start < currentText.length) {
|
|
2084
|
-
val breakIter = java.text.BreakIterator.getCharacterInstance()
|
|
2085
|
-
breakIter.setText(currentText)
|
|
2086
|
-
val nextBoundary = breakIter.following(start)
|
|
2087
|
-
val nextUtf16 = if (nextBoundary == java.text.BreakIterator.DONE) {
|
|
2088
|
-
currentText.length
|
|
2089
|
-
} else {
|
|
2090
|
-
nextBoundary
|
|
2091
|
-
}
|
|
2092
|
-
val (utf16Start, utf16End) = PositionBridge.snapRangeToScalarBoundaries(
|
|
2093
|
-
start,
|
|
2094
|
-
nextUtf16.coerceIn(start, currentText.length),
|
|
2095
|
-
currentText
|
|
2096
|
-
)
|
|
2097
|
-
val scalarStart = PositionBridge.utf16ToScalar(utf16Start, currentText)
|
|
2098
|
-
val scalarEnd = PositionBridge.utf16ToScalar(utf16End, currentText)
|
|
2099
|
-
if (scalarStart < scalarEnd) {
|
|
2100
|
-
deleteRangeInRust(scalarStart, scalarEnd)
|
|
2101
|
-
}
|
|
2102
|
-
}
|
|
2103
|
-
}
|
|
2104
|
-
|
|
2105
|
-
// ── Input Handling: Return Key ──────────────────────────────────────
|
|
2106
|
-
|
|
2107
|
-
/**
|
|
2108
|
-
* Handle return/enter key as a block split operation.
|
|
2109
|
-
*/
|
|
2110
|
-
fun handleReturnKey() {
|
|
2111
|
-
if (!isEditable) return
|
|
2112
|
-
if (isApplyingRustState) return
|
|
2113
|
-
|
|
2114
|
-
val currentText = text?.toString() ?: ""
|
|
2115
|
-
val (start, end) = normalizedUtf16SelectionRange() ?: return
|
|
2116
|
-
|
|
2117
|
-
if (editorId == 0L) {
|
|
2118
|
-
// Dev mode: insert newline directly.
|
|
2119
|
-
val editable = this.text ?: return
|
|
2120
|
-
editable.replace(start, end, "\n")
|
|
2121
|
-
return
|
|
2122
|
-
}
|
|
2123
|
-
if (discardTransientInputForDestroyedEditorIfNeeded()) return
|
|
2124
|
-
|
|
2125
|
-
if (start != end) {
|
|
2126
|
-
// Range selection: atomic delete-and-split via Rust.
|
|
2127
|
-
val (scalarStart, scalarEnd) = normalizedScalarSelectionRange(currentText) ?: return
|
|
2128
|
-
deleteAndSplitInRust(scalarStart, scalarEnd)
|
|
2129
|
-
} else {
|
|
2130
|
-
val scalarPos = PositionBridge.utf16ToScalar(start, currentText)
|
|
2131
|
-
splitBlockInRust(scalarPos)
|
|
2132
|
-
}
|
|
2133
|
-
}
|
|
2134
|
-
|
|
2135
|
-
/**
|
|
2136
|
-
* Handle Shift+Enter as an inline hard break insertion.
|
|
2137
|
-
*/
|
|
2138
|
-
fun handleHardBreak() {
|
|
2139
|
-
if (!isEditable) return
|
|
2140
|
-
if (isApplyingRustState) return
|
|
2141
|
-
|
|
2142
|
-
if (editorId == 0L) {
|
|
2143
|
-
val editable = this.text ?: return
|
|
2144
|
-
val start = selectionStart
|
|
2145
|
-
val end = selectionEnd
|
|
2146
|
-
editable.replace(start, end, "\n")
|
|
2147
|
-
return
|
|
2148
|
-
}
|
|
2149
|
-
if (discardTransientInputForDestroyedEditorIfNeeded()) return
|
|
2150
|
-
|
|
2151
|
-
val selection = currentScalarSelection() ?: return
|
|
2152
|
-
val updateJSON = editorInsertNodeAtSelectionScalar(
|
|
2153
|
-
editorId.toULong(),
|
|
2154
|
-
selection.first.toUInt(),
|
|
2155
|
-
selection.second.toUInt(),
|
|
2156
|
-
"hardBreak"
|
|
2157
|
-
)
|
|
2158
|
-
applyUpdateJSON(updateJSON)
|
|
2159
|
-
}
|
|
2160
|
-
|
|
2161
|
-
/**
|
|
2162
|
-
* Handle hardware Tab / Shift+Tab as list indent / outdent when the caret is in a list.
|
|
2163
|
-
*/
|
|
2164
|
-
fun handleTab(shiftPressed: Boolean): Boolean {
|
|
2165
|
-
if (!isEditable) return false
|
|
2166
|
-
if (isApplyingRustState) return false
|
|
2167
|
-
if (!hasLiveEditor()) return false
|
|
2168
|
-
if (!isSelectionInsideList()) return false
|
|
2169
|
-
val selection = currentScalarSelection() ?: return false
|
|
2170
|
-
|
|
2171
|
-
val updateJSON = if (shiftPressed) {
|
|
2172
|
-
editorOutdentListItemAtSelectionScalar(
|
|
2173
|
-
editorId.toULong(),
|
|
2174
|
-
selection.first.toUInt(),
|
|
2175
|
-
selection.second.toUInt()
|
|
2176
|
-
)
|
|
2177
|
-
} else {
|
|
2178
|
-
editorIndentListItemAtSelectionScalar(
|
|
2179
|
-
editorId.toULong(),
|
|
2180
|
-
selection.first.toUInt(),
|
|
2181
|
-
selection.second.toUInt()
|
|
2182
|
-
)
|
|
2183
|
-
}
|
|
2184
|
-
applyUpdateJSON(updateJSON)
|
|
2185
|
-
return true
|
|
2186
|
-
}
|
|
2187
|
-
|
|
2188
|
-
fun handleHardwareKeyDown(keyCode: Int, shiftPressed: Boolean): Boolean {
|
|
2189
|
-
if (!isEditable || isApplyingRustState) return false
|
|
2190
|
-
return when (keyCode) {
|
|
2191
|
-
KeyEvent.KEYCODE_DEL -> {
|
|
2192
|
-
handleBackspace()
|
|
2193
|
-
true
|
|
2194
|
-
}
|
|
2195
|
-
KeyEvent.KEYCODE_FORWARD_DEL -> {
|
|
2196
|
-
handleForwardDelete()
|
|
2197
|
-
true
|
|
2198
|
-
}
|
|
2199
|
-
KeyEvent.KEYCODE_ENTER, KeyEvent.KEYCODE_NUMPAD_ENTER -> {
|
|
2200
|
-
if (shiftPressed) {
|
|
2201
|
-
handleHardBreak()
|
|
2202
|
-
} else {
|
|
2203
|
-
handleReturnKey()
|
|
2204
|
-
}
|
|
2205
|
-
true
|
|
2206
|
-
}
|
|
2207
|
-
KeyEvent.KEYCODE_TAB -> handleTab(shiftPressed)
|
|
2208
|
-
else -> false
|
|
2209
|
-
}
|
|
2210
|
-
}
|
|
2211
|
-
|
|
2212
|
-
private fun isSupportedHardwareMutationKey(keyCode: Int): Boolean =
|
|
2213
|
-
when (keyCode) {
|
|
2214
|
-
KeyEvent.KEYCODE_DEL,
|
|
2215
|
-
KeyEvent.KEYCODE_FORWARD_DEL,
|
|
2216
|
-
KeyEvent.KEYCODE_ENTER,
|
|
2217
|
-
KeyEvent.KEYCODE_NUMPAD_ENTER,
|
|
2218
|
-
KeyEvent.KEYCODE_TAB -> true
|
|
2219
|
-
else -> false
|
|
2220
|
-
}
|
|
2221
|
-
|
|
2222
|
-
internal fun isReadOnlyTextMutationKeyEvent(event: KeyEvent): Boolean {
|
|
2223
|
-
if (isSupportedHardwareMutationKey(event.keyCode) ||
|
|
2224
|
-
event.keyCode == KeyEvent.KEYCODE_FORWARD_DEL
|
|
2225
|
-
) {
|
|
2226
|
-
return true
|
|
2227
|
-
}
|
|
2228
|
-
if (event.keyCode == KeyEvent.KEYCODE_INSERT && event.isShiftPressed) {
|
|
2229
|
-
return true
|
|
2230
|
-
}
|
|
2231
|
-
if (event.isCtrlPressed || event.isMetaPressed) {
|
|
2232
|
-
return when (event.keyCode) {
|
|
2233
|
-
KeyEvent.KEYCODE_V,
|
|
2234
|
-
KeyEvent.KEYCODE_X,
|
|
2235
|
-
KeyEvent.KEYCODE_Z,
|
|
2236
|
-
KeyEvent.KEYCODE_Y -> true
|
|
2237
|
-
else -> false
|
|
2238
|
-
}
|
|
2239
|
-
}
|
|
2240
|
-
if (!keyEventCharacters(event).isNullOrEmpty()) return true
|
|
2241
|
-
return event.unicodeChar != 0
|
|
2242
|
-
}
|
|
2243
|
-
|
|
2244
|
-
fun handleHardwareKeyEvent(event: KeyEvent?): Boolean {
|
|
2245
|
-
if (event == null || !isEditable || isApplyingRustState) return false
|
|
2246
|
-
|
|
2247
|
-
return when (event.action) {
|
|
2248
|
-
KeyEvent.ACTION_DOWN -> {
|
|
2249
|
-
if (!isSupportedHardwareMutationKey(event.keyCode)) return false
|
|
2250
|
-
|
|
2251
|
-
val signature = hardwareKeyEventSignature(event)
|
|
2252
|
-
if (
|
|
2253
|
-
lastHandledHardwareKeySignature == signature ||
|
|
2254
|
-
didRecentlyHandleHardwareKeyDown(signature)
|
|
2255
|
-
) {
|
|
2256
|
-
return true
|
|
2257
|
-
}
|
|
2258
|
-
|
|
2259
|
-
if (handleHardwareKeyDown(event.keyCode, event.isShiftPressed)) {
|
|
2260
|
-
markHandledHardwareKeyDown(signature)
|
|
2261
|
-
true
|
|
2262
|
-
} else {
|
|
2263
|
-
false
|
|
2264
|
-
}
|
|
2265
|
-
}
|
|
2266
|
-
|
|
2267
|
-
KeyEvent.ACTION_UP -> {
|
|
2268
|
-
if (lastHandledHardwareKeySignature?.let {
|
|
2269
|
-
it.keyCode == event.keyCode && it.downTime == event.downTime
|
|
2270
|
-
} == true) {
|
|
2271
|
-
lastHandledHardwareKeySignature = null
|
|
2272
|
-
true
|
|
2273
|
-
} else {
|
|
2274
|
-
false
|
|
2275
|
-
}
|
|
2276
|
-
}
|
|
2277
|
-
|
|
2278
|
-
else -> false
|
|
2279
|
-
}
|
|
2280
|
-
}
|
|
2281
|
-
|
|
2282
|
-
internal fun handlePrintableHardwareKeyEvent(
|
|
2283
|
-
event: KeyEvent,
|
|
2284
|
-
applyBaseEvent: () -> Boolean
|
|
2285
|
-
): Boolean {
|
|
2286
|
-
if (!isEditable || isApplyingRustState || !isPrintableHardwareMutationKey(event)) {
|
|
2287
|
-
return false
|
|
2288
|
-
}
|
|
2289
|
-
val signature = hardwareKeyEventSignature(event)
|
|
2290
|
-
return when (event.action) {
|
|
2291
|
-
KeyEvent.ACTION_DOWN -> {
|
|
2292
|
-
if (
|
|
2293
|
-
lastHandledHardwareKeySignature == signature ||
|
|
2294
|
-
didRecentlyHandleHardwareKeyDown(signature)
|
|
2295
|
-
) {
|
|
2296
|
-
true
|
|
2297
|
-
} else {
|
|
2298
|
-
val inputConnection = activeInputConnection?.takeIf {
|
|
2299
|
-
it.hasPendingComposition()
|
|
2300
|
-
}
|
|
2301
|
-
if (inputConnection != null) {
|
|
2302
|
-
var didMutate = false
|
|
2303
|
-
runWithTransientInputMutationGuard {
|
|
2304
|
-
didMutate = insertTransientHardwareText(keyEventText(event))
|
|
2305
|
-
didMutate
|
|
2306
|
-
}
|
|
2307
|
-
if (!didMutate) return false
|
|
2308
|
-
inputConnection.refreshComposingTextFromEditableForEditor()
|
|
2309
|
-
} else {
|
|
2310
|
-
applyBaseEvent()
|
|
2311
|
-
}
|
|
2312
|
-
markHandledHardwareKeyDown(signature)
|
|
2313
|
-
true
|
|
2314
|
-
}
|
|
2315
|
-
}
|
|
2316
|
-
KeyEvent.ACTION_UP -> {
|
|
2317
|
-
if (lastHandledHardwareKeySignature?.let {
|
|
2318
|
-
it.keyCode == event.keyCode && it.downTime == event.downTime
|
|
2319
|
-
} == true) {
|
|
2320
|
-
lastHandledHardwareKeySignature = null
|
|
2321
|
-
}
|
|
2322
|
-
false
|
|
2323
|
-
}
|
|
2324
|
-
else -> false
|
|
2325
|
-
}
|
|
2326
|
-
}
|
|
2327
|
-
|
|
2328
|
-
private fun isPrintableHardwareMutationKey(event: KeyEvent): Boolean {
|
|
2329
|
-
if (isSupportedHardwareMutationKey(event.keyCode)) return false
|
|
2330
|
-
if (event.isCtrlPressed || event.isMetaPressed) return false
|
|
2331
|
-
return !keyEventText(event).isNullOrEmpty()
|
|
2332
|
-
}
|
|
2333
|
-
|
|
2334
|
-
private fun hardwareKeyEventSignature(event: KeyEvent): HardwareKeyEventSignature =
|
|
2335
|
-
HardwareKeyEventSignature(
|
|
2336
|
-
keyCode = event.keyCode,
|
|
2337
|
-
downTime = event.downTime,
|
|
2338
|
-
repeatCount = event.repeatCount
|
|
2339
|
-
)
|
|
2340
|
-
|
|
2341
|
-
@Suppress("DEPRECATION")
|
|
2342
|
-
private fun keyEventCharacters(event: KeyEvent): String? = event.characters
|
|
2343
|
-
|
|
2344
|
-
private fun keyEventText(event: KeyEvent): String? {
|
|
2345
|
-
val characters = keyEventCharacters(event)
|
|
2346
|
-
if (!characters.isNullOrEmpty()) return characters
|
|
2347
|
-
val unicodeChar = event.unicodeChar
|
|
2348
|
-
if (unicodeChar == 0) return null
|
|
2349
|
-
return runCatching {
|
|
2350
|
-
String(Character.toChars(unicodeChar))
|
|
2351
|
-
}.getOrNull()
|
|
2352
|
-
}
|
|
2353
|
-
|
|
2354
|
-
private fun insertTransientHardwareText(insertedText: String?): Boolean {
|
|
2355
|
-
if (insertedText.isNullOrEmpty()) return false
|
|
2356
|
-
val editable = text ?: return false
|
|
2357
|
-
val currentText = editable.toString()
|
|
2358
|
-
val rawStart = selectionStart
|
|
2359
|
-
val rawEnd = selectionEnd
|
|
2360
|
-
if (rawStart < 0 || rawEnd < 0) return false
|
|
2361
|
-
val start = rawStart.coerceIn(0, editable.length)
|
|
2362
|
-
val end = rawEnd.coerceIn(0, editable.length)
|
|
2363
|
-
val normalizedStart = minOf(start, end)
|
|
2364
|
-
val normalizedEnd = maxOf(start, end)
|
|
2365
|
-
val (replaceStart, replaceEnd) = PositionBridge.snapRangeToScalarBoundaries(
|
|
2366
|
-
normalizedStart,
|
|
2367
|
-
normalizedEnd,
|
|
2368
|
-
currentText
|
|
2369
|
-
)
|
|
2370
|
-
editable.replace(replaceStart, replaceEnd, insertedText)
|
|
2371
|
-
val cursor = (replaceStart + insertedText.length).coerceIn(0, editable.length)
|
|
2372
|
-
setSelection(cursor)
|
|
2373
|
-
return true
|
|
2374
|
-
}
|
|
2375
|
-
|
|
2376
|
-
private fun markHandledHardwareKeyDown(signature: HardwareKeyEventSignature) {
|
|
2377
|
-
lastHandledHardwareKeySignature = signature
|
|
2378
|
-
recentHandledHardwareKeyDownSignature = signature
|
|
2379
|
-
recentHandledHardwareKeyDownUptimeMs = SystemClock.uptimeMillis()
|
|
2380
|
-
}
|
|
2381
|
-
|
|
2382
|
-
private fun didRecentlyHandleHardwareKeyDown(signature: HardwareKeyEventSignature): Boolean {
|
|
2383
|
-
val recentSignature = recentHandledHardwareKeyDownSignature ?: return false
|
|
2384
|
-
val elapsedMs = SystemClock.uptimeMillis() - recentHandledHardwareKeyDownUptimeMs
|
|
2385
|
-
if (elapsedMs > RECENT_HANDLED_HARDWARE_KEY_DOWN_WINDOW_MS) {
|
|
2386
|
-
recentHandledHardwareKeyDownSignature = null
|
|
2387
|
-
recentHandledHardwareKeyDownUptimeMs = 0L
|
|
2388
|
-
return false
|
|
2389
|
-
}
|
|
2390
|
-
return recentSignature == signature
|
|
2391
|
-
}
|
|
2392
|
-
|
|
2393
|
-
fun performToolbarToggleMark(markName: String) {
|
|
2394
|
-
if (!isEditable || isApplyingRustState || !hasLiveEditor()) return
|
|
2395
|
-
val selection = currentScalarSelection() ?: return
|
|
2396
|
-
val updateJSON = editorToggleMarkAtSelectionScalar(
|
|
2397
|
-
editorId.toULong(),
|
|
2398
|
-
selection.first.toUInt(),
|
|
2399
|
-
selection.second.toUInt(),
|
|
2400
|
-
markName
|
|
2401
|
-
)
|
|
2402
|
-
applyUpdateJSON(updateJSON)
|
|
2403
|
-
}
|
|
2404
|
-
|
|
2405
|
-
fun performToolbarToggleList(listType: String, isActive: Boolean) {
|
|
2406
|
-
if (!isEditable || isApplyingRustState || !hasLiveEditor()) return
|
|
2407
|
-
val selection = currentScalarSelection() ?: return
|
|
2408
|
-
val updateJSON = if (isActive) {
|
|
2409
|
-
editorUnwrapFromListAtSelectionScalar(
|
|
2410
|
-
editorId.toULong(),
|
|
2411
|
-
selection.first.toUInt(),
|
|
2412
|
-
selection.second.toUInt()
|
|
2413
|
-
)
|
|
2414
|
-
} else {
|
|
2415
|
-
editorWrapInListAtSelectionScalar(
|
|
2416
|
-
editorId.toULong(),
|
|
2417
|
-
selection.first.toUInt(),
|
|
2418
|
-
selection.second.toUInt(),
|
|
2419
|
-
listType
|
|
2420
|
-
)
|
|
2421
|
-
}
|
|
2422
|
-
applyUpdateJSON(updateJSON)
|
|
2423
|
-
}
|
|
2424
|
-
|
|
2425
|
-
fun performToolbarToggleBlockquote() {
|
|
2426
|
-
if (!isEditable || isApplyingRustState || !hasLiveEditor()) return
|
|
2427
|
-
val selection = currentScalarSelection() ?: return
|
|
2428
|
-
val updateJSON = editorToggleBlockquoteAtSelectionScalar(
|
|
2429
|
-
editorId.toULong(),
|
|
2430
|
-
selection.first.toUInt(),
|
|
2431
|
-
selection.second.toUInt()
|
|
2432
|
-
)
|
|
2433
|
-
applyUpdateJSON(updateJSON)
|
|
2434
|
-
}
|
|
2435
|
-
|
|
2436
|
-
fun performToolbarToggleHeading(level: Int) {
|
|
2437
|
-
if (!isEditable || isApplyingRustState || !hasLiveEditor()) return
|
|
2438
|
-
if (level !in 1..6) return
|
|
2439
|
-
val selection = currentScalarSelection() ?: return
|
|
2440
|
-
val updateJSON = editorToggleHeadingAtSelectionScalar(
|
|
2441
|
-
editorId.toULong(),
|
|
2442
|
-
selection.first.toUInt(),
|
|
2443
|
-
selection.second.toUInt(),
|
|
2444
|
-
level.toUByte()
|
|
2445
|
-
)
|
|
2446
|
-
applyUpdateJSON(updateJSON)
|
|
2447
|
-
}
|
|
2448
|
-
|
|
2449
|
-
fun performToolbarIndentListItem() {
|
|
2450
|
-
if (!isEditable || isApplyingRustState || !hasLiveEditor()) return
|
|
2451
|
-
val selection = currentScalarSelection() ?: return
|
|
2452
|
-
val updateJSON = editorIndentListItemAtSelectionScalar(
|
|
2453
|
-
editorId.toULong(),
|
|
2454
|
-
selection.first.toUInt(),
|
|
2455
|
-
selection.second.toUInt()
|
|
2456
|
-
)
|
|
2457
|
-
applyUpdateJSON(updateJSON)
|
|
2458
|
-
}
|
|
2459
|
-
|
|
2460
|
-
fun performToolbarOutdentListItem() {
|
|
2461
|
-
if (!isEditable || isApplyingRustState || !hasLiveEditor()) return
|
|
2462
|
-
val selection = currentScalarSelection() ?: return
|
|
2463
|
-
val updateJSON = editorOutdentListItemAtSelectionScalar(
|
|
2464
|
-
editorId.toULong(),
|
|
2465
|
-
selection.first.toUInt(),
|
|
2466
|
-
selection.second.toUInt()
|
|
2467
|
-
)
|
|
2468
|
-
applyUpdateJSON(updateJSON)
|
|
2469
|
-
}
|
|
2470
|
-
|
|
2471
|
-
fun performToolbarInsertNode(nodeType: String) {
|
|
2472
|
-
if (!isEditable || isApplyingRustState || !hasLiveEditor()) return
|
|
2473
|
-
val selection = currentScalarSelection() ?: return
|
|
2474
|
-
val updateJSON = editorInsertNodeAtSelectionScalar(
|
|
2475
|
-
editorId.toULong(),
|
|
2476
|
-
selection.first.toUInt(),
|
|
2477
|
-
selection.second.toUInt(),
|
|
2478
|
-
nodeType
|
|
2479
|
-
)
|
|
2480
|
-
applyUpdateJSON(updateJSON)
|
|
2481
|
-
}
|
|
2482
|
-
|
|
2483
|
-
fun performToolbarUndo() {
|
|
2484
|
-
if (!isEditable || isApplyingRustState || !hasLiveEditor()) return
|
|
2485
|
-
applyUpdateJSON(editorUndo(editorId.toULong()))
|
|
2486
|
-
}
|
|
2487
|
-
|
|
2488
|
-
fun performToolbarRedo() {
|
|
2489
|
-
if (!isEditable || isApplyingRustState || !hasLiveEditor()) return
|
|
2490
|
-
applyUpdateJSON(editorRedo(editorId.toULong()))
|
|
2491
|
-
}
|
|
2492
|
-
|
|
2493
|
-
// ── Input Handling: Paste ────────────────────────────────────────────
|
|
2494
|
-
|
|
2495
|
-
/**
|
|
2496
|
-
* Intercept paste operations to route content through Rust.
|
|
2497
|
-
*
|
|
2498
|
-
* Attempts to extract HTML from the clipboard first (for rich text paste),
|
|
2499
|
-
* falling back to plain text.
|
|
2500
|
-
*/
|
|
2501
|
-
override fun onTextContextMenuItem(id: Int): Boolean {
|
|
2502
|
-
if (!isEditable && isMutatingContextMenuItem(id)) return true
|
|
2503
|
-
if (id == android.R.id.cut) {
|
|
2504
|
-
handleCut()
|
|
2505
|
-
return true
|
|
2506
|
-
}
|
|
2507
|
-
if (id == android.R.id.paste || id == android.R.id.pasteAsPlainText) {
|
|
2508
|
-
handlePaste(plainTextOnly = id == android.R.id.pasteAsPlainText)
|
|
2509
|
-
return true
|
|
2510
|
-
}
|
|
2511
|
-
return super.onTextContextMenuItem(id)
|
|
2512
|
-
}
|
|
2513
|
-
|
|
2514
|
-
private fun isMutatingContextMenuItem(id: Int): Boolean =
|
|
2515
|
-
id == android.R.id.paste ||
|
|
2516
|
-
id == android.R.id.pasteAsPlainText ||
|
|
2517
|
-
id == android.R.id.cut
|
|
2518
|
-
|
|
2519
|
-
/**
|
|
2520
|
-
* Block accessibility-initiated text mutations (paste, cut, set text) when not editable.
|
|
2521
|
-
* Selection and copy actions remain available.
|
|
2522
|
-
*/
|
|
2523
|
-
override fun performAccessibilityAction(action: Int, arguments: android.os.Bundle?): Boolean {
|
|
2524
|
-
if (!isEditable && (
|
|
2525
|
-
action == android.view.accessibility.AccessibilityNodeInfo.ACTION_SET_TEXT ||
|
|
2526
|
-
action == android.view.accessibility.AccessibilityNodeInfo.ACTION_PASTE ||
|
|
2527
|
-
action == android.view.accessibility.AccessibilityNodeInfo.ACTION_CUT
|
|
2528
|
-
)
|
|
2529
|
-
) {
|
|
2530
|
-
return false
|
|
2531
|
-
}
|
|
2532
|
-
if (action == android.view.accessibility.AccessibilityNodeInfo.ACTION_SET_TEXT) {
|
|
2533
|
-
return handleAccessibilitySetText(arguments)
|
|
2534
|
-
}
|
|
2535
|
-
return super.performAccessibilityAction(action, arguments)
|
|
2536
|
-
}
|
|
2537
|
-
|
|
2538
|
-
private fun handlePaste(plainTextOnly: Boolean) {
|
|
2539
|
-
if (editorId == 0L) {
|
|
2540
|
-
// Dev mode: default paste behavior.
|
|
2541
|
-
super.onTextContextMenuItem(
|
|
2542
|
-
if (plainTextOnly) android.R.id.pasteAsPlainText else android.R.id.paste
|
|
2543
|
-
)
|
|
2544
|
-
return
|
|
2545
|
-
}
|
|
2546
|
-
if (discardTransientInputForDestroyedEditorIfNeeded()) return
|
|
2547
|
-
if (!prepareForExternalEditorUpdate()) return
|
|
2548
|
-
|
|
2549
|
-
val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as? ClipboardManager
|
|
2550
|
-
?: return
|
|
2551
|
-
val clip = clipboard.primaryClip ?: return
|
|
2552
|
-
if (clip.itemCount == 0) return
|
|
2553
|
-
|
|
2554
|
-
val item = clip.getItemAt(0)
|
|
2555
|
-
|
|
2556
|
-
// Try HTML first for rich paste.
|
|
2557
|
-
val htmlText = item.htmlText
|
|
2558
|
-
if (!plainTextOnly && htmlText != null) {
|
|
2559
|
-
pasteHTML(htmlText)
|
|
2560
|
-
return
|
|
2561
|
-
}
|
|
2562
|
-
|
|
2563
|
-
// Fallback to plain text.
|
|
2564
|
-
val plainText = item.text?.toString() ?: item.coerceToText(context)?.toString()
|
|
2565
|
-
if (plainText != null) {
|
|
2566
|
-
pastePlainText(plainText)
|
|
2567
|
-
}
|
|
2568
|
-
}
|
|
2569
|
-
|
|
2570
|
-
private fun handleCut() {
|
|
2571
|
-
if (editorId == 0L) {
|
|
2572
|
-
super.onTextContextMenuItem(android.R.id.cut)
|
|
2573
|
-
return
|
|
2574
|
-
}
|
|
2575
|
-
if (discardTransientInputForDestroyedEditorIfNeeded()) return
|
|
2576
|
-
if (!prepareForExternalEditorUpdate()) return
|
|
2577
|
-
|
|
2578
|
-
val currentText = text?.toString() ?: return
|
|
2579
|
-
val (selectionStart, selectionEnd) = normalizedUtf16SelectionRange(currentText) ?: return
|
|
2580
|
-
if (selectionStart == selectionEnd) return
|
|
2581
|
-
|
|
2582
|
-
val (utf16Start, utf16End) = PositionBridge.snapRangeToScalarBoundaries(
|
|
2583
|
-
selectionStart,
|
|
2584
|
-
selectionEnd,
|
|
2585
|
-
currentText
|
|
2586
|
-
)
|
|
2587
|
-
if (utf16Start >= utf16End) return
|
|
2588
|
-
|
|
2589
|
-
val selectedText = currentText.substring(utf16Start, utf16End)
|
|
2590
|
-
val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as? ClipboardManager
|
|
2591
|
-
clipboard?.setPrimaryClip(ClipData.newPlainText(null, selectedText))
|
|
2592
|
-
|
|
2593
|
-
val scalarStart = PositionBridge.utf16ToScalar(utf16Start, currentText)
|
|
2594
|
-
val scalarEnd = PositionBridge.utf16ToScalar(utf16End, currentText)
|
|
2595
|
-
deleteRangeInRust(scalarStart, scalarEnd)
|
|
2596
|
-
}
|
|
2597
|
-
|
|
2598
|
-
private fun handleAccessibilitySetText(arguments: android.os.Bundle?): Boolean {
|
|
2599
|
-
val replacement = arguments
|
|
2600
|
-
?.getCharSequence(
|
|
2601
|
-
android.view.accessibility.AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE
|
|
2602
|
-
)
|
|
2603
|
-
?.toString()
|
|
2604
|
-
?: return false
|
|
2605
|
-
if (editorId == 0L) {
|
|
2606
|
-
return super.performAccessibilityAction(
|
|
2607
|
-
android.view.accessibility.AccessibilityNodeInfo.ACTION_SET_TEXT,
|
|
2608
|
-
arguments
|
|
2609
|
-
)
|
|
2610
|
-
}
|
|
2611
|
-
if (discardTransientInputForDestroyedEditorIfNeeded()) return false
|
|
2612
|
-
if (!prepareForExternalEditorUpdate()) return false
|
|
2613
|
-
|
|
2614
|
-
val currentText = text?.toString() ?: return false
|
|
2615
|
-
val scalarStart = 0
|
|
2616
|
-
val scalarEnd = currentText.codePointCount(0, currentText.length)
|
|
2617
|
-
insertPlainTextRangeInRust(scalarStart, scalarEnd, replacement)
|
|
2618
|
-
return true
|
|
2619
|
-
}
|
|
2620
|
-
|
|
2621
|
-
// ── Selection Change ────────────────────────────────────────────────
|
|
2622
|
-
|
|
2623
|
-
/**
|
|
2624
|
-
* Override to notify the listener when selection changes.
|
|
2625
|
-
*
|
|
2626
|
-
* Converts the EditText selection to scalar offsets and notifies both
|
|
2627
|
-
* the listener and the Rust editor.
|
|
2628
|
-
*/
|
|
2629
|
-
override fun onSelectionChanged(selStart: Int, selEnd: Int) {
|
|
2630
|
-
super.onSelectionChanged(selStart, selEnd)
|
|
2631
|
-
if (isApplyingRustState) return
|
|
2632
|
-
ensureSelectionVisible()
|
|
2633
|
-
onSelectionOrContentMayChange?.invoke()
|
|
2634
|
-
// Keep the custom caret solid at its new position, then resume blinking.
|
|
2635
|
-
restartCaretBlink()
|
|
2636
|
-
|
|
2637
|
-
syncCurrentSelectionToRust()
|
|
2638
|
-
}
|
|
2639
|
-
|
|
2640
|
-
private fun syncCurrentSelectionToRust() {
|
|
2641
|
-
if (!hasLiveEditor()) return
|
|
2642
|
-
|
|
2643
|
-
val currentText = text?.toString() ?: ""
|
|
2644
|
-
if (currentText != lastAuthorizedText) return
|
|
2645
|
-
val (scalarAnchor, scalarHead) = rawScalarSelection(currentText) ?: return
|
|
2646
|
-
|
|
2647
|
-
val selectionHook = onSetSelectionScalarInRustForTesting
|
|
2648
|
-
val docAnchor: Int
|
|
2649
|
-
val docHead: Int
|
|
2650
|
-
if (selectionHook != null) {
|
|
2651
|
-
selectionHook(scalarAnchor, scalarHead)
|
|
2652
|
-
docAnchor = scalarAnchor
|
|
2653
|
-
docHead = scalarHead
|
|
2654
|
-
} else {
|
|
2655
|
-
// Sync selection to Rust (converts scalar→doc internally).
|
|
2656
|
-
editorSetSelectionScalar(
|
|
2657
|
-
editorId.toULong(),
|
|
2658
|
-
scalarAnchor.toUInt(),
|
|
2659
|
-
scalarHead.toUInt()
|
|
2660
|
-
)
|
|
2661
|
-
|
|
2662
|
-
// Emit doc positions (not scalar offsets) to match the Selection contract.
|
|
2663
|
-
docAnchor = editorScalarToDoc(editorId.toULong(), scalarAnchor.toUInt()).toInt()
|
|
2664
|
-
docHead = editorScalarToDoc(editorId.toULong(), scalarHead.toUInt()).toInt()
|
|
2665
|
-
}
|
|
2666
|
-
editorListener?.onSelectionChanged(docAnchor, docHead)
|
|
2667
|
-
}
|
|
2668
|
-
|
|
2669
|
-
// ── Rust Integration ────────────────────────────────────────────────
|
|
2670
|
-
|
|
2671
|
-
// Samsung Keyboard may call finishComposingText() and then commitText(" ")
|
|
2672
|
-
// for one space tap. Defer the render from finishComposingText() by one
|
|
2673
|
-
// loop so setText() does not restart input before the pending space arrives.
|
|
2674
|
-
internal fun runWithDeferredRustUpdateApplication(block: () -> Unit) {
|
|
2675
|
-
recordImeTraceForTesting(
|
|
2676
|
-
"deferRustUpdateBegin",
|
|
2677
|
-
"depth=$deferredRustUpdateApplicationDepth pending=${deferredRustUpdateJSON != null}"
|
|
2678
|
-
)
|
|
2679
|
-
deferredRustUpdateApplicationDepth += 1
|
|
2680
|
-
try {
|
|
2681
|
-
block()
|
|
2682
|
-
} finally {
|
|
2683
|
-
deferredRustUpdateApplicationDepth -= 1
|
|
2684
|
-
recordImeTraceForTesting(
|
|
2685
|
-
"deferRustUpdateEnd",
|
|
2686
|
-
"depth=$deferredRustUpdateApplicationDepth pending=${deferredRustUpdateJSON != null}"
|
|
2687
|
-
)
|
|
2688
|
-
if (deferredRustUpdateApplicationDepth == 0) {
|
|
2689
|
-
scheduleDeferredRustUpdateApplication()
|
|
2690
|
-
}
|
|
2691
|
-
}
|
|
2692
|
-
}
|
|
2693
|
-
|
|
2694
|
-
private fun applyRustUpdateJSON(updateJSON: String) {
|
|
2695
|
-
if (deferredRustUpdateApplicationDepth > 0) {
|
|
2696
|
-
deferredRustUpdateJSON = updateJSON
|
|
2697
|
-
recordImeTraceForTesting(
|
|
2698
|
-
"rustUpdateDeferred",
|
|
2699
|
-
"jsonLength=${updateJSON.length} depth=$deferredRustUpdateApplicationDepth"
|
|
2700
|
-
)
|
|
2701
|
-
authorizeCurrentVisibleTextForDeferredRustUpdate()
|
|
2702
|
-
return
|
|
2703
|
-
}
|
|
2704
|
-
cancelDeferredRustUpdateApplication()
|
|
2705
|
-
recordImeTraceForTesting(
|
|
2706
|
-
"rustUpdateApply",
|
|
2707
|
-
"mode=immediate jsonLength=${updateJSON.length}"
|
|
2708
|
-
)
|
|
2709
|
-
applyUpdateJSON(updateJSON)
|
|
2710
|
-
}
|
|
2711
|
-
|
|
2712
|
-
private fun authorizeCurrentVisibleTextForDeferredRustUpdate() {
|
|
2713
|
-
lastAuthorizedText = text?.toString().orEmpty()
|
|
2714
|
-
lastAuthorizedRenderedText = text?.let { SpannableStringBuilder(it) }
|
|
2715
|
-
lastAuthorizedTextRevision += 1L
|
|
2716
|
-
clearNativeTextMutationAdoptionSuppression()
|
|
2717
|
-
clearNativeTextMutationAfterBlurWindow()
|
|
2718
|
-
}
|
|
2719
|
-
|
|
2720
|
-
internal fun authorizeCurrentVisibleTextForPendingImeOperationForEditor() {
|
|
2721
|
-
pendingOptimisticRenderText = null
|
|
2722
|
-
authorizeCurrentVisibleTextForDeferredRustUpdate()
|
|
2723
|
-
recordImeTraceForTesting(
|
|
2724
|
-
"authorizePendingImeVisibleText",
|
|
2725
|
-
"textLength=${lastAuthorizedText.length}"
|
|
2726
|
-
)
|
|
2727
|
-
}
|
|
2728
|
-
|
|
2729
|
-
internal fun deleteScalarRangeForPendingImeOperationForEditor(scalarFrom: Int, scalarTo: Int) {
|
|
2730
|
-
deleteRangeInRust(scalarFrom, scalarTo)
|
|
2731
|
-
}
|
|
2732
|
-
|
|
2733
|
-
internal fun applyVisibleCompositionCommitForPendingImeOperationForEditor(
|
|
2734
|
-
committedText: String,
|
|
2735
|
-
replacementStartUtf16: Int,
|
|
2736
|
-
replacementEndUtf16: Int,
|
|
2737
|
-
newCursorPosition: Int
|
|
2738
|
-
): Boolean {
|
|
2739
|
-
val editable = text ?: return false
|
|
2740
|
-
val currentText = editable.toString()
|
|
2741
|
-
val (startUtf16, endUtf16) = PositionBridge.snapRangeToScalarBoundaries(
|
|
2742
|
-
replacementStartUtf16,
|
|
2743
|
-
replacementEndUtf16,
|
|
2744
|
-
currentText
|
|
2745
|
-
)
|
|
2746
|
-
if (startUtf16 > endUtf16 || endUtf16 > editable.length) return false
|
|
2747
|
-
var didApply = false
|
|
2748
|
-
runWithTransientInputMutationGuard {
|
|
2749
|
-
editable.replace(startUtf16, endUtf16, committedText)
|
|
2750
|
-
val insertedEnd = startUtf16 + committedText.length
|
|
2751
|
-
val requestedCursor = when {
|
|
2752
|
-
newCursorPosition > 0 -> insertedEnd + newCursorPosition - 1
|
|
2753
|
-
newCursorPosition < 0 -> startUtf16 + newCursorPosition
|
|
2754
|
-
else -> insertedEnd
|
|
2755
|
-
}.coerceIn(0, editable.length)
|
|
2756
|
-
Selection.setSelection(editable, requestedCursor, requestedCursor)
|
|
2757
|
-
didApply = true
|
|
2758
|
-
true
|
|
2759
|
-
}
|
|
2760
|
-
if (didApply) {
|
|
2761
|
-
pendingOptimisticRenderText = null
|
|
2762
|
-
}
|
|
2763
|
-
return didApply
|
|
2764
|
-
}
|
|
2765
|
-
|
|
2766
|
-
internal fun commitAlreadyVisibleCompositionMutationForPendingImeOperationForEditor(
|
|
2767
|
-
committedText: String,
|
|
2768
|
-
newCursorPosition: Int
|
|
2769
|
-
): Boolean {
|
|
2770
|
-
if (committedText.isEmpty()) return false
|
|
2771
|
-
val currentText = text?.toString() ?: return false
|
|
2772
|
-
val mutation = nativeTextMutationFromAuthorizedDiff(currentText) ?: return false
|
|
2773
|
-
val tokenRange = committedTokenRangeAroundMutation(
|
|
2774
|
-
currentText,
|
|
2775
|
-
mutation.replacementStartUtf16,
|
|
2776
|
-
mutation.replacementEndUtf16
|
|
2777
|
-
) ?: run {
|
|
2778
|
-
recordImeTraceForTesting(
|
|
2779
|
-
"alreadyVisibleCompositionNoop",
|
|
2780
|
-
"reason=noToken committedLength=${committedText.length} visibleRange=${mutation.replacementStartUtf16}..${mutation.replacementEndUtf16}"
|
|
2781
|
-
)
|
|
2782
|
-
return false
|
|
2783
|
-
}
|
|
2784
|
-
val visibleToken = currentText.substring(tokenRange.first, tokenRange.second)
|
|
2785
|
-
if (visibleToken != committedText) {
|
|
2786
|
-
recordImeTraceForTesting(
|
|
2787
|
-
"alreadyVisibleCompositionNoop",
|
|
2788
|
-
"reason=tokenMismatch committedLength=${committedText.length} tokenLength=${visibleToken.length} visibleRange=${mutation.replacementStartUtf16}..${mutation.replacementEndUtf16}"
|
|
2789
|
-
)
|
|
2790
|
-
return false
|
|
2791
|
-
}
|
|
2792
|
-
|
|
2793
|
-
val authorizedText = lastAuthorizedText
|
|
2794
|
-
val requestedCursor = requestedCursorScalar(
|
|
2795
|
-
mutation.scalarFrom,
|
|
2796
|
-
mutation.scalarTo,
|
|
2797
|
-
authorizedText,
|
|
2798
|
-
mutation.replacementText,
|
|
2799
|
-
newCursorPosition
|
|
2800
|
-
)
|
|
2801
|
-
recordImeTraceForTesting(
|
|
2802
|
-
"alreadyVisibleCompositionApply",
|
|
2803
|
-
"range=${mutation.scalarFrom}..${mutation.scalarTo} replacementLength=${mutation.replacementText.length} committedLength=${committedText.length} requestedCursor=$requestedCursor"
|
|
2804
|
-
)
|
|
2805
|
-
pendingOptimisticRenderText = null
|
|
2806
|
-
insertPlainTextRangeInRust(
|
|
2807
|
-
mutation.scalarFrom,
|
|
2808
|
-
mutation.scalarTo,
|
|
2809
|
-
mutation.replacementText,
|
|
2810
|
-
requestedCursorScalar = requestedCursor
|
|
2811
|
-
)
|
|
2812
|
-
return true
|
|
2813
|
-
}
|
|
2814
|
-
|
|
2815
|
-
private fun committedTokenRangeAroundMutation(
|
|
2816
|
-
currentText: String,
|
|
2817
|
-
replacementStartUtf16: Int,
|
|
2818
|
-
replacementEndUtf16: Int
|
|
2819
|
-
): Pair<Int, Int>? {
|
|
2820
|
-
if (currentText.isEmpty()) return null
|
|
2821
|
-
val start = replacementStartUtf16.coerceIn(0, currentText.length)
|
|
2822
|
-
val end = replacementEndUtf16.coerceIn(start, currentText.length)
|
|
2823
|
-
val probe = when {
|
|
2824
|
-
start < end -> start
|
|
2825
|
-
start < currentText.length -> start
|
|
2826
|
-
start > 0 -> Character.offsetByCodePoints(currentText, start, -1)
|
|
2827
|
-
else -> return null
|
|
2828
|
-
}
|
|
2829
|
-
val tokenRange = missingOldTextCorrectionTokenRange(currentText, probe) ?: return null
|
|
2830
|
-
return if (start < end) {
|
|
2831
|
-
tokenRange.takeIf { it.first <= start && it.second >= end }
|
|
2832
|
-
} else {
|
|
2833
|
-
tokenRange.takeIf { start >= it.first && start <= it.second }
|
|
2834
|
-
}
|
|
2835
|
-
}
|
|
2836
|
-
|
|
2837
|
-
private fun scheduleDeferredRustUpdateApplication() {
|
|
2838
|
-
val pendingUpdateJSON = deferredRustUpdateJSON ?: return
|
|
2839
|
-
val generation = ++deferredRustUpdateGeneration
|
|
2840
|
-
recordImeTraceForTesting(
|
|
2841
|
-
"rustUpdateDeferredScheduled",
|
|
2842
|
-
"generation=$generation jsonLength=${pendingUpdateJSON.length}"
|
|
2843
|
-
)
|
|
2844
|
-
Handler(Looper.getMainLooper()).post {
|
|
2845
|
-
if (generation != deferredRustUpdateGeneration) {
|
|
2846
|
-
recordImeTraceForTesting(
|
|
2847
|
-
"rustUpdateDeferredSkip",
|
|
2848
|
-
"reason=generation generation=$generation current=$deferredRustUpdateGeneration"
|
|
2849
|
-
)
|
|
2850
|
-
return@post
|
|
2851
|
-
}
|
|
2852
|
-
if (deferredRustUpdateJSON != pendingUpdateJSON) {
|
|
2853
|
-
recordImeTraceForTesting("rustUpdateDeferredSkip", "reason=replaced generation=$generation")
|
|
2854
|
-
return@post
|
|
2855
|
-
}
|
|
2856
|
-
deferredRustUpdateJSON = null
|
|
2857
|
-
recordImeTraceForTesting(
|
|
2858
|
-
"rustUpdateApply",
|
|
2859
|
-
"mode=deferred generation=$generation jsonLength=${pendingUpdateJSON.length}"
|
|
2860
|
-
)
|
|
2861
|
-
applyUpdateJSON(pendingUpdateJSON)
|
|
2862
|
-
}
|
|
2863
|
-
}
|
|
2864
|
-
|
|
2865
|
-
private fun cancelDeferredRustUpdateApplication() {
|
|
2866
|
-
if (deferredRustUpdateJSON == null) return
|
|
2867
|
-
recordImeTraceForTesting(
|
|
2868
|
-
"rustUpdateDeferredCancel",
|
|
2869
|
-
"generation=$deferredRustUpdateGeneration"
|
|
2870
|
-
)
|
|
2871
|
-
deferredRustUpdateJSON = null
|
|
2872
|
-
deferredRustUpdateGeneration += 1L
|
|
2873
|
-
}
|
|
2874
|
-
|
|
2875
|
-
/**
|
|
2876
|
-
* Insert text at a scalar position via the Rust editor.
|
|
2877
|
-
*/
|
|
2878
|
-
private fun insertTextInRust(text: String, atScalarPos: Int) {
|
|
2879
|
-
if (!hasLiveEditor()) return
|
|
2880
|
-
onInsertTextInRustForTesting?.let { callback ->
|
|
2881
|
-
callback(text, atScalarPos)
|
|
2882
|
-
return
|
|
2883
|
-
}
|
|
2884
|
-
val startedAt = System.nanoTime()
|
|
2885
|
-
val updateJSON = editorInsertTextScalar(editorId.toULong(), atScalarPos.toUInt(), text)
|
|
2886
|
-
recordImeTraceForTesting(
|
|
2887
|
-
"rustInsertText",
|
|
2888
|
-
"at=$atScalarPos textLength=${text.length} rustUs=${nanosToMicros(System.nanoTime() - startedAt)} jsonLength=${updateJSON.length}"
|
|
2889
|
-
)
|
|
2890
|
-
applyRustUpdateJSON(updateJSON)
|
|
2891
|
-
}
|
|
2892
|
-
|
|
2893
|
-
private fun replaceTextRangeInRust(scalarFrom: Int, scalarTo: Int, text: String) {
|
|
2894
|
-
if (!hasLiveEditor()) return
|
|
2895
|
-
onReplaceTextInRustForTesting?.let { callback ->
|
|
2896
|
-
callback(scalarFrom, scalarTo, text)
|
|
2897
|
-
return
|
|
2898
|
-
}
|
|
2899
|
-
val startedAt = System.nanoTime()
|
|
2900
|
-
val updateJSON = editorReplaceTextScalar(
|
|
2901
|
-
editorId.toULong(),
|
|
2902
|
-
scalarFrom.toUInt(),
|
|
2903
|
-
scalarTo.toUInt(),
|
|
2904
|
-
text
|
|
2905
|
-
)
|
|
2906
|
-
recordImeTraceForTesting(
|
|
2907
|
-
"rustReplaceText",
|
|
2908
|
-
"range=$scalarFrom..$scalarTo textLength=${text.length} rustUs=${nanosToMicros(System.nanoTime() - startedAt)} jsonLength=${updateJSON.length}"
|
|
2909
|
-
)
|
|
2910
|
-
applyRustUpdateJSON(updateJSON)
|
|
2911
|
-
}
|
|
2912
|
-
|
|
2913
|
-
private fun insertPlainTextRangeInRust(
|
|
2914
|
-
scalarFrom: Int,
|
|
2915
|
-
scalarTo: Int,
|
|
2916
|
-
text: String,
|
|
2917
|
-
requestedCursorScalar: Int? = null
|
|
2918
|
-
) {
|
|
2919
|
-
if (!hasLiveEditor()) return
|
|
2920
|
-
recordImeTraceForTesting(
|
|
2921
|
-
"rustPlainTextRoute",
|
|
2922
|
-
"range=$scalarFrom..$scalarTo textLength=${text.length} requestedCursor=$requestedCursorScalar"
|
|
2923
|
-
)
|
|
2924
|
-
if (text.isEmpty()) {
|
|
2925
|
-
if (scalarFrom != scalarTo) {
|
|
2926
|
-
deleteRangeInRust(scalarFrom, scalarTo)
|
|
2927
|
-
}
|
|
2928
|
-
applyRequestedCursorScalar(requestedCursorScalar)
|
|
2929
|
-
return
|
|
2930
|
-
}
|
|
2931
|
-
if (text.indexOf('\n') >= 0 || text.indexOf('\r') >= 0) {
|
|
2932
|
-
val docJson = plainTextDocumentFragmentJson(text)
|
|
2933
|
-
onInsertContentJsonAtSelectionScalarForTesting?.let { callback ->
|
|
2934
|
-
callback(scalarFrom, scalarTo, docJson)
|
|
2935
|
-
applyRequestedCursorScalar(requestedCursorScalar)
|
|
2936
|
-
return
|
|
2937
|
-
}
|
|
2938
|
-
val startedAt = System.nanoTime()
|
|
2939
|
-
val updateJSON = editorInsertContentJsonAtSelectionScalar(
|
|
2940
|
-
editorId.toULong(),
|
|
2941
|
-
scalarFrom.toUInt(),
|
|
2942
|
-
scalarTo.toUInt(),
|
|
2943
|
-
docJson
|
|
2944
|
-
)
|
|
2945
|
-
recordImeTraceForTesting(
|
|
2946
|
-
"rustInsertContentJson",
|
|
2947
|
-
"range=$scalarFrom..$scalarTo textLength=${text.length} rustUs=${nanosToMicros(System.nanoTime() - startedAt)} jsonLength=${updateJSON.length}"
|
|
2948
|
-
)
|
|
2949
|
-
applyRustUpdateJSON(updateJSON)
|
|
2950
|
-
applyRequestedCursorScalar(requestedCursorScalar)
|
|
2951
|
-
return
|
|
2952
|
-
}
|
|
2953
|
-
|
|
2954
|
-
if (scalarFrom != scalarTo) {
|
|
2955
|
-
replaceTextRangeInRust(scalarFrom, scalarTo, text)
|
|
2956
|
-
} else {
|
|
2957
|
-
insertTextInRust(text, scalarFrom)
|
|
2958
|
-
}
|
|
2959
|
-
applyRequestedCursorScalar(requestedCursorScalar)
|
|
2960
|
-
}
|
|
2961
|
-
|
|
2962
|
-
private fun requestedCursorScalar(
|
|
2963
|
-
scalarFrom: Int,
|
|
2964
|
-
scalarTo: Int,
|
|
2965
|
-
currentText: String,
|
|
2966
|
-
insertedText: String,
|
|
2967
|
-
newCursorPosition: Int
|
|
2968
|
-
): Int? {
|
|
2969
|
-
if (newCursorPosition == 1) return null
|
|
2970
|
-
val insertedScalarLength = insertedText.codePointCount(0, insertedText.length)
|
|
2971
|
-
val currentScalarLength = currentText.codePointCount(0, currentText.length)
|
|
2972
|
-
val nextScalarLength =
|
|
2973
|
-
(currentScalarLength - (scalarTo - scalarFrom) + insertedScalarLength).coerceAtLeast(0)
|
|
2974
|
-
val requested = if (newCursorPosition > 0) {
|
|
2975
|
-
scalarFrom + insertedScalarLength + newCursorPosition - 1
|
|
2976
|
-
} else {
|
|
2977
|
-
scalarFrom + newCursorPosition
|
|
2978
|
-
}
|
|
2979
|
-
return requested.coerceIn(0, nextScalarLength)
|
|
2980
|
-
}
|
|
2981
|
-
|
|
2982
|
-
private fun applyRequestedCursorScalar(requestedCursorScalar: Int?) {
|
|
2983
|
-
val requested = requestedCursorScalar ?: return
|
|
2984
|
-
if (!hasLiveEditor()) return
|
|
2985
|
-
val currentText = text?.toString().orEmpty()
|
|
2986
|
-
val safeScalar = requested.coerceAtLeast(0)
|
|
2987
|
-
onSetSelectionScalarInRustForTesting?.let { callback ->
|
|
2988
|
-
callback(safeScalar, safeScalar)
|
|
2989
|
-
} ?: editorSetSelectionScalar(
|
|
2990
|
-
editorId.toULong(),
|
|
2991
|
-
safeScalar.toUInt(),
|
|
2992
|
-
safeScalar.toUInt()
|
|
2993
|
-
)
|
|
2994
|
-
val localScalar = safeScalar.coerceIn(0, currentText.codePointCount(0, currentText.length))
|
|
2995
|
-
val safeUtf16 = PositionBridge.scalarToUtf16(localScalar, currentText)
|
|
2996
|
-
.coerceIn(0, currentText.length)
|
|
2997
|
-
if (selectionStart != safeUtf16 || selectionEnd != safeUtf16) {
|
|
2998
|
-
setSelection(safeUtf16, safeUtf16)
|
|
2999
|
-
}
|
|
3000
|
-
}
|
|
3001
|
-
|
|
3002
|
-
private fun plainTextDocumentFragmentJson(text: String): String {
|
|
3003
|
-
val normalizedText = text.replace("\r\n", "\n").replace('\r', '\n')
|
|
3004
|
-
val content = org.json.JSONArray()
|
|
3005
|
-
for (line in normalizedText.split('\n')) {
|
|
3006
|
-
val paragraph = org.json.JSONObject().put("type", "paragraph")
|
|
3007
|
-
if (line.isNotEmpty()) {
|
|
3008
|
-
paragraph.put(
|
|
3009
|
-
"content",
|
|
3010
|
-
org.json.JSONArray().put(
|
|
3011
|
-
org.json.JSONObject()
|
|
3012
|
-
.put("type", "text")
|
|
3013
|
-
.put("text", line)
|
|
3014
|
-
)
|
|
3015
|
-
)
|
|
3016
|
-
}
|
|
3017
|
-
content.put(paragraph)
|
|
3018
|
-
}
|
|
3019
|
-
return org.json.JSONObject()
|
|
3020
|
-
.put("type", "doc")
|
|
3021
|
-
.put("content", content)
|
|
3022
|
-
.toString()
|
|
3023
|
-
}
|
|
3024
|
-
|
|
3025
|
-
private fun nativeTextMutationFromAuthorizedDiff(currentText: String): NativeTextMutation? {
|
|
3026
|
-
val authorizedText = lastAuthorizedText
|
|
3027
|
-
if (currentText == authorizedText) return null
|
|
3028
|
-
|
|
3029
|
-
var prefix = 0
|
|
3030
|
-
val sharedLength = minOf(authorizedText.length, currentText.length)
|
|
3031
|
-
while (
|
|
3032
|
-
prefix < sharedLength &&
|
|
3033
|
-
authorizedText[prefix] == currentText[prefix]
|
|
3034
|
-
) {
|
|
3035
|
-
prefix++
|
|
3036
|
-
}
|
|
3037
|
-
prefix = minOf(
|
|
3038
|
-
PositionBridge.snapToScalarBoundary(prefix, authorizedText, biasForward = false),
|
|
3039
|
-
PositionBridge.snapToScalarBoundary(prefix, currentText, biasForward = false)
|
|
3040
|
-
)
|
|
3041
|
-
|
|
3042
|
-
var authorizedEnd = authorizedText.length
|
|
3043
|
-
var currentEnd = currentText.length
|
|
3044
|
-
while (
|
|
3045
|
-
authorizedEnd > prefix &&
|
|
3046
|
-
currentEnd > prefix &&
|
|
3047
|
-
authorizedText[authorizedEnd - 1] == currentText[currentEnd - 1]
|
|
3048
|
-
) {
|
|
3049
|
-
authorizedEnd--
|
|
3050
|
-
currentEnd--
|
|
3051
|
-
}
|
|
3052
|
-
authorizedEnd = PositionBridge.snapToScalarBoundary(
|
|
3053
|
-
authorizedEnd,
|
|
3054
|
-
authorizedText,
|
|
3055
|
-
biasForward = true
|
|
3056
|
-
)
|
|
3057
|
-
currentEnd = PositionBridge.snapToScalarBoundary(
|
|
3058
|
-
currentEnd,
|
|
3059
|
-
currentText,
|
|
3060
|
-
biasForward = true
|
|
3061
|
-
)
|
|
3062
|
-
|
|
3063
|
-
val replacementText = currentText.substring(prefix, currentEnd)
|
|
3064
|
-
val rawSelectionStart = selectionStart
|
|
3065
|
-
val rawSelectionEnd = selectionEnd
|
|
3066
|
-
val selectionAnchorUtf16 = rawSelectionStart
|
|
3067
|
-
.takeIf { it >= 0 }
|
|
3068
|
-
?.let { PositionBridge.snapToScalarBoundary(it, currentText, biasForward = true) }
|
|
3069
|
-
val selectionHeadUtf16 = rawSelectionEnd
|
|
3070
|
-
.takeIf { it >= 0 }
|
|
3071
|
-
?.let { PositionBridge.snapToScalarBoundary(it, currentText, biasForward = true) }
|
|
3072
|
-
return NativeTextMutation(
|
|
3073
|
-
scalarFrom = PositionBridge.utf16ToScalar(prefix, authorizedText),
|
|
3074
|
-
scalarTo = PositionBridge.utf16ToScalar(authorizedEnd, authorizedText),
|
|
3075
|
-
replacementText = replacementText,
|
|
3076
|
-
resultingText = currentText,
|
|
3077
|
-
replacementStartUtf16 = prefix,
|
|
3078
|
-
replacementEndUtf16 = currentEnd,
|
|
3079
|
-
selectionScalarAnchor = selectionAnchorUtf16?.let {
|
|
3080
|
-
PositionBridge.utf16ToScalar(it, currentText)
|
|
3081
|
-
},
|
|
3082
|
-
selectionScalarHead = selectionHeadUtf16?.let {
|
|
3083
|
-
PositionBridge.utf16ToScalar(it, currentText)
|
|
3084
|
-
}
|
|
3085
|
-
)
|
|
3086
|
-
}
|
|
3087
|
-
|
|
3088
|
-
private fun shouldAdoptNativeTextMutation(
|
|
3089
|
-
mutation: NativeTextMutation,
|
|
3090
|
-
allowAfterBlur: Boolean = false
|
|
3091
|
-
): Boolean {
|
|
3092
|
-
if (!isEditable) return false
|
|
3093
|
-
if (isNativeTextMutationAdoptionSuppressedForCurrentRevision()) return false
|
|
3094
|
-
if (!hasFocus()) {
|
|
3095
|
-
return allowAfterBlur &&
|
|
3096
|
-
canAdoptNativeTextMutationAfterBlur() &&
|
|
3097
|
-
shouldAdoptFinalNativeTextMutation(mutation)
|
|
3098
|
-
}
|
|
3099
|
-
return shouldAdoptFinalNativeTextMutation(mutation)
|
|
3100
|
-
}
|
|
3101
|
-
|
|
3102
|
-
private fun shouldAdoptFinalNativeTextMutation(mutation: NativeTextMutation): Boolean {
|
|
3103
|
-
if (composingTextForEditor() != null) return false
|
|
3104
|
-
val trackedRange = compositionReplacementRange() ?: return true
|
|
3105
|
-
val authorizedText = lastAuthorizedText
|
|
3106
|
-
val trackedStart = PositionBridge.utf16ToScalar(trackedRange.first, authorizedText)
|
|
3107
|
-
val trackedEnd = PositionBridge.utf16ToScalar(trackedRange.second, authorizedText)
|
|
3108
|
-
if (trackedStart == trackedEnd) {
|
|
3109
|
-
return mutation.scalarFrom == trackedStart &&
|
|
3110
|
-
mutation.scalarTo == trackedStart &&
|
|
3111
|
-
mutation.replacementText.isNotEmpty()
|
|
3112
|
-
}
|
|
3113
|
-
if (mutation.scalarFrom == mutation.scalarTo) {
|
|
3114
|
-
return mutation.replacementText.isNotEmpty() &&
|
|
3115
|
-
mutation.scalarFrom >= trackedStart &&
|
|
3116
|
-
mutation.scalarFrom <= trackedEnd
|
|
3117
|
-
}
|
|
3118
|
-
return mutation.scalarFrom < trackedEnd && mutation.scalarTo > trackedStart
|
|
3119
|
-
}
|
|
3120
|
-
|
|
3121
|
-
private fun drainNativeTextMutationIfNeeded(
|
|
3122
|
-
allowAfterBlur: Boolean,
|
|
3123
|
-
preserveInputConnectionForExternalUpdate: Boolean = false
|
|
3124
|
-
): Boolean {
|
|
3125
|
-
if (editorId == 0L) return true
|
|
3126
|
-
if (discardTransientInputForDestroyedEditorIfNeeded()) return false
|
|
3127
|
-
val editable = text
|
|
3128
|
-
val currentText = editable?.toString() ?: ""
|
|
3129
|
-
if (currentText == lastAuthorizedText) return true
|
|
3130
|
-
|
|
3131
|
-
val mutation = nativeTextMutationFromAuthorizedDiff(currentText)
|
|
3132
|
-
if (mutation != null && shouldAdoptNativeTextMutation(mutation, allowAfterBlur)) {
|
|
3133
|
-
commitNativeTextMutation(
|
|
3134
|
-
mutation,
|
|
3135
|
-
preserveInputConnectionForExternalUpdate = preserveInputConnectionForExternalUpdate
|
|
3136
|
-
)
|
|
3137
|
-
return true
|
|
3138
|
-
}
|
|
3139
|
-
recordImeTraceForTesting(
|
|
3140
|
-
"nativeMutationNoop",
|
|
3141
|
-
"reason=${if (mutation == null) "noDiffRange" else "notAdoptable"} allowAfterBlur=$allowAfterBlur currentLength=${currentText.length} authorizedLength=${lastAuthorizedText.length}"
|
|
3142
|
-
)
|
|
3143
|
-
return false
|
|
3144
|
-
}
|
|
3145
|
-
|
|
3146
|
-
private fun beginNativeTextMutationAfterBlurWindow() {
|
|
3147
|
-
if (!hasLiveEditor()) {
|
|
3148
|
-
clearNativeTextMutationAfterBlurWindow()
|
|
3149
|
-
return
|
|
3150
|
-
}
|
|
3151
|
-
nativeTextMutationAfterBlurWindow = NativeTextMutationAfterBlurWindow(
|
|
3152
|
-
editorId = editorId,
|
|
3153
|
-
authorizedTextRevision = lastAuthorizedTextRevision,
|
|
3154
|
-
deadlineMs = SystemClock.uptimeMillis() + NATIVE_TEXT_MUTATION_AFTER_BLUR_WINDOW_MS
|
|
3155
|
-
)
|
|
3156
|
-
}
|
|
3157
|
-
|
|
3158
|
-
private fun clearNativeTextMutationAfterBlurWindow() {
|
|
3159
|
-
nativeTextMutationAfterBlurWindow = null
|
|
3160
|
-
}
|
|
3161
|
-
|
|
3162
|
-
private fun suppressNativeTextMutationAdoptionForCurrentRevision() {
|
|
3163
|
-
if (!hasLiveEditor()) {
|
|
3164
|
-
clearNativeTextMutationAdoptionSuppression()
|
|
3165
|
-
return
|
|
3166
|
-
}
|
|
3167
|
-
nativeTextMutationAdoptionSuppression = NativeTextMutationAdoptionSuppression(
|
|
3168
|
-
editorId = editorId,
|
|
3169
|
-
authorizedTextRevision = lastAuthorizedTextRevision
|
|
3170
|
-
)
|
|
3171
|
-
}
|
|
3172
|
-
|
|
3173
|
-
private fun clearNativeTextMutationAdoptionSuppression() {
|
|
3174
|
-
nativeTextMutationAdoptionSuppression = null
|
|
3175
|
-
}
|
|
3176
|
-
|
|
3177
|
-
private fun isNativeTextMutationAdoptionSuppressedForCurrentRevision(): Boolean {
|
|
3178
|
-
val suppression = nativeTextMutationAdoptionSuppression ?: return false
|
|
3179
|
-
if (
|
|
3180
|
-
suppression.editorId != editorId ||
|
|
3181
|
-
suppression.authorizedTextRevision != lastAuthorizedTextRevision
|
|
3182
|
-
) {
|
|
3183
|
-
nativeTextMutationAdoptionSuppression = null
|
|
3184
|
-
return false
|
|
3185
|
-
}
|
|
3186
|
-
return true
|
|
3187
|
-
}
|
|
3188
|
-
|
|
3189
|
-
private fun canAdoptNativeTextMutationAfterBlur(): Boolean {
|
|
3190
|
-
val window = nativeTextMutationAfterBlurWindow ?: return false
|
|
3191
|
-
val now = SystemClock.uptimeMillis()
|
|
3192
|
-
if (now > window.deadlineMs ||
|
|
3193
|
-
window.editorId != editorId ||
|
|
3194
|
-
window.authorizedTextRevision != lastAuthorizedTextRevision ||
|
|
3195
|
-
window.didAdoptMutation
|
|
3196
|
-
) {
|
|
3197
|
-
nativeTextMutationAfterBlurWindow = null
|
|
3198
|
-
return false
|
|
3199
|
-
}
|
|
3200
|
-
return true
|
|
3201
|
-
}
|
|
3202
|
-
|
|
3203
|
-
private fun commitNativeTextMutation(
|
|
3204
|
-
mutation: NativeTextMutation,
|
|
3205
|
-
preserveInputConnectionForExternalUpdate: Boolean = false
|
|
3206
|
-
) {
|
|
3207
|
-
if (!hasLiveEditor()) return
|
|
3208
|
-
val startedAt = System.nanoTime()
|
|
3209
|
-
if ((text?.toString() ?: "") != mutation.resultingText) {
|
|
3210
|
-
recordImeTraceForTesting(
|
|
3211
|
-
"nativeMutationNoop",
|
|
3212
|
-
"reason=staleResult range=${mutation.scalarFrom}..${mutation.scalarTo} replacementLength=${mutation.replacementText.length}"
|
|
3213
|
-
)
|
|
3214
|
-
return
|
|
3215
|
-
}
|
|
3216
|
-
val shouldRestartInput = hasFocus()
|
|
3217
|
-
if (preserveInputConnectionForExternalUpdate) {
|
|
3218
|
-
clearInputStateForExternalReplacementPreservingConnection()
|
|
3219
|
-
} else {
|
|
3220
|
-
retireInputConnectionForEditor()
|
|
3221
|
-
}
|
|
3222
|
-
nativeTextMutationAfterBlurWindow?.didAdoptMutation = true
|
|
3223
|
-
clearNativeTextMutationAfterBlurWindow()
|
|
3224
|
-
|
|
3225
|
-
recordImeTraceForTesting(
|
|
3226
|
-
"nativeMutationApply",
|
|
3227
|
-
"range=${mutation.scalarFrom}..${mutation.scalarTo} replacementLength=${mutation.replacementText.length} restartInput=$shouldRestartInput preserveInputConnection=$preserveInputConnectionForExternalUpdate"
|
|
3228
|
-
)
|
|
3229
|
-
if (mutation.replacementText.isEmpty()) {
|
|
3230
|
-
deleteRangeInRust(mutation.scalarFrom, mutation.scalarTo)
|
|
3231
|
-
} else {
|
|
3232
|
-
insertPlainTextRangeInRust(
|
|
3233
|
-
mutation.scalarFrom,
|
|
3234
|
-
mutation.scalarTo,
|
|
3235
|
-
mutation.replacementText
|
|
3236
|
-
)
|
|
3237
|
-
}
|
|
3238
|
-
restoreSelectionAfterNativeTextMutation(mutation)
|
|
3239
|
-
if (shouldRestartInput) {
|
|
3240
|
-
restartInputForEditor(
|
|
3241
|
-
if (preserveInputConnectionForExternalUpdate) "externalUpdatePreflight" else "explicit"
|
|
3242
|
-
)
|
|
3243
|
-
}
|
|
3244
|
-
recordImeTraceForTesting(
|
|
3245
|
-
"nativeMutationApplyDone",
|
|
3246
|
-
"totalUs=${nanosToMicros(System.nanoTime() - startedAt)} restartInput=$shouldRestartInput"
|
|
3247
|
-
)
|
|
3248
|
-
}
|
|
3249
|
-
|
|
3250
|
-
private fun restoreSelectionAfterNativeTextMutation(mutation: NativeTextMutation) {
|
|
3251
|
-
val selectionScalarAnchor = mutation.selectionScalarAnchor ?: return
|
|
3252
|
-
val selectionScalarHead = mutation.selectionScalarHead ?: return
|
|
3253
|
-
val currentText = text?.toString() ?: return
|
|
3254
|
-
val anchorUtf16 = PositionBridge.scalarToUtf16(selectionScalarAnchor, currentText)
|
|
3255
|
-
val headUtf16 = PositionBridge.scalarToUtf16(selectionScalarHead, currentText)
|
|
3256
|
-
val length = currentText.length
|
|
3257
|
-
setSelection(anchorUtf16.coerceIn(0, length), headUtf16.coerceIn(0, length))
|
|
3258
|
-
}
|
|
3259
|
-
|
|
3260
|
-
/**
|
|
3261
|
-
* Delete a scalar range via the Rust editor.
|
|
3262
|
-
*
|
|
3263
|
-
* @param scalarFrom Start scalar offset (inclusive).
|
|
3264
|
-
* @param scalarTo End scalar offset (exclusive).
|
|
3265
|
-
*/
|
|
3266
|
-
private fun deleteRangeInRust(scalarFrom: Int, scalarTo: Int) {
|
|
3267
|
-
if (!hasLiveEditor()) return
|
|
3268
|
-
if (scalarFrom >= scalarTo) return
|
|
3269
|
-
onDeleteRangeInRustForTesting?.let { callback ->
|
|
3270
|
-
callback(scalarFrom, scalarTo)
|
|
3271
|
-
return
|
|
3272
|
-
}
|
|
3273
|
-
val startedAt = System.nanoTime()
|
|
3274
|
-
val updateJSON = editorDeleteScalarRange(editorId.toULong(), scalarFrom.toUInt(), scalarTo.toUInt())
|
|
3275
|
-
recordImeTraceForTesting(
|
|
3276
|
-
"rustDeleteRange",
|
|
3277
|
-
"range=$scalarFrom..$scalarTo rustUs=${nanosToMicros(System.nanoTime() - startedAt)} jsonLength=${updateJSON.length}"
|
|
3278
|
-
)
|
|
3279
|
-
applyRustUpdateJSON(updateJSON)
|
|
3280
|
-
}
|
|
3281
|
-
|
|
3282
|
-
private fun deleteBackwardAtSelectionScalarInRust(scalarAnchor: Int, scalarHead: Int) {
|
|
3283
|
-
if (!hasLiveEditor()) return
|
|
3284
|
-
onDeleteBackwardAtSelectionScalarInRustForTesting?.let { callback ->
|
|
3285
|
-
callback(scalarAnchor, scalarHead)
|
|
3286
|
-
return
|
|
3287
|
-
}
|
|
3288
|
-
val startedAt = System.nanoTime()
|
|
3289
|
-
val updateJSON = editorDeleteBackwardAtSelectionScalar(
|
|
3290
|
-
editorId.toULong(),
|
|
3291
|
-
scalarAnchor.toUInt(),
|
|
3292
|
-
scalarHead.toUInt()
|
|
3293
|
-
)
|
|
3294
|
-
recordImeTraceForTesting(
|
|
3295
|
-
"rustDeleteBackward",
|
|
3296
|
-
"selection=$scalarAnchor..$scalarHead rustUs=${nanosToMicros(System.nanoTime() - startedAt)} jsonLength=${updateJSON.length}"
|
|
3297
|
-
)
|
|
3298
|
-
applyRustUpdateJSON(updateJSON)
|
|
3299
|
-
}
|
|
3300
|
-
|
|
3301
|
-
private fun toggleTaskItemCheckedAtSelectionScalarInRust(scalarAnchor: Int, scalarHead: Int) {
|
|
3302
|
-
if (!hasLiveEditor()) return
|
|
3303
|
-
onToggleTaskItemCheckedAtSelectionScalarInRustForTesting?.let { callback ->
|
|
3304
|
-
callback(scalarAnchor, scalarHead)
|
|
3305
|
-
return
|
|
3306
|
-
}
|
|
3307
|
-
val startedAt = System.nanoTime()
|
|
3308
|
-
val updateJSON = editorToggleTaskItemCheckedAtSelectionScalar(
|
|
3309
|
-
editorId.toULong(),
|
|
3310
|
-
scalarAnchor.toUInt(),
|
|
3311
|
-
scalarHead.toUInt()
|
|
3312
|
-
)
|
|
3313
|
-
recordImeTraceForTesting(
|
|
3314
|
-
"rustToggleTaskItemChecked",
|
|
3315
|
-
"selection=$scalarAnchor..$scalarHead rustUs=${nanosToMicros(System.nanoTime() - startedAt)} jsonLength=${updateJSON.length}"
|
|
3316
|
-
)
|
|
3317
|
-
applyRustUpdateJSON(updateJSON)
|
|
3318
|
-
}
|
|
3319
|
-
|
|
3320
|
-
/**
|
|
3321
|
-
* Split a block at a scalar position via the Rust editor.
|
|
3322
|
-
*/
|
|
3323
|
-
private fun splitBlockInRust(atScalarPos: Int) {
|
|
3324
|
-
if (!hasLiveEditor()) return
|
|
3325
|
-
val startedAt = System.nanoTime()
|
|
3326
|
-
val updateJSON = editorSplitBlockScalar(editorId.toULong(), atScalarPos.toUInt())
|
|
3327
|
-
recordImeTraceForTesting(
|
|
3328
|
-
"rustSplitBlock",
|
|
3329
|
-
"at=$atScalarPos rustUs=${nanosToMicros(System.nanoTime() - startedAt)} jsonLength=${updateJSON.length}"
|
|
3330
|
-
)
|
|
3331
|
-
applyRustUpdateJSON(updateJSON)
|
|
3332
|
-
scheduleLineBoundaryInputRefreshForEditor("splitBlock")
|
|
3333
|
-
}
|
|
3334
|
-
|
|
3335
|
-
private fun deleteAndSplitInRust(scalarFrom: Int, scalarTo: Int) {
|
|
3336
|
-
if (!hasLiveEditor()) return
|
|
3337
|
-
onDeleteAndSplitScalarInRustForTesting?.let { callback ->
|
|
3338
|
-
callback(scalarFrom, scalarTo)
|
|
3339
|
-
return
|
|
3340
|
-
}
|
|
3341
|
-
val startedAt = System.nanoTime()
|
|
3342
|
-
val updateJSON = editorDeleteAndSplitScalar(
|
|
3343
|
-
editorId.toULong(),
|
|
3344
|
-
scalarFrom.toUInt(),
|
|
3345
|
-
scalarTo.toUInt()
|
|
3346
|
-
)
|
|
3347
|
-
recordImeTraceForTesting(
|
|
3348
|
-
"rustDeleteAndSplit",
|
|
3349
|
-
"range=$scalarFrom..$scalarTo rustUs=${nanosToMicros(System.nanoTime() - startedAt)} jsonLength=${updateJSON.length}"
|
|
3350
|
-
)
|
|
3351
|
-
applyRustUpdateJSON(updateJSON)
|
|
3352
|
-
scheduleLineBoundaryInputRefreshForEditor("deleteAndSplit")
|
|
3353
|
-
}
|
|
3354
|
-
|
|
3355
|
-
internal fun currentScalarSelection(): Pair<Int, Int>? {
|
|
3356
|
-
val currentText = text?.toString() ?: return null
|
|
3357
|
-
return normalizedScalarSelectionRange(currentText)
|
|
3358
|
-
}
|
|
3359
|
-
|
|
3360
|
-
internal fun cursorCapsModeForEditor(reqModes: Int, baseCapsMode: Int): Int {
|
|
3361
|
-
val sentenceCapsMode = InputType.TYPE_TEXT_FLAG_CAP_SENTENCES
|
|
3362
|
-
if ((reqModes and sentenceCapsMode) != sentenceCapsMode) return baseCapsMode
|
|
3363
|
-
if ((baseCapsMode and sentenceCapsMode) == sentenceCapsMode) return baseCapsMode
|
|
3364
|
-
if (!isCursorAtRenderedLineStartForSentenceCaps()) return baseCapsMode
|
|
3365
|
-
return baseCapsMode or sentenceCapsMode
|
|
3366
|
-
}
|
|
3367
|
-
|
|
3368
|
-
internal fun textBeforeCursorForImeContextForEditor(n: Int, flags: Int): CharSequence? {
|
|
3369
|
-
if (n <= 0) return ""
|
|
3370
|
-
val content = text ?: return null
|
|
3371
|
-
val start = selectionStart
|
|
3372
|
-
val end = selectionEnd
|
|
3373
|
-
if (start < 0 || end < 0) return null
|
|
3374
|
-
val cursor = minOf(start, end).coerceIn(0, content.length)
|
|
3375
|
-
var effectiveCursor = cursor
|
|
3376
|
-
while (
|
|
3377
|
-
effectiveCursor > 0 &&
|
|
3378
|
-
content[effectiveCursor - 1] == InputSnapshotConstants.SYNTHETIC_PLACEHOLDER_CHARACTER[0]
|
|
3379
|
-
) {
|
|
3380
|
-
effectiveCursor -= 1
|
|
3381
|
-
}
|
|
3382
|
-
val contextStart = maxOf(0, effectiveCursor - n)
|
|
3383
|
-
val context = content.subSequence(contextStart, effectiveCursor)
|
|
3384
|
-
return if ((flags and InputConnection.GET_TEXT_WITH_STYLES) != 0) {
|
|
3385
|
-
context
|
|
3386
|
-
} else {
|
|
3387
|
-
context.toString()
|
|
3388
|
-
}
|
|
3389
|
-
}
|
|
3390
|
-
|
|
3391
|
-
internal fun initialSurroundingTextForImeForEditor(): ImeInitialSurroundingText? {
|
|
3392
|
-
val rawText = text?.toString() ?: return null
|
|
3393
|
-
val placeholder = InputSnapshotConstants.SYNTHETIC_PLACEHOLDER_CHARACTER[0]
|
|
3394
|
-
if (rawText.indexOf(placeholder) < 0) return null
|
|
3395
|
-
val start = selectionStart
|
|
3396
|
-
val end = selectionEnd
|
|
3397
|
-
if (start < 0 || end < 0) return null
|
|
3398
|
-
val rawSelectionStart = start.coerceIn(0, rawText.length)
|
|
3399
|
-
val rawSelectionEnd = end.coerceIn(0, rawText.length)
|
|
3400
|
-
|
|
3401
|
-
val sanitized = StringBuilder(rawText.length)
|
|
3402
|
-
var removedCount = 0
|
|
3403
|
-
var removedBeforeSelectionStart = 0
|
|
3404
|
-
var removedBeforeSelectionEnd = 0
|
|
3405
|
-
rawText.forEachIndexed { index, ch ->
|
|
3406
|
-
if (ch == placeholder) {
|
|
3407
|
-
removedCount += 1
|
|
3408
|
-
if (index < rawSelectionStart) removedBeforeSelectionStart += 1
|
|
3409
|
-
if (index < rawSelectionEnd) removedBeforeSelectionEnd += 1
|
|
3410
|
-
} else {
|
|
3411
|
-
sanitized.append(ch)
|
|
3412
|
-
}
|
|
3413
|
-
}
|
|
3414
|
-
|
|
3415
|
-
return ImeInitialSurroundingText(
|
|
3416
|
-
text = sanitized.toString(),
|
|
3417
|
-
selectionStart = rawSelectionStart - removedBeforeSelectionStart,
|
|
3418
|
-
selectionEnd = rawSelectionEnd - removedBeforeSelectionEnd,
|
|
3419
|
-
originalSelectionStart = rawSelectionStart,
|
|
3420
|
-
originalSelectionEnd = rawSelectionEnd,
|
|
3421
|
-
removedPlaceholderCount = removedCount
|
|
3422
|
-
)
|
|
3423
|
-
}
|
|
3424
|
-
|
|
3425
|
-
private fun isCursorAtRenderedLineStartForSentenceCaps(): Boolean {
|
|
3426
|
-
val currentText = text?.toString() ?: return false
|
|
3427
|
-
val start = selectionStart
|
|
3428
|
-
val end = selectionEnd
|
|
3429
|
-
if (start < 0 || end < 0 || start != end) return false
|
|
3430
|
-
|
|
3431
|
-
val cursor = end.coerceIn(0, currentText.length)
|
|
3432
|
-
return isRenderedLineStartForSentenceCaps(currentText, cursor)
|
|
3433
|
-
}
|
|
3434
|
-
|
|
3435
|
-
private fun isRenderedLineStartForSentenceCaps(text: String, cursor: Int): Boolean {
|
|
3436
|
-
val cursor = cursor.coerceIn(0, text.length)
|
|
3437
|
-
if (cursor == 0) return true
|
|
3438
|
-
|
|
3439
|
-
val lineStart = lastRenderedLineBreakBefore(text, cursor) + 1
|
|
3440
|
-
var index = lineStart
|
|
3441
|
-
while (index < cursor && isIgnoredSentenceCapsLinePrefix(text[index])) {
|
|
3442
|
-
index += 1
|
|
3443
|
-
}
|
|
3444
|
-
if (index == cursor) return true
|
|
3445
|
-
|
|
3446
|
-
val markerEnd = renderedListMarkerEnd(text, index, cursor) ?: return false
|
|
3447
|
-
index = markerEnd
|
|
3448
|
-
while (index < cursor && isIgnoredSentenceCapsLinePrefix(text[index])) {
|
|
3449
|
-
index += 1
|
|
3450
|
-
}
|
|
3451
|
-
return index == cursor
|
|
3452
|
-
}
|
|
3453
|
-
|
|
3454
|
-
private fun isSamsungKeyboardActiveForEditor(): Boolean {
|
|
3455
|
-
val inputMethodId = Settings.Secure.getString(
|
|
3456
|
-
context.contentResolver,
|
|
3457
|
-
Settings.Secure.DEFAULT_INPUT_METHOD
|
|
3458
|
-
) ?: return false
|
|
3459
|
-
return inputMethodId.contains("samsung", ignoreCase = true) ||
|
|
3460
|
-
inputMethodId.contains("honeyboard", ignoreCase = true)
|
|
3461
|
-
}
|
|
3462
|
-
|
|
3463
|
-
private fun lastRenderedLineBreakBefore(text: String, cursor: Int): Int {
|
|
3464
|
-
var index = cursor.coerceAtMost(text.length) - 1
|
|
3465
|
-
while (index >= 0) {
|
|
3466
|
-
when (text[index]) {
|
|
3467
|
-
'\n', '\r' -> return index
|
|
3468
|
-
}
|
|
3469
|
-
index -= 1
|
|
3470
|
-
}
|
|
3471
|
-
return -1
|
|
3472
|
-
}
|
|
3473
|
-
|
|
3474
|
-
private fun isIgnoredSentenceCapsLinePrefix(ch: Char): Boolean =
|
|
3475
|
-
ch == ' ' ||
|
|
3476
|
-
ch == '\t' ||
|
|
3477
|
-
ch == '\u00A0' ||
|
|
3478
|
-
ch == InputSnapshotConstants.SYNTHETIC_PLACEHOLDER_CHARACTER[0]
|
|
3479
|
-
|
|
3480
|
-
private fun renderedListMarkerEnd(text: String, start: Int, endExclusive: Int): Int? {
|
|
3481
|
-
if (start >= endExclusive) return null
|
|
3482
|
-
if (renderedTaskListMarkerEnd(text, start, endExclusive) != null) {
|
|
3483
|
-
return start + 2
|
|
3484
|
-
}
|
|
3485
|
-
if (text[start] == InputSnapshotConstants.UNORDERED_LIST_BULLET[0]) {
|
|
3486
|
-
return start + 1
|
|
3487
|
-
}
|
|
3488
|
-
|
|
3489
|
-
var index = start
|
|
3490
|
-
while (index < endExclusive && text[index].isDigit()) {
|
|
3491
|
-
index += 1
|
|
3492
|
-
}
|
|
3493
|
-
if (index == start || index >= endExclusive) return null
|
|
3494
|
-
return when (text[index]) {
|
|
3495
|
-
'.', ')' -> index + 1
|
|
3496
|
-
else -> null
|
|
3497
|
-
}
|
|
3498
|
-
}
|
|
3499
|
-
|
|
3500
|
-
private fun renderedTaskListMarkerEnd(text: String, start: Int, endExclusive: Int): Int? {
|
|
3501
|
-
if (start + 1 >= endExclusive) return null
|
|
3502
|
-
val marker = text[start]
|
|
3503
|
-
if (marker != '\u2610' && marker != '\u2611') return null
|
|
3504
|
-
return if (text[start + 1] == ' ') start + 2 else null
|
|
3505
|
-
}
|
|
3506
|
-
|
|
3507
|
-
private fun normalizedUtf16SelectionRange(currentText: String): Pair<Int, Int>? {
|
|
3508
|
-
val start = selectionStart
|
|
3509
|
-
val end = selectionEnd
|
|
3510
|
-
if (start < 0 || end < 0) return null
|
|
3511
|
-
val clampedStart = start.coerceIn(0, currentText.length)
|
|
3512
|
-
val clampedEnd = end.coerceIn(0, currentText.length)
|
|
3513
|
-
return minOf(clampedStart, clampedEnd) to maxOf(clampedStart, clampedEnd)
|
|
3514
|
-
}
|
|
3515
|
-
|
|
3516
|
-
private fun normalizedUtf16SelectionRange(): Pair<Int, Int>? {
|
|
3517
|
-
val currentText = text?.toString() ?: return null
|
|
3518
|
-
return normalizedUtf16SelectionRange(currentText)
|
|
3519
|
-
}
|
|
3520
|
-
|
|
3521
|
-
private fun normalizedScalarSelectionRange(currentText: String): Pair<Int, Int>? {
|
|
3522
|
-
val (start, end) = normalizedUtf16SelectionRange(currentText) ?: return null
|
|
3523
|
-
val (snappedStart, snappedEnd) = if (start == end) {
|
|
3524
|
-
val snapped = PositionBridge.snapToScalarBoundary(
|
|
3525
|
-
start,
|
|
3526
|
-
currentText,
|
|
3527
|
-
biasForward = true
|
|
3528
|
-
)
|
|
3529
|
-
snapped to snapped
|
|
3530
|
-
} else {
|
|
3531
|
-
PositionBridge.snapRangeToScalarBoundaries(start, end, currentText)
|
|
3532
|
-
}
|
|
3533
|
-
return PositionBridge.utf16ToScalar(snappedStart, currentText) to
|
|
3534
|
-
PositionBridge.utf16ToScalar(snappedEnd, currentText)
|
|
3535
|
-
}
|
|
3536
|
-
|
|
3537
|
-
private fun rawScalarSelection(currentText: String): Pair<Int, Int>? {
|
|
3538
|
-
val anchor = selectionStart
|
|
3539
|
-
val head = selectionEnd
|
|
3540
|
-
if (anchor < 0 || head < 0) return null
|
|
3541
|
-
val clampedAnchor = anchor.coerceIn(0, currentText.length)
|
|
3542
|
-
val clampedHead = head.coerceIn(0, currentText.length)
|
|
3543
|
-
if (clampedAnchor == clampedHead) {
|
|
3544
|
-
val snapped = PositionBridge.snapToScalarBoundary(
|
|
3545
|
-
clampedAnchor,
|
|
3546
|
-
currentText,
|
|
3547
|
-
biasForward = true
|
|
3548
|
-
)
|
|
3549
|
-
val scalar = PositionBridge.utf16ToScalar(snapped, currentText)
|
|
3550
|
-
return scalar to scalar
|
|
3551
|
-
}
|
|
3552
|
-
val (rangeStart, rangeEnd) = PositionBridge.snapRangeToScalarBoundaries(
|
|
3553
|
-
minOf(clampedAnchor, clampedHead),
|
|
3554
|
-
maxOf(clampedAnchor, clampedHead),
|
|
3555
|
-
currentText
|
|
3556
|
-
)
|
|
3557
|
-
val snappedAnchor = if (clampedAnchor < clampedHead) rangeStart else rangeEnd
|
|
3558
|
-
val snappedHead = if (clampedAnchor < clampedHead) rangeEnd else rangeStart
|
|
3559
|
-
return PositionBridge.utf16ToScalar(snappedAnchor, currentText) to
|
|
3560
|
-
PositionBridge.utf16ToScalar(snappedHead, currentText)
|
|
3561
|
-
}
|
|
3562
|
-
|
|
3563
|
-
|
|
3564
|
-
|
|
3565
|
-
private fun isSelectionInsideList(): Boolean {
|
|
3566
|
-
if (!hasLiveEditor()) return false
|
|
3567
|
-
|
|
3568
|
-
return try {
|
|
3569
|
-
val state = org.json.JSONObject(editorGetCurrentState(editorId.toULong()))
|
|
3570
|
-
val nodes = state.optJSONObject("activeState")?.optJSONObject("nodes")
|
|
3571
|
-
nodes?.optBoolean("bulletList", false) == true ||
|
|
3572
|
-
nodes?.optBoolean("orderedList", false) == true
|
|
3573
|
-
} catch (_: Exception) {
|
|
3574
|
-
false
|
|
3575
|
-
}
|
|
3576
|
-
}
|
|
3577
|
-
|
|
3578
|
-
/**
|
|
3579
|
-
* Paste HTML content through Rust.
|
|
3580
|
-
*/
|
|
3581
|
-
private fun pasteHTML(html: String) {
|
|
3582
|
-
if (!hasLiveEditor()) return
|
|
3583
|
-
syncCurrentSelectionToRust()
|
|
3584
|
-
onInsertContentHtmlInRustForTesting?.let { callback ->
|
|
3585
|
-
callback(html)
|
|
3586
|
-
return
|
|
3587
|
-
}
|
|
3588
|
-
val updateJSON = editorInsertContentHtml(editorId.toULong(), html)
|
|
3589
|
-
applyUpdateJSON(updateJSON)
|
|
3590
|
-
}
|
|
3591
|
-
|
|
3592
|
-
/**
|
|
3593
|
-
* Paste plain text through Rust.
|
|
3594
|
-
*/
|
|
3595
|
-
private fun pastePlainText(text: String) {
|
|
3596
|
-
val (scalarStart, scalarEnd) = currentScalarSelection() ?: return
|
|
3597
|
-
insertPlainTextRangeInRust(scalarStart, scalarEnd, text)
|
|
3598
|
-
}
|
|
3599
|
-
|
|
3600
|
-
// ── Applying Rust State ─────────────────────────────────────────────
|
|
3601
|
-
|
|
3602
|
-
|
|
3603
|
-
private fun applyRenderedSpannable(
|
|
3604
|
-
spannable: CharSequence,
|
|
3605
|
-
replaceRange: RenderReplaceRange? = null,
|
|
3606
|
-
usedPatch: Boolean,
|
|
3607
|
-
preserveInputConnectionForExternalUpdate: Boolean = false
|
|
3608
|
-
) {
|
|
3609
|
-
val startedAt = System.nanoTime()
|
|
3610
|
-
val previousScrollX = scrollX
|
|
3611
|
-
val previousScrollY = scrollY
|
|
3612
|
-
val hadCompositionTracking = hasCompositionTrackingForEditor()
|
|
3613
|
-
var shouldRestartInput = false
|
|
3614
|
-
val mode = if (replaceRange != null) "replace" else "setText"
|
|
3615
|
-
isApplyingRustState = true
|
|
3616
|
-
beginBatchEdit()
|
|
3617
|
-
try {
|
|
3618
|
-
if (replaceRange != null) {
|
|
3619
|
-
editableText.replace(replaceRange.start, replaceRange.endExclusive, spannable)
|
|
3620
|
-
} else {
|
|
3621
|
-
setText(spannable)
|
|
3622
|
-
}
|
|
3623
|
-
lastAuthorizedText = text?.toString().orEmpty()
|
|
3624
|
-
lastAuthorizedRenderedText = text?.let { SpannableStringBuilder(it) }
|
|
3625
|
-
lastAuthorizedTextRevision += 1L
|
|
3626
|
-
clearNativeTextMutationAdoptionSuppression()
|
|
3627
|
-
if (hadCompositionTracking && preserveInputConnectionForExternalUpdate) {
|
|
3628
|
-
clearInputStateForExternalReplacementPreservingConnection()
|
|
3629
|
-
shouldRestartInput = true
|
|
3630
|
-
} else if (hadCompositionTracking) {
|
|
3631
|
-
retireInputConnectionForEditor()
|
|
3632
|
-
shouldRestartInput = true
|
|
3633
|
-
} else {
|
|
3634
|
-
clearCompositionTrackingForEditor()
|
|
3635
|
-
}
|
|
3636
|
-
lastRenderAppliedPatchForTesting = usedPatch
|
|
3637
|
-
clearNativeTextMutationAfterBlurWindow()
|
|
3638
|
-
} finally {
|
|
3639
|
-
endBatchEdit()
|
|
3640
|
-
isApplyingRustState = false
|
|
3641
|
-
}
|
|
3642
|
-
recordImeTraceForTesting(
|
|
3643
|
-
"applyRenderedSpannable",
|
|
3644
|
-
"mode=$mode usedPatch=$usedPatch incomingLength=${spannable.length} replace=${replaceRange?.start}..${replaceRange?.endExclusive} hadComposition=$hadCompositionTracking restartInput=$shouldRestartInput applyUs=${nanosToMicros(System.nanoTime() - startedAt)} scroll=$previousScrollX,$previousScrollY->$scrollX,$scrollY layout=${layout != null}"
|
|
3645
|
-
)
|
|
3646
|
-
invalidateRenderedContent()
|
|
3647
|
-
restartInputAfterCompositionInvalidationIfNeeded(shouldRestartInput)
|
|
3648
|
-
}
|
|
3649
|
-
|
|
3650
|
-
private fun invalidateRenderedContent() {
|
|
3651
|
-
invalidate()
|
|
3652
|
-
postInvalidateOnAnimation()
|
|
3653
|
-
}
|
|
3654
|
-
|
|
3655
|
-
private fun authorizeVisibleTextForMatchedOptimisticRender(spannable: CharSequence) {
|
|
3656
|
-
val startedAt = System.nanoTime()
|
|
3657
|
-
val visibleText = text?.toString().orEmpty()
|
|
3658
|
-
lastAuthorizedText = visibleText
|
|
3659
|
-
lastAuthorizedRenderedText = text?.let { SpannableStringBuilder(it) }
|
|
3660
|
-
?: SpannableStringBuilder(spannable)
|
|
3661
|
-
lastAuthorizedTextRevision += 1L
|
|
3662
|
-
clearNativeTextMutationAdoptionSuppression()
|
|
3663
|
-
clearCompositionTrackingForEditor()
|
|
3664
|
-
lastRenderAppliedPatchForTesting = false
|
|
3665
|
-
clearNativeTextMutationAfterBlurWindow()
|
|
3666
|
-
recordImeTraceForTesting(
|
|
3667
|
-
"reuseOptimisticVisibleTextRender",
|
|
3668
|
-
"textLength=${visibleText.length} applyUs=${nanosToMicros(System.nanoTime() - startedAt)}"
|
|
3669
|
-
)
|
|
3670
|
-
}
|
|
3671
|
-
|
|
3672
|
-
/**
|
|
3673
|
-
* Apply a structural editor update from Rust to the hidden input EditText.
|
|
3674
|
-
*
|
|
3675
|
-
* The visible editor surface renders the block tree. This EditText keeps a
|
|
3676
|
-
* plain text snapshot for input, selection, and IME routing only.
|
|
3677
|
-
*
|
|
3678
|
-
* @param updateJSON The JSON string from editor_insert_text, etc.
|
|
3679
|
-
*/
|
|
3680
|
-
fun applyUpdateJSON(
|
|
3681
|
-
updateJSON: String,
|
|
3682
|
-
notifyListener: Boolean = true,
|
|
3683
|
-
refreshInputConnectionForExternalUpdate: Boolean = false
|
|
3684
|
-
) {
|
|
3685
|
-
val totalStartedAt = System.nanoTime()
|
|
3686
|
-
val previousVisibleText = text?.toString().orEmpty()
|
|
3687
|
-
val parseStartedAt = totalStartedAt
|
|
3688
|
-
val update = try {
|
|
3689
|
-
org.json.JSONObject(updateJSON)
|
|
3690
|
-
} catch (error: Exception) {
|
|
3691
|
-
recordImeTraceForTesting(
|
|
3692
|
-
"applyUpdateJSONNoop",
|
|
3693
|
-
"reason=parseError jsonLength=${updateJSON.length} error=${error.javaClass.simpleName}"
|
|
3694
|
-
)
|
|
3695
|
-
return
|
|
3696
|
-
}
|
|
3697
|
-
cancelDeferredRustUpdateApplication()
|
|
3698
|
-
val parseNanos = System.nanoTime() - parseStartedAt
|
|
3699
|
-
|
|
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)
|
|
3708
|
-
}
|
|
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
|
-
}
|
|
3717
|
-
val shouldSkipRender = !refreshInputConnectionForExternalUpdate &&
|
|
3718
|
-
inputSnapshot != null &&
|
|
3719
|
-
text?.toString() == inputSnapshot &&
|
|
3720
|
-
text?.toString() == lastAuthorizedText &&
|
|
3721
|
-
lastAppliedRenderAppearanceRevision == renderAppearanceRevision
|
|
3722
|
-
val previousScrollX = scrollX
|
|
3723
|
-
val previousScrollY = scrollY
|
|
3724
|
-
|
|
3725
|
-
val buildRenderNanos: Long
|
|
3726
|
-
val applyRenderNanos: Long
|
|
3727
|
-
if (shouldSkipRender) {
|
|
3728
|
-
pendingOptimisticRenderText = null
|
|
3729
|
-
lastRenderAppliedPatchForTesting = false
|
|
3730
|
-
clearNativeTextMutationAdoptionSuppression()
|
|
3731
|
-
clearNativeTextMutationAfterBlurWindow()
|
|
3732
|
-
buildRenderNanos = 0L
|
|
3733
|
-
applyRenderNanos = 0L
|
|
3734
|
-
} else {
|
|
3735
|
-
val buildStartedAt = System.nanoTime()
|
|
3736
|
-
if (inputSnapshot == null) {
|
|
3737
|
-
recordImeTraceForTesting(
|
|
3738
|
-
"applyUpdateJSONNoop",
|
|
3739
|
-
"reason=noRenderTree jsonLength=${updateJSON.length}"
|
|
3740
|
-
)
|
|
3741
|
-
return
|
|
3742
|
-
}
|
|
3743
|
-
val fullSpannable = android.text.SpannableStringBuilder(inputSnapshot)
|
|
3744
|
-
buildRenderNanos = System.nanoTime() - buildStartedAt
|
|
3745
|
-
val applyStartedAt = System.nanoTime()
|
|
3746
|
-
val optimisticText = pendingOptimisticRenderText
|
|
3747
|
-
val canReuseOptimisticVisibleText =
|
|
3748
|
-
optimisticText != null &&
|
|
3749
|
-
text?.toString() == optimisticText &&
|
|
3750
|
-
fullSpannable.toString() == optimisticText
|
|
3751
|
-
if (canReuseOptimisticVisibleText) {
|
|
3752
|
-
authorizeVisibleTextForMatchedOptimisticRender(fullSpannable)
|
|
3753
|
-
} else {
|
|
3754
|
-
applyRenderedSpannable(
|
|
3755
|
-
fullSpannable,
|
|
3756
|
-
usedPatch = false,
|
|
3757
|
-
preserveInputConnectionForExternalUpdate = refreshInputConnectionForExternalUpdate
|
|
3758
|
-
)
|
|
3759
|
-
}
|
|
3760
|
-
pendingOptimisticRenderText = null
|
|
3761
|
-
applyRenderNanos = System.nanoTime() - applyStartedAt
|
|
3762
|
-
lastAppliedRenderAppearanceRevision = renderAppearanceRevision
|
|
3763
|
-
}
|
|
3764
|
-
|
|
3765
|
-
// Apply the selection from the update.
|
|
3766
|
-
val selectionStartedAt = System.nanoTime()
|
|
3767
|
-
val selection = update.optJSONObject("selection")
|
|
3768
|
-
if (selection != null) {
|
|
3769
|
-
applySelectionFromJSON(selection)
|
|
3770
|
-
}
|
|
3771
|
-
val selectionNanos = System.nanoTime() - selectionStartedAt
|
|
3772
|
-
|
|
3773
|
-
val postApplyStartedAt = System.nanoTime()
|
|
3774
|
-
if (notifyListener) {
|
|
3775
|
-
editorListener?.onEditorUpdate(updateJSON)
|
|
3776
|
-
}
|
|
3777
|
-
onUpdateJSONApplied?.invoke(updateJSON)
|
|
3778
|
-
onSelectionOrContentMayChange?.invoke()
|
|
3779
|
-
if (heightBehavior == EditorHeightBehavior.AUTO_GROW) {
|
|
3780
|
-
requestLayout()
|
|
3781
|
-
} else {
|
|
3782
|
-
preserveScrollPosition(previousScrollX, previousScrollY)
|
|
3783
|
-
}
|
|
3784
|
-
refreshInputConnectionAfterExternalTextReplacementIfNeeded(
|
|
3785
|
-
enabled = refreshInputConnectionForExternalUpdate,
|
|
3786
|
-
previousVisibleText = previousVisibleText
|
|
3787
|
-
)
|
|
3788
|
-
val postApplyNanos = System.nanoTime() - postApplyStartedAt
|
|
3789
|
-
|
|
3790
|
-
val totalNanos = System.nanoTime() - totalStartedAt
|
|
3791
|
-
recordImeTraceForTesting(
|
|
3792
|
-
"applyUpdateJSON",
|
|
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)}"
|
|
3794
|
-
)
|
|
3795
|
-
|
|
3796
|
-
if (captureApplyUpdateTraceForTesting) {
|
|
3797
|
-
lastApplyUpdateTraceForTesting = ApplyUpdateTrace(
|
|
3798
|
-
attemptedPatch = false,
|
|
3799
|
-
usedPatch = false,
|
|
3800
|
-
skippedRender = shouldSkipRender,
|
|
3801
|
-
parseNanos = parseNanos,
|
|
3802
|
-
resolveRenderBlocksNanos = resolveRenderBlocksNanos,
|
|
3803
|
-
patchEligibilityNanos = 0L,
|
|
3804
|
-
buildRenderNanos = buildRenderNanos,
|
|
3805
|
-
applyRenderNanos = applyRenderNanos,
|
|
3806
|
-
selectionNanos = selectionNanos,
|
|
3807
|
-
postApplyNanos = postApplyNanos,
|
|
3808
|
-
totalNanos = totalNanos
|
|
3809
|
-
)
|
|
3810
|
-
}
|
|
3811
|
-
}
|
|
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
|
-
|
|
3867
|
-
private fun refreshInputConnectionAfterExternalTextReplacementIfNeeded(
|
|
3868
|
-
enabled: Boolean,
|
|
3869
|
-
previousVisibleText: String
|
|
3870
|
-
) {
|
|
3871
|
-
if (!enabled || !hasFocus()) return
|
|
3872
|
-
val currentVisibleText = text?.toString().orEmpty()
|
|
3873
|
-
if (currentVisibleText == previousVisibleText) return
|
|
3874
|
-
clearInputStateForExternalReplacementPreservingConnection()
|
|
3875
|
-
restartInputForEditor("externalUpdate")
|
|
3876
|
-
}
|
|
3877
|
-
|
|
3878
|
-
private fun clearInputStateForExternalReplacementPreservingConnection() {
|
|
3879
|
-
activeInputConnection?.clearCompositionTrackingForEditor()
|
|
3880
|
-
clearCompositionTrackingForEditor()
|
|
3881
|
-
clearCompositionInvalidationForEditor()
|
|
3882
|
-
clearNativeComposingSpans()
|
|
3883
|
-
}
|
|
3884
|
-
|
|
3885
|
-
private fun textOffsetHitAt(x: Float, y: Float): Pair<Spanned, Int>? {
|
|
3886
|
-
val spannable = text as? Spanned ?: return null
|
|
3887
|
-
val layout = layout ?: return null
|
|
3888
|
-
if (spannable.isEmpty()) return null
|
|
3889
|
-
|
|
3890
|
-
val localX = x - totalPaddingLeft + scrollX
|
|
3891
|
-
val localY = y - totalPaddingTop + scrollY
|
|
3892
|
-
if (localY < 0f || localY > layout.height.toFloat()) {
|
|
3893
|
-
return null
|
|
3894
|
-
}
|
|
3895
|
-
|
|
3896
|
-
val line = layout.getLineForVertical(localY.toInt())
|
|
3897
|
-
val lineLeft = layout.getLineLeft(line)
|
|
3898
|
-
val lineRight = layout.getLineRight(line)
|
|
3899
|
-
if (localX < lineLeft || localX > lineRight) {
|
|
3900
|
-
return null
|
|
3901
|
-
}
|
|
3902
|
-
|
|
3903
|
-
val offset = layout.getOffsetForHorizontal(line, localX)
|
|
3904
|
-
.coerceIn(0, maxOf(spannable.length - 1, 0))
|
|
3905
|
-
return spannable to offset
|
|
3906
|
-
}
|
|
3907
|
-
|
|
3908
|
-
|
|
3909
|
-
|
|
3910
|
-
|
|
3911
|
-
|
|
3912
|
-
|
|
3913
|
-
|
|
3914
|
-
|
|
3915
|
-
override fun onFocusChanged(focused: Boolean, direction: Int, previouslyFocusedRect: Rect?) {
|
|
3916
|
-
super.onFocusChanged(focused, direction, previouslyFocusedRect)
|
|
3917
|
-
if (focused) {
|
|
3918
|
-
clearNativeTextMutationAfterBlurWindow()
|
|
3919
|
-
restartCaretBlink()
|
|
3920
|
-
} else {
|
|
3921
|
-
beginNativeTextMutationAfterBlurWindow()
|
|
3922
|
-
stopCaretBlink()
|
|
3923
|
-
}
|
|
3924
|
-
}
|
|
3925
|
-
|
|
3926
|
-
override fun onWindowFocusChanged(hasWindowFocus: Boolean) {
|
|
3927
|
-
super.onWindowFocusChanged(hasWindowFocus)
|
|
3928
|
-
if (hasWindowFocus) restartCaretBlink() else stopCaretBlink()
|
|
3929
|
-
}
|
|
3930
|
-
|
|
3931
|
-
override fun onAttachedToWindow() {
|
|
3932
|
-
super.onAttachedToWindow()
|
|
3933
|
-
restartCaretBlink()
|
|
3934
|
-
}
|
|
3935
|
-
|
|
3936
|
-
override fun onDetachedFromWindow() {
|
|
3937
|
-
removeCallbacks(caretBlinkRunnable)
|
|
3938
|
-
super.onDetachedFromWindow()
|
|
3939
|
-
}
|
|
3940
|
-
|
|
3941
|
-
|
|
3942
|
-
|
|
3943
|
-
|
|
3944
|
-
|
|
3945
|
-
|
|
3946
|
-
|
|
3947
|
-
/**
|
|
3948
|
-
* Apply a selection from a parsed JSON selection object.
|
|
3949
|
-
*
|
|
3950
|
-
* The selection JSON matches the format from `serialize_editor_update`:
|
|
3951
|
-
* ```json
|
|
3952
|
-
* {"type": "text", "anchor": 5, "head": 5}
|
|
3953
|
-
* {"type": "node", "pos": 10}
|
|
3954
|
-
* {"type": "all"}
|
|
3955
|
-
* ```
|
|
3956
|
-
*
|
|
3957
|
-
* anchor/head from Rust are **document positions** (include structural tokens).
|
|
3958
|
-
* We convert doc→scalar via [editorDocToScalar] before converting to UTF-16.
|
|
3959
|
-
*/
|
|
3960
|
-
private fun applySelectionFromJSON(selection: org.json.JSONObject) {
|
|
3961
|
-
val type = selection.optString("type", "") ?: return
|
|
3962
|
-
if (isEditorDestroyedForInput()) return
|
|
3963
|
-
|
|
3964
|
-
isApplyingRustState = true
|
|
3965
|
-
try {
|
|
3966
|
-
val currentText = text?.toString() ?: ""
|
|
3967
|
-
when (type) {
|
|
3968
|
-
"text" -> {
|
|
3969
|
-
val docAnchor = selection.optInt("anchor", 0)
|
|
3970
|
-
val docHead = selection.optInt("head", 0)
|
|
3971
|
-
// Convert doc positions to scalar offsets.
|
|
3972
|
-
val scalarAnchor = editorDocToScalar(editorId.toULong(), docAnchor.toUInt()).toInt()
|
|
3973
|
-
val scalarHead = editorDocToScalar(editorId.toULong(), docHead.toUInt()).toInt()
|
|
3974
|
-
val anchorUtf16 = PositionBridge.scalarToUtf16(scalarAnchor, currentText)
|
|
3975
|
-
val headUtf16 = PositionBridge.scalarToUtf16(scalarHead, currentText)
|
|
3976
|
-
val len = text?.length ?: 0
|
|
3977
|
-
setSelection(
|
|
3978
|
-
anchorUtf16.coerceIn(0, len),
|
|
3979
|
-
headUtf16.coerceIn(0, len)
|
|
3980
|
-
)
|
|
3981
|
-
}
|
|
3982
|
-
"node" -> {
|
|
3983
|
-
val docPos = selection.optInt("pos", 0)
|
|
3984
|
-
// Convert doc position to scalar offset.
|
|
3985
|
-
val scalarPos = editorDocToScalar(editorId.toULong(), docPos.toUInt()).toInt()
|
|
3986
|
-
val startUtf16 = PositionBridge.scalarToUtf16(scalarPos, currentText)
|
|
3987
|
-
val len = text?.length ?: 0
|
|
3988
|
-
val clamped = startUtf16.coerceIn(0, len)
|
|
3989
|
-
// Select one character (the void node placeholder).
|
|
3990
|
-
val endClamped = (clamped + 1).coerceAtMost(len)
|
|
3991
|
-
setSelection(clamped, endClamped)
|
|
3992
|
-
}
|
|
3993
|
-
"all" -> {
|
|
3994
|
-
selectAll()
|
|
3995
|
-
}
|
|
3996
|
-
}
|
|
3997
|
-
} finally {
|
|
3998
|
-
isApplyingRustState = false
|
|
3999
|
-
}
|
|
4000
|
-
}
|
|
4001
|
-
|
|
4002
|
-
// ── Reconciliation ─────────────────────────────────────────────────
|
|
4003
|
-
|
|
4004
|
-
/**
|
|
4005
|
-
* [TextWatcher] that detects when the EditText's text diverges from the
|
|
4006
|
-
* last Rust-authorized content (e.g., due to IME autocorrect, accessibility
|
|
4007
|
-
* services, or other Android framework mutations that bypass our
|
|
4008
|
-
* [EditorInputConnection]).
|
|
4009
|
-
*
|
|
4010
|
-
* When divergence is detected, Rust's current state is re-fetched and
|
|
4011
|
-
* re-applied — "Rust wins" — to maintain the invariant that the Rust
|
|
4012
|
-
* editor-core is the single source of truth for document content.
|
|
4013
|
-
*/
|
|
4014
|
-
private inner class ReconciliationWatcher : TextWatcher {
|
|
4015
|
-
|
|
4016
|
-
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
|
|
4017
|
-
// No-op: we only need afterTextChanged.
|
|
4018
|
-
}
|
|
4019
|
-
|
|
4020
|
-
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
|
|
4021
|
-
// No-op: we only need afterTextChanged.
|
|
4022
|
-
}
|
|
4023
|
-
|
|
4024
|
-
override fun afterTextChanged(s: Editable?) {
|
|
4025
|
-
if (isApplyingRustState) return
|
|
4026
|
-
if (!hasLiveEditor()) return
|
|
4027
|
-
|
|
4028
|
-
val currentText = s?.toString() ?: ""
|
|
4029
|
-
if (currentText == lastAuthorizedText) return
|
|
4030
|
-
|
|
4031
|
-
val mutation = nativeTextMutationFromAuthorizedDiff(currentText)
|
|
4032
|
-
if (mutation != null && shouldAdoptNativeTextMutation(mutation, allowAfterBlur = true)) {
|
|
4033
|
-
commitNativeTextMutation(mutation)
|
|
4034
|
-
return
|
|
4035
|
-
}
|
|
4036
|
-
|
|
4037
|
-
// Text has diverged from Rust's authorized state.
|
|
4038
|
-
reconciliationCount++
|
|
4039
|
-
Log.w(
|
|
4040
|
-
LOG_TAG,
|
|
4041
|
-
"reconciliation: EditText diverged from Rust state" +
|
|
4042
|
-
" (count=$reconciliationCount," +
|
|
4043
|
-
" editText=${currentText.length} chars," +
|
|
4044
|
-
" authorized=${lastAuthorizedText.length} chars)"
|
|
4045
|
-
)
|
|
4046
|
-
|
|
4047
|
-
// Re-fetch Rust's current state and re-apply ("Rust wins").
|
|
4048
|
-
val stateJSON = editorGetCurrentState(editorId.toULong())
|
|
4049
|
-
applyUpdateJSON(stateJSON)
|
|
4050
|
-
}
|
|
4051
|
-
}
|
|
4052
|
-
|
|
4053
|
-
companion object {
|
|
4054
|
-
private const val DEFAULT_AUTO_CAPITALIZE = "sentences"
|
|
4055
|
-
private const val DEFAULT_AUTO_CORRECT = true
|
|
4056
|
-
private const val DEFAULT_KEYBOARD_TYPE = "default"
|
|
4057
|
-
private const val EMPTY_BLOCK_PLACEHOLDER = '\u200B'
|
|
4058
|
-
private const val IME_TRACE_LIMIT_FOR_TESTING = 80
|
|
4059
|
-
private const val IME_TRACE_LOG_TAG = "NativeEditorIme"
|
|
4060
|
-
private const val NATIVE_TEXT_MUTATION_AFTER_BLUR_WINDOW_MS = 750L
|
|
4061
|
-
private const val RECENT_HANDLED_HARDWARE_KEY_DOWN_WINDOW_MS = 750L
|
|
4062
|
-
private const val LOG_TAG = "NativeEditor"
|
|
4063
|
-
|
|
4064
|
-
// Platform caret blink half-period (Editor.BLINK).
|
|
4065
|
-
private const val CARET_BLINK_INTERVAL_MS = 500L
|
|
4066
|
-
private const val MIN_CARET_WIDTH_PX = 2f
|
|
4067
|
-
}
|
|
4068
|
-
}
|