@openeditor/react-native-prose-editor 0.0.6 → 0.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/android/build.gradle +35 -0
- package/android/src/main/java/com/apollohg/editor/CaretGeometry.kt +2 -2
- package/android/src/main/java/com/apollohg/editor/EditorEditText.kt +138 -597
- package/android/src/main/java/com/apollohg/editor/EditorInputConnection.kt +13 -3
- package/android/src/main/java/com/apollohg/editor/InputSnapshotSupport.kt +6 -0
- package/android/src/main/java/com/apollohg/editor/NativeBlockEditorSurface.kt +1086 -0
- package/android/src/main/java/com/apollohg/editor/NativeEditorExpoView.kt +1 -1
- package/android/src/main/java/com/apollohg/editor/NativeEditorModule.kt +26 -50
- package/android/src/main/java/com/apollohg/editor/NativeImageLoader.kt +89 -0
- package/android/src/main/java/com/apollohg/editor/RemoteSelectionOverlayView.kt +31 -66
- package/android/src/main/java/com/apollohg/editor/RichTextEditorView.kt +98 -33
- package/dist/EditorToolbar.d.ts +1 -1
- package/dist/EditorToolbar.js +19 -0
- package/dist/NativeEditorBridge.d.ts +62 -35
- package/dist/NativeEditorBridge.js +122 -79
- package/dist/NativeProseViewer.d.ts +13 -34
- package/dist/NativeProseViewer.js +40 -465
- package/dist/NativeRichTextEditor.d.ts +10 -1
- package/dist/NativeRichTextEditor.js +72 -34
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -1
- package/dist/schemas.d.ts +11 -0
- package/dist/schemas.js +149 -0
- package/dist/useNativeEditor.d.ts +3 -3
- package/dist/useNativeEditor.js +3 -3
- package/ios/EditorCore.xcframework/ios-arm64/libeditor_core.a +0 -0
- package/ios/EditorCore.xcframework/ios-arm64_x86_64-simulator/libeditor_core.a +0 -0
- package/ios/Generated_editor_core.swift +97 -2
- package/ios/NativeBlockEditorSurface.swift +1393 -0
- package/ios/NativeEditorExpoView.swift +3 -3
- package/ios/NativeEditorModule.swift +30 -45
- package/ios/NativeInputSupport.swift +396 -0
- package/ios/PositionBridge.swift +4 -4
- package/ios/RichTextEditorView.swift +333 -786
- package/ios/editor_coreFFI/editor_coreFFI.h +147 -70
- package/package.json +1 -1
- package/rust/android/arm64-v8a/libeditor_core.so +0 -0
- package/rust/android/armeabi-v7a/libeditor_core.so +0 -0
- package/rust/android/x86_64/libeditor_core.so +0 -0
- package/rust/bindings/kotlin/uniffi/editor_core/editor_core.kt +2087 -1399
- package/android/src/androidTest/AndroidManifest.xml +0 -8
- package/android/src/androidTest/java/com/apollohg/editor/MeasureHeightInstrumentedTest.kt +0 -53
- package/android/src/androidTest/java/com/apollohg/editor/NativeDeviceCollaborationInitialSyncTest.kt +0 -241
- package/android/src/androidTest/java/com/apollohg/editor/NativeDeviceImeRegressionTest.kt +0 -338
- package/android/src/androidTest/java/com/apollohg/editor/NativeDeviceOutsideTapTest.kt +0 -789
- package/android/src/androidTest/java/com/apollohg/editor/NativeDevicePerformanceTest.kt +0 -350
- package/android/src/androidTest/java/com/apollohg/editor/NativeEditorOutsideTapActivity.kt +0 -5
- package/android/src/main/java/com/apollohg/editor/NativeProseViewerExpoView.kt +0 -315
- package/android/src/main/java/com/apollohg/editor/RenderBridge.kt +0 -2172
- package/android/src/sharedTest/java/com/apollohg/editor/NativePerformanceSupport.kt +0 -482
- package/android/src/test/java/com/apollohg/editor/CaretGeometryTest.kt +0 -137
- package/android/src/test/java/com/apollohg/editor/EditorEditTextHardwareKeyTest.kt +0 -321
- package/android/src/test/java/com/apollohg/editor/EditorInputConnectionTest.kt +0 -4202
- package/android/src/test/java/com/apollohg/editor/NativeEditorExpoViewTest.kt +0 -2581
- package/android/src/test/java/com/apollohg/editor/NativeEditorModuleTest.kt +0 -27
- package/android/src/test/java/com/apollohg/editor/NativePerformanceTest.kt +0 -197
- package/android/src/test/java/com/apollohg/editor/NativeProseViewerExpoViewTest.kt +0 -84
- package/android/src/test/java/com/apollohg/editor/NativeToolbarTest.kt +0 -544
- package/android/src/test/java/com/apollohg/editor/PositionBridgeTest.kt +0 -461
- package/android/src/test/java/com/apollohg/editor/RenderBridgeTest.kt +0 -2054
- package/android/src/test/java/com/apollohg/editor/RichTextEditorViewTest.kt +0 -1367
- package/ios/EditorLayoutManager.swift +0 -774
- package/ios/NativeProseViewerExpoView.swift +0 -276
- package/ios/RenderBridge.swift +0 -1708
- package/ios/Tests/NativePerformanceTests.swift +0 -687
- package/ios/Tests/PositionBridgeTests.swift +0 -706
- package/ios/Tests/RenderBridgeTests.swift +0 -2236
- package/ios/Tests/RichTextEditorViewTests.swift +0 -6548
|
@@ -1,1367 +0,0 @@
|
|
|
1
|
-
package com.openeditor.editor
|
|
2
|
-
|
|
3
|
-
import android.graphics.Color
|
|
4
|
-
import android.graphics.Bitmap
|
|
5
|
-
import android.graphics.Canvas
|
|
6
|
-
import android.text.SpannableStringBuilder
|
|
7
|
-
import android.text.StaticLayout
|
|
8
|
-
import android.text.TextPaint
|
|
9
|
-
import android.text.Spanned
|
|
10
|
-
import android.widget.LinearLayout
|
|
11
|
-
import android.view.MotionEvent
|
|
12
|
-
import android.view.View
|
|
13
|
-
import android.view.ViewGroup
|
|
14
|
-
import android.widget.FrameLayout
|
|
15
|
-
import org.json.JSONArray
|
|
16
|
-
import org.json.JSONObject
|
|
17
|
-
import org.junit.Assert.assertEquals
|
|
18
|
-
import org.junit.Assert.assertFalse
|
|
19
|
-
import org.junit.Assert.assertNotNull
|
|
20
|
-
import org.junit.Assert.assertNull
|
|
21
|
-
import org.junit.Assert.assertTrue
|
|
22
|
-
import org.junit.Test
|
|
23
|
-
import org.junit.runner.RunWith
|
|
24
|
-
import org.robolectric.RobolectricTestRunner
|
|
25
|
-
import org.robolectric.RuntimeEnvironment
|
|
26
|
-
import org.robolectric.annotation.Config
|
|
27
|
-
|
|
28
|
-
@RunWith(RobolectricTestRunner::class)
|
|
29
|
-
@Config(sdk = [34])
|
|
30
|
-
class RichTextEditorViewTest {
|
|
31
|
-
private class InterceptAwareFrameLayout(context: android.content.Context) : FrameLayout(context) {
|
|
32
|
-
var disallowInterceptRequested = false
|
|
33
|
-
|
|
34
|
-
override fun requestDisallowInterceptTouchEvent(disallowIntercept: Boolean) {
|
|
35
|
-
disallowInterceptRequested = disallowIntercept
|
|
36
|
-
super.requestDisallowInterceptTouchEvent(disallowIntercept)
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
private fun exampleTheme(markerScale: Float = 2f): EditorTheme? =
|
|
41
|
-
EditorTheme.fromJson(
|
|
42
|
-
"""
|
|
43
|
-
{
|
|
44
|
-
"backgroundColor": "#f6f1e8",
|
|
45
|
-
"text": { "color": "#2a2118", "fontSize": 17 },
|
|
46
|
-
"paragraph": { "spacingAfter": 16 },
|
|
47
|
-
"list": { "indent": 14, "itemSpacing": 6, "markerColor": "#9a4f2d", "markerScale": $markerScale }
|
|
48
|
-
}
|
|
49
|
-
""".trimIndent()
|
|
50
|
-
)
|
|
51
|
-
|
|
52
|
-
private fun exampleRenderJson(): String = """
|
|
53
|
-
[
|
|
54
|
-
{"type":"blockStart","nodeType":"paragraph","depth":0},
|
|
55
|
-
{"type":"textRun","text":"Native Editor example app.","marks":["bold"]},
|
|
56
|
-
{"type":"blockEnd"},
|
|
57
|
-
{"type":"blockStart","nodeType":"paragraph","depth":0},
|
|
58
|
-
{"type":"textRun","text":"Use this screen to test focus, theme updates, lists, line breaks, toolbar behavior, and optional addons.","marks":[]},
|
|
59
|
-
{"type":"blockEnd"},
|
|
60
|
-
{"type":"blockStart","nodeType":"paragraph","depth":0},
|
|
61
|
-
{"type":"textRun","text":"Enable mentions above, then type @ after a space, on a blank line, or after punctuation to show native mention suggestions in the toolbar.","marks":[]},
|
|
62
|
-
{"type":"blockEnd"},
|
|
63
|
-
{"type":"blockStart","nodeType":"listItem","depth":1,"listContext":{"ordered":false,"index":1,"total":2,"start":1,"isFirst":true,"isLast":false}},
|
|
64
|
-
{"type":"blockStart","nodeType":"paragraph","depth":2},
|
|
65
|
-
{"type":"textRun","text":"Try typing","marks":[]},
|
|
66
|
-
{"type":"blockEnd"},
|
|
67
|
-
{"type":"blockEnd"},
|
|
68
|
-
{"type":"blockStart","nodeType":"listItem","depth":1,"listContext":{"ordered":false,"index":2,"total":2,"start":1,"isFirst":false,"isLast":true}},
|
|
69
|
-
{"type":"blockStart","nodeType":"paragraph","depth":2},
|
|
70
|
-
{"type":"textRun","text":"Try list indenting","marks":[]},
|
|
71
|
-
{"type":"blockEnd"},
|
|
72
|
-
{"type":"blockEnd"},
|
|
73
|
-
{"type":"blockStart","nodeType":"paragraph","depth":0},
|
|
74
|
-
{"type":"blockEnd"}
|
|
75
|
-
]
|
|
76
|
-
""".trimIndent()
|
|
77
|
-
|
|
78
|
-
private fun singleBulletListRenderJson(): String = """
|
|
79
|
-
[
|
|
80
|
-
{"type":"blockStart","nodeType":"listItem","depth":0,"listContext":{"ordered":false,"index":1,"total":1,"start":1,"isFirst":true,"isLast":true}},
|
|
81
|
-
{"type":"blockStart","nodeType":"paragraph","depth":1},
|
|
82
|
-
{"type":"textRun","text":"Bullet item","marks":[]},
|
|
83
|
-
{"type":"blockEnd"},
|
|
84
|
-
{"type":"blockEnd"}
|
|
85
|
-
]
|
|
86
|
-
""".trimIndent()
|
|
87
|
-
|
|
88
|
-
private fun emptyParagraphRenderJson(): String = """
|
|
89
|
-
[
|
|
90
|
-
{"type":"blockStart","nodeType":"paragraph","depth":0},
|
|
91
|
-
{"type":"textRun","text":"\u200B","marks":[]},
|
|
92
|
-
{"type":"blockEnd"}
|
|
93
|
-
]
|
|
94
|
-
""".trimIndent()
|
|
95
|
-
|
|
96
|
-
private fun imageRenderJson(): String = """
|
|
97
|
-
[
|
|
98
|
-
{"type":"blockStart","nodeType":"paragraph","depth":0},
|
|
99
|
-
{"type":"textRun","text":"Hello","marks":[]},
|
|
100
|
-
{"type":"blockEnd"},
|
|
101
|
-
{"type":"voidBlock","nodeType":"image","docPos":7,"attrs":{"src":"https://example.com/cat.png","width":140,"height":80}},
|
|
102
|
-
{"type":"blockStart","nodeType":"paragraph","depth":0},
|
|
103
|
-
{"type":"blockEnd"}
|
|
104
|
-
]
|
|
105
|
-
""".trimIndent()
|
|
106
|
-
|
|
107
|
-
private fun paragraphRenderBlock(text: String): JSONArray {
|
|
108
|
-
return JSONArray().apply {
|
|
109
|
-
put(
|
|
110
|
-
JSONObject()
|
|
111
|
-
.put("type", "blockStart")
|
|
112
|
-
.put("nodeType", "paragraph")
|
|
113
|
-
.put("depth", 0)
|
|
114
|
-
)
|
|
115
|
-
put(
|
|
116
|
-
JSONObject()
|
|
117
|
-
.put("type", "textRun")
|
|
118
|
-
.put("text", text)
|
|
119
|
-
.put("marks", JSONArray())
|
|
120
|
-
)
|
|
121
|
-
put(JSONObject().put("type", "blockEnd"))
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
private fun renderUpdateJson(
|
|
126
|
-
blocks: JSONArray,
|
|
127
|
-
includeFullRenderBlocks: Boolean = true,
|
|
128
|
-
renderPatch: JSONObject? = null
|
|
129
|
-
): String {
|
|
130
|
-
return JSONObject().apply {
|
|
131
|
-
if (includeFullRenderBlocks) {
|
|
132
|
-
put("renderBlocks", blocks)
|
|
133
|
-
}
|
|
134
|
-
if (renderPatch != null) {
|
|
135
|
-
put("renderPatch", renderPatch)
|
|
136
|
-
}
|
|
137
|
-
}.toString()
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
@Test
|
|
141
|
-
fun `placeholder shows for rendered empty paragraph`() {
|
|
142
|
-
val editText = EditorEditText(RuntimeEnvironment.getApplication())
|
|
143
|
-
editText.placeholderText = "Type here"
|
|
144
|
-
editText.applyRenderJSON(emptyParagraphRenderJson())
|
|
145
|
-
|
|
146
|
-
assertTrue(editText.shouldDisplayPlaceholderForTesting())
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
@Test
|
|
150
|
-
fun `apply update json resolves patch-only payload for middle paragraph split`() {
|
|
151
|
-
val editText = EditorEditText(RuntimeEnvironment.getApplication())
|
|
152
|
-
val initialBlocks = JSONArray().apply {
|
|
153
|
-
put(paragraphRenderBlock("Alpha"))
|
|
154
|
-
put(paragraphRenderBlock("Beta"))
|
|
155
|
-
put(paragraphRenderBlock("Gamma"))
|
|
156
|
-
}
|
|
157
|
-
editText.applyUpdateJSON(renderUpdateJson(initialBlocks), notifyListener = false)
|
|
158
|
-
|
|
159
|
-
val patchedBlocks = JSONArray().apply {
|
|
160
|
-
put(paragraphRenderBlock("Alpha"))
|
|
161
|
-
put(paragraphRenderBlock("Beta"))
|
|
162
|
-
put(paragraphRenderBlock("\u200B"))
|
|
163
|
-
put(paragraphRenderBlock("Gamma"))
|
|
164
|
-
}
|
|
165
|
-
val renderPatch = JSONObject()
|
|
166
|
-
.put("startIndex", 1)
|
|
167
|
-
.put("deleteCount", 2)
|
|
168
|
-
.put(
|
|
169
|
-
"renderBlocks",
|
|
170
|
-
JSONArray().apply {
|
|
171
|
-
put(paragraphRenderBlock("Beta"))
|
|
172
|
-
put(paragraphRenderBlock("\u200B"))
|
|
173
|
-
put(paragraphRenderBlock("Gamma"))
|
|
174
|
-
}
|
|
175
|
-
)
|
|
176
|
-
|
|
177
|
-
editText.applyUpdateJSON(
|
|
178
|
-
renderUpdateJson(
|
|
179
|
-
patchedBlocks,
|
|
180
|
-
includeFullRenderBlocks = false,
|
|
181
|
-
renderPatch = renderPatch
|
|
182
|
-
),
|
|
183
|
-
notifyListener = false
|
|
184
|
-
)
|
|
185
|
-
|
|
186
|
-
assertEquals("Alpha\nBeta\n\u200B\nGamma", editText.text?.toString())
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
@Test
|
|
190
|
-
fun `apply update json skips render work when render blocks are unchanged`() {
|
|
191
|
-
val editText = EditorEditText(RuntimeEnvironment.getApplication()).apply {
|
|
192
|
-
captureApplyUpdateTraceForTesting = true
|
|
193
|
-
}
|
|
194
|
-
val initialBlocks = JSONArray().apply {
|
|
195
|
-
put(paragraphRenderBlock("Alpha"))
|
|
196
|
-
put(paragraphRenderBlock("Beta"))
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
editText.applyUpdateJSON(renderUpdateJson(initialBlocks), notifyListener = false)
|
|
200
|
-
editText.lastApplyUpdateTrace()
|
|
201
|
-
|
|
202
|
-
val selectionOnlyUpdate = JSONObject()
|
|
203
|
-
.put("renderBlocks", JSONArray(initialBlocks.toString()))
|
|
204
|
-
.toString()
|
|
205
|
-
|
|
206
|
-
editText.applyUpdateJSON(selectionOnlyUpdate, notifyListener = false)
|
|
207
|
-
|
|
208
|
-
val trace = editText.lastApplyUpdateTrace()
|
|
209
|
-
assertNotNull(trace)
|
|
210
|
-
assertTrue(trace?.skippedRender == true)
|
|
211
|
-
assertFalse(trace?.usedPatch == true)
|
|
212
|
-
assertEquals(0L, trace?.buildRenderNanos)
|
|
213
|
-
assertEquals(0L, trace?.applyRenderNanos)
|
|
214
|
-
assertEquals("Alpha\nBeta", editText.text?.toString())
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
@Test
|
|
218
|
-
fun `placeholder hides for rendered non-empty paragraph`() {
|
|
219
|
-
val editText = EditorEditText(RuntimeEnvironment.getApplication())
|
|
220
|
-
editText.placeholderText = "Type here"
|
|
221
|
-
editText.setText("Hello")
|
|
222
|
-
|
|
223
|
-
assertTrue(!editText.shouldDisplayPlaceholderForTesting())
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
@Test
|
|
227
|
-
fun `multiline placeholder expands empty editor height`() {
|
|
228
|
-
val editText = EditorEditText(RuntimeEnvironment.getApplication())
|
|
229
|
-
editText.setHeightBehavior(EditorHeightBehavior.AUTO_GROW)
|
|
230
|
-
editText.placeholderText =
|
|
231
|
-
"Type a much longer placeholder that should wrap onto multiple lines in the empty editor"
|
|
232
|
-
editText.applyRenderJSON(emptyParagraphRenderJson())
|
|
233
|
-
|
|
234
|
-
val widthSpec = View.MeasureSpec.makeMeasureSpec(220, View.MeasureSpec.EXACTLY)
|
|
235
|
-
val heightSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)
|
|
236
|
-
editText.measure(widthSpec, heightSpec)
|
|
237
|
-
editText.layout(0, 0, editText.measuredWidth, editText.measuredHeight)
|
|
238
|
-
|
|
239
|
-
val availableWidth =
|
|
240
|
-
editText.measuredWidth - editText.compoundPaddingLeft - editText.compoundPaddingRight
|
|
241
|
-
val expectedPlaceholderHeight =
|
|
242
|
-
StaticLayout.Builder
|
|
243
|
-
.obtain(
|
|
244
|
-
editText.placeholderText,
|
|
245
|
-
0,
|
|
246
|
-
editText.placeholderText.length,
|
|
247
|
-
editText.paint,
|
|
248
|
-
availableWidth
|
|
249
|
-
)
|
|
250
|
-
.setAlignment(android.text.Layout.Alignment.ALIGN_NORMAL)
|
|
251
|
-
.setIncludePad(editText.includeFontPadding)
|
|
252
|
-
.build()
|
|
253
|
-
.height +
|
|
254
|
-
editText.compoundPaddingTop +
|
|
255
|
-
editText.compoundPaddingBottom
|
|
256
|
-
|
|
257
|
-
assertTrue(editText.measuredHeight >= expectedPlaceholderHeight)
|
|
258
|
-
assertTrue(editText.resolveAutoGrowHeight() >= expectedPlaceholderHeight)
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
@Test
|
|
262
|
-
fun `placeholder uses paragraph font size from theme`() {
|
|
263
|
-
val context = RuntimeEnvironment.getApplication()
|
|
264
|
-
val density = context.resources.displayMetrics.density
|
|
265
|
-
val editText = EditorEditText(context)
|
|
266
|
-
editText.setBaseStyle(24f * density, Color.BLACK, Color.WHITE)
|
|
267
|
-
editText.setHeightBehavior(EditorHeightBehavior.AUTO_GROW)
|
|
268
|
-
editText.placeholderText = "Placeholder wraps"
|
|
269
|
-
editText.applyTheme(
|
|
270
|
-
EditorTheme.fromJson(
|
|
271
|
-
"""
|
|
272
|
-
{
|
|
273
|
-
"text": { "fontSize": 12 },
|
|
274
|
-
"paragraph": { "fontSize": 10 }
|
|
275
|
-
}
|
|
276
|
-
""".trimIndent()
|
|
277
|
-
)
|
|
278
|
-
)
|
|
279
|
-
editText.applyRenderJSON(emptyParagraphRenderJson())
|
|
280
|
-
|
|
281
|
-
val widthSpec = View.MeasureSpec.makeMeasureSpec(220, View.MeasureSpec.EXACTLY)
|
|
282
|
-
val heightSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)
|
|
283
|
-
editText.measure(widthSpec, heightSpec)
|
|
284
|
-
editText.layout(0, 0, editText.measuredWidth, editText.measuredHeight)
|
|
285
|
-
|
|
286
|
-
val availableWidth =
|
|
287
|
-
editText.measuredWidth - editText.compoundPaddingLeft - editText.compoundPaddingRight
|
|
288
|
-
val expectedPlaceholderHeight =
|
|
289
|
-
StaticLayout.Builder
|
|
290
|
-
.obtain(
|
|
291
|
-
editText.placeholderText,
|
|
292
|
-
0,
|
|
293
|
-
editText.placeholderText.length,
|
|
294
|
-
TextPaint(editText.paint).apply {
|
|
295
|
-
textSize = 10f * density
|
|
296
|
-
},
|
|
297
|
-
availableWidth
|
|
298
|
-
)
|
|
299
|
-
.setAlignment(android.text.Layout.Alignment.ALIGN_NORMAL)
|
|
300
|
-
.setIncludePad(editText.includeFontPadding)
|
|
301
|
-
.build()
|
|
302
|
-
.height +
|
|
303
|
-
editText.compoundPaddingTop +
|
|
304
|
-
editText.compoundPaddingBottom
|
|
305
|
-
|
|
306
|
-
assertEquals(expectedPlaceholderHeight, editText.resolveAutoGrowHeight())
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
@Test
|
|
310
|
-
fun `editor disables clickable links`() {
|
|
311
|
-
val editText = EditorEditText(RuntimeEnvironment.getApplication())
|
|
312
|
-
|
|
313
|
-
assertTrue(!editText.linksClickable)
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
@Test
|
|
317
|
-
fun `editor auto grow height resolves from text layout`() {
|
|
318
|
-
val editText = EditorEditText(RuntimeEnvironment.getApplication())
|
|
319
|
-
editText.setText("Line one\nLine two\nLine three")
|
|
320
|
-
|
|
321
|
-
val widthSpec = View.MeasureSpec.makeMeasureSpec(600, View.MeasureSpec.EXACTLY)
|
|
322
|
-
val heightSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)
|
|
323
|
-
editText.measure(widthSpec, heightSpec)
|
|
324
|
-
editText.layout(0, 0, editText.measuredWidth, editText.measuredHeight)
|
|
325
|
-
|
|
326
|
-
val expectedHeight =
|
|
327
|
-
(editText.layout?.height ?: 0) + editText.compoundPaddingTop + editText.compoundPaddingBottom
|
|
328
|
-
|
|
329
|
-
assertTrue(expectedHeight > 0)
|
|
330
|
-
assertEquals(expectedHeight, editText.resolveAutoGrowHeight())
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
@Test
|
|
334
|
-
fun `rich text editor auto grow measures to content height within parent limit`() {
|
|
335
|
-
val richTextEditorView = RichTextEditorView(RuntimeEnvironment.getApplication())
|
|
336
|
-
richTextEditorView.setHeightBehavior(EditorHeightBehavior.AUTO_GROW)
|
|
337
|
-
richTextEditorView.editorEditText.setText("Short content")
|
|
338
|
-
|
|
339
|
-
val widthSpec = View.MeasureSpec.makeMeasureSpec(600, View.MeasureSpec.EXACTLY)
|
|
340
|
-
val heightSpec = View.MeasureSpec.makeMeasureSpec(1600, View.MeasureSpec.AT_MOST)
|
|
341
|
-
richTextEditorView.measure(widthSpec, heightSpec)
|
|
342
|
-
richTextEditorView.layout(
|
|
343
|
-
0,
|
|
344
|
-
0,
|
|
345
|
-
richTextEditorView.measuredWidth,
|
|
346
|
-
richTextEditorView.measuredHeight
|
|
347
|
-
)
|
|
348
|
-
|
|
349
|
-
val contentHeight = richTextEditorView.editorEditText.resolveAutoGrowHeight()
|
|
350
|
-
|
|
351
|
-
assertTrue(contentHeight > 0)
|
|
352
|
-
assertEquals(contentHeight, richTextEditorView.measuredHeight)
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
@Test
|
|
356
|
-
fun `rich text editor auto grow ignores oversized exact parent height`() {
|
|
357
|
-
val richTextEditorView = RichTextEditorView(RuntimeEnvironment.getApplication())
|
|
358
|
-
richTextEditorView.setHeightBehavior(EditorHeightBehavior.AUTO_GROW)
|
|
359
|
-
richTextEditorView.editorEditText.setText("Short content")
|
|
360
|
-
|
|
361
|
-
val widthSpec = View.MeasureSpec.makeMeasureSpec(600, View.MeasureSpec.EXACTLY)
|
|
362
|
-
val wrapHeightSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)
|
|
363
|
-
richTextEditorView.measure(widthSpec, wrapHeightSpec)
|
|
364
|
-
richTextEditorView.layout(
|
|
365
|
-
0,
|
|
366
|
-
0,
|
|
367
|
-
richTextEditorView.measuredWidth,
|
|
368
|
-
richTextEditorView.measuredHeight
|
|
369
|
-
)
|
|
370
|
-
val expectedContentHeight = richTextEditorView.editorEditText.resolveAutoGrowHeight()
|
|
371
|
-
|
|
372
|
-
val oversizedExactHeightSpec = View.MeasureSpec.makeMeasureSpec(1600, View.MeasureSpec.EXACTLY)
|
|
373
|
-
richTextEditorView.measure(widthSpec, oversizedExactHeightSpec)
|
|
374
|
-
richTextEditorView.layout(
|
|
375
|
-
0,
|
|
376
|
-
0,
|
|
377
|
-
richTextEditorView.measuredWidth,
|
|
378
|
-
richTextEditorView.measuredHeight
|
|
379
|
-
)
|
|
380
|
-
|
|
381
|
-
assertEquals(expectedContentHeight, richTextEditorView.measuredHeight)
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
@Test
|
|
385
|
-
fun `editor auto grow height ignores stale exact measured height before layout`() {
|
|
386
|
-
val context = RuntimeEnvironment.getApplication()
|
|
387
|
-
val expectedView = EditorEditText(context)
|
|
388
|
-
expectedView.setText("Short content")
|
|
389
|
-
|
|
390
|
-
val widthSpec = View.MeasureSpec.makeMeasureSpec(600, View.MeasureSpec.EXACTLY)
|
|
391
|
-
val wrapHeightSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)
|
|
392
|
-
expectedView.measure(widthSpec, wrapHeightSpec)
|
|
393
|
-
expectedView.layout(0, 0, expectedView.measuredWidth, expectedView.measuredHeight)
|
|
394
|
-
val expectedHeight = expectedView.resolveAutoGrowHeight()
|
|
395
|
-
|
|
396
|
-
val subject = EditorEditText(context)
|
|
397
|
-
subject.setText("Short content")
|
|
398
|
-
val fixedHeightSpec = View.MeasureSpec.makeMeasureSpec(1200, View.MeasureSpec.EXACTLY)
|
|
399
|
-
subject.measure(widthSpec, fixedHeightSpec)
|
|
400
|
-
|
|
401
|
-
assertEquals(1200, subject.measuredHeight)
|
|
402
|
-
val resolvedHeight = subject.resolveAutoGrowHeight()
|
|
403
|
-
assertEquals(
|
|
404
|
-
"expected=$expectedHeight resolved=$resolvedHeight " +
|
|
405
|
-
"isLaidOut=${subject.isLaidOut} measuredWidth=${subject.measuredWidth} " +
|
|
406
|
-
"layoutHeight=${subject.layout?.height} lineHeight=${subject.lineHeight} " +
|
|
407
|
-
"compoundPaddingTop=${subject.compoundPaddingTop} compoundPaddingBottom=${subject.compoundPaddingBottom}",
|
|
408
|
-
expectedHeight,
|
|
409
|
-
resolvedHeight
|
|
410
|
-
)
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
@Test
|
|
414
|
-
fun `editor auto grow height ignores stale exact height after layout`() {
|
|
415
|
-
val context = RuntimeEnvironment.getApplication()
|
|
416
|
-
val widthSpec = View.MeasureSpec.makeMeasureSpec(600, View.MeasureSpec.EXACTLY)
|
|
417
|
-
val wrapHeightSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)
|
|
418
|
-
|
|
419
|
-
val expectedView = EditorEditText(context)
|
|
420
|
-
expectedView.layoutParams = ViewGroup.LayoutParams(
|
|
421
|
-
ViewGroup.LayoutParams.MATCH_PARENT,
|
|
422
|
-
ViewGroup.LayoutParams.WRAP_CONTENT
|
|
423
|
-
)
|
|
424
|
-
expectedView.setText("Short content")
|
|
425
|
-
expectedView.measure(widthSpec, wrapHeightSpec)
|
|
426
|
-
expectedView.layout(0, 0, expectedView.measuredWidth, expectedView.measuredHeight)
|
|
427
|
-
val expectedHeight = expectedView.resolveAutoGrowHeight()
|
|
428
|
-
|
|
429
|
-
val subject = EditorEditText(context)
|
|
430
|
-
subject.layoutParams = ViewGroup.LayoutParams(
|
|
431
|
-
ViewGroup.LayoutParams.MATCH_PARENT,
|
|
432
|
-
ViewGroup.LayoutParams.WRAP_CONTENT
|
|
433
|
-
)
|
|
434
|
-
subject.setText("Short content")
|
|
435
|
-
val staleHeight = expectedHeight + 320
|
|
436
|
-
val exactHeightSpec = View.MeasureSpec.makeMeasureSpec(staleHeight, View.MeasureSpec.EXACTLY)
|
|
437
|
-
subject.measure(widthSpec, exactHeightSpec)
|
|
438
|
-
subject.layout(0, 0, subject.measuredWidth, subject.measuredHeight)
|
|
439
|
-
|
|
440
|
-
assertEquals(staleHeight, subject.height)
|
|
441
|
-
val resolvedHeight = subject.resolveAutoGrowHeight()
|
|
442
|
-
|
|
443
|
-
assertEquals(expectedHeight, resolvedHeight)
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
@Test
|
|
447
|
-
fun `editor auto grow height expands after exact-height feedback loop`() {
|
|
448
|
-
val context = RuntimeEnvironment.getApplication()
|
|
449
|
-
val widthSpec = View.MeasureSpec.makeMeasureSpec(600, View.MeasureSpec.EXACTLY)
|
|
450
|
-
val wrapHeightSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)
|
|
451
|
-
val shortText = "Short content"
|
|
452
|
-
val tallText = "Line one\nLine two\nLine three\nLine four\nLine five"
|
|
453
|
-
|
|
454
|
-
val expectedTallView = EditorEditText(context)
|
|
455
|
-
expectedTallView.layoutParams = ViewGroup.LayoutParams(
|
|
456
|
-
ViewGroup.LayoutParams.MATCH_PARENT,
|
|
457
|
-
ViewGroup.LayoutParams.WRAP_CONTENT
|
|
458
|
-
)
|
|
459
|
-
expectedTallView.setText(tallText)
|
|
460
|
-
expectedTallView.measure(widthSpec, wrapHeightSpec)
|
|
461
|
-
expectedTallView.layout(
|
|
462
|
-
0,
|
|
463
|
-
0,
|
|
464
|
-
expectedTallView.measuredWidth,
|
|
465
|
-
expectedTallView.measuredHeight
|
|
466
|
-
)
|
|
467
|
-
val expectedTallHeight = expectedTallView.resolveAutoGrowHeight()
|
|
468
|
-
|
|
469
|
-
val subject = EditorEditText(context)
|
|
470
|
-
subject.layoutParams = ViewGroup.LayoutParams(
|
|
471
|
-
ViewGroup.LayoutParams.MATCH_PARENT,
|
|
472
|
-
ViewGroup.LayoutParams.WRAP_CONTENT
|
|
473
|
-
)
|
|
474
|
-
subject.setText(shortText)
|
|
475
|
-
subject.measure(widthSpec, wrapHeightSpec)
|
|
476
|
-
subject.layout(0, 0, subject.measuredWidth, subject.measuredHeight)
|
|
477
|
-
val shortHeight = subject.resolveAutoGrowHeight()
|
|
478
|
-
|
|
479
|
-
// Simulate React Native feeding the previous contentHeight back as an exact height.
|
|
480
|
-
val exactShortHeightSpec = View.MeasureSpec.makeMeasureSpec(shortHeight, View.MeasureSpec.EXACTLY)
|
|
481
|
-
subject.measure(widthSpec, exactShortHeightSpec)
|
|
482
|
-
subject.layout(0, 0, subject.measuredWidth, subject.measuredHeight)
|
|
483
|
-
|
|
484
|
-
subject.setText(tallText)
|
|
485
|
-
val expandedHeight = subject.resolveAutoGrowHeight()
|
|
486
|
-
|
|
487
|
-
assertTrue(expandedHeight > shortHeight)
|
|
488
|
-
assertEquals(expectedTallHeight, expandedHeight)
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
@Test
|
|
492
|
-
fun `editor auto grow height shrinks after exact-height feedback loop`() {
|
|
493
|
-
val context = RuntimeEnvironment.getApplication()
|
|
494
|
-
val widthSpec = View.MeasureSpec.makeMeasureSpec(600, View.MeasureSpec.EXACTLY)
|
|
495
|
-
val wrapHeightSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)
|
|
496
|
-
val shortText = "Short content"
|
|
497
|
-
val tallText = "Line one\nLine two\nLine three\nLine four\nLine five"
|
|
498
|
-
|
|
499
|
-
val expectedShortView = EditorEditText(context)
|
|
500
|
-
expectedShortView.layoutParams = ViewGroup.LayoutParams(
|
|
501
|
-
ViewGroup.LayoutParams.MATCH_PARENT,
|
|
502
|
-
ViewGroup.LayoutParams.WRAP_CONTENT
|
|
503
|
-
)
|
|
504
|
-
expectedShortView.setText(shortText)
|
|
505
|
-
expectedShortView.measure(widthSpec, wrapHeightSpec)
|
|
506
|
-
expectedShortView.layout(
|
|
507
|
-
0,
|
|
508
|
-
0,
|
|
509
|
-
expectedShortView.measuredWidth,
|
|
510
|
-
expectedShortView.measuredHeight
|
|
511
|
-
)
|
|
512
|
-
val expectedShortHeight = expectedShortView.resolveAutoGrowHeight()
|
|
513
|
-
|
|
514
|
-
val subject = EditorEditText(context)
|
|
515
|
-
subject.layoutParams = ViewGroup.LayoutParams(
|
|
516
|
-
ViewGroup.LayoutParams.MATCH_PARENT,
|
|
517
|
-
ViewGroup.LayoutParams.WRAP_CONTENT
|
|
518
|
-
)
|
|
519
|
-
subject.setText(tallText)
|
|
520
|
-
subject.measure(widthSpec, wrapHeightSpec)
|
|
521
|
-
subject.layout(0, 0, subject.measuredWidth, subject.measuredHeight)
|
|
522
|
-
val tallHeight = subject.resolveAutoGrowHeight()
|
|
523
|
-
|
|
524
|
-
// Simulate React Native feeding the previous contentHeight back as an exact height.
|
|
525
|
-
val exactTallHeightSpec = View.MeasureSpec.makeMeasureSpec(tallHeight, View.MeasureSpec.EXACTLY)
|
|
526
|
-
subject.measure(widthSpec, exactTallHeightSpec)
|
|
527
|
-
subject.layout(0, 0, subject.measuredWidth, subject.measuredHeight)
|
|
528
|
-
|
|
529
|
-
subject.setText(shortText)
|
|
530
|
-
val shrunkHeight = subject.resolveAutoGrowHeight()
|
|
531
|
-
|
|
532
|
-
assertTrue(shrunkHeight < tallHeight)
|
|
533
|
-
assertEquals(expectedShortHeight, shrunkHeight)
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
@Test
|
|
537
|
-
fun `rich text editor auto grow expands after content changes`() {
|
|
538
|
-
val richTextEditorView = RichTextEditorView(RuntimeEnvironment.getApplication())
|
|
539
|
-
richTextEditorView.setHeightBehavior(EditorHeightBehavior.AUTO_GROW)
|
|
540
|
-
val widthSpec = View.MeasureSpec.makeMeasureSpec(600, View.MeasureSpec.EXACTLY)
|
|
541
|
-
val heightSpec = View.MeasureSpec.makeMeasureSpec(1600, View.MeasureSpec.AT_MOST)
|
|
542
|
-
|
|
543
|
-
richTextEditorView.editorEditText.setText("Short content")
|
|
544
|
-
richTextEditorView.measure(widthSpec, heightSpec)
|
|
545
|
-
richTextEditorView.layout(
|
|
546
|
-
0,
|
|
547
|
-
0,
|
|
548
|
-
richTextEditorView.measuredWidth,
|
|
549
|
-
richTextEditorView.measuredHeight
|
|
550
|
-
)
|
|
551
|
-
val shortHeight = richTextEditorView.measuredHeight
|
|
552
|
-
|
|
553
|
-
richTextEditorView.editorEditText.setText("Line one\nLine two\nLine three\nLine four")
|
|
554
|
-
richTextEditorView.measure(widthSpec, heightSpec)
|
|
555
|
-
richTextEditorView.layout(
|
|
556
|
-
0,
|
|
557
|
-
0,
|
|
558
|
-
richTextEditorView.measuredWidth,
|
|
559
|
-
richTextEditorView.measuredHeight
|
|
560
|
-
)
|
|
561
|
-
val tallHeight = richTextEditorView.measuredHeight
|
|
562
|
-
|
|
563
|
-
assertTrue("Auto-grow height should expand when content grows", tallHeight > shortHeight)
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
@Test
|
|
567
|
-
fun `rich text editor auto grow keeps edit text height aligned with container`() {
|
|
568
|
-
val richTextEditorView = RichTextEditorView(RuntimeEnvironment.getApplication())
|
|
569
|
-
richTextEditorView.setHeightBehavior(EditorHeightBehavior.AUTO_GROW)
|
|
570
|
-
richTextEditorView.editorEditText.setText(
|
|
571
|
-
"Line one\nLine two\nLine three\nLine four\nLine five"
|
|
572
|
-
)
|
|
573
|
-
|
|
574
|
-
val widthSpec = View.MeasureSpec.makeMeasureSpec(600, View.MeasureSpec.EXACTLY)
|
|
575
|
-
val heightSpec = View.MeasureSpec.makeMeasureSpec(1600, View.MeasureSpec.AT_MOST)
|
|
576
|
-
richTextEditorView.measure(widthSpec, heightSpec)
|
|
577
|
-
richTextEditorView.layout(
|
|
578
|
-
0,
|
|
579
|
-
0,
|
|
580
|
-
richTextEditorView.measuredWidth,
|
|
581
|
-
richTextEditorView.measuredHeight
|
|
582
|
-
)
|
|
583
|
-
|
|
584
|
-
assertEquals(
|
|
585
|
-
"EditText should fill the auto-grow container height",
|
|
586
|
-
richTextEditorView.measuredHeight,
|
|
587
|
-
richTextEditorView.editorEditText.measuredHeight
|
|
588
|
-
)
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
@Test
|
|
592
|
-
fun `rich text editor auto grow lays out edit text to container height`() {
|
|
593
|
-
val richTextEditorView = RichTextEditorView(RuntimeEnvironment.getApplication())
|
|
594
|
-
richTextEditorView.setHeightBehavior(EditorHeightBehavior.AUTO_GROW)
|
|
595
|
-
richTextEditorView.editorEditText.setText(
|
|
596
|
-
"Line one\nLine two\nLine three\nLine four\nLine five"
|
|
597
|
-
)
|
|
598
|
-
|
|
599
|
-
val widthSpec = View.MeasureSpec.makeMeasureSpec(600, View.MeasureSpec.EXACTLY)
|
|
600
|
-
val heightSpec = View.MeasureSpec.makeMeasureSpec(1600, View.MeasureSpec.AT_MOST)
|
|
601
|
-
richTextEditorView.measure(widthSpec, heightSpec)
|
|
602
|
-
richTextEditorView.layout(
|
|
603
|
-
0,
|
|
604
|
-
0,
|
|
605
|
-
richTextEditorView.measuredWidth,
|
|
606
|
-
richTextEditorView.measuredHeight
|
|
607
|
-
)
|
|
608
|
-
|
|
609
|
-
assertEquals(
|
|
610
|
-
"EditText should be laid out to the container height in auto-grow mode",
|
|
611
|
-
richTextEditorView.height,
|
|
612
|
-
richTextEditorView.editorEditText.height
|
|
613
|
-
)
|
|
614
|
-
}
|
|
615
|
-
|
|
616
|
-
@Test
|
|
617
|
-
fun `fixed height editor disallows parent intercept while scrolling`() {
|
|
618
|
-
val context = RuntimeEnvironment.getApplication()
|
|
619
|
-
val parent = InterceptAwareFrameLayout(context)
|
|
620
|
-
val richTextEditorView = RichTextEditorView(context)
|
|
621
|
-
richTextEditorView.layoutParams = FrameLayout.LayoutParams(
|
|
622
|
-
FrameLayout.LayoutParams.MATCH_PARENT,
|
|
623
|
-
200
|
|
624
|
-
)
|
|
625
|
-
richTextEditorView.setHeightBehavior(EditorHeightBehavior.FIXED)
|
|
626
|
-
richTextEditorView.editorEditText.setText((1..40).joinToString("\n") { "Line $it" })
|
|
627
|
-
parent.addView(richTextEditorView)
|
|
628
|
-
|
|
629
|
-
val widthSpec = View.MeasureSpec.makeMeasureSpec(600, View.MeasureSpec.EXACTLY)
|
|
630
|
-
val heightSpec = View.MeasureSpec.makeMeasureSpec(200, View.MeasureSpec.EXACTLY)
|
|
631
|
-
parent.measure(widthSpec, heightSpec)
|
|
632
|
-
parent.layout(0, 0, parent.measuredWidth, parent.measuredHeight)
|
|
633
|
-
|
|
634
|
-
assertTrue(
|
|
635
|
-
"Expected fixed editor content to overflow vertically",
|
|
636
|
-
richTextEditorView.editorScrollView.canScrollVertically(1)
|
|
637
|
-
)
|
|
638
|
-
|
|
639
|
-
val down = MotionEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, 10f, 10f, 0)
|
|
640
|
-
richTextEditorView.editorScrollView.onTouchEvent(down)
|
|
641
|
-
down.recycle()
|
|
642
|
-
|
|
643
|
-
assertTrue(
|
|
644
|
-
"Fixed-height editor should disallow parent intercept while scrolling",
|
|
645
|
-
parent.disallowInterceptRequested
|
|
646
|
-
)
|
|
647
|
-
|
|
648
|
-
val up = MotionEvent.obtain(0, 16, MotionEvent.ACTION_UP, 10f, 40f, 0)
|
|
649
|
-
richTextEditorView.editorScrollView.onTouchEvent(up)
|
|
650
|
-
up.recycle()
|
|
651
|
-
|
|
652
|
-
assertTrue(
|
|
653
|
-
"Fixed-height editor should release parent intercept after the gesture ends",
|
|
654
|
-
!parent.disallowInterceptRequested
|
|
655
|
-
)
|
|
656
|
-
}
|
|
657
|
-
|
|
658
|
-
@Test
|
|
659
|
-
fun `selected image shows resize overlay at rendered image bounds`() {
|
|
660
|
-
val context = RuntimeEnvironment.getApplication()
|
|
661
|
-
val view = RichTextEditorView(context)
|
|
662
|
-
view.editorEditText.applyRenderJSON(imageRenderJson())
|
|
663
|
-
|
|
664
|
-
val widthSpec = View.MeasureSpec.makeMeasureSpec(600, View.MeasureSpec.EXACTLY)
|
|
665
|
-
val heightSpec = View.MeasureSpec.makeMeasureSpec(240, View.MeasureSpec.EXACTLY)
|
|
666
|
-
view.measure(widthSpec, heightSpec)
|
|
667
|
-
view.layout(0, 0, view.measuredWidth, view.measuredHeight)
|
|
668
|
-
|
|
669
|
-
val text = view.editorEditText.text as? Spanned
|
|
670
|
-
assertNotNull("Expected rendered text with spans", text)
|
|
671
|
-
text ?: return
|
|
672
|
-
|
|
673
|
-
val imageSpan = text.getSpans(0, text.length, BlockImageSpan::class.java).firstOrNull()
|
|
674
|
-
assertNotNull("Expected a rendered image span", imageSpan)
|
|
675
|
-
imageSpan ?: return
|
|
676
|
-
|
|
677
|
-
val spanStart = text.getSpanStart(imageSpan)
|
|
678
|
-
val spanEnd = text.getSpanEnd(imageSpan)
|
|
679
|
-
view.editorEditText.setSelection(spanStart, spanEnd)
|
|
680
|
-
view.editorEditText.onSelectionOrContentMayChange?.invoke()
|
|
681
|
-
|
|
682
|
-
val overlayRect = view.imageResizeOverlayRectForTesting()
|
|
683
|
-
assertNotNull("Selecting an image should show the resize overlay", overlayRect)
|
|
684
|
-
overlayRect ?: return
|
|
685
|
-
assertEquals(140f, overlayRect.width(), 1f)
|
|
686
|
-
assertEquals(80f, overlayRect.height(), 1f)
|
|
687
|
-
}
|
|
688
|
-
|
|
689
|
-
@Test
|
|
690
|
-
fun `tapping rendered image selects it for resize overlay`() {
|
|
691
|
-
val context = RuntimeEnvironment.getApplication()
|
|
692
|
-
val view = RichTextEditorView(context)
|
|
693
|
-
view.editorEditText.applyRenderJSON(imageRenderJson())
|
|
694
|
-
|
|
695
|
-
val widthSpec = View.MeasureSpec.makeMeasureSpec(600, View.MeasureSpec.EXACTLY)
|
|
696
|
-
val heightSpec = View.MeasureSpec.makeMeasureSpec(240, View.MeasureSpec.EXACTLY)
|
|
697
|
-
view.measure(widthSpec, heightSpec)
|
|
698
|
-
view.layout(0, 0, view.measuredWidth, view.measuredHeight)
|
|
699
|
-
|
|
700
|
-
val text = view.editorEditText.text as? Spanned
|
|
701
|
-
assertNotNull("Expected rendered text with spans", text)
|
|
702
|
-
text ?: return
|
|
703
|
-
|
|
704
|
-
val imageSpan = text.getSpans(0, text.length, BlockImageSpan::class.java).firstOrNull()
|
|
705
|
-
assertNotNull("Expected a rendered image span", imageSpan)
|
|
706
|
-
imageSpan ?: return
|
|
707
|
-
|
|
708
|
-
val spanStart = text.getSpanStart(imageSpan)
|
|
709
|
-
val spanEnd = text.getSpanEnd(imageSpan)
|
|
710
|
-
val canvasBitmap = Bitmap.createBitmap(view.width, view.height, Bitmap.Config.ARGB_8888)
|
|
711
|
-
view.editorEditText.draw(Canvas(canvasBitmap))
|
|
712
|
-
|
|
713
|
-
val drawnRect = imageSpan.currentDrawRect()
|
|
714
|
-
assertNotNull("Expected drawn image bounds", drawnRect)
|
|
715
|
-
drawnRect ?: return
|
|
716
|
-
val tapX = drawnRect.centerX()
|
|
717
|
-
val tapY = drawnRect.centerY()
|
|
718
|
-
|
|
719
|
-
val down = MotionEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, tapX, tapY, 0)
|
|
720
|
-
view.editorEditText.onTouchEvent(down)
|
|
721
|
-
down.recycle()
|
|
722
|
-
|
|
723
|
-
val up = MotionEvent.obtain(0, 16, MotionEvent.ACTION_UP, tapX, tapY, 0)
|
|
724
|
-
view.editorEditText.onTouchEvent(up)
|
|
725
|
-
up.recycle()
|
|
726
|
-
|
|
727
|
-
assertEquals(spanStart, view.editorEditText.selectionStart)
|
|
728
|
-
assertEquals(spanEnd, view.editorEditText.selectionEnd)
|
|
729
|
-
|
|
730
|
-
val overlayRect = view.imageResizeOverlayRectForTesting()
|
|
731
|
-
assertNotNull("Tapping an image should show the resize overlay", overlayRect)
|
|
732
|
-
overlayRect ?: return
|
|
733
|
-
assertEquals(140f, overlayRect.width(), 1f)
|
|
734
|
-
assertEquals(80f, overlayRect.height(), 1f)
|
|
735
|
-
}
|
|
736
|
-
|
|
737
|
-
@Test
|
|
738
|
-
fun `disabling image resizing keeps image taps from showing resize overlay`() {
|
|
739
|
-
val context = RuntimeEnvironment.getApplication()
|
|
740
|
-
val view = RichTextEditorView(context)
|
|
741
|
-
view.editorEditText.applyRenderJSON(imageRenderJson())
|
|
742
|
-
view.setImageResizingEnabled(false)
|
|
743
|
-
|
|
744
|
-
val widthSpec = View.MeasureSpec.makeMeasureSpec(600, View.MeasureSpec.EXACTLY)
|
|
745
|
-
val heightSpec = View.MeasureSpec.makeMeasureSpec(240, View.MeasureSpec.EXACTLY)
|
|
746
|
-
view.measure(widthSpec, heightSpec)
|
|
747
|
-
view.layout(0, 0, view.measuredWidth, view.measuredHeight)
|
|
748
|
-
|
|
749
|
-
val text = view.editorEditText.text as? Spanned
|
|
750
|
-
assertNotNull("Expected rendered text with spans", text)
|
|
751
|
-
text ?: return
|
|
752
|
-
|
|
753
|
-
val imageSpan = text.getSpans(0, text.length, BlockImageSpan::class.java).firstOrNull()
|
|
754
|
-
assertNotNull("Expected a rendered image span", imageSpan)
|
|
755
|
-
imageSpan ?: return
|
|
756
|
-
|
|
757
|
-
val canvasBitmap = Bitmap.createBitmap(view.width, view.height, Bitmap.Config.ARGB_8888)
|
|
758
|
-
view.editorEditText.draw(Canvas(canvasBitmap))
|
|
759
|
-
|
|
760
|
-
val drawnRect = imageSpan.currentDrawRect()
|
|
761
|
-
assertNotNull("Expected drawn image bounds", drawnRect)
|
|
762
|
-
drawnRect ?: return
|
|
763
|
-
val tapX = drawnRect.centerX()
|
|
764
|
-
val tapY = drawnRect.centerY()
|
|
765
|
-
|
|
766
|
-
val down = MotionEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, tapX, tapY, 0)
|
|
767
|
-
view.editorEditText.onTouchEvent(down)
|
|
768
|
-
down.recycle()
|
|
769
|
-
|
|
770
|
-
val up = MotionEvent.obtain(0, 16, MotionEvent.ACTION_UP, tapX, tapY, 0)
|
|
771
|
-
view.editorEditText.onTouchEvent(up)
|
|
772
|
-
up.recycle()
|
|
773
|
-
|
|
774
|
-
assertNull("Tapping an image should not show the resize overlay when disabled", view.imageResizeOverlayRectForTesting())
|
|
775
|
-
}
|
|
776
|
-
|
|
777
|
-
@Test
|
|
778
|
-
fun `editor theme contentInsets apply padding in density-scaled pixels`() {
|
|
779
|
-
val context = RuntimeEnvironment.getApplication()
|
|
780
|
-
val editText = EditorEditText(context)
|
|
781
|
-
val density = context.resources.displayMetrics.density
|
|
782
|
-
editText.setHeightBehavior(EditorHeightBehavior.AUTO_GROW)
|
|
783
|
-
val theme = EditorTheme.fromJson(
|
|
784
|
-
"""
|
|
785
|
-
{
|
|
786
|
-
"contentInsets": { "top": 8, "right": 10, "bottom": 12, "left": 14 }
|
|
787
|
-
}
|
|
788
|
-
""".trimIndent()
|
|
789
|
-
)
|
|
790
|
-
|
|
791
|
-
editText.applyTheme(theme)
|
|
792
|
-
|
|
793
|
-
assertEquals((14f * density).toInt(), editText.paddingLeft)
|
|
794
|
-
assertEquals((8f * density).toInt(), editText.paddingTop)
|
|
795
|
-
assertEquals((10f * density).toInt(), editText.paddingRight)
|
|
796
|
-
assertEquals((12f * density).toInt(), editText.paddingBottom)
|
|
797
|
-
}
|
|
798
|
-
|
|
799
|
-
@Test
|
|
800
|
-
fun `editor theme borderRadius applies to scroll container in density-scaled pixels`() {
|
|
801
|
-
val context = RuntimeEnvironment.getApplication()
|
|
802
|
-
val richTextEditorView = RichTextEditorView(context)
|
|
803
|
-
val density = context.resources.displayMetrics.density
|
|
804
|
-
val theme = EditorTheme.fromJson(
|
|
805
|
-
"""
|
|
806
|
-
{
|
|
807
|
-
"backgroundColor": "#d7e4ff",
|
|
808
|
-
"borderRadius": 18
|
|
809
|
-
}
|
|
810
|
-
""".trimIndent()
|
|
811
|
-
)
|
|
812
|
-
|
|
813
|
-
richTextEditorView.applyTheme(theme)
|
|
814
|
-
|
|
815
|
-
assertEquals(18f * density, richTextEditorView.appliedCornerRadiusPx, 0.1f)
|
|
816
|
-
assertTrue(richTextEditorView.editorViewport.clipToOutline)
|
|
817
|
-
}
|
|
818
|
-
|
|
819
|
-
@Test
|
|
820
|
-
fun `editor theme transparent backgroundColor applies transparent viewport background`() {
|
|
821
|
-
val context = RuntimeEnvironment.getApplication()
|
|
822
|
-
val richTextEditorView = RichTextEditorView(context)
|
|
823
|
-
richTextEditorView.configure(
|
|
824
|
-
textSizePx = 16f * context.resources.displayMetrics.density,
|
|
825
|
-
textColor = Color.BLACK,
|
|
826
|
-
backgroundColor = Color.WHITE
|
|
827
|
-
)
|
|
828
|
-
|
|
829
|
-
val theme = EditorTheme.fromJson(
|
|
830
|
-
"""
|
|
831
|
-
{
|
|
832
|
-
"backgroundColor": "transparent"
|
|
833
|
-
}
|
|
834
|
-
""".trimIndent()
|
|
835
|
-
)
|
|
836
|
-
|
|
837
|
-
assertEquals(Color.TRANSPARENT, theme?.backgroundColor)
|
|
838
|
-
|
|
839
|
-
richTextEditorView.applyTheme(theme)
|
|
840
|
-
|
|
841
|
-
assertEquals(Color.TRANSPARENT, richTextEditorView.appliedBackgroundColorForTesting)
|
|
842
|
-
}
|
|
843
|
-
|
|
844
|
-
@Test
|
|
845
|
-
fun `fixed height editor reserves viewport inset in effective bottom padding`() {
|
|
846
|
-
val context = RuntimeEnvironment.getApplication()
|
|
847
|
-
val richTextEditorView = RichTextEditorView(context)
|
|
848
|
-
richTextEditorView.setHeightBehavior(EditorHeightBehavior.FIXED)
|
|
849
|
-
richTextEditorView.applyTheme(
|
|
850
|
-
EditorTheme.fromJson(
|
|
851
|
-
"""
|
|
852
|
-
{
|
|
853
|
-
"contentInsets": { "bottom": 12 }
|
|
854
|
-
}
|
|
855
|
-
""".trimIndent()
|
|
856
|
-
)
|
|
857
|
-
)
|
|
858
|
-
|
|
859
|
-
richTextEditorView.setViewportBottomInsetPx(96)
|
|
860
|
-
|
|
861
|
-
val density = context.resources.displayMetrics.density
|
|
862
|
-
assertEquals((12f * density).toInt() + 96, richTextEditorView.editorScrollView.paddingBottom)
|
|
863
|
-
assertEquals(0, richTextEditorView.editorEditText.paddingBottom)
|
|
864
|
-
}
|
|
865
|
-
|
|
866
|
-
@Test
|
|
867
|
-
fun `fixed height editor scrolls vertical contentInsets away while preserving viewport inset`() {
|
|
868
|
-
val context = RuntimeEnvironment.getApplication()
|
|
869
|
-
val richTextEditorView = RichTextEditorView(context)
|
|
870
|
-
val density = context.resources.displayMetrics.density
|
|
871
|
-
richTextEditorView.setHeightBehavior(EditorHeightBehavior.FIXED)
|
|
872
|
-
richTextEditorView.applyTheme(
|
|
873
|
-
EditorTheme.fromJson(
|
|
874
|
-
"""
|
|
875
|
-
{
|
|
876
|
-
"contentInsets": { "top": 8, "bottom": 12 }
|
|
877
|
-
}
|
|
878
|
-
""".trimIndent()
|
|
879
|
-
)
|
|
880
|
-
)
|
|
881
|
-
|
|
882
|
-
richTextEditorView.setViewportBottomInsetPx(96)
|
|
883
|
-
|
|
884
|
-
assertTrue(!richTextEditorView.editorScrollView.clipToPadding)
|
|
885
|
-
assertEquals((8f * density).toInt(), richTextEditorView.editorScrollView.paddingTop)
|
|
886
|
-
assertEquals((12f * density).toInt() + 96, richTextEditorView.editorScrollView.paddingBottom)
|
|
887
|
-
assertEquals(0, richTextEditorView.editorEditText.paddingTop)
|
|
888
|
-
assertEquals(0, richTextEditorView.editorEditText.paddingBottom)
|
|
889
|
-
}
|
|
890
|
-
|
|
891
|
-
@Test
|
|
892
|
-
fun `caret rect is reported in editor view coordinates`() {
|
|
893
|
-
val context = RuntimeEnvironment.getApplication()
|
|
894
|
-
val richTextEditorView = RichTextEditorView(context)
|
|
895
|
-
richTextEditorView.editorEditText.setText("Hello world")
|
|
896
|
-
|
|
897
|
-
val widthSpec = View.MeasureSpec.makeMeasureSpec(600, View.MeasureSpec.EXACTLY)
|
|
898
|
-
val heightSpec = View.MeasureSpec.makeMeasureSpec(240, View.MeasureSpec.EXACTLY)
|
|
899
|
-
richTextEditorView.measure(widthSpec, heightSpec)
|
|
900
|
-
richTextEditorView.layout(
|
|
901
|
-
0,
|
|
902
|
-
0,
|
|
903
|
-
richTextEditorView.measuredWidth,
|
|
904
|
-
richTextEditorView.measuredHeight
|
|
905
|
-
)
|
|
906
|
-
richTextEditorView.editorEditText.setSelection(5)
|
|
907
|
-
|
|
908
|
-
val editTextRect = richTextEditorView.editorEditText.caretRect()
|
|
909
|
-
val actual = richTextEditorView.caretRect()
|
|
910
|
-
|
|
911
|
-
assertNotNull(editTextRect)
|
|
912
|
-
assertNotNull(actual)
|
|
913
|
-
assertEquals(
|
|
914
|
-
richTextEditorView.editorViewport.left +
|
|
915
|
-
richTextEditorView.editorScrollView.left +
|
|
916
|
-
richTextEditorView.editorEditText.left +
|
|
917
|
-
editTextRect!!.left,
|
|
918
|
-
actual!!.left,
|
|
919
|
-
0.1f
|
|
920
|
-
)
|
|
921
|
-
assertEquals(
|
|
922
|
-
richTextEditorView.editorViewport.top +
|
|
923
|
-
richTextEditorView.editorScrollView.top +
|
|
924
|
-
richTextEditorView.editorEditText.top +
|
|
925
|
-
editTextRect.top -
|
|
926
|
-
richTextEditorView.editorScrollView.scrollY,
|
|
927
|
-
actual.top,
|
|
928
|
-
0.1f
|
|
929
|
-
)
|
|
930
|
-
assertTrue(actual.height() > 0f)
|
|
931
|
-
}
|
|
932
|
-
|
|
933
|
-
@Test
|
|
934
|
-
fun `painted caret rect is clipped to glyph height on a spacer line`() {
|
|
935
|
-
val context = RuntimeEnvironment.getApplication()
|
|
936
|
-
val editText = EditorEditText(context)
|
|
937
|
-
editText.layoutParams = ViewGroup.LayoutParams(600, 240)
|
|
938
|
-
val spanned = SpannableStringBuilder("Hello\nWorld")
|
|
939
|
-
spanned.setSpan(
|
|
940
|
-
ParagraphSpacerSpan(spacingPx = 60, baseFontSize = 16, textColor = Color.BLACK),
|
|
941
|
-
5,
|
|
942
|
-
6,
|
|
943
|
-
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
|
|
944
|
-
)
|
|
945
|
-
editText.setText(spanned)
|
|
946
|
-
|
|
947
|
-
val widthSpec = View.MeasureSpec.makeMeasureSpec(600, View.MeasureSpec.EXACTLY)
|
|
948
|
-
val heightSpec = View.MeasureSpec.makeMeasureSpec(240, View.MeasureSpec.EXACTLY)
|
|
949
|
-
editText.measure(widthSpec, heightSpec)
|
|
950
|
-
editText.layout(0, 0, editText.measuredWidth, editText.measuredHeight)
|
|
951
|
-
editText.setSelection(5) // collapsed caret on the spacer line
|
|
952
|
-
|
|
953
|
-
val layout = editText.layout!!
|
|
954
|
-
val inflatedLineHeight = (layout.getLineBottom(0) - layout.getLineTop(0)).toFloat()
|
|
955
|
-
// The exact rectangle drawCustomCaret paints (content coordinates, caret width).
|
|
956
|
-
val caret = editText.customCaretDrawRect()
|
|
957
|
-
|
|
958
|
-
assertNotNull("a caret rect should be produced for a collapsed selection", caret)
|
|
959
|
-
assertTrue("painted caret should have width", caret!!.width() > 0f)
|
|
960
|
-
assertTrue("painted caret should have height", caret.height() > 0f)
|
|
961
|
-
assertTrue(
|
|
962
|
-
"painted caret height ${caret.height()} must exclude the 60px gap (inflated=$inflatedLineHeight)",
|
|
963
|
-
caret.height() < inflatedLineHeight - 20f
|
|
964
|
-
)
|
|
965
|
-
}
|
|
966
|
-
|
|
967
|
-
@Test
|
|
968
|
-
fun `caret rect height excludes the paragraph spacer gap`() {
|
|
969
|
-
val context = RuntimeEnvironment.getApplication()
|
|
970
|
-
val editText = EditorEditText(context)
|
|
971
|
-
editText.layoutParams = ViewGroup.LayoutParams(600, 240)
|
|
972
|
-
val spanned = SpannableStringBuilder("Hello\nWorld")
|
|
973
|
-
// Spacer on the inter-block newline inflates the descent of line 0.
|
|
974
|
-
spanned.setSpan(
|
|
975
|
-
ParagraphSpacerSpan(spacingPx = 60, baseFontSize = 16, textColor = Color.BLACK),
|
|
976
|
-
5,
|
|
977
|
-
6,
|
|
978
|
-
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
|
|
979
|
-
)
|
|
980
|
-
editText.setText(spanned)
|
|
981
|
-
|
|
982
|
-
val widthSpec = View.MeasureSpec.makeMeasureSpec(600, View.MeasureSpec.EXACTLY)
|
|
983
|
-
val heightSpec = View.MeasureSpec.makeMeasureSpec(240, View.MeasureSpec.EXACTLY)
|
|
984
|
-
editText.measure(widthSpec, heightSpec)
|
|
985
|
-
editText.layout(0, 0, editText.measuredWidth, editText.measuredHeight)
|
|
986
|
-
editText.setSelection(5) // caret on the spacer line (line 0)
|
|
987
|
-
|
|
988
|
-
val layout = editText.layout!!
|
|
989
|
-
val line = 0
|
|
990
|
-
val inflatedLineHeight = (layout.getLineBottom(line) - layout.getLineTop(line)).toFloat()
|
|
991
|
-
val rect = editText.caretRect()!!
|
|
992
|
-
|
|
993
|
-
assertTrue(
|
|
994
|
-
"reproduction guard: spacer should inflate the line box",
|
|
995
|
-
layout.getLineDescent(line) > editText.paint.fontMetrics.descent
|
|
996
|
-
)
|
|
997
|
-
assertTrue("caret height should be positive", rect.height() > 0f)
|
|
998
|
-
assertTrue(
|
|
999
|
-
"caret height ${rect.height()} must exclude the 60px paragraph gap (inflated line height=$inflatedLineHeight)",
|
|
1000
|
-
rect.height() < inflatedLineHeight - 20f
|
|
1001
|
-
)
|
|
1002
|
-
}
|
|
1003
|
-
|
|
1004
|
-
@Test
|
|
1005
|
-
fun `remote selections expose focused caret geometry without a badge`() {
|
|
1006
|
-
val context = RuntimeEnvironment.getApplication()
|
|
1007
|
-
val view = RichTextEditorView(context)
|
|
1008
|
-
view.setRemoteSelectionEditorIdForTesting(1L)
|
|
1009
|
-
view.editorEditText.setText("Hello world")
|
|
1010
|
-
view.setRemoteSelectionScalarResolverForTesting { _, docPos -> docPos }
|
|
1011
|
-
|
|
1012
|
-
val widthSpec = View.MeasureSpec.makeMeasureSpec(600, View.MeasureSpec.EXACTLY)
|
|
1013
|
-
val heightSpec = View.MeasureSpec.makeMeasureSpec(240, View.MeasureSpec.EXACTLY)
|
|
1014
|
-
view.measure(widthSpec, heightSpec)
|
|
1015
|
-
view.layout(0, 0, view.measuredWidth, view.measuredHeight)
|
|
1016
|
-
|
|
1017
|
-
view.setRemoteSelections(
|
|
1018
|
-
listOf(
|
|
1019
|
-
RemoteSelectionDecoration(
|
|
1020
|
-
clientId = 7,
|
|
1021
|
-
anchor = 6,
|
|
1022
|
-
head = 6,
|
|
1023
|
-
color = Color.parseColor("#ff6b35"),
|
|
1024
|
-
name = "Alice",
|
|
1025
|
-
isFocused = true,
|
|
1026
|
-
)
|
|
1027
|
-
)
|
|
1028
|
-
)
|
|
1029
|
-
|
|
1030
|
-
val snapshot = view.remoteSelectionDebugSnapshotsForTesting().single()
|
|
1031
|
-
assertEquals(7, snapshot.clientId)
|
|
1032
|
-
assertNotNull(snapshot.caretRect)
|
|
1033
|
-
assertTrue(snapshot.caretRect!!.height() > 0f)
|
|
1034
|
-
}
|
|
1035
|
-
|
|
1036
|
-
@Test
|
|
1037
|
-
fun `unfocused collapsed remote selection does not expose caret or badge geometry`() {
|
|
1038
|
-
val context = RuntimeEnvironment.getApplication()
|
|
1039
|
-
val view = RichTextEditorView(context)
|
|
1040
|
-
view.setRemoteSelectionEditorIdForTesting(1L)
|
|
1041
|
-
view.editorEditText.setText("Hello world")
|
|
1042
|
-
view.setRemoteSelectionScalarResolverForTesting { _, docPos -> docPos }
|
|
1043
|
-
|
|
1044
|
-
val widthSpec = View.MeasureSpec.makeMeasureSpec(600, View.MeasureSpec.EXACTLY)
|
|
1045
|
-
val heightSpec = View.MeasureSpec.makeMeasureSpec(240, View.MeasureSpec.EXACTLY)
|
|
1046
|
-
view.measure(widthSpec, heightSpec)
|
|
1047
|
-
view.layout(0, 0, view.measuredWidth, view.measuredHeight)
|
|
1048
|
-
|
|
1049
|
-
view.setRemoteSelections(
|
|
1050
|
-
listOf(
|
|
1051
|
-
RemoteSelectionDecoration(
|
|
1052
|
-
clientId = 8,
|
|
1053
|
-
anchor = 6,
|
|
1054
|
-
head = 6,
|
|
1055
|
-
color = Color.parseColor("#007aff"),
|
|
1056
|
-
name = "Alice",
|
|
1057
|
-
isFocused = false,
|
|
1058
|
-
)
|
|
1059
|
-
)
|
|
1060
|
-
)
|
|
1061
|
-
|
|
1062
|
-
val snapshot = view.remoteSelectionDebugSnapshotsForTesting().single()
|
|
1063
|
-
assertEquals(8, snapshot.clientId)
|
|
1064
|
-
assertTrue(snapshot.caretRect == null)
|
|
1065
|
-
}
|
|
1066
|
-
|
|
1067
|
-
@Test
|
|
1068
|
-
fun `remote selection geometry is cached across redraws`() {
|
|
1069
|
-
val context = RuntimeEnvironment.getApplication()
|
|
1070
|
-
val view = RichTextEditorView(context)
|
|
1071
|
-
view.setRemoteSelectionEditorIdForTesting(1L)
|
|
1072
|
-
view.editorEditText.setText("Hello world from remote selections")
|
|
1073
|
-
|
|
1074
|
-
var resolverCalls = 0
|
|
1075
|
-
view.setRemoteSelectionScalarResolverForTesting { _, docPos ->
|
|
1076
|
-
resolverCalls += 1
|
|
1077
|
-
docPos
|
|
1078
|
-
}
|
|
1079
|
-
|
|
1080
|
-
val widthSpec = View.MeasureSpec.makeMeasureSpec(600, View.MeasureSpec.EXACTLY)
|
|
1081
|
-
val heightSpec = View.MeasureSpec.makeMeasureSpec(240, View.MeasureSpec.EXACTLY)
|
|
1082
|
-
view.measure(widthSpec, heightSpec)
|
|
1083
|
-
view.layout(0, 0, view.measuredWidth, view.measuredHeight)
|
|
1084
|
-
|
|
1085
|
-
view.setRemoteSelections(
|
|
1086
|
-
listOf(
|
|
1087
|
-
RemoteSelectionDecoration(
|
|
1088
|
-
clientId = 11,
|
|
1089
|
-
anchor = 6,
|
|
1090
|
-
head = 12,
|
|
1091
|
-
color = Color.parseColor("#ff9500"),
|
|
1092
|
-
name = "Range",
|
|
1093
|
-
isFocused = true,
|
|
1094
|
-
)
|
|
1095
|
-
)
|
|
1096
|
-
)
|
|
1097
|
-
|
|
1098
|
-
val bitmap = Bitmap.createBitmap(600, 240, Bitmap.Config.ARGB_8888)
|
|
1099
|
-
val canvas = Canvas(bitmap)
|
|
1100
|
-
resolverCalls = 0
|
|
1101
|
-
|
|
1102
|
-
view.draw(canvas)
|
|
1103
|
-
view.draw(canvas)
|
|
1104
|
-
|
|
1105
|
-
assertEquals(0, resolverCalls)
|
|
1106
|
-
}
|
|
1107
|
-
|
|
1108
|
-
@Test
|
|
1109
|
-
fun `setting identical remote selections does not invalidate cached geometry`() {
|
|
1110
|
-
val context = RuntimeEnvironment.getApplication()
|
|
1111
|
-
val view = RichTextEditorView(context)
|
|
1112
|
-
view.setRemoteSelectionEditorIdForTesting(1L)
|
|
1113
|
-
view.editorEditText.setText("Hello world from remote selections")
|
|
1114
|
-
|
|
1115
|
-
var resolverCalls = 0
|
|
1116
|
-
view.setRemoteSelectionScalarResolverForTesting { _, docPos ->
|
|
1117
|
-
resolverCalls += 1
|
|
1118
|
-
docPos
|
|
1119
|
-
}
|
|
1120
|
-
|
|
1121
|
-
val widthSpec = View.MeasureSpec.makeMeasureSpec(600, View.MeasureSpec.EXACTLY)
|
|
1122
|
-
val heightSpec = View.MeasureSpec.makeMeasureSpec(240, View.MeasureSpec.EXACTLY)
|
|
1123
|
-
view.measure(widthSpec, heightSpec)
|
|
1124
|
-
view.layout(0, 0, view.measuredWidth, view.measuredHeight)
|
|
1125
|
-
|
|
1126
|
-
val initialSelections = listOf(
|
|
1127
|
-
RemoteSelectionDecoration(
|
|
1128
|
-
clientId = 12,
|
|
1129
|
-
anchor = 6,
|
|
1130
|
-
head = 12,
|
|
1131
|
-
color = Color.parseColor("#34c759"),
|
|
1132
|
-
name = "Range",
|
|
1133
|
-
isFocused = true,
|
|
1134
|
-
)
|
|
1135
|
-
)
|
|
1136
|
-
view.setRemoteSelections(initialSelections)
|
|
1137
|
-
view.remoteSelectionDebugSnapshotsForTesting()
|
|
1138
|
-
|
|
1139
|
-
resolverCalls = 0
|
|
1140
|
-
val identicalSelections = listOf(
|
|
1141
|
-
RemoteSelectionDecoration(
|
|
1142
|
-
clientId = 12,
|
|
1143
|
-
anchor = 6,
|
|
1144
|
-
head = 12,
|
|
1145
|
-
color = Color.parseColor("#34c759"),
|
|
1146
|
-
name = "Range",
|
|
1147
|
-
isFocused = true,
|
|
1148
|
-
)
|
|
1149
|
-
)
|
|
1150
|
-
view.setRemoteSelections(identicalSelections)
|
|
1151
|
-
view.remoteSelectionDebugSnapshotsForTesting()
|
|
1152
|
-
|
|
1153
|
-
assertEquals(0, resolverCalls)
|
|
1154
|
-
}
|
|
1155
|
-
|
|
1156
|
-
@Test
|
|
1157
|
-
fun `remote selection json parsing tolerates invalid colors`() {
|
|
1158
|
-
val context = RuntimeEnvironment.getApplication()
|
|
1159
|
-
|
|
1160
|
-
val selections = RemoteSelectionDecoration.fromJson(
|
|
1161
|
-
context,
|
|
1162
|
-
"""
|
|
1163
|
-
[
|
|
1164
|
-
{
|
|
1165
|
-
"clientId": 19,
|
|
1166
|
-
"anchor": 2,
|
|
1167
|
-
"head": 2,
|
|
1168
|
-
"color": "not-a-color",
|
|
1169
|
-
"name": "Alice",
|
|
1170
|
-
"isFocused": true
|
|
1171
|
-
}
|
|
1172
|
-
]
|
|
1173
|
-
""".trimIndent()
|
|
1174
|
-
)
|
|
1175
|
-
|
|
1176
|
-
assertEquals(1, selections.size)
|
|
1177
|
-
assertEquals(19, selections.single().clientId)
|
|
1178
|
-
}
|
|
1179
|
-
|
|
1180
|
-
@Test
|
|
1181
|
-
fun `unordered marker scale does not change list item height`() {
|
|
1182
|
-
val context = RuntimeEnvironment.getApplication()
|
|
1183
|
-
val renderJson = singleBulletListRenderJson()
|
|
1184
|
-
val widthSpec = View.MeasureSpec.makeMeasureSpec(600, View.MeasureSpec.EXACTLY)
|
|
1185
|
-
val heightSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)
|
|
1186
|
-
|
|
1187
|
-
fun measureHeight(markerScale: Float): Int {
|
|
1188
|
-
val theme = EditorTheme.fromJson(
|
|
1189
|
-
"""
|
|
1190
|
-
{
|
|
1191
|
-
"text": { "fontSize": 17 },
|
|
1192
|
-
"list": { "markerScale": $markerScale }
|
|
1193
|
-
}
|
|
1194
|
-
""".trimIndent()
|
|
1195
|
-
)
|
|
1196
|
-
val editText = EditorEditText(context)
|
|
1197
|
-
editText.setText(
|
|
1198
|
-
RenderBridge.buildSpannable(
|
|
1199
|
-
renderJson,
|
|
1200
|
-
17f,
|
|
1201
|
-
Color.BLACK,
|
|
1202
|
-
theme,
|
|
1203
|
-
1f
|
|
1204
|
-
)
|
|
1205
|
-
)
|
|
1206
|
-
editText.measure(widthSpec, heightSpec)
|
|
1207
|
-
editText.layout(0, 0, editText.measuredWidth, editText.measuredHeight)
|
|
1208
|
-
return editText.measuredHeight
|
|
1209
|
-
}
|
|
1210
|
-
|
|
1211
|
-
val normalHeight = measureHeight(1f)
|
|
1212
|
-
val scaledHeight = measureHeight(2f)
|
|
1213
|
-
|
|
1214
|
-
assertEquals(normalHeight, scaledHeight)
|
|
1215
|
-
}
|
|
1216
|
-
|
|
1217
|
-
@Test
|
|
1218
|
-
fun `unordered marker scale does not change spacer heavy example height`() {
|
|
1219
|
-
val context = RuntimeEnvironment.getApplication()
|
|
1220
|
-
val renderJson = exampleRenderJson()
|
|
1221
|
-
val widthSpec = View.MeasureSpec.makeMeasureSpec(902, View.MeasureSpec.EXACTLY)
|
|
1222
|
-
val heightSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)
|
|
1223
|
-
|
|
1224
|
-
fun measureHeight(markerScale: Float): Int {
|
|
1225
|
-
val theme = exampleTheme(markerScale)
|
|
1226
|
-
val editText = EditorEditText(context)
|
|
1227
|
-
editText.setBaseStyle(
|
|
1228
|
-
17f * 2.625f,
|
|
1229
|
-
Color.parseColor("#2a2118"),
|
|
1230
|
-
Color.parseColor("#f6f1e8")
|
|
1231
|
-
)
|
|
1232
|
-
editText.applyTheme(theme)
|
|
1233
|
-
editText.setText(
|
|
1234
|
-
RenderBridge.buildSpannable(
|
|
1235
|
-
renderJson,
|
|
1236
|
-
17f,
|
|
1237
|
-
Color.parseColor("#2a2118"),
|
|
1238
|
-
theme,
|
|
1239
|
-
2.625f
|
|
1240
|
-
)
|
|
1241
|
-
)
|
|
1242
|
-
editText.measure(widthSpec, heightSpec)
|
|
1243
|
-
editText.layout(0, 0, editText.measuredWidth, editText.measuredHeight)
|
|
1244
|
-
return editText.measuredHeight
|
|
1245
|
-
}
|
|
1246
|
-
|
|
1247
|
-
val normalHeight = measureHeight(1f)
|
|
1248
|
-
val scaledHeight = measureHeight(2f)
|
|
1249
|
-
|
|
1250
|
-
assertEquals(normalHeight, scaledHeight)
|
|
1251
|
-
}
|
|
1252
|
-
|
|
1253
|
-
@Test
|
|
1254
|
-
fun `editor auto grow height recomputes from new text before relayout`() {
|
|
1255
|
-
val context = RuntimeEnvironment.getApplication()
|
|
1256
|
-
val widthSpec = View.MeasureSpec.makeMeasureSpec(600, View.MeasureSpec.EXACTLY)
|
|
1257
|
-
val wrapHeightSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)
|
|
1258
|
-
|
|
1259
|
-
val subject = EditorEditText(context)
|
|
1260
|
-
subject.layoutParams = ViewGroup.LayoutParams(
|
|
1261
|
-
ViewGroup.LayoutParams.MATCH_PARENT,
|
|
1262
|
-
ViewGroup.LayoutParams.WRAP_CONTENT
|
|
1263
|
-
)
|
|
1264
|
-
subject.setText("Short content")
|
|
1265
|
-
subject.measure(widthSpec, wrapHeightSpec)
|
|
1266
|
-
subject.layout(0, 0, subject.measuredWidth, subject.measuredHeight)
|
|
1267
|
-
val shortHeight = subject.resolveAutoGrowHeight()
|
|
1268
|
-
|
|
1269
|
-
val tallText = "Line one\nLine two\nLine three\nLine four\nLine five"
|
|
1270
|
-
val expectedView = EditorEditText(context)
|
|
1271
|
-
expectedView.layoutParams = ViewGroup.LayoutParams(
|
|
1272
|
-
ViewGroup.LayoutParams.MATCH_PARENT,
|
|
1273
|
-
ViewGroup.LayoutParams.WRAP_CONTENT
|
|
1274
|
-
)
|
|
1275
|
-
expectedView.setText(tallText)
|
|
1276
|
-
expectedView.measure(widthSpec, wrapHeightSpec)
|
|
1277
|
-
expectedView.layout(0, 0, expectedView.measuredWidth, expectedView.measuredHeight)
|
|
1278
|
-
val expectedTallHeight = expectedView.resolveAutoGrowHeight()
|
|
1279
|
-
|
|
1280
|
-
subject.setText(tallText)
|
|
1281
|
-
|
|
1282
|
-
val resolvedBeforeRelayout = subject.resolveAutoGrowHeight()
|
|
1283
|
-
|
|
1284
|
-
assertTrue(
|
|
1285
|
-
"Expected taller content height to exceed original height",
|
|
1286
|
-
expectedTallHeight > shortHeight
|
|
1287
|
-
)
|
|
1288
|
-
assertEquals(expectedTallHeight, resolvedBeforeRelayout)
|
|
1289
|
-
}
|
|
1290
|
-
|
|
1291
|
-
@Test
|
|
1292
|
-
fun `rich text editor auto grow keeps measured spacer content height before layout`() {
|
|
1293
|
-
val richTextEditorView = RichTextEditorView(RuntimeEnvironment.getApplication())
|
|
1294
|
-
richTextEditorView.setHeightBehavior(EditorHeightBehavior.AUTO_GROW)
|
|
1295
|
-
val spannable = RenderBridge.buildSpannable(
|
|
1296
|
-
exampleRenderJson(),
|
|
1297
|
-
17f,
|
|
1298
|
-
Color.BLACK,
|
|
1299
|
-
exampleTheme(),
|
|
1300
|
-
2.625f
|
|
1301
|
-
)
|
|
1302
|
-
richTextEditorView.editorEditText.setText(spannable)
|
|
1303
|
-
|
|
1304
|
-
val widthSpec = View.MeasureSpec.makeMeasureSpec(902, View.MeasureSpec.EXACTLY)
|
|
1305
|
-
val heightSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.EXACTLY)
|
|
1306
|
-
richTextEditorView.measure(widthSpec, heightSpec)
|
|
1307
|
-
|
|
1308
|
-
assertTrue(
|
|
1309
|
-
"Spacer-heavy content should have a positive measured height",
|
|
1310
|
-
richTextEditorView.measuredHeight > 0
|
|
1311
|
-
)
|
|
1312
|
-
assertEquals(
|
|
1313
|
-
"Auto-grow container should track the measured child height before layout",
|
|
1314
|
-
richTextEditorView.editorEditText.measuredHeight,
|
|
1315
|
-
richTextEditorView.measuredHeight
|
|
1316
|
-
)
|
|
1317
|
-
assertTrue(
|
|
1318
|
-
"Pre-layout fallback height should not exceed the measured spacer layout height",
|
|
1319
|
-
richTextEditorView.editorEditText.resolveAutoGrowHeight() <= richTextEditorView.measuredHeight
|
|
1320
|
-
)
|
|
1321
|
-
}
|
|
1322
|
-
|
|
1323
|
-
@Test
|
|
1324
|
-
fun `example content layout does not end with multiple blank lines`() {
|
|
1325
|
-
val editText = EditorEditText(RuntimeEnvironment.getApplication())
|
|
1326
|
-
val theme = exampleTheme()
|
|
1327
|
-
editText.setBaseStyle(17f * 2.625f, Color.parseColor("#2a2118"), Color.parseColor("#f6f1e8"))
|
|
1328
|
-
editText.applyTheme(theme)
|
|
1329
|
-
editText.setText(
|
|
1330
|
-
RenderBridge.buildSpannable(
|
|
1331
|
-
exampleRenderJson(),
|
|
1332
|
-
17f,
|
|
1333
|
-
Color.parseColor("#2a2118"),
|
|
1334
|
-
theme,
|
|
1335
|
-
2.625f
|
|
1336
|
-
)
|
|
1337
|
-
)
|
|
1338
|
-
|
|
1339
|
-
val widthSpec = View.MeasureSpec.makeMeasureSpec(902, View.MeasureSpec.EXACTLY)
|
|
1340
|
-
val heightSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)
|
|
1341
|
-
editText.measure(widthSpec, heightSpec)
|
|
1342
|
-
editText.layout(0, 0, editText.measuredWidth, editText.measuredHeight)
|
|
1343
|
-
|
|
1344
|
-
val layout = editText.layout
|
|
1345
|
-
assertTrue("Expected layout for example content", layout != null)
|
|
1346
|
-
layout ?: return
|
|
1347
|
-
|
|
1348
|
-
val text = editText.text?.toString().orEmpty()
|
|
1349
|
-
var trailingBlankLines = 0
|
|
1350
|
-
for (line in layout.lineCount - 1 downTo 0) {
|
|
1351
|
-
val start = layout.getLineStart(line)
|
|
1352
|
-
val end = layout.getLineEnd(line)
|
|
1353
|
-
val lineText = text.substring(start, end).replace("\n", "").trim()
|
|
1354
|
-
if (lineText.isEmpty()) {
|
|
1355
|
-
trailingBlankLines += 1
|
|
1356
|
-
continue
|
|
1357
|
-
}
|
|
1358
|
-
break
|
|
1359
|
-
}
|
|
1360
|
-
|
|
1361
|
-
val spacerSpans = editText.text?.getSpans(0, text.length, ParagraphSpacerSpan::class.java) ?: emptyArray()
|
|
1362
|
-
assertTrue(
|
|
1363
|
-
"Trailing blank lines=$trailingBlankLines lineCount=${layout.lineCount} text='${text.replace("\n", "\\n")}' spacerCount=${spacerSpans.size} measuredHeight=${editText.measuredHeight}",
|
|
1364
|
-
trailingBlankLines <= 1
|
|
1365
|
-
)
|
|
1366
|
-
}
|
|
1367
|
-
}
|