@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.
Files changed (68) hide show
  1. package/android/build.gradle +35 -0
  2. package/android/src/main/java/com/apollohg/editor/CaretGeometry.kt +2 -2
  3. package/android/src/main/java/com/apollohg/editor/EditorEditText.kt +138 -597
  4. package/android/src/main/java/com/apollohg/editor/EditorInputConnection.kt +13 -3
  5. package/android/src/main/java/com/apollohg/editor/InputSnapshotSupport.kt +6 -0
  6. package/android/src/main/java/com/apollohg/editor/NativeBlockEditorSurface.kt +1086 -0
  7. package/android/src/main/java/com/apollohg/editor/NativeEditorExpoView.kt +1 -1
  8. package/android/src/main/java/com/apollohg/editor/NativeEditorModule.kt +26 -50
  9. package/android/src/main/java/com/apollohg/editor/NativeImageLoader.kt +89 -0
  10. package/android/src/main/java/com/apollohg/editor/RemoteSelectionOverlayView.kt +31 -66
  11. package/android/src/main/java/com/apollohg/editor/RichTextEditorView.kt +98 -33
  12. package/dist/EditorToolbar.d.ts +1 -1
  13. package/dist/EditorToolbar.js +19 -0
  14. package/dist/NativeEditorBridge.d.ts +62 -35
  15. package/dist/NativeEditorBridge.js +122 -79
  16. package/dist/NativeProseViewer.d.ts +13 -34
  17. package/dist/NativeProseViewer.js +40 -465
  18. package/dist/NativeRichTextEditor.d.ts +10 -1
  19. package/dist/NativeRichTextEditor.js +72 -34
  20. package/dist/index.d.ts +2 -2
  21. package/dist/index.js +2 -1
  22. package/dist/schemas.d.ts +11 -0
  23. package/dist/schemas.js +149 -0
  24. package/dist/useNativeEditor.d.ts +3 -3
  25. package/dist/useNativeEditor.js +3 -3
  26. package/ios/EditorCore.xcframework/ios-arm64/libeditor_core.a +0 -0
  27. package/ios/EditorCore.xcframework/ios-arm64_x86_64-simulator/libeditor_core.a +0 -0
  28. package/ios/Generated_editor_core.swift +97 -2
  29. package/ios/NativeBlockEditorSurface.swift +1393 -0
  30. package/ios/NativeEditorExpoView.swift +3 -3
  31. package/ios/NativeEditorModule.swift +30 -45
  32. package/ios/NativeInputSupport.swift +396 -0
  33. package/ios/PositionBridge.swift +4 -4
  34. package/ios/RichTextEditorView.swift +333 -786
  35. package/ios/editor_coreFFI/editor_coreFFI.h +147 -70
  36. package/package.json +1 -1
  37. package/rust/android/arm64-v8a/libeditor_core.so +0 -0
  38. package/rust/android/armeabi-v7a/libeditor_core.so +0 -0
  39. package/rust/android/x86_64/libeditor_core.so +0 -0
  40. package/rust/bindings/kotlin/uniffi/editor_core/editor_core.kt +2087 -1399
  41. package/android/src/androidTest/AndroidManifest.xml +0 -8
  42. package/android/src/androidTest/java/com/apollohg/editor/MeasureHeightInstrumentedTest.kt +0 -53
  43. package/android/src/androidTest/java/com/apollohg/editor/NativeDeviceCollaborationInitialSyncTest.kt +0 -241
  44. package/android/src/androidTest/java/com/apollohg/editor/NativeDeviceImeRegressionTest.kt +0 -338
  45. package/android/src/androidTest/java/com/apollohg/editor/NativeDeviceOutsideTapTest.kt +0 -789
  46. package/android/src/androidTest/java/com/apollohg/editor/NativeDevicePerformanceTest.kt +0 -350
  47. package/android/src/androidTest/java/com/apollohg/editor/NativeEditorOutsideTapActivity.kt +0 -5
  48. package/android/src/main/java/com/apollohg/editor/NativeProseViewerExpoView.kt +0 -315
  49. package/android/src/main/java/com/apollohg/editor/RenderBridge.kt +0 -2172
  50. package/android/src/sharedTest/java/com/apollohg/editor/NativePerformanceSupport.kt +0 -482
  51. package/android/src/test/java/com/apollohg/editor/CaretGeometryTest.kt +0 -137
  52. package/android/src/test/java/com/apollohg/editor/EditorEditTextHardwareKeyTest.kt +0 -321
  53. package/android/src/test/java/com/apollohg/editor/EditorInputConnectionTest.kt +0 -4202
  54. package/android/src/test/java/com/apollohg/editor/NativeEditorExpoViewTest.kt +0 -2581
  55. package/android/src/test/java/com/apollohg/editor/NativeEditorModuleTest.kt +0 -27
  56. package/android/src/test/java/com/apollohg/editor/NativePerformanceTest.kt +0 -197
  57. package/android/src/test/java/com/apollohg/editor/NativeProseViewerExpoViewTest.kt +0 -84
  58. package/android/src/test/java/com/apollohg/editor/NativeToolbarTest.kt +0 -544
  59. package/android/src/test/java/com/apollohg/editor/PositionBridgeTest.kt +0 -461
  60. package/android/src/test/java/com/apollohg/editor/RenderBridgeTest.kt +0 -2054
  61. package/android/src/test/java/com/apollohg/editor/RichTextEditorViewTest.kt +0 -1367
  62. package/ios/EditorLayoutManager.swift +0 -774
  63. package/ios/NativeProseViewerExpoView.swift +0 -276
  64. package/ios/RenderBridge.swift +0 -1708
  65. package/ios/Tests/NativePerformanceTests.swift +0 -687
  66. package/ios/Tests/PositionBridgeTests.swift +0 -706
  67. package/ios/Tests/RenderBridgeTests.swift +0 -2236
  68. package/ios/Tests/RichTextEditorViewTests.swift +0 -6548
@@ -959,7 +959,7 @@ class NativeEditorExpoView(
959
959
  cancelPendingToolbarRefocus()
960
960
  clearPendingNativeActionRetry()
961
961
  }
962
- richTextView.editorEditText.isEditable = editable
962
+ richTextView.setEditable(editable)
963
963
  updateKeyboardToolbarVisibility()
964
964
  }
965
965
 
@@ -165,6 +165,26 @@ class NativeEditorModule : Module() {
165
165
  val editorId = nativeULong(id) ?: return@Function nativeArgumentError("editor id")
166
166
  editorOutdentListItem(editorId)
167
167
  }
168
+ Function("editorInsertTableRow") { id: Int, after: Boolean ->
169
+ val editorId = nativeULong(id) ?: return@Function nativeArgumentError("editor id")
170
+ editorInsertTableRow(editorId, after)
171
+ }
172
+ Function("editorDeleteTableRow") { id: Int ->
173
+ val editorId = nativeULong(id) ?: return@Function nativeArgumentError("editor id")
174
+ editorDeleteTableRow(editorId)
175
+ }
176
+ Function("editorInsertTableColumn") { id: Int, after: Boolean ->
177
+ val editorId = nativeULong(id) ?: return@Function nativeArgumentError("editor id")
178
+ editorInsertTableColumn(editorId, after)
179
+ }
180
+ Function("editorDeleteTableColumn") { id: Int ->
181
+ val editorId = nativeULong(id) ?: return@Function nativeArgumentError("editor id")
182
+ editorDeleteTableColumn(editorId)
183
+ }
184
+ Function("editorMoveTableCell") { id: Int, forward: Boolean ->
185
+ val editorId = nativeULong(id) ?: return@Function nativeArgumentError("editor id")
186
+ editorMoveTableCell(editorId, forward)
187
+ }
168
188
  Function("editorInsertNode") { id: Int, nodeType: String ->
169
189
  val editorId = nativeULong(id) ?: return@Function nativeArgumentError("editor id")
170
190
  editorInsertNode(editorId, nodeType)
@@ -359,6 +379,12 @@ class NativeEditorModule : Module() {
359
379
  val headPosition = nativeUInt(head) ?: return@Function
360
380
  editorSetSelection(editorId, anchorPosition, headPosition)
361
381
  }
382
+ Function("editorSetCellSelection") { id: Int, anchor: Int, head: Int ->
383
+ val editorId = nativeULong(id) ?: return@Function
384
+ val anchorPosition = nativeUInt(anchor) ?: return@Function
385
+ val headPosition = nativeUInt(head) ?: return@Function
386
+ editorSetCellSelection(editorId, anchorPosition, headPosition)
387
+ }
362
388
  Function("editorSetSelectionScalar") { id: Int, scalarAnchor: Int, scalarHead: Int ->
363
389
  val editorId = nativeULong(id) ?: return@Function
364
390
  val anchor = nativeUInt(scalarAnchor) ?: return@Function
@@ -405,33 +431,6 @@ class NativeEditorModule : Module() {
405
431
  val editorId = nativeULong(id) ?: return@Function false
406
432
  editorCanRedo(editorId)
407
433
  }
408
- Function("renderDocumentJson") { configJson: String, json: String ->
409
- val editorId = editorCreate(configJson)
410
- try {
411
- editorSetJson(editorId, json)
412
- } finally {
413
- editorDestroy(editorId)
414
- }
415
- }
416
- Function("measureContentHeight") { renderJson: String, themeJson: String?, width: Double ->
417
- val density = appContext.reactContext?.resources?.displayMetrics?.density ?: 1f
418
- val height = RenderBridge.measureHeight(
419
- json = renderJson,
420
- themeJson = themeJson,
421
- width = width.toFloat(),
422
- density = density
423
- )
424
- height.toDouble()
425
- }
426
- Function("renderDocumentHtml") { configJson: String, html: String ->
427
- val editorId = editorCreate(configJson)
428
- try {
429
- editorSetHtml(editorId, html)
430
- } finally {
431
- editorDestroy(editorId)
432
- }
433
- }
434
-
435
434
  View(NativeEditorExpoView::class) {
436
435
  Events(
437
436
  "onEditorUpdate",
@@ -534,28 +533,5 @@ class NativeEditorModule : Module() {
534
533
  }
535
534
 
536
535
  }
537
-
538
- View(NativeProseViewerExpoView::class) {
539
- Name("NativeProseViewer")
540
- Events("onContentHeightChange", "onPressLink", "onPressMention")
541
-
542
- Prop("renderJson") { view: NativeProseViewerExpoView, renderJson: String? ->
543
- view.setRenderJson(renderJson)
544
- }
545
- Prop("themeJson") { view: NativeProseViewerExpoView, themeJson: String? ->
546
- view.setThemeJson(themeJson)
547
- }
548
- Prop("collapsesWhenEmpty") {
549
- view: NativeProseViewerExpoView,
550
- collapsesWhenEmpty: Boolean? ->
551
- view.setCollapsesWhenEmpty(collapsesWhenEmpty)
552
- }
553
- Prop("enableLinkTaps") { view: NativeProseViewerExpoView, enableLinkTaps: Boolean? ->
554
- view.setEnableLinkTaps(enableLinkTaps)
555
- }
556
- Prop("interceptLinkTaps") { view: NativeProseViewerExpoView, interceptLinkTaps: Boolean? ->
557
- view.setInterceptLinkTaps(interceptLinkTaps)
558
- }
559
- }
560
536
  }
561
537
  }
@@ -0,0 +1,89 @@
1
+ package com.openeditor.editor
2
+
3
+ import android.graphics.Bitmap
4
+ import android.graphics.BitmapFactory
5
+ import android.util.Base64
6
+ import android.util.Log
7
+ import android.util.LruCache
8
+ import java.net.URL
9
+ import java.util.concurrent.ArrayBlockingQueue
10
+ import java.util.concurrent.ThreadPoolExecutor
11
+ import java.util.concurrent.TimeUnit
12
+
13
+ internal object NativeImageDecoder {
14
+ private const val MAX_DECODE_DIMENSION_PX = 2048
15
+ private const val LOG_TAG = "NativeEditorImage"
16
+
17
+ fun decodeSource(source: String): Bitmap? {
18
+ val bytes = decodeDataUrlBytes(source) ?: runCatching {
19
+ URL(source).openStream().use { it.readBytes() }
20
+ }.getOrNull() ?: return null
21
+ val bounds = BitmapFactory.Options().apply { inJustDecodeBounds = true }
22
+ BitmapFactory.decodeByteArray(bytes, 0, bytes.size, bounds)
23
+ if (bounds.outWidth <= 0 || bounds.outHeight <= 0) return null
24
+ val options = BitmapFactory.Options().apply {
25
+ inSampleSize = calculateInSampleSize(bounds.outWidth, bounds.outHeight)
26
+ }
27
+ return BitmapFactory.decodeByteArray(bytes, 0, bytes.size, options)
28
+ }
29
+
30
+ fun decodeDataUrlBytes(source: String): ByteArray? {
31
+ val trimmed = source.trim()
32
+ if (!trimmed.startsWith("data:image/", ignoreCase = true)) return null
33
+ val comma = trimmed.indexOf(',')
34
+ if (comma <= 0 || !trimmed.substring(0, comma).contains(";base64", ignoreCase = true)) return null
35
+ val payload = trimmed.substring(comma + 1).filterNot(Char::isWhitespace)
36
+ for (flags in intArrayOf(Base64.DEFAULT, Base64.NO_WRAP, Base64.URL_SAFE or Base64.NO_WRAP)) {
37
+ runCatching { Base64.decode(payload, flags) }.getOrNull()?.let { return it }
38
+ }
39
+ Log.w(LOG_TAG, "Unsupported image data URL")
40
+ return null
41
+ }
42
+
43
+ fun calculateInSampleSize(width: Int, height: Int): Int {
44
+ var sample = 1
45
+ while (width / sample > MAX_DECODE_DIMENSION_PX || height / sample > MAX_DECODE_DIMENSION_PX) {
46
+ sample *= 2
47
+ }
48
+ return sample
49
+ }
50
+ }
51
+
52
+ internal object NativeImageLoader {
53
+ private val cache = object : LruCache<String, Bitmap>(32 * 1024 * 1024) {
54
+ override fun sizeOf(key: String, value: Bitmap): Int = value.byteCount
55
+ }
56
+ private val executor = ThreadPoolExecutor(
57
+ 2,
58
+ 2,
59
+ 30,
60
+ TimeUnit.SECONDS,
61
+ ArrayBlockingQueue(64),
62
+ ThreadPoolExecutor.DiscardOldestPolicy()
63
+ )
64
+ private val lock = Any()
65
+ private val inFlight = mutableMapOf<String, MutableList<(Bitmap?) -> Unit>>()
66
+
67
+ @Volatile
68
+ internal var decodeSourceOverride: ((String) -> Bitmap?)? = null
69
+
70
+ internal fun resetForTesting() {
71
+ synchronized(cache) { cache.evictAll() }
72
+ synchronized(lock) { inFlight.clear() }
73
+ decodeSourceOverride = null
74
+ }
75
+
76
+ fun load(source: String, onLoaded: (Bitmap?) -> Unit) {
77
+ synchronized(cache) { cache.get(source) }?.let(onLoaded)?.also { return }
78
+ synchronized(lock) {
79
+ inFlight[source]?.let { it += onLoaded; return }
80
+ inFlight[source] = mutableListOf(onLoaded)
81
+ }
82
+ executor.execute {
83
+ val bitmap = decodeSourceOverride?.invoke(source) ?: NativeImageDecoder.decodeSource(source)
84
+ if (bitmap != null) synchronized(cache) { cache.put(source, bitmap) }
85
+ val callbacks = synchronized(lock) { inFlight.remove(source).orEmpty() }
86
+ callbacks.forEach { it(bitmap) }
87
+ }
88
+ }
89
+ }
@@ -11,7 +11,6 @@ import android.util.TypedValue
11
11
  import android.view.View
12
12
  import androidx.appcompat.content.res.AppCompatResources
13
13
  import org.json.JSONArray
14
- import uniffi.editor_core.editorDocToScalar
15
14
 
16
15
  data class RemoteSelectionDecoration(
17
16
  val clientId: Int,
@@ -102,11 +101,9 @@ class RemoteSelectionOverlayView @JvmOverloads constructor(
102
101
 
103
102
  private data class GeometrySnapshot(
104
103
  val editorId: Long,
105
- val text: String,
106
- val layoutWidth: Int,
107
- val layoutHeight: Int,
108
- val baseX: Int,
109
- val baseY: Int,
104
+ val surfaceRevision: Long,
105
+ val surfaceWidth: Int,
106
+ val surfaceHeight: Int,
110
107
  val width: Int,
111
108
  val height: Int,
112
109
  val selections: List<RemoteSelectionDecoration>,
@@ -114,8 +111,9 @@ class RemoteSelectionOverlayView @JvmOverloads constructor(
114
111
 
115
112
  private data class GeometryContext(
116
113
  val snapshot: GeometrySnapshot,
117
- val layout: android.text.Layout,
118
- val caretWidth: Float,
114
+ val surface: NativeBlockEditorSurface,
115
+ val baseX: Float,
116
+ val baseY: Float,
119
117
  )
120
118
 
121
119
  private var editorView: RichTextEditorView? = null
@@ -123,9 +121,6 @@ class RemoteSelectionOverlayView @JvmOverloads constructor(
123
121
  private var cachedSnapshot: GeometrySnapshot? = null
124
122
  private var cachedGeometry: List<CachedSelectionGeometry> = emptyList()
125
123
  internal var editorIdOverrideForTesting: Long? = null
126
- internal var docToScalarResolver: (Long, Int) -> Int = { editorId, docPos ->
127
- editorDocToScalar(editorId.toULong(), docPos.toUInt()).toInt()
128
- }
129
124
  private val selectionPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
130
125
  style = Paint.Style.FILL
131
126
  }
@@ -208,20 +203,21 @@ class RemoteSelectionOverlayView @JvmOverloads constructor(
208
203
  return cachedGeometry
209
204
  }
210
205
 
211
- val text = context.snapshot.text
212
- val editorId = context.snapshot.editorId
213
206
  val geometry = remoteSelections.map { selection ->
214
207
  val startDoc = minOf(selection.anchor, selection.head)
215
208
  val endDoc = maxOf(selection.anchor, selection.head)
216
- val startScalar = docToScalarResolver(editorId, startDoc)
217
- val endScalar = docToScalarResolver(editorId, endDoc)
218
- val startUtf16 = PositionBridge.scalarToUtf16(startScalar, text).coerceIn(0, text.length)
219
- val endUtf16 = PositionBridge.scalarToUtf16(endScalar, text).coerceIn(0, text.length)
220
-
221
- val selectionPath = if (startUtf16 != endUtf16) {
209
+ val selectionRects = context.surface.selectionRectsForDocRange(startDoc, endDoc)
210
+ val selectionPath = if (selectionRects.isNotEmpty()) {
222
211
  Path().apply {
223
- context.layout.getSelectionPath(startUtf16, endUtf16, this)
224
- offset(context.snapshot.baseX.toFloat(), context.snapshot.baseY.toFloat())
212
+ selectionRects.forEach { rect ->
213
+ addRect(
214
+ rect.left + context.baseX,
215
+ rect.top + context.baseY,
216
+ rect.right + context.baseX,
217
+ rect.bottom + context.baseY,
218
+ Path.Direction.CW
219
+ )
220
+ }
225
221
  }
226
222
  } else {
227
223
  null
@@ -231,15 +227,11 @@ class RemoteSelectionOverlayView @JvmOverloads constructor(
231
227
  clientId = selection.clientId,
232
228
  selectionPath = selectionPath,
233
229
  selectionColor = withAlpha(selection.color, 0.18f),
234
- caretRect = caretRectForOffset(
235
- endUtf16 = endUtf16,
236
- textLength = text.length,
237
- layout = context.layout,
238
- baseX = context.snapshot.baseX.toFloat(),
239
- baseY = context.snapshot.baseY.toFloat(),
240
- caretWidth = context.caretWidth,
241
- isFocused = selection.isFocused,
242
- ),
230
+ caretRect = if (selection.isFocused) {
231
+ context.surface.caretRectForDocPosition(selection.head)?.apply {
232
+ offset(context.baseX, context.baseY)
233
+ }
234
+ } else null,
243
235
  caretColor = selection.color,
244
236
  )
245
237
  }
@@ -254,29 +246,23 @@ class RemoteSelectionOverlayView @JvmOverloads constructor(
254
246
  val editorId = resolvedEditorId(editorView)
255
247
  if (editorId == 0L || remoteSelections.isEmpty()) return null
256
248
 
257
- val editText = editorView.editorEditText
258
- val layout = editText.layout ?: return null
259
- val text = editText.text?.toString() ?: return null
260
- val baseX = editorView.editorViewport.left + editorView.editorScrollView.left + editText.left +
261
- editText.compoundPaddingLeft
262
- val baseY = editorView.editorViewport.top + editorView.editorScrollView.top + editText.top +
263
- editText.compoundPaddingTop - editorView.editorScrollView.scrollY
264
- val caretWidth = maxOf(2f, resources.displayMetrics.density)
249
+ val surface = editorView.blockSurface
250
+ val baseX = (editorView.editorViewport.left + surface.left).toFloat()
251
+ val baseY = (editorView.editorViewport.top + surface.top).toFloat()
265
252
 
266
253
  return GeometryContext(
267
254
  snapshot = GeometrySnapshot(
268
255
  editorId = editorId,
269
- text = text,
270
- layoutWidth = layout.width,
271
- layoutHeight = layout.height,
272
- baseX = baseX,
273
- baseY = baseY,
256
+ surfaceRevision = surface.geometryRevision,
257
+ surfaceWidth = surface.width,
258
+ surfaceHeight = surface.height,
274
259
  width = width,
275
260
  height = height,
276
261
  selections = remoteSelections,
277
262
  ),
278
- layout = layout,
279
- caretWidth = caretWidth,
263
+ surface = surface,
264
+ baseX = baseX,
265
+ baseY = baseY,
280
266
  )
281
267
  }
282
268
 
@@ -284,27 +270,6 @@ class RemoteSelectionOverlayView @JvmOverloads constructor(
284
270
  cachedSnapshot = null
285
271
  }
286
272
 
287
- private fun caretRectForOffset(
288
- endUtf16: Int,
289
- textLength: Int,
290
- layout: android.text.Layout,
291
- baseX: Float,
292
- baseY: Float,
293
- caretWidth: Float,
294
- isFocused: Boolean,
295
- ): RectF? {
296
- if (!isFocused) return null
297
-
298
- val clampedOffset = endUtf16.coerceIn(0, textLength)
299
- val lineLookupOffset = clampedOffset.coerceAtMost(maxOf(textLength - 1, 0))
300
- val line = layout.getLineForOffset(lineLookupOffset)
301
- val horizontal = layout.getPrimaryHorizontal(clampedOffset)
302
- val caretLeft = baseX + horizontal
303
- val caretTop = baseY + layout.getLineTop(line)
304
- val caretBottom = baseY + layout.getLineBottom(line)
305
- return RectF(caretLeft, caretTop, caretLeft + caretWidth, caretBottom)
306
- }
307
-
308
273
  private fun withAlpha(color: Int, alphaFraction: Float): Int {
309
274
  val alpha = (255f * alphaFraction).toInt().coerceIn(0, 255)
310
275
  return Color.argb(alpha, Color.red(color), Color.green(color), Color.blue(color))
@@ -7,6 +7,7 @@ import android.graphics.drawable.GradientDrawable
7
7
  import android.util.AttributeSet
8
8
  import android.view.MotionEvent
9
9
  import android.view.ViewGroup
10
+ import android.view.inputmethod.InputMethodManager
10
11
  import android.widget.FrameLayout
11
12
  import android.widget.LinearLayout
12
13
  import android.widget.ScrollView
@@ -20,7 +21,7 @@ class RichTextEditorView @JvmOverloads constructor(
20
21
  ) : LinearLayout(context, attrs, defStyleAttr) {
21
22
  val editorViewport: FrameLayout
22
23
 
23
- private class EditorScrollView(context: Context) : ScrollView(context) {
24
+ class EditorScrollView(context: Context) : ScrollView(context) {
24
25
  private fun updateParentIntercept(action: Int) {
25
26
  val canScroll = canScrollVertically(-1) || canScrollVertically(1)
26
27
  if (!canScroll) return
@@ -44,7 +45,8 @@ class RichTextEditorView @JvmOverloads constructor(
44
45
  }
45
46
 
46
47
  val editorEditText: EditorEditText
47
- val editorScrollView: ScrollView
48
+ val editorScrollView: EditorScrollView
49
+ val blockSurface: NativeBlockEditorSurface
48
50
  private val remoteSelectionOverlayView: RemoteSelectionOverlayView
49
51
  private val imageResizeOverlayView: ImageResizeOverlayView
50
52
 
@@ -58,6 +60,7 @@ class RichTextEditorView @JvmOverloads constructor(
58
60
 
59
61
  private var currentEditorId: Long = 0
60
62
  private var deferEditorUnbindOnDetach = false
63
+ private var isSyncingBlockSurfaceUpdateToInputAdapter = false
61
64
  internal var onBeforeDetachedFromWindow: (() -> Unit)? = null
62
65
 
63
66
  /** Binds or unbinds the Rust editor instance. */
@@ -74,7 +77,9 @@ class RichTextEditorView @JvmOverloads constructor(
74
77
  editorScrollView = EditorScrollView(context).apply {
75
78
  clipToPadding = false
76
79
  isFillViewport = false
80
+ alpha = 0.01f
77
81
  }
82
+ blockSurface = NativeBlockEditorSurface(context)
78
83
  editorViewport = FrameLayout(context)
79
84
  remoteSelectionOverlayView = RemoteSelectionOverlayView(context)
80
85
  imageResizeOverlayView = ImageResizeOverlayView(context)
@@ -86,6 +91,13 @@ class RichTextEditorView @JvmOverloads constructor(
86
91
  ViewGroup.LayoutParams.MATCH_PARENT
87
92
  )
88
93
  )
94
+ editorViewport.addView(
95
+ blockSurface,
96
+ FrameLayout.LayoutParams(
97
+ ViewGroup.LayoutParams.MATCH_PARENT,
98
+ ViewGroup.LayoutParams.MATCH_PARENT
99
+ )
100
+ )
89
101
  editorViewport.addView(
90
102
  remoteSelectionOverlayView,
91
103
  FrameLayout.LayoutParams(
@@ -105,7 +117,43 @@ class RichTextEditorView @JvmOverloads constructor(
105
117
  editorScrollView.setOnScrollChangeListener { _, _, _, _, _ ->
106
118
  refreshOverlays()
107
119
  }
108
- editorEditText.onSelectionOrContentMayChange = { refreshOverlays() }
120
+ editorEditText.onSelectionOrContentMayChange = {
121
+ if (editorId != 0L && !isSyncingBlockSurfaceUpdateToInputAdapter) {
122
+ blockSurface.applyUpdateJSON(
123
+ editorGetSelectionState(editorId.toULong()),
124
+ editorId
125
+ )
126
+ }
127
+ refreshOverlays()
128
+ }
129
+ editorEditText.onUpdateJSONApplied = appliedUpdate@ { updateJSON ->
130
+ if (isSyncingBlockSurfaceUpdateToInputAdapter) return@appliedUpdate
131
+ blockSurface.applyUpdateJSON(updateJSON, editorId)
132
+ }
133
+ blockSurface.onAppliedUpdate = { updateJSON ->
134
+ isSyncingBlockSurfaceUpdateToInputAdapter = true
135
+ try {
136
+ editorEditText.applyUpdateJSON(updateJSON, notifyListener = true)
137
+ } finally {
138
+ isSyncingBlockSurfaceUpdateToInputAdapter = false
139
+ }
140
+ }
141
+ blockSurface.onRequestTextInput = requestTextInput@ { docPos ->
142
+ if (editorId == 0L || !editorEditText.isEditable) return@requestTextInput
143
+ editorSetSelection(editorId.toULong(), docPos.toUInt(), docPos.toUInt())
144
+ isSyncingBlockSurfaceUpdateToInputAdapter = true
145
+ try {
146
+ editorEditText.applyUpdateJSON(
147
+ editorGetSelectionState(editorId.toULong()),
148
+ notifyListener = true
149
+ )
150
+ } finally {
151
+ isSyncingBlockSurfaceUpdateToInputAdapter = false
152
+ }
153
+ editorEditText.requestFocus()
154
+ context.getSystemService(InputMethodManager::class.java)
155
+ ?.showSoftInput(editorEditText, InputMethodManager.SHOW_IMPLICIT)
156
+ }
109
157
 
110
158
  addView(editorViewport, createContainerLayoutParams())
111
159
  updateScrollContainerAppearance()
@@ -119,6 +167,7 @@ class RichTextEditorView @JvmOverloads constructor(
119
167
  ) {
120
168
  baseBackgroundColor = backgroundColor
121
169
  editorEditText.setBaseStyle(textSizePx, textColor, backgroundColor)
170
+ blockSurface.configure(textSizePx, textColor, backgroundColor)
122
171
  updateScrollContainerAppearance()
123
172
  refreshOverlays()
124
173
  }
@@ -127,6 +176,7 @@ class RichTextEditorView @JvmOverloads constructor(
127
176
  this.theme = theme
128
177
  val previousScrollY = editorScrollView.scrollY
129
178
  editorEditText.applyTheme(theme)
179
+ blockSurface.applyTheme(theme)
130
180
  updateScrollContainerAppearance()
131
181
  updateScrollContainerInsets()
132
182
  if (heightBehavior == EditorHeightBehavior.FIXED) {
@@ -143,6 +193,12 @@ class RichTextEditorView @JvmOverloads constructor(
143
193
  refreshOverlays()
144
194
  }
145
195
 
196
+ fun setEditable(editable: Boolean) {
197
+ editorEditText.isEditable = editable
198
+ blockSurface.isEditable = editable
199
+ if (!editable) editorEditText.clearFocus()
200
+ }
201
+
146
202
  fun setHeightBehavior(heightBehavior: EditorHeightBehavior) {
147
203
  if (this.heightBehavior == heightBehavior) return
148
204
  this.heightBehavior = heightBehavior
@@ -155,6 +211,13 @@ class RichTextEditorView @JvmOverloads constructor(
155
211
  } else {
156
212
  OVER_SCROLL_NEVER
157
213
  }
214
+ blockSurface.isScrollingEnabled = heightBehavior == EditorHeightBehavior.FIXED
215
+ blockSurface.isVerticalScrollBarEnabled = heightBehavior == EditorHeightBehavior.FIXED
216
+ blockSurface.overScrollMode = if (heightBehavior == EditorHeightBehavior.FIXED) {
217
+ OVER_SCROLL_IF_CONTENT_SCROLLS
218
+ } else {
219
+ OVER_SCROLL_NEVER
220
+ }
158
221
  updateScrollContainerInsets()
159
222
  refreshOverlays()
160
223
  requestLayout()
@@ -198,10 +261,6 @@ class RichTextEditorView @JvmOverloads constructor(
198
261
  fun remoteSelectionDebugSnapshotsForTesting(): List<RemoteSelectionDebugSnapshot> =
199
262
  remoteSelectionOverlayView.debugSnapshotsForTesting()
200
263
 
201
- fun setRemoteSelectionScalarResolverForTesting(resolver: (Long, Int) -> Int) {
202
- remoteSelectionOverlayView.docToScalarResolver = resolver
203
- }
204
-
205
264
  fun setRemoteSelectionEditorIdForTesting(editorId: Long) {
206
265
  remoteSelectionOverlayView.editorIdOverrideForTesting = editorId
207
266
  }
@@ -209,21 +268,27 @@ class RichTextEditorView @JvmOverloads constructor(
209
268
  fun setContent(html: String) {
210
269
  if (editorId == 0L) return
211
270
  editorSetHtml(editorId.toULong(), html)
212
- editorEditText.applyUpdateJSON(
213
- editorGetCurrentState(editorId.toULong()),
214
- notifyListener = false,
215
- refreshInputConnectionForExternalUpdate = true
216
- )
271
+ applyFullStateToInputAndSurface(editorGetCurrentState(editorId.toULong()))
217
272
  }
218
273
 
219
274
  fun setContent(json: org.json.JSONObject) {
220
275
  if (editorId == 0L) return
221
276
  editorSetJson(editorId.toULong(), json.toString())
222
- editorEditText.applyUpdateJSON(
223
- editorGetCurrentState(editorId.toULong()),
224
- notifyListener = false,
225
- refreshInputConnectionForExternalUpdate = true
226
- )
277
+ applyFullStateToInputAndSurface(editorGetCurrentState(editorId.toULong()))
278
+ }
279
+
280
+ private fun applyFullStateToInputAndSurface(stateJSON: String) {
281
+ isSyncingBlockSurfaceUpdateToInputAdapter = true
282
+ try {
283
+ editorEditText.applyUpdateJSON(
284
+ stateJSON,
285
+ notifyListener = false,
286
+ refreshInputConnectionForExternalUpdate = true
287
+ )
288
+ } finally {
289
+ isSyncingBlockSurfaceUpdateToInputAdapter = false
290
+ }
291
+ blockSurface.applyUpdateJSON(stateJSON, editorId)
227
292
  }
228
293
 
229
294
  internal fun rebindEditorIfNeeded(notifyListener: Boolean = true) {
@@ -259,6 +324,7 @@ class RichTextEditorView @JvmOverloads constructor(
259
324
  editorEditText.discardTransientNativeInputForEditorRebind()
260
325
  }
261
326
  currentEditorId = value
327
+ blockSurface.bind(value)
262
328
  if (bindEditor && value != 0L) {
263
329
  editorEditText.bindEditor(value, notifyListener = notifyListener)
264
330
  } else {
@@ -346,34 +412,32 @@ class RichTextEditorView @JvmOverloads constructor(
346
412
  )
347
413
 
348
414
  internal fun selectedImageGeometry(): EditorEditText.SelectedImageGeometry? {
349
- val geometry = editorEditText.selectedImageGeometry() ?: return null
415
+ if (!imageResizingEnabled) return null
416
+ val geometry = blockSurface.selectedImageGeometry() ?: return null
350
417
  return EditorEditText.SelectedImageGeometry(
351
418
  docPos = geometry.docPos,
352
419
  rect = RectF(
353
- editorViewport.left + editorScrollView.left + editorEditText.left + geometry.rect.left,
354
- editorViewport.top + editorScrollView.top + editorEditText.top + geometry.rect.top - editorScrollView.scrollY,
355
- editorViewport.left + editorScrollView.left + editorEditText.left + geometry.rect.right,
356
- editorViewport.top + editorScrollView.top + editorEditText.top + geometry.rect.bottom - editorScrollView.scrollY
420
+ editorViewport.left + blockSurface.left + geometry.rect.left,
421
+ editorViewport.top + blockSurface.top + geometry.rect.top,
422
+ editorViewport.left + blockSurface.left + geometry.rect.right,
423
+ editorViewport.top + blockSurface.top + geometry.rect.bottom
357
424
  )
358
425
  )
359
426
  }
360
427
 
361
428
  internal fun caretRect(): RectF? {
362
- val rect = editorEditText.caretRect() ?: return null
429
+ blockSurface.isInputFocused = editorEditText.hasFocus()
430
+ val rect = blockSurface.selectedCaretRect() ?: return null
363
431
  return RectF(
364
- editorViewport.left + editorScrollView.left + editorEditText.left + rect.left,
365
- editorViewport.top + editorScrollView.top + editorEditText.top + rect.top - editorScrollView.scrollY,
366
- editorViewport.left + editorScrollView.left + editorEditText.left + rect.right,
367
- editorViewport.top + editorScrollView.top + editorEditText.top + rect.bottom - editorScrollView.scrollY
432
+ editorViewport.left + blockSurface.left + rect.left,
433
+ editorViewport.top + blockSurface.top + rect.top,
434
+ editorViewport.left + blockSurface.left + rect.right,
435
+ editorViewport.top + blockSurface.top + rect.bottom
368
436
  )
369
437
  }
370
438
 
371
439
  internal fun maximumImageWidthPx(): Float {
372
- val availableWidth =
373
- maxOf(editorEditText.width, editorEditText.measuredWidth) -
374
- editorEditText.compoundPaddingLeft -
375
- editorEditText.compoundPaddingRight
376
- return availableWidth.coerceAtLeast(48).toFloat()
440
+ return blockSurface.maximumImageWidthPx()
377
441
  }
378
442
 
379
443
  internal fun clampImageSize(
@@ -389,10 +453,11 @@ class RichTextEditorView @JvmOverloads constructor(
389
453
 
390
454
  internal fun resizeImage(docPos: Int, widthPx: Float, heightPx: Float) {
391
455
  val (clampedWidth, clampedHeight) = clampImageSize(widthPx, heightPx)
392
- editorEditText.resizeImageAtDocPos(docPos, clampedWidth, clampedHeight)
456
+ blockSurface.resizeImage(docPos, clampedWidth, clampedHeight)
393
457
  }
394
458
 
395
459
  private fun refreshOverlays() {
460
+ blockSurface.isInputFocused = editorEditText.hasFocus()
396
461
  remoteSelectionOverlayView.refreshGeometry()
397
462
  imageResizeOverlayView.refresh()
398
463
  }
@@ -4,7 +4,7 @@ import type { EditorMentionTheme, EditorToolbarTheme } from './EditorTheme';
4
4
  import type { MentionSuggestion } from './addons';
5
5
  export type EditorToolbarListType = 'bulletList' | 'orderedList';
6
6
  export type EditorToolbarHeadingLevel = 1 | 2 | 3 | 4 | 5 | 6;
7
- export type EditorToolbarCommand = 'indentList' | 'outdentList' | 'undo' | 'redo';
7
+ export type EditorToolbarCommand = 'indentList' | 'outdentList' | 'insertTableRowBefore' | 'insertTableRowAfter' | 'deleteTableRow' | 'insertTableColumnBefore' | 'insertTableColumnAfter' | 'deleteTableColumn' | 'moveToNextTableCell' | 'moveToPreviousTableCell' | 'undo' | 'redo';
8
8
  export type EditorToolbarGroupPresentation = 'expand' | 'menu';
9
9
  export type EditorToolbarItemPlacement = 'start' | 'scroll' | 'end';
10
10
  export type EditorToolbarDefaultIconId = 'bold' | 'italic' | 'underline' | 'strike' | 'link' | 'image' | 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'blockquote' | 'bulletList' | 'orderedList' | 'indentList' | 'outdentList' | 'lineBreak' | 'horizontalRule' | 'undo' | 'redo';
@@ -342,6 +342,15 @@ function EditorToolbar({ activeState, historyState, onToggleBold, onToggleItalic
342
342
  return onIndentList ?? null;
343
343
  case 'outdentList':
344
344
  return onOutdentList ?? null;
345
+ case 'insertTableRowBefore':
346
+ case 'insertTableRowAfter':
347
+ case 'deleteTableRow':
348
+ case 'insertTableColumnBefore':
349
+ case 'insertTableColumnAfter':
350
+ case 'deleteTableColumn':
351
+ case 'moveToNextTableCell':
352
+ case 'moveToPreviousTableCell':
353
+ return null;
345
354
  case 'undo':
346
355
  return onUndo;
347
356
  case 'redo':
@@ -434,6 +443,16 @@ function EditorToolbar({ activeState, historyState, onToggleBold, onToggleItalic
434
443
  case 'outdentList':
435
444
  isDisabled = !canOutdentList;
436
445
  break;
446
+ case 'insertTableRowBefore':
447
+ case 'insertTableRowAfter':
448
+ case 'deleteTableRow':
449
+ case 'insertTableColumnBefore':
450
+ case 'insertTableColumnAfter':
451
+ case 'deleteTableColumn':
452
+ case 'moveToNextTableCell':
453
+ case 'moveToPreviousTableCell':
454
+ isDisabled = !commands[item.command];
455
+ break;
437
456
  case 'undo':
438
457
  isDisabled = !historyState.canUndo;
439
458
  break;