@openeditor/react-native-prose-editor 0.0.7 → 0.0.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/android/build.gradle +35 -0
- package/android/src/main/java/com/apollohg/editor/CaretGeometry.kt +2 -2
- package/android/src/main/java/com/apollohg/editor/EditorEditText.kt +138 -597
- package/android/src/main/java/com/apollohg/editor/EditorInputConnection.kt +13 -3
- package/android/src/main/java/com/apollohg/editor/InputSnapshotSupport.kt +6 -0
- package/android/src/main/java/com/apollohg/editor/NativeBlockEditorSurface.kt +1086 -0
- package/android/src/main/java/com/apollohg/editor/NativeEditorExpoView.kt +1 -1
- package/android/src/main/java/com/apollohg/editor/NativeEditorModule.kt +26 -50
- package/android/src/main/java/com/apollohg/editor/NativeImageLoader.kt +89 -0
- package/android/src/main/java/com/apollohg/editor/RemoteSelectionOverlayView.kt +31 -66
- package/android/src/main/java/com/apollohg/editor/RichTextEditorView.kt +98 -33
- package/dist/EditorToolbar.d.ts +1 -1
- package/dist/EditorToolbar.js +19 -0
- package/dist/NativeEditorBridge.d.ts +62 -35
- package/dist/NativeEditorBridge.js +122 -79
- package/dist/NativeProseViewer.d.ts +13 -34
- package/dist/NativeProseViewer.js +40 -465
- package/dist/NativeRichTextEditor.d.ts +10 -1
- package/dist/NativeRichTextEditor.js +72 -34
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -1
- package/dist/schemas.d.ts +11 -0
- package/dist/schemas.js +149 -0
- package/dist/useNativeEditor.d.ts +3 -3
- package/dist/useNativeEditor.js +3 -3
- package/ios/EditorCore.xcframework/ios-arm64/libeditor_core.a +0 -0
- package/ios/EditorCore.xcframework/ios-arm64_x86_64-simulator/libeditor_core.a +0 -0
- package/ios/Generated_editor_core.swift +97 -2
- package/ios/NativeBlockEditorSurface.swift +1445 -0
- package/ios/NativeEditorExpoView.swift +3 -3
- package/ios/NativeEditorModule.swift +31 -46
- package/ios/NativeInputSupport.swift +396 -0
- package/ios/PositionBridge.swift +4 -4
- package/ios/RichTextEditorView.swift +352 -786
- package/ios/editor_coreFFI/editor_coreFFI.h +147 -70
- package/package.json +1 -1
- package/rust/android/arm64-v8a/libeditor_core.so +0 -0
- package/rust/android/armeabi-v7a/libeditor_core.so +0 -0
- package/rust/android/x86_64/libeditor_core.so +0 -0
- package/rust/bindings/kotlin/uniffi/editor_core/editor_core.kt +2087 -1399
- package/android/src/androidTest/AndroidManifest.xml +0 -8
- package/android/src/androidTest/java/com/apollohg/editor/MeasureHeightInstrumentedTest.kt +0 -53
- package/android/src/androidTest/java/com/apollohg/editor/NativeDeviceCollaborationInitialSyncTest.kt +0 -241
- package/android/src/androidTest/java/com/apollohg/editor/NativeDeviceImeRegressionTest.kt +0 -338
- package/android/src/androidTest/java/com/apollohg/editor/NativeDeviceOutsideTapTest.kt +0 -789
- package/android/src/androidTest/java/com/apollohg/editor/NativeDevicePerformanceTest.kt +0 -350
- package/android/src/androidTest/java/com/apollohg/editor/NativeEditorOutsideTapActivity.kt +0 -5
- package/android/src/main/java/com/apollohg/editor/NativeProseViewerExpoView.kt +0 -315
- package/android/src/main/java/com/apollohg/editor/RenderBridge.kt +0 -2172
- package/android/src/sharedTest/java/com/apollohg/editor/NativePerformanceSupport.kt +0 -482
- package/android/src/test/java/com/apollohg/editor/CaretGeometryTest.kt +0 -137
- package/android/src/test/java/com/apollohg/editor/EditorEditTextHardwareKeyTest.kt +0 -321
- package/android/src/test/java/com/apollohg/editor/EditorInputConnectionTest.kt +0 -4202
- package/android/src/test/java/com/apollohg/editor/NativeEditorExpoViewTest.kt +0 -2581
- package/android/src/test/java/com/apollohg/editor/NativeEditorModuleTest.kt +0 -27
- package/android/src/test/java/com/apollohg/editor/NativePerformanceTest.kt +0 -197
- package/android/src/test/java/com/apollohg/editor/NativeProseViewerExpoViewTest.kt +0 -84
- package/android/src/test/java/com/apollohg/editor/NativeToolbarTest.kt +0 -544
- package/android/src/test/java/com/apollohg/editor/PositionBridgeTest.kt +0 -461
- package/android/src/test/java/com/apollohg/editor/RenderBridgeTest.kt +0 -2054
- package/android/src/test/java/com/apollohg/editor/RichTextEditorViewTest.kt +0 -1367
- package/ios/EditorLayoutManager.swift +0 -774
- package/ios/NativeProseViewerExpoView.swift +0 -276
- package/ios/RenderBridge.swift +0 -1708
- package/ios/Tests/NativePerformanceTests.swift +0 -687
- package/ios/Tests/PositionBridgeTests.swift +0 -706
- package/ios/Tests/RenderBridgeTests.swift +0 -2236
- package/ios/Tests/RichTextEditorViewTests.swift +0 -6548
|
@@ -1,350 +0,0 @@
|
|
|
1
|
-
package com.openeditor.editor
|
|
2
|
-
|
|
3
|
-
import android.app.Instrumentation
|
|
4
|
-
import android.content.Context
|
|
5
|
-
import android.graphics.Bitmap
|
|
6
|
-
import android.graphics.Canvas
|
|
7
|
-
import android.graphics.Color
|
|
8
|
-
import android.os.Bundle
|
|
9
|
-
import android.view.View
|
|
10
|
-
import android.view.ViewGroup
|
|
11
|
-
import androidx.test.core.app.ApplicationProvider
|
|
12
|
-
import androidx.test.ext.junit.runners.AndroidJUnit4
|
|
13
|
-
import androidx.test.filters.LargeTest
|
|
14
|
-
import androidx.test.platform.app.InstrumentationRegistry
|
|
15
|
-
import java.util.concurrent.atomic.AtomicReference
|
|
16
|
-
import org.junit.Assert.assertEquals
|
|
17
|
-
import org.junit.Assert.assertFalse
|
|
18
|
-
import org.junit.Assert.assertNotNull
|
|
19
|
-
import org.junit.Assert.assertTrue
|
|
20
|
-
import org.junit.Test
|
|
21
|
-
import org.junit.runner.RunWith
|
|
22
|
-
import uniffi.editor_core.*
|
|
23
|
-
|
|
24
|
-
@RunWith(AndroidJUnit4::class)
|
|
25
|
-
@LargeTest
|
|
26
|
-
class NativeDevicePerformanceTest {
|
|
27
|
-
private val instrumentation = InstrumentationRegistry.getInstrumentation()
|
|
28
|
-
private val context = ApplicationProvider.getApplicationContext<Context>()
|
|
29
|
-
private val baseFontSize = 16f
|
|
30
|
-
private val textColor = Color.BLACK
|
|
31
|
-
|
|
32
|
-
@Test
|
|
33
|
-
fun performance_renderBridgeLargeDocument() {
|
|
34
|
-
val renderJson = NativePerformanceFixtureFactory.largeRenderJson()
|
|
35
|
-
|
|
36
|
-
val stats = measureOperation("renderBridgeLargeDocument") {
|
|
37
|
-
val spannable = runOnMainSyncWithResult {
|
|
38
|
-
RenderBridge.buildSpannable(
|
|
39
|
-
renderJson,
|
|
40
|
-
baseFontSize,
|
|
41
|
-
textColor,
|
|
42
|
-
density = context.resources.displayMetrics.density
|
|
43
|
-
)
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
assertTrue("rendered spannable should not be empty", spannable.isNotEmpty())
|
|
47
|
-
if (spannable.isNotEmpty()) {
|
|
48
|
-
runOnMainSyncWithResult {
|
|
49
|
-
spannable.getSpans(0, minOf(spannable.length, 1), Any::class.java)
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
reportStats(stats)
|
|
55
|
-
assertTrue("average render time should be positive", stats.averageMillis > 0.0)
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
@Test
|
|
59
|
-
fun performance_applyUpdateJsonLargeDocument() {
|
|
60
|
-
val updateJson = NativePerformanceFixtureFactory.largeUpdateJson()
|
|
61
|
-
val editText = runOnMainSyncWithResult {
|
|
62
|
-
EditorEditText(context).apply {
|
|
63
|
-
layoutParams = ViewGroup.LayoutParams(
|
|
64
|
-
ViewGroup.LayoutParams.MATCH_PARENT,
|
|
65
|
-
ViewGroup.LayoutParams.WRAP_CONTENT
|
|
66
|
-
)
|
|
67
|
-
setBaseStyle(baseFontSize, textColor, Color.WHITE)
|
|
68
|
-
applyUpdateJSON(updateJson, notifyListener = false)
|
|
69
|
-
layoutView(this, widthPx = 1080, heightPx = 2400, heightMode = View.MeasureSpec.AT_MOST)
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
val stats = measureOperation("applyUpdateJsonLargeDocument") {
|
|
74
|
-
runOnMainSyncWithResult {
|
|
75
|
-
editText.applyUpdateJSON(updateJson, notifyListener = false)
|
|
76
|
-
layoutView(editText, widthPx = 1080, heightPx = 2400, heightMode = View.MeasureSpec.AT_MOST)
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
assertFalse("edit text should contain rendered content", editText.text.isNullOrEmpty())
|
|
80
|
-
assertNotNull("edit text should have a layout after applying update", editText.layout)
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
reportStats(stats)
|
|
84
|
-
assertTrue("average applyUpdateJSON time should be positive", stats.averageMillis > 0.0)
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
@Test
|
|
88
|
-
fun performance_applyRenderPatchLargeDocument() {
|
|
89
|
-
val initialUpdateJson = NativePerformanceFixtureFactory.largeUpdateJson()
|
|
90
|
-
val patchedUpdateJson = NativePerformanceFixtureFactory.largePatchedUpdateJson()
|
|
91
|
-
val editText = runOnMainSyncWithResult {
|
|
92
|
-
EditorEditText(context).apply {
|
|
93
|
-
layoutParams = ViewGroup.LayoutParams(
|
|
94
|
-
ViewGroup.LayoutParams.MATCH_PARENT,
|
|
95
|
-
ViewGroup.LayoutParams.WRAP_CONTENT
|
|
96
|
-
)
|
|
97
|
-
setBaseStyle(baseFontSize, textColor, Color.WHITE)
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
val stats = measureOperation(
|
|
102
|
-
name = "applyRenderPatchLargeDocument",
|
|
103
|
-
beforeEach = {
|
|
104
|
-
runOnMainSyncWithResult {
|
|
105
|
-
editText.applyUpdateJSON(initialUpdateJson, notifyListener = false)
|
|
106
|
-
layoutView(editText, widthPx = 1080, heightPx = 2400, heightMode = View.MeasureSpec.AT_MOST)
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
) {
|
|
110
|
-
runOnMainSyncWithResult {
|
|
111
|
-
editText.applyUpdateJSON(patchedUpdateJson, notifyListener = false)
|
|
112
|
-
layoutView(editText, widthPx = 1080, heightPx = 2400, heightMode = View.MeasureSpec.AT_MOST)
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
assertFalse("edit text should contain rendered content", editText.text.isNullOrEmpty())
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
reportStats(stats)
|
|
119
|
-
assertTrue("average patch apply time should be positive", stats.averageMillis > 0.0)
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
@Test
|
|
123
|
-
fun performance_typingRoundTripLargeDocument() {
|
|
124
|
-
val editorId = editorCreate("{}").toLong()
|
|
125
|
-
try {
|
|
126
|
-
NativePerformanceFixtureFactory.loadLargeDocumentIntoEditor(editorId.toULong())
|
|
127
|
-
val editText = runOnMainSyncWithResult {
|
|
128
|
-
EditorEditText(context).apply {
|
|
129
|
-
layoutParams = ViewGroup.LayoutParams(
|
|
130
|
-
ViewGroup.LayoutParams.MATCH_PARENT,
|
|
131
|
-
ViewGroup.LayoutParams.WRAP_CONTENT
|
|
132
|
-
)
|
|
133
|
-
setBaseStyle(baseFontSize, textColor, Color.WHITE)
|
|
134
|
-
bindEditor(editorId)
|
|
135
|
-
layoutView(this, widthPx = 1080, heightPx = 2400, heightMode = View.MeasureSpec.AT_MOST)
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
val typingOffset = NativePerformanceFixtureFactory.typingCursorOffset(editText.text ?: "")
|
|
139
|
-
|
|
140
|
-
val stats = measureOperation("typingRoundTripLargeDocument") {
|
|
141
|
-
runOnMainSyncWithResult {
|
|
142
|
-
editText.setSelection(typingOffset)
|
|
143
|
-
editText.handleTextCommit("!")
|
|
144
|
-
editText.handleDelete(1, 0)
|
|
145
|
-
layoutView(editText, widthPx = 1080, heightPx = 2400, heightMode = View.MeasureSpec.AT_MOST)
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
assertFalse("edit text should contain rendered content", editText.text.isNullOrEmpty())
|
|
149
|
-
assertNotNull("edit text should have a layout after typing", editText.layout)
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
reportStats(stats)
|
|
153
|
-
assertTrue("average typing time should be positive", stats.averageMillis > 0.0)
|
|
154
|
-
} finally {
|
|
155
|
-
editorDestroy(editorId.toULong())
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
@Test
|
|
160
|
-
fun performance_paragraphSplitRoundTripLargeDocument() {
|
|
161
|
-
val editorId = editorCreate("{}").toLong()
|
|
162
|
-
try {
|
|
163
|
-
val largeDocumentJson = NativePerformanceFixtureFactory.largeDocumentJson()
|
|
164
|
-
NativePerformanceFixtureFactory.loadLargeDocumentIntoEditor(editorId.toULong())
|
|
165
|
-
val editText = runOnMainSyncWithResult {
|
|
166
|
-
EditorEditText(context).apply {
|
|
167
|
-
layoutParams = ViewGroup.LayoutParams(
|
|
168
|
-
ViewGroup.LayoutParams.MATCH_PARENT,
|
|
169
|
-
ViewGroup.LayoutParams.WRAP_CONTENT
|
|
170
|
-
)
|
|
171
|
-
setBaseStyle(baseFontSize, textColor, Color.WHITE)
|
|
172
|
-
bindEditor(editorId)
|
|
173
|
-
layoutView(this, widthPx = 1080, heightPx = 2400, heightMode = View.MeasureSpec.AT_MOST)
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
val splitOffset = NativePerformanceFixtureFactory.typingCursorOffset(editText.text ?: "")
|
|
177
|
-
|
|
178
|
-
val stats = measureOperation(
|
|
179
|
-
name = "paragraphSplitRoundTripLargeDocument",
|
|
180
|
-
beforeEach = {
|
|
181
|
-
runOnMainSyncWithResult {
|
|
182
|
-
editorSetJson(editorId.toULong(), largeDocumentJson)
|
|
183
|
-
editText.applyUpdateJSON(editorGetCurrentState(editorId.toULong()), notifyListener = false)
|
|
184
|
-
editText.setSelection(splitOffset)
|
|
185
|
-
layoutView(editText, widthPx = 1080, heightPx = 2400, heightMode = View.MeasureSpec.AT_MOST)
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
) {
|
|
189
|
-
runOnMainSyncWithResult {
|
|
190
|
-
editText.handleTextCommit("\n")
|
|
191
|
-
layoutView(editText, widthPx = 1080, heightPx = 2400, heightMode = View.MeasureSpec.AT_MOST)
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
assertNotNull("edit text should have a layout after paragraph split", editText.layout)
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
reportStats(stats)
|
|
198
|
-
assertTrue("average paragraph split time should be positive", stats.averageMillis > 0.0)
|
|
199
|
-
} finally {
|
|
200
|
-
editorDestroy(editorId.toULong())
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
@Test
|
|
205
|
-
fun performance_selectionScrubLargeDocument() {
|
|
206
|
-
val editorId = editorCreate("{}").toLong()
|
|
207
|
-
try {
|
|
208
|
-
NativePerformanceFixtureFactory.loadLargeDocumentIntoEditor(editorId.toULong())
|
|
209
|
-
val editText = runOnMainSyncWithResult {
|
|
210
|
-
EditorEditText(context).apply {
|
|
211
|
-
layoutParams = ViewGroup.LayoutParams(
|
|
212
|
-
ViewGroup.LayoutParams.MATCH_PARENT,
|
|
213
|
-
ViewGroup.LayoutParams.WRAP_CONTENT
|
|
214
|
-
)
|
|
215
|
-
setBaseStyle(baseFontSize, textColor, Color.WHITE)
|
|
216
|
-
bindEditor(editorId)
|
|
217
|
-
layoutView(this, widthPx = 1080, heightPx = 2400, heightMode = View.MeasureSpec.AT_MOST)
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
val scrubOffsets = NativePerformanceFixtureFactory.selectionScrubOffsets(
|
|
221
|
-
editText.text ?: "",
|
|
222
|
-
points = 48
|
|
223
|
-
)
|
|
224
|
-
|
|
225
|
-
val stats = measureOperation("selectionScrubLargeDocument") {
|
|
226
|
-
runOnMainSyncWithResult {
|
|
227
|
-
for (offset in scrubOffsets) {
|
|
228
|
-
editText.setSelection(offset)
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
assertEquals(
|
|
233
|
-
"selection should land on the final scrub offset",
|
|
234
|
-
scrubOffsets.last(),
|
|
235
|
-
editText.selectionStart
|
|
236
|
-
)
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
reportStats(stats)
|
|
240
|
-
assertTrue("average selection scrub time should be positive", stats.averageMillis > 0.0)
|
|
241
|
-
} finally {
|
|
242
|
-
editorDestroy(editorId.toULong())
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
@Test
|
|
247
|
-
fun performance_remoteSelectionOverlayRefreshMultiPeerLargeDocument() {
|
|
248
|
-
val updateJson = NativePerformanceFixtureFactory.largeUpdateJson()
|
|
249
|
-
val richTextView = runOnMainSyncWithResult {
|
|
250
|
-
RichTextEditorView(context).apply {
|
|
251
|
-
configure(textSizePx = baseFontSize, textColor = textColor, backgroundColor = Color.WHITE)
|
|
252
|
-
setRemoteSelectionEditorIdForTesting(1L)
|
|
253
|
-
setRemoteSelectionScalarResolverForTesting { _, docPos -> docPos }
|
|
254
|
-
editorEditText.applyUpdateJSON(updateJson, notifyListener = false)
|
|
255
|
-
layoutView(this, widthPx = 1080, heightPx = 1600, heightMode = View.MeasureSpec.EXACTLY)
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
val totalScalar = richTextView.editorEditText.text?.length ?: 0
|
|
260
|
-
val selections = NativePerformanceFixtureFactory.remoteSelections(
|
|
261
|
-
totalScalar,
|
|
262
|
-
peerCount = 24,
|
|
263
|
-
selectionWidth = 24
|
|
264
|
-
)
|
|
265
|
-
runOnMainSyncWithResult {
|
|
266
|
-
richTextView.setRemoteSelections(selections)
|
|
267
|
-
layoutView(richTextView, widthPx = 1080, heightPx = 1600, heightMode = View.MeasureSpec.EXACTLY)
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
val bitmap = Bitmap.createBitmap(1080, 1600, Bitmap.Config.ARGB_8888)
|
|
271
|
-
val canvas = Canvas(bitmap)
|
|
272
|
-
|
|
273
|
-
val stats = measureOperation("remoteSelectionOverlayRefreshMultiPeerLargeDocument") {
|
|
274
|
-
val hasVisibleCaret = runOnMainSyncWithResult {
|
|
275
|
-
bitmap.eraseColor(Color.TRANSPARENT)
|
|
276
|
-
richTextView.setRemoteSelections(selections)
|
|
277
|
-
layoutView(richTextView, widthPx = 1080, heightPx = 1600, heightMode = View.MeasureSpec.EXACTLY)
|
|
278
|
-
richTextView.draw(canvas)
|
|
279
|
-
richTextView.remoteSelectionDebugSnapshotsForTesting().any { it.caretRect != null }
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
assertTrue("remote selection overlay should resolve visible carets", hasVisibleCaret)
|
|
283
|
-
assertEquals("expected one snapshot per peer", selections.size, richTextView.remoteSelectionDebugSnapshotsForTesting().size)
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
reportStats(stats)
|
|
287
|
-
assertTrue("average remote selection refresh time should be positive", stats.averageMillis > 0.0)
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
private fun layoutView(
|
|
291
|
-
view: View,
|
|
292
|
-
widthPx: Int,
|
|
293
|
-
heightPx: Int,
|
|
294
|
-
heightMode: Int
|
|
295
|
-
) {
|
|
296
|
-
val widthSpec = View.MeasureSpec.makeMeasureSpec(widthPx, View.MeasureSpec.EXACTLY)
|
|
297
|
-
val heightSpec = View.MeasureSpec.makeMeasureSpec(heightPx, heightMode)
|
|
298
|
-
view.measure(widthSpec, heightSpec)
|
|
299
|
-
view.layout(0, 0, view.measuredWidth, view.measuredHeight)
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
private fun measureOperation(
|
|
303
|
-
name: String,
|
|
304
|
-
warmupIterations: Int = 3,
|
|
305
|
-
measuredIterations: Int = 5,
|
|
306
|
-
beforeEach: (() -> Unit)? = null,
|
|
307
|
-
block: () -> Unit
|
|
308
|
-
): TimingStats {
|
|
309
|
-
repeat(warmupIterations) {
|
|
310
|
-
beforeEach?.invoke()
|
|
311
|
-
block()
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
val samples = MutableList(measuredIterations) {
|
|
315
|
-
beforeEach?.invoke()
|
|
316
|
-
val startedAt = System.nanoTime()
|
|
317
|
-
block()
|
|
318
|
-
System.nanoTime() - startedAt
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
return TimingStats(name, samples)
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
private fun reportStats(stats: TimingStats) {
|
|
325
|
-
instrumentation.sendStatus(
|
|
326
|
-
0,
|
|
327
|
-
Bundle().apply {
|
|
328
|
-
putString(
|
|
329
|
-
Instrumentation.REPORT_KEY_STREAMRESULT,
|
|
330
|
-
stats.summaryString(tag = "NativeDevicePerformanceTest") + "\n"
|
|
331
|
-
)
|
|
332
|
-
}
|
|
333
|
-
)
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
@Suppress("UNCHECKED_CAST")
|
|
337
|
-
private fun <T> runOnMainSyncWithResult(block: () -> T): T {
|
|
338
|
-
val result = AtomicReference<Any?>()
|
|
339
|
-
val error = AtomicReference<Throwable?>()
|
|
340
|
-
instrumentation.runOnMainSync {
|
|
341
|
-
try {
|
|
342
|
-
result.set(block())
|
|
343
|
-
} catch (throwable: Throwable) {
|
|
344
|
-
error.set(throwable)
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
error.get()?.let { throw it }
|
|
348
|
-
return result.get() as T
|
|
349
|
-
}
|
|
350
|
-
}
|
|
@@ -1,315 +0,0 @@
|
|
|
1
|
-
package com.openeditor.editor
|
|
2
|
-
|
|
3
|
-
import android.content.Intent
|
|
4
|
-
import android.content.Context
|
|
5
|
-
import android.graphics.Color
|
|
6
|
-
import android.net.Uri
|
|
7
|
-
import android.view.MotionEvent
|
|
8
|
-
import android.view.View
|
|
9
|
-
import android.view.ViewGroup
|
|
10
|
-
import expo.modules.kotlin.AppContext
|
|
11
|
-
import expo.modules.kotlin.viewevent.EventDispatcher
|
|
12
|
-
import expo.modules.kotlin.views.ExpoView
|
|
13
|
-
import kotlin.math.abs
|
|
14
|
-
import org.json.JSONArray
|
|
15
|
-
|
|
16
|
-
class NativeProseViewerExpoView(
|
|
17
|
-
context: Context,
|
|
18
|
-
appContext: AppContext
|
|
19
|
-
) : ExpoView(context, appContext) {
|
|
20
|
-
|
|
21
|
-
private val proseView = EditorEditText(context)
|
|
22
|
-
private val onContentHeightChange by EventDispatcher<Map<String, Any>>()
|
|
23
|
-
@Suppress("unused")
|
|
24
|
-
private val onPressLink by EventDispatcher<Map<String, Any>>()
|
|
25
|
-
@Suppress("unused")
|
|
26
|
-
private val onPressMention by EventDispatcher<Map<String, Any>>()
|
|
27
|
-
|
|
28
|
-
private var lastRenderJson: String? = null
|
|
29
|
-
private var lastThemeJson: String? = null
|
|
30
|
-
private var lastEmittedContentHeight = 0
|
|
31
|
-
private var collapsesWhenEmpty = true
|
|
32
|
-
private var isCollapsedEmptyContent = false
|
|
33
|
-
private var enableLinkTaps = true
|
|
34
|
-
private var interceptLinkTaps = false
|
|
35
|
-
internal var suppressContentHeightEventsForTesting = false
|
|
36
|
-
|
|
37
|
-
init {
|
|
38
|
-
proseView.setBaseStyle(
|
|
39
|
-
proseView.textSize,
|
|
40
|
-
proseView.currentTextColor,
|
|
41
|
-
Color.TRANSPARENT
|
|
42
|
-
)
|
|
43
|
-
proseView.isEditable = false
|
|
44
|
-
proseView.inputType = android.text.InputType.TYPE_CLASS_TEXT or
|
|
45
|
-
android.text.InputType.TYPE_TEXT_FLAG_MULTI_LINE or
|
|
46
|
-
android.text.InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS
|
|
47
|
-
proseView.setImageResizingEnabled(false)
|
|
48
|
-
proseView.setHeightBehavior(EditorHeightBehavior.AUTO_GROW)
|
|
49
|
-
proseView.isFocusable = false
|
|
50
|
-
proseView.isFocusableInTouchMode = false
|
|
51
|
-
proseView.isCursorVisible = false
|
|
52
|
-
proseView.isLongClickable = false
|
|
53
|
-
proseView.setTextIsSelectable(false)
|
|
54
|
-
proseView.showSoftInputOnFocus = false
|
|
55
|
-
proseView.setOnTouchListener { _, event ->
|
|
56
|
-
if (event.actionMasked != MotionEvent.ACTION_UP) {
|
|
57
|
-
return@setOnTouchListener false
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
proseView.mentionHitAt(event.x, event.y)?.let { mention ->
|
|
61
|
-
onPressMention(mapOf("docPos" to mention.docPos, "label" to mention.label))
|
|
62
|
-
return@setOnTouchListener true
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
if (!enableLinkTaps) {
|
|
66
|
-
return@setOnTouchListener false
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
val link = proseView.linkHitAt(event.x, event.y) ?: return@setOnTouchListener false
|
|
70
|
-
if (interceptLinkTaps) {
|
|
71
|
-
onPressLink(
|
|
72
|
-
mapOf(
|
|
73
|
-
"href" to link.href,
|
|
74
|
-
"text" to link.text
|
|
75
|
-
)
|
|
76
|
-
)
|
|
77
|
-
return@setOnTouchListener true
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
return@setOnTouchListener openLink(link.href)
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
addView(
|
|
84
|
-
proseView,
|
|
85
|
-
LayoutParams(
|
|
86
|
-
ViewGroup.LayoutParams.MATCH_PARENT,
|
|
87
|
-
ViewGroup.LayoutParams.WRAP_CONTENT
|
|
88
|
-
)
|
|
89
|
-
)
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
fun setRenderJson(renderJson: String?) {
|
|
93
|
-
if (lastRenderJson == renderJson) return
|
|
94
|
-
lastRenderJson = renderJson
|
|
95
|
-
applyRenderJson()
|
|
96
|
-
requestLayout()
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
fun setThemeJson(themeJson: String?) {
|
|
100
|
-
if (lastThemeJson == themeJson) return
|
|
101
|
-
lastThemeJson = themeJson
|
|
102
|
-
proseView.applyTheme(EditorTheme.fromJson(themeJson))
|
|
103
|
-
applyRenderJson()
|
|
104
|
-
requestLayout()
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
fun setCollapsesWhenEmpty(collapsesWhenEmpty: Boolean?) {
|
|
108
|
-
val nextValue = collapsesWhenEmpty ?: true
|
|
109
|
-
if (this.collapsesWhenEmpty == nextValue) return
|
|
110
|
-
this.collapsesWhenEmpty = nextValue
|
|
111
|
-
updateCollapsedEmptyState()
|
|
112
|
-
requestLayout()
|
|
113
|
-
emitContentHeightIfNeeded(force = true)
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
fun setEnableLinkTaps(enableLinkTaps: Boolean?) {
|
|
117
|
-
this.enableLinkTaps = enableLinkTaps ?: true
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
fun setInterceptLinkTaps(interceptLinkTaps: Boolean?) {
|
|
121
|
-
this.interceptLinkTaps = interceptLinkTaps ?: false
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
|
125
|
-
if (isCollapsedEmptyContent) {
|
|
126
|
-
setMeasuredDimension(resolveSize(0, widthMeasureSpec), 0)
|
|
127
|
-
emitContentHeightIfNeeded()
|
|
128
|
-
return
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
val childWidthSpec = getChildMeasureSpec(
|
|
132
|
-
widthMeasureSpec,
|
|
133
|
-
paddingLeft + paddingRight,
|
|
134
|
-
proseView.layoutParams.width
|
|
135
|
-
)
|
|
136
|
-
val childHeightSpec = android.view.View.MeasureSpec.makeMeasureSpec(
|
|
137
|
-
0,
|
|
138
|
-
android.view.View.MeasureSpec.UNSPECIFIED
|
|
139
|
-
)
|
|
140
|
-
proseView.measure(childWidthSpec, childHeightSpec)
|
|
141
|
-
|
|
142
|
-
val resolvedContentHeight = proseView.resolveAutoGrowHeight()
|
|
143
|
-
val desiredWidth = proseView.measuredWidth + paddingLeft + paddingRight
|
|
144
|
-
val desiredHeight = resolvedContentHeight + paddingTop + paddingBottom
|
|
145
|
-
val measuredHeight = when (View.MeasureSpec.getMode(heightMeasureSpec)) {
|
|
146
|
-
View.MeasureSpec.AT_MOST -> desiredHeight.coerceAtMost(
|
|
147
|
-
View.MeasureSpec.getSize(heightMeasureSpec)
|
|
148
|
-
)
|
|
149
|
-
else -> desiredHeight
|
|
150
|
-
}
|
|
151
|
-
setMeasuredDimension(
|
|
152
|
-
resolveSize(desiredWidth, widthMeasureSpec),
|
|
153
|
-
measuredHeight
|
|
154
|
-
)
|
|
155
|
-
emitContentHeightIfNeeded(measuredContentHeight = desiredHeight)
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
|
|
159
|
-
if (isCollapsedEmptyContent) {
|
|
160
|
-
proseView.layout(paddingLeft, paddingTop, right - left - paddingRight, paddingTop)
|
|
161
|
-
emitContentHeightIfNeeded()
|
|
162
|
-
return
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
val childLeft = paddingLeft
|
|
166
|
-
val childTop = paddingTop
|
|
167
|
-
proseView.layout(
|
|
168
|
-
childLeft,
|
|
169
|
-
childTop,
|
|
170
|
-
right - left - paddingRight,
|
|
171
|
-
childTop + proseView.measuredHeight
|
|
172
|
-
)
|
|
173
|
-
emitContentHeightIfNeeded()
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
private fun applyRenderJson() {
|
|
177
|
-
updateCollapsedEmptyState()
|
|
178
|
-
proseView.applyRenderJSON(lastRenderJson ?: "[]")
|
|
179
|
-
proseView.visibility = if (isCollapsedEmptyContent) View.GONE else View.VISIBLE
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
private fun updateCollapsedEmptyState() {
|
|
183
|
-
isCollapsedEmptyContent = collapsesWhenEmpty &&
|
|
184
|
-
renderJsonContainsOnlyEmptyParagraphs(lastRenderJson ?: "[]")
|
|
185
|
-
proseView.visibility = if (isCollapsedEmptyContent) View.GONE else View.VISIBLE
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
private fun emitContentHeightIfNeeded(
|
|
189
|
-
force: Boolean = false,
|
|
190
|
-
measuredContentHeight: Int? = null
|
|
191
|
-
) {
|
|
192
|
-
val contentHeight = if (isCollapsedEmptyContent) {
|
|
193
|
-
0
|
|
194
|
-
} else {
|
|
195
|
-
(
|
|
196
|
-
measuredContentHeight ?: (measureContentHeightPx() + paddingTop + paddingBottom)
|
|
197
|
-
).coerceAtLeast(0)
|
|
198
|
-
}
|
|
199
|
-
if (contentHeight <= 0 && !isCollapsedEmptyContent) {
|
|
200
|
-
return
|
|
201
|
-
}
|
|
202
|
-
if (!force && contentHeight == lastEmittedContentHeight) {
|
|
203
|
-
return
|
|
204
|
-
}
|
|
205
|
-
lastEmittedContentHeight = contentHeight
|
|
206
|
-
if (suppressContentHeightEventsForTesting) {
|
|
207
|
-
return
|
|
208
|
-
}
|
|
209
|
-
onContentHeightChange(mapOf("contentHeight" to contentHeight))
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
private fun measureContentHeightPx(): Int {
|
|
213
|
-
if (isCollapsedEmptyContent) {
|
|
214
|
-
return 0
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
val availableWidthPx = resolveAvailableWidthPx()
|
|
218
|
-
if (
|
|
219
|
-
proseView.measuredWidth <= 0 ||
|
|
220
|
-
abs(proseView.measuredWidth - availableWidthPx) > 1
|
|
221
|
-
) {
|
|
222
|
-
val childWidthSpec = View.MeasureSpec.makeMeasureSpec(
|
|
223
|
-
availableWidthPx,
|
|
224
|
-
View.MeasureSpec.EXACTLY
|
|
225
|
-
)
|
|
226
|
-
val childHeightSpec = View.MeasureSpec.makeMeasureSpec(
|
|
227
|
-
0,
|
|
228
|
-
View.MeasureSpec.UNSPECIFIED
|
|
229
|
-
)
|
|
230
|
-
proseView.measure(childWidthSpec, childHeightSpec)
|
|
231
|
-
}
|
|
232
|
-
return proseView.resolveAutoGrowHeight()
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
private fun resolveAvailableWidthPx(): Int {
|
|
236
|
-
val localWidth = width - paddingLeft - paddingRight
|
|
237
|
-
if (localWidth > 0) {
|
|
238
|
-
return localWidth
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
val parentWidth = ((parent as? View)?.width ?: 0) - paddingLeft - paddingRight
|
|
242
|
-
if (parentWidth > 0) {
|
|
243
|
-
return parentWidth
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
return (resources.displayMetrics.widthPixels - paddingLeft - paddingRight).coerceAtLeast(1)
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
private fun openLink(href: String): Boolean {
|
|
250
|
-
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(href)).apply {
|
|
251
|
-
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
|
252
|
-
}
|
|
253
|
-
return runCatching {
|
|
254
|
-
context.startActivity(intent)
|
|
255
|
-
true
|
|
256
|
-
}.getOrDefault(false)
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
companion object {
|
|
260
|
-
private const val EMPTY_TEXT_BLOCK_PLACEHOLDER = '\u200B'
|
|
261
|
-
|
|
262
|
-
internal fun renderJsonContainsOnlyEmptyParagraphs(renderJson: String): Boolean {
|
|
263
|
-
val elements = try {
|
|
264
|
-
JSONArray(renderJson)
|
|
265
|
-
} catch (_: Exception) {
|
|
266
|
-
return false
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
if (elements.length() == 0) {
|
|
270
|
-
return true
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
var hasParagraph = false
|
|
274
|
-
var paragraphIsOpen = false
|
|
275
|
-
|
|
276
|
-
for (index in 0 until elements.length()) {
|
|
277
|
-
val element = elements.optJSONObject(index) ?: return false
|
|
278
|
-
when (element.optString("type", "")) {
|
|
279
|
-
"blockStart" -> {
|
|
280
|
-
if (
|
|
281
|
-
paragraphIsOpen ||
|
|
282
|
-
element.optString("nodeType", "") != "paragraph" ||
|
|
283
|
-
element.optInt("depth", 0) != 0
|
|
284
|
-
) {
|
|
285
|
-
return false
|
|
286
|
-
}
|
|
287
|
-
paragraphIsOpen = true
|
|
288
|
-
hasParagraph = true
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
"textRun" -> {
|
|
292
|
-
val text = element.optString("text", "")
|
|
293
|
-
if (
|
|
294
|
-
!paragraphIsOpen ||
|
|
295
|
-
!text.all { it == EMPTY_TEXT_BLOCK_PLACEHOLDER }
|
|
296
|
-
) {
|
|
297
|
-
return false
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
"blockEnd" -> {
|
|
302
|
-
if (!paragraphIsOpen) {
|
|
303
|
-
return false
|
|
304
|
-
}
|
|
305
|
-
paragraphIsOpen = false
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
else -> return false
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
return hasParagraph && !paragraphIsOpen
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
}
|