@openeditor/react-native-prose-editor 0.0.8 → 0.0.10

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.
@@ -1,15 +1,15 @@
1
1
  package com.openeditor.editor
2
2
 
3
3
  import android.content.Context
4
- import android.graphics.Canvas
5
4
  import android.graphics.Color
6
- import android.graphics.Paint
7
5
  import android.graphics.Rect
8
6
  import android.graphics.RectF
9
7
  import android.graphics.Typeface
10
8
  import android.graphics.drawable.GradientDrawable
11
9
  import android.text.SpannableStringBuilder
12
10
  import android.text.Spanned
11
+ import android.text.Editable
12
+ import android.text.TextWatcher
13
13
  import android.text.style.BackgroundColorSpan
14
14
  import android.text.style.ForegroundColorSpan
15
15
  import android.text.style.StrikethroughSpan
@@ -19,20 +19,32 @@ import android.text.style.UnderlineSpan
19
19
  import android.view.MotionEvent
20
20
  import android.view.View
21
21
  import android.view.ViewGroup
22
+ import android.view.inputmethod.BaseInputConnection
23
+ import android.view.inputmethod.EditorInfo
24
+ import android.view.inputmethod.InputConnection
25
+ import android.view.inputmethod.InputConnectionWrapper
26
+ import android.view.inputmethod.InputMethodManager
22
27
  import android.util.TypedValue
28
+ import android.text.InputType
23
29
  import android.widget.ImageView
24
30
  import android.widget.LinearLayout
25
31
  import android.widget.ScrollView
26
32
  import android.widget.TextView
33
+ import androidx.appcompat.widget.AppCompatEditText
27
34
  import org.json.JSONObject
28
35
  import uniffi.editor_core.editorDocToScalar
36
+ import uniffi.editor_core.editorDeleteBackwardAtSelectionScalar
29
37
  import uniffi.editor_core.editorGetSelectionState
38
+ import uniffi.editor_core.editorReplaceSelectionText
30
39
  import uniffi.editor_core.editorResizeImageAtDocPos
31
40
  import uniffi.editor_core.editorSetCellSelection
32
41
  import uniffi.editor_core.editorSetNodeSelection
33
42
  import uniffi.editor_core.editorSetSelection
43
+ import uniffi.editor_core.editorSplitBlock
34
44
  import uniffi.editor_core.editorToggleTaskItemCheckedAtSelectionScalar
35
45
 
46
+ internal data class SelectedImageGeometry(val docPos: Int, val rect: RectF)
47
+
36
48
  class NativeBlockEditorSurface(context: Context) : ScrollView(context) {
37
49
  var isScrollingEnabled: Boolean = true
38
50
 
@@ -48,8 +60,61 @@ class NativeBlockEditorSurface(context: Context) : ScrollView(context) {
48
60
  val isAtom: Boolean
49
61
  )
50
62
 
51
- private class TextLeaf(context: Context) : TextView(context) {
63
+ private class TextLeaf(context: Context) : AppCompatEditText(context) {
52
64
  var positionSegments: List<PositionSegment> = emptyList()
65
+ var editorSurface: NativeBlockEditorSurface? = null
66
+ private var beforeText: String = ""
67
+ private var compositionBaselineText: String? = null
68
+ private var beforeSelectionStart: Int = 0
69
+ private var beforeSelectionEnd: Int = 0
70
+
71
+ init {
72
+ addTextChangedListener(object : TextWatcher {
73
+ override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
74
+ beforeText = s?.toString().orEmpty()
75
+ if (editorSurface?.isApplyingRemoteUpdate != true && compositionBaselineText == null) {
76
+ compositionBaselineText = beforeText
77
+ }
78
+ beforeSelectionStart = selectionStart.coerceAtLeast(0)
79
+ beforeSelectionEnd = selectionEnd.coerceAtLeast(beforeSelectionStart)
80
+ }
81
+
82
+ override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) = Unit
83
+
84
+ override fun afterTextChanged(s: Editable?) {
85
+ val surface = editorSurface ?: return
86
+ if (surface.isApplyingRemoteUpdate) return
87
+ if (s != null && BaseInputConnection.getComposingSpanStart(s) >= 0) return
88
+ val baseline = compositionBaselineText ?: beforeText
89
+ compositionBaselineText = null
90
+ surface.commitLeafTextMutation(this@TextLeaf, baseline, s?.toString().orEmpty())
91
+ }
92
+ })
93
+ }
94
+
95
+ override fun onSelectionChanged(selStart: Int, selEnd: Int) {
96
+ super.onSelectionChanged(selStart, selEnd)
97
+ editorSurface?.selectionChanged(this, selStart, selEnd)
98
+ }
99
+
100
+ override fun onCreateInputConnection(outAttrs: EditorInfo): InputConnection? {
101
+ val target = super.onCreateInputConnection(outAttrs) ?: return null
102
+ return object : InputConnectionWrapper(target, false) {
103
+ override fun deleteSurroundingText(beforeLength: Int, afterLength: Int): Boolean {
104
+ if (beforeLength > 0 && selectionStart == 0 && selectionEnd == 0 &&
105
+ editorSurface?.deleteBackwardAtLeafBoundary(this@TextLeaf) == true
106
+ ) return true
107
+ return super.deleteSurroundingText(beforeLength, afterLength)
108
+ }
109
+
110
+ override fun deleteSurroundingTextInCodePoints(beforeLength: Int, afterLength: Int): Boolean {
111
+ if (beforeLength > 0 && selectionStart == 0 && selectionEnd == 0 &&
112
+ editorSurface?.deleteBackwardAtLeafBoundary(this@TextLeaf) == true
113
+ ) return true
114
+ return super.deleteSurroundingTextInCodePoints(beforeLength, afterLength)
115
+ }
116
+ }
117
+ }
53
118
 
54
119
  fun docPositionForOffset(offset: Int): Int? {
55
120
  val segment = positionSegments.firstOrNull {
@@ -82,8 +147,9 @@ class NativeBlockEditorSurface(context: Context) : ScrollView(context) {
82
147
 
83
148
  fun caretRectForDocPosition(docPosition: Int, caretWidth: Float): RectF? {
84
149
  val textLayout = layout ?: return null
85
- val offset = nativeOffsetForDocPosition(docPosition)?.coerceIn(0, text.length) ?: return null
86
- val lookupOffset = offset.coerceAtMost((text.length - 1).coerceAtLeast(0))
150
+ val textLength = text?.length ?: 0
151
+ val offset = nativeOffsetForDocPosition(docPosition)?.coerceIn(0, textLength) ?: return null
152
+ val lookupOffset = offset.coerceAtMost((textLength - 1).coerceAtLeast(0))
87
153
  val line = textLayout.getLineForOffset(lookupOffset)
88
154
  val left = compoundPaddingLeft + textLayout.getPrimaryHorizontal(offset)
89
155
  return RectF(
@@ -96,8 +162,9 @@ class NativeBlockEditorSurface(context: Context) : ScrollView(context) {
96
162
 
97
163
  fun selectionRectsForDocRange(from: Int, to: Int): List<RectF> {
98
164
  val textLayout = layout ?: return emptyList()
99
- val start = nativeOffsetForDocPosition(from)?.coerceIn(0, text.length) ?: return emptyList()
100
- val end = nativeOffsetForDocPosition(to)?.coerceIn(start, text.length) ?: return emptyList()
165
+ val textLength = text?.length ?: 0
166
+ val start = nativeOffsetForDocPosition(from)?.coerceIn(0, textLength) ?: return emptyList()
167
+ val end = nativeOffsetForDocPosition(to)?.coerceIn(start, textLength) ?: return emptyList()
101
168
  if (start == end) return emptyList()
102
169
  val firstLine = textLayout.getLineForOffset(start)
103
170
  val lastLine = textLayout.getLineForOffset((end - 1).coerceAtLeast(start))
@@ -162,22 +229,117 @@ class NativeBlockEditorSurface(context: Context) : ScrollView(context) {
162
229
  private val textLeafViews = mutableListOf<TextLeaf>()
163
230
  private var selectedNodePos: Int? = null
164
231
  private var selectedTextPos: Int? = null
165
- private val caretPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { color = 0xFF2F80ED.toInt() }
166
-
167
- var isInputFocused: Boolean = false
232
+ var onAppliedUpdate: ((String) -> Unit)? = null
233
+ var onSelectionChange: ((Int, Int) -> Unit)? = null
234
+ var onEditorUpdate: ((String) -> Unit)? = null
235
+ var onFocusChange: ((Boolean) -> Unit)? = null
236
+ var placeholder: String = ""
168
237
  set(value) {
169
238
  field = value
170
- invalidate()
239
+ textLeafViews.forEach { updateLeafHint(it) }
171
240
  }
172
-
173
- var onAppliedUpdate: ((String) -> Unit)? = null
174
- var onRequestTextInput: ((Int) -> Unit)? = null
241
+ private var configuredAutoCapitalize: String? = null
242
+ private var configuredAutoCorrect: Boolean? = null
243
+ private var configuredKeyboardType: String? = null
175
244
  var isEditable: Boolean = true
176
245
  set(value) {
177
246
  field = value
247
+ textLeafViews.forEach {
248
+ it.isEnabled = value
249
+ it.isFocusable = value
250
+ it.isFocusableInTouchMode = value
251
+ it.setTextIsSelectable(value)
252
+ }
178
253
  refreshImageSelection()
179
254
  }
180
255
 
256
+ internal val activeEditText: AppCompatEditText?
257
+ get() = textLeafViews.firstOrNull { it.hasFocus() }
258
+ ?: selectedTextPos?.let { position ->
259
+ textLeafViews.firstOrNull { it.nativeOffsetForDocPosition(position) != null }
260
+ }
261
+ ?: textLeafViews.firstOrNull()
262
+
263
+ val isEditorFocused: Boolean get() = textLeafViews.any { it.hasFocus() }
264
+
265
+ fun focus(): Boolean {
266
+ val leaf = activeEditText ?: return false
267
+ val focused = leaf.requestFocus()
268
+ if (focused) {
269
+ context.getSystemService(InputMethodManager::class.java)
270
+ ?.showSoftInput(leaf, InputMethodManager.SHOW_IMPLICIT)
271
+ }
272
+ return focused
273
+ }
274
+
275
+ fun blur() {
276
+ activeEditText?.clearFocus()
277
+ }
278
+
279
+ fun hasActiveComposition(): Boolean {
280
+ val editable = activeEditText?.text ?: return false
281
+ return BaseInputConnection.getComposingSpanStart(editable) >= 0
282
+ }
283
+
284
+ fun currentScalarSelection(): Pair<Int, Int>? {
285
+ val selection = activeDocSelection() ?: return null
286
+ if (currentEditorId == 0L) return null
287
+ return editorDocToScalar(currentEditorId.toULong(), selection.first.toUInt()).toInt() to
288
+ editorDocToScalar(currentEditorId.toULong(), selection.second.toUInt()).toInt()
289
+ }
290
+
291
+ fun activeDocPositionForUtf16Offset(offset: Int): Int? {
292
+ val leaf = activeEditText as? TextLeaf ?: return null
293
+ return leaf.docPositionForOffset(offset)
294
+ }
295
+
296
+ fun setAutoCapitalize(value: String?) {
297
+ configuredAutoCapitalize = value
298
+ val flag = when (value) {
299
+ "none" -> 0
300
+ "words" -> InputType.TYPE_TEXT_FLAG_CAP_WORDS
301
+ "characters" -> InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS
302
+ else -> InputType.TYPE_TEXT_FLAG_CAP_SENTENCES
303
+ }
304
+ textLeafViews.forEach { leaf ->
305
+ leaf.inputType = (leaf.inputType and (
306
+ InputType.TYPE_TEXT_FLAG_CAP_WORDS or
307
+ InputType.TYPE_TEXT_FLAG_CAP_SENTENCES or
308
+ InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS
309
+ ).inv()) or flag
310
+ }
311
+ }
312
+
313
+ fun setAutoCorrect(enabled: Boolean?) {
314
+ configuredAutoCorrect = enabled
315
+ textLeafViews.forEach { leaf ->
316
+ leaf.inputType = if (enabled == true) {
317
+ (leaf.inputType and InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS.inv()) or InputType.TYPE_TEXT_FLAG_AUTO_CORRECT
318
+ } else {
319
+ (leaf.inputType and InputType.TYPE_TEXT_FLAG_AUTO_CORRECT.inv()) or InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS
320
+ }
321
+ }
322
+ }
323
+
324
+ fun setKeyboardType(value: String?) {
325
+ configuredKeyboardType = value
326
+ val inputClass = when (value) {
327
+ "numeric", "number-pad", "decimal-pad" -> InputType.TYPE_CLASS_NUMBER
328
+ "phone-pad" -> InputType.TYPE_CLASS_PHONE
329
+ else -> InputType.TYPE_CLASS_TEXT
330
+ }
331
+ textLeafViews.forEach { it.inputType = (it.inputType and InputType.TYPE_MASK_CLASS.inv()) or inputClass }
332
+ }
333
+
334
+ private fun activeDocSelection(): Pair<Int, Int>? {
335
+ val leaf = activeEditText as? TextLeaf ?: return null
336
+ val start = leaf.selectionStart.coerceAtLeast(0)
337
+ val end = leaf.selectionEnd.coerceAtLeast(start)
338
+ val anchor = leaf.docPositionForOffset(start) ?: return null
339
+ val head = leaf.docPositionForOffset(end) ?: return null
340
+ return anchor to head
341
+ }
342
+
181
343
  init {
182
344
  isFillViewport = false
183
345
  clipToPadding = false
@@ -193,12 +355,6 @@ class NativeBlockEditorSurface(context: Context) : ScrollView(context) {
193
355
  override fun onTouchEvent(event: MotionEvent): Boolean =
194
356
  isScrollingEnabled && super.onTouchEvent(event)
195
357
 
196
- override fun dispatchDraw(canvas: Canvas) {
197
- super.dispatchDraw(canvas)
198
- if (!isEditable || !isInputFocused || selectedNodePos != null) return
199
- selectedCaretRect()?.let { canvas.drawRect(it, caretPaint) }
200
- }
201
-
202
358
  fun bind(editorId: Long) {
203
359
  currentEditorId = editorId
204
360
  if (editorId == 0L) {
@@ -227,10 +383,26 @@ class NativeBlockEditorSurface(context: Context) : ScrollView(context) {
227
383
  val bottomInset = ((theme?.contentInsets?.bottom ?: 0f) * density).toInt()
228
384
  val leftInset = ((theme?.contentInsets?.left ?: 0f) * density).toInt()
229
385
  setPadding(leftInset, topInset, rightInset, bottomInset)
386
+ currentRoot?.let { root ->
387
+ val focusedSelection = activeDocSelection()
388
+ // Retire the old input connection before replacing themed leaves. Keeping
389
+ // a detached EditText as the IME target can make Android replay its text
390
+ // into the newly focused canonical leaf after a uiMode change.
391
+ if (focusedSelection != null) activeEditText?.clearFocus()
392
+ isApplyingRemoteUpdate = true
393
+ rebuildAll(root)
394
+ refreshTableCellRegistry()
395
+ isApplyingRemoteUpdate = false
396
+ focusedSelection?.let { applyNativeSelection(it.first, it.second, true) }
397
+ }
230
398
  }
231
399
 
232
400
  fun applyUpdateJSON(updateJSON: String, editorId: Long) {
401
+ val shouldPreserveTextFocus = textLeafViews.any { it.hasFocus() }
233
402
  val update = try { JSONObject(updateJSON) } catch (_: Throwable) { return }
403
+ val textSelection = update.optJSONObject("selection")?.takeIf {
404
+ it.optString("type") == "text"
405
+ }?.let { it.optInt("anchor") to it.optInt("head") }
234
406
  update.optJSONObject("selection")?.let { selection ->
235
407
  selectedNodePos = if (selection.optString("type") == "node") {
236
408
  selection.optInt("pos")
@@ -243,7 +415,11 @@ class NativeBlockEditorSurface(context: Context) : ScrollView(context) {
243
415
  ) selection.optInt("anchor") else null
244
416
  refreshImageSelection()
245
417
  }
246
- val root = update.optJSONObject("renderTree")?.optJSONObject("root") ?: return
418
+ val root = update.optJSONObject("renderTree")?.optJSONObject("root")
419
+ if (root == null) {
420
+ textSelection?.let { applyNativeSelection(it.first, it.second, shouldPreserveTextFocus) }
421
+ return
422
+ }
247
423
  currentEditorId = editorId
248
424
  isApplyingRemoteUpdate = true
249
425
  val operations = update.optJSONObject("renderPatch")?.optJSONArray("operations")
@@ -256,6 +432,7 @@ class NativeBlockEditorSurface(context: Context) : ScrollView(context) {
256
432
  refreshImageSelection()
257
433
  currentRoot = root
258
434
  isApplyingRemoteUpdate = false
435
+ textSelection?.let { applyNativeSelection(it.first, it.second, shouldPreserveTextFocus) }
259
436
  geometryRevision += 1
260
437
  requestLayout()
261
438
  }
@@ -345,6 +522,10 @@ class NativeBlockEditorSurface(context: Context) : ScrollView(context) {
345
522
  rebuildAll(root)
346
523
  return
347
524
  }
525
+ if (oldView is TextLeaf) {
526
+ configureTextLeaf(oldView, node)
527
+ return
528
+ }
348
529
  val parent = oldView.parent as? ViewGroup ?: run {
349
530
  rebuildAll(root)
350
531
  return
@@ -429,6 +610,11 @@ class NativeBlockEditorSurface(context: Context) : ScrollView(context) {
429
610
  internal val topLevelViewCountForTesting: Int
430
611
  get() = stack.childCount
431
612
 
613
+ internal fun textColorForTesting(value: String): Int? =
614
+ textLeafViews.firstOrNull { it.text?.toString() == value }?.currentTextColor
615
+
616
+ internal fun tableFillColorsForTesting(): List<Int> = tableCells.map { it.baseFillColor }
617
+
432
618
  private fun viewForNode(node: JSONObject, depth: Int, path: List<Int>): View {
433
619
  val rendered = when (node.optString("kind")) {
434
620
  "list" -> listView(node, depth, path)
@@ -573,7 +759,11 @@ class NativeBlockEditorSurface(context: Context) : ScrollView(context) {
573
759
  val tableId = nextTableId++
574
760
  val table = LinearLayout(context).apply {
575
761
  orientation = LinearLayout.VERTICAL
576
- background = borderDrawable(Color.TRANSPARENT, Color.LTGRAY, 1f)
762
+ background = borderDrawable(
763
+ Color.TRANSPARENT,
764
+ currentTheme?.table?.borderColor ?: baseTextColor,
765
+ 1f
766
+ )
577
767
  }
578
768
  val rows = node.optJSONArray("children")
579
769
  for (index in 0 until (rows?.length() ?: 0)) {
@@ -599,7 +789,15 @@ class NativeBlockEditorSurface(context: Context) : ScrollView(context) {
599
789
 
600
790
  private fun cellView(node: JSONObject, tableId: Int, rowIndex: Int, columnIndex: Int, path: List<Int>): View {
601
791
  val density = resources.displayMetrics.density
602
- val fillColor = if (node.optString("nodeType") == "tableHeader") 0xFFF4F5F7.toInt() else Color.WHITE
792
+ val fillColor = if (node.optString("nodeType") == "tableHeader") {
793
+ currentTheme?.table?.headerBackgroundColor
794
+ ?: currentTheme?.codeBlock?.backgroundColor
795
+ ?: Color.TRANSPARENT
796
+ } else {
797
+ currentTheme?.table?.cellBackgroundColor
798
+ ?: currentTheme?.backgroundColor
799
+ ?: Color.TRANSPARENT
800
+ }
603
801
  val cell = CellView(context).apply {
604
802
  docPos = node.optInt("docPos", 0)
605
803
  this.tableId = tableId
@@ -610,7 +808,7 @@ class NativeBlockEditorSurface(context: Context) : ScrollView(context) {
610
808
  setPadding((8 * density).toInt(), (8 * density).toInt(), (8 * density).toInt(), (8 * density).toInt())
611
809
  background = borderDrawable(
612
810
  fillColor,
613
- Color.LTGRAY,
811
+ currentTheme?.table?.borderColor ?: baseTextColor,
614
812
  0.5f
615
813
  )
616
814
  isClickable = true
@@ -628,17 +826,29 @@ class NativeBlockEditorSurface(context: Context) : ScrollView(context) {
628
826
  }
629
827
 
630
828
  private fun textBlockView(node: JSONObject, depth: Int): View {
829
+ return TextLeaf(context).also { configureTextLeaf(it, node) }
830
+ }
831
+
832
+ private fun configureTextLeaf(leaf: TextLeaf, node: JSONObject) {
631
833
  val rendered = attributedInlineContent(node)
632
- val leaf = TextLeaf(context).apply leaf@ {
834
+ val hadFocus = leaf.hasFocus()
835
+ val previousSelectionStart = leaf.selectionStart.coerceAtLeast(0)
836
+ val previousSelectionEnd = leaf.selectionEnd.coerceAtLeast(previousSelectionStart)
837
+ leaf.apply leaf@ {
633
838
  text = rendered.first
634
839
  positionSegments = if (rendered.second.isEmpty()) {
635
- listOf(PositionSegment(0, 0, "", node.optInt("docPos", 0), node.optInt("docSize", 0), false))
840
+ listOf(PositionSegment(0, 0, "", node.optInt("docPos", 0) + if (node.optInt("docSize", 0) >= 2) 1 else 0, 0, false))
636
841
  } else {
637
842
  rendered.second
638
843
  }
639
- setTextColor(baseTextColor)
640
- setTextSize(TypedValue.COMPLEX_UNIT_PX, textSizeFor(node))
641
- typeface = typefaceFor(node)
844
+ val style = currentTheme?.effectiveTextStyle(node.optString("nodeType"))
845
+ setTextColor(style?.color ?: baseTextColor)
846
+ setTextSize(
847
+ TypedValue.COMPLEX_UNIT_PX,
848
+ style?.fontSize?.let(::spToPx) ?: textSizeFor(node)
849
+ )
850
+ typeface = style?.let { Typeface.create(it.fontFamily, it.typefaceStyle()) }
851
+ ?: typefaceFor(node)
642
852
  minLines = 1
643
853
  background = if (node.optString("nodeType") == "codeBlock" || node.optString("nodeType") == "code_block") {
644
854
  roundedDrawable(
@@ -657,26 +867,59 @@ class NativeBlockEditorSurface(context: Context) : ScrollView(context) {
657
867
  } else {
658
868
  setPadding(0, 0, 0, 0)
659
869
  }
660
- isFocusable = false
661
- isFocusableInTouchMode = false
662
- setTextIsSelectable(false)
663
- setOnTouchListener { view, event ->
664
- if (event.actionMasked != MotionEvent.ACTION_UP || !this@NativeBlockEditorSurface.isEditable) {
665
- return@setOnTouchListener true
666
- }
667
- val textView = view as TextLeaf
668
- textView.docPositionForOffset(textView.getOffsetForPosition(event.x, event.y))
669
- ?.let { onRequestTextInput?.invoke(it) }
670
- true
870
+ isEnabled = this@NativeBlockEditorSurface.isEditable
871
+ isFocusable = this@NativeBlockEditorSurface.isEditable
872
+ isFocusableInTouchMode = this@NativeBlockEditorSurface.isEditable
873
+ setTextIsSelectable(this@NativeBlockEditorSurface.isEditable)
874
+ isSingleLine = false
875
+ editorSurface = this@NativeBlockEditorSurface
876
+ updateLeafHint(this)
877
+ onFocusChangeListener = OnFocusChangeListener { _, focused ->
878
+ if (focused) onFocusChange?.invoke(true)
879
+ else if (!this@NativeBlockEditorSurface.isEditorFocused) onFocusChange?.invoke(false)
671
880
  }
881
+ applyConfiguredInputTraits(this)
882
+ if (hadFocus) {
883
+ setSelection(
884
+ previousSelectionStart.coerceAtMost(length()),
885
+ previousSelectionEnd.coerceAtMost(length())
886
+ )
887
+ }
888
+ }
889
+ }
890
+
891
+ private fun updateLeafHint(leaf: TextLeaf) {
892
+ leaf.hint = if ((textLeafViews.firstOrNull() == null || textLeafViews.firstOrNull() === leaf) && leaf.text.isNullOrEmpty()) {
893
+ placeholder
894
+ } else {
895
+ null
896
+ }
897
+ }
898
+
899
+ private fun applyConfiguredInputTraits(leaf: TextLeaf) {
900
+ val capitalFlag = when (configuredAutoCapitalize) {
901
+ "none" -> 0
902
+ "words" -> InputType.TYPE_TEXT_FLAG_CAP_WORDS
903
+ "characters" -> InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS
904
+ else -> InputType.TYPE_TEXT_FLAG_CAP_SENTENCES
905
+ }
906
+ val inputClass = when (configuredKeyboardType) {
907
+ "numeric", "number-pad", "decimal-pad" -> InputType.TYPE_CLASS_NUMBER
908
+ "phone-pad" -> InputType.TYPE_CLASS_PHONE
909
+ else -> InputType.TYPE_CLASS_TEXT
910
+ }
911
+ val suggestionFlag = if (configuredAutoCorrect == true) {
912
+ InputType.TYPE_TEXT_FLAG_AUTO_CORRECT
913
+ } else {
914
+ InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS
672
915
  }
673
- return leaf
916
+ leaf.inputType = inputClass or capitalFlag or suggestionFlag or InputType.TYPE_TEXT_FLAG_MULTI_LINE
674
917
  }
675
918
 
676
919
  private fun positionSegmentsFor(node: JSONObject): List<PositionSegment> {
677
920
  val segments = attributedInlineContent(node).second
678
921
  return if (segments.isEmpty()) {
679
- listOf(PositionSegment(0, 0, "", node.optInt("docPos", 0), node.optInt("docSize", 0), false))
922
+ listOf(PositionSegment(0, 0, "", node.optInt("docPos", 0) + if (node.optInt("docSize", 0) >= 2) 1 else 0, 0, false))
680
923
  } else {
681
924
  segments
682
925
  }
@@ -779,10 +1022,14 @@ class NativeBlockEditorSurface(context: Context) : ScrollView(context) {
779
1022
  }
780
1023
  return TextView(context).apply {
781
1024
  text = node.optString("label", node.optString("nodeType", "Block"))
782
- setTextColor(Color.DKGRAY)
1025
+ setTextColor(currentTheme?.text?.color ?: baseTextColor)
783
1026
  textSize = 14f
784
1027
  setPadding(12, 8, 12, 8)
785
- background = borderDrawable(0xFFF2F3F5.toInt(), Color.TRANSPARENT, 0f)
1028
+ background = borderDrawable(
1029
+ currentTheme?.codeBlock?.backgroundColor ?: Color.TRANSPARENT,
1030
+ Color.TRANSPARENT,
1031
+ 0f
1032
+ )
786
1033
  }
787
1034
  }
788
1035
 
@@ -800,7 +1047,12 @@ class NativeBlockEditorSurface(context: Context) : ScrollView(context) {
800
1047
  ?.let { (it * density).toInt() }
801
1048
  scaleType = ImageView.ScaleType.FIT_CENTER
802
1049
  adjustViewBounds = true
803
- background = roundedDrawable(0xFFF2F3F5.toInt(), Color.TRANSPARENT, 0f, 8f)
1050
+ background = roundedDrawable(
1051
+ currentTheme?.codeBlock?.backgroundColor ?: Color.TRANSPARENT,
1052
+ Color.TRANSPARENT,
1053
+ 0f,
1054
+ 8f
1055
+ )
804
1056
  isClickable = true
805
1057
  setOnClickListener { selectImage(this) }
806
1058
  }
@@ -834,13 +1086,13 @@ class NativeBlockEditorSurface(context: Context) : ScrollView(context) {
834
1086
  }
835
1087
  }
836
1088
 
837
- internal fun selectedImageGeometry(): EditorEditText.SelectedImageGeometry? {
1089
+ internal fun selectedImageGeometry(): SelectedImageGeometry? {
838
1090
  val docPos = selectedNodePos ?: return null
839
1091
  val imageView = imageViews.firstOrNull { it.docPos == docPos } ?: return null
840
1092
  if (!isEditable || imageView.width <= 0 || imageView.height <= 0) return null
841
1093
  val rect = Rect(0, 0, imageView.width, imageView.height)
842
1094
  offsetDescendantRectToMyCoords(imageView, rect)
843
- return EditorEditText.SelectedImageGeometry(docPos, RectF(rect))
1095
+ return SelectedImageGeometry(docPos, RectF(rect))
844
1096
  }
845
1097
 
846
1098
  internal fun selectedCaretRect(): RectF? =
@@ -874,6 +1126,80 @@ class NativeBlockEditorSurface(context: Context) : ScrollView(context) {
874
1126
  }
875
1127
  }
876
1128
 
1129
+ private fun applyNativeSelection(anchor: Int, head: Int, preservingFocus: Boolean) {
1130
+ val candidates = textLeafViews.filter {
1131
+ it.nativeOffsetForDocPosition(anchor) != null &&
1132
+ it.nativeOffsetForDocPosition(head) != null
1133
+ }
1134
+ val leaf = candidates.firstOrNull { it.hasFocus() }
1135
+ ?: (if (anchor == head) candidates.lastOrNull() else candidates.firstOrNull())
1136
+ ?: return
1137
+ val start = leaf.nativeOffsetForDocPosition(minOf(anchor, head)) ?: return
1138
+ val end = leaf.nativeOffsetForDocPosition(maxOf(anchor, head)) ?: return
1139
+ isApplyingRemoteUpdate = true
1140
+ try {
1141
+ leaf.setSelection(start.coerceIn(0, leaf.length()), end.coerceIn(0, leaf.length()))
1142
+ if (preservingFocus && !leaf.hasFocus()) leaf.requestFocus()
1143
+ } finally {
1144
+ isApplyingRemoteUpdate = false
1145
+ }
1146
+ }
1147
+
1148
+ private fun selectionChanged(leaf: TextLeaf, start: Int, end: Int) {
1149
+ if (isApplyingRemoteUpdate || !isEditable || currentEditorId == 0L) return
1150
+ val anchor = leaf.docPositionForOffset(start.coerceAtLeast(0)) ?: return
1151
+ val head = leaf.docPositionForOffset(end.coerceAtLeast(start)) ?: return
1152
+ editorSetSelection(currentEditorId.toULong(), anchor.toUInt(), head.toUInt())
1153
+ onSelectionChange?.invoke(anchor, head)
1154
+ onAppliedUpdate?.invoke(editorGetSelectionState(currentEditorId.toULong()))
1155
+ }
1156
+
1157
+ private fun commitLeafTextMutation(leaf: TextLeaf, before: String, after: String) {
1158
+ if (isApplyingRemoteUpdate || !isEditable || currentEditorId == 0L || before == after) return
1159
+ var prefix = 0
1160
+ val prefixLimit = minOf(before.length, after.length)
1161
+ while (prefix < prefixLimit && before[prefix] == after[prefix]) prefix += 1
1162
+ var suffix = 0
1163
+ while (
1164
+ suffix < before.length - prefix &&
1165
+ suffix < after.length - prefix &&
1166
+ before[before.length - 1 - suffix] == after[after.length - 1 - suffix]
1167
+ ) suffix += 1
1168
+
1169
+ val oldEnd = before.length - suffix
1170
+ val newEnd = after.length - suffix
1171
+ val anchor = leaf.docPositionForOffset(prefix) ?: return
1172
+ val head = leaf.docPositionForOffset(oldEnd) ?: return
1173
+ val replacement = after.substring(prefix, newEnd)
1174
+ editorSetSelection(currentEditorId.toULong(), anchor.toUInt(), head.toUInt())
1175
+ val update = if (replacement == "\n") {
1176
+ if (anchor != head) {
1177
+ editorReplaceSelectionText(currentEditorId.toULong(), "")
1178
+ }
1179
+ editorSplitBlock(currentEditorId.toULong(), anchor.toUInt())
1180
+ } else {
1181
+ editorReplaceSelectionText(currentEditorId.toULong(), replacement)
1182
+ }
1183
+ applyUpdateJSON(update, currentEditorId)
1184
+ onEditorUpdate?.invoke(update)
1185
+ onAppliedUpdate?.invoke(update)
1186
+ }
1187
+
1188
+ private fun deleteBackwardAtLeafBoundary(leaf: TextLeaf): Boolean {
1189
+ if (isApplyingRemoteUpdate || !isEditable || currentEditorId == 0L) return false
1190
+ val docPosition = leaf.docPositionForOffset(0) ?: return false
1191
+ val scalar = editorDocToScalar(currentEditorId.toULong(), docPosition.toUInt())
1192
+ val update = editorDeleteBackwardAtSelectionScalar(
1193
+ currentEditorId.toULong(),
1194
+ scalar,
1195
+ scalar
1196
+ )
1197
+ applyUpdateJSON(update, currentEditorId)
1198
+ onEditorUpdate?.invoke(update)
1199
+ onAppliedUpdate?.invoke(update)
1200
+ return true
1201
+ }
1202
+
877
1203
  private fun descendantRectInSurface(descendant: View, local: RectF): RectF {
878
1204
  val origin = Rect(0, 0, 0, 0)
879
1205
  offsetDescendantRectToMyCoords(descendant, origin)
@@ -993,8 +1319,14 @@ class NativeBlockEditorSurface(context: Context) : ScrollView(context) {
993
1319
  cell == anchor || cell == head
994
1320
  }
995
1321
  cell.background = borderDrawable(
996
- if (selected) 0xFFE8F1FF.toInt() else cell.baseFillColor,
997
- if (selected) 0xFF2F80ED.toInt() else Color.LTGRAY,
1322
+ if (selected) {
1323
+ currentTheme?.table?.selectionBackgroundColor ?: cell.baseFillColor
1324
+ } else cell.baseFillColor,
1325
+ if (selected) {
1326
+ currentTheme?.table?.selectionBorderColor ?: baseTextColor
1327
+ } else {
1328
+ currentTheme?.table?.borderColor ?: baseTextColor
1329
+ },
998
1330
  if (selected) 1.5f else 0.5f
999
1331
  )
1000
1332
  }