@openeditor/react-native-prose-editor 0.0.9 → 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.
@@ -449,7 +449,7 @@ class NativeEditorModule : Module() {
449
449
  view.setEditable(editable)
450
450
  }
451
451
  Prop("placeholder") { view: NativeEditorExpoView, placeholder: String ->
452
- view.richTextView.editorEditText.placeholderText = placeholder
452
+ view.richTextView.blockSurface.placeholder = placeholder
453
453
  }
454
454
  Prop("autoFocus") { view: NativeEditorExpoView, autoFocus: Boolean ->
455
455
  view.setAutoFocus(autoFocus)
@@ -5,159 +5,62 @@ import android.graphics.Color
5
5
  import android.graphics.RectF
6
6
  import android.graphics.drawable.GradientDrawable
7
7
  import android.util.AttributeSet
8
- import android.view.MotionEvent
9
8
  import android.view.ViewGroup
10
- import android.view.inputmethod.InputMethodManager
11
9
  import android.widget.FrameLayout
12
10
  import android.widget.LinearLayout
13
- import android.widget.ScrollView
14
11
  import uniffi.editor_core.*
15
12
 
16
- /** Container view that owns the native editor text field. */
13
+ /**
14
+ * The single Android editor host. Its only text inputs are the editable leaves
15
+ * owned by [NativeBlockEditorSurface]; no whole-document EditText exists.
16
+ */
17
17
  class RichTextEditorView @JvmOverloads constructor(
18
18
  context: Context,
19
19
  attrs: AttributeSet? = null,
20
20
  defStyleAttr: Int = 0
21
21
  ) : LinearLayout(context, attrs, defStyleAttr) {
22
- val editorViewport: FrameLayout
22
+ val editorViewport = FrameLayout(context)
23
+ val blockSurface = NativeBlockEditorSurface(context)
24
+ private val remoteSelectionOverlayView = RemoteSelectionOverlayView(context)
25
+ private val imageResizeOverlayView = ImageResizeOverlayView(context)
23
26
 
24
- class EditorScrollView(context: Context) : ScrollView(context) {
25
- private fun updateParentIntercept(action: Int) {
26
- val canScroll = canScrollVertically(-1) || canScrollVertically(1)
27
- if (!canScroll) return
28
- when (action) {
29
- MotionEvent.ACTION_DOWN,
30
- MotionEvent.ACTION_MOVE -> parent?.requestDisallowInterceptTouchEvent(true)
31
- MotionEvent.ACTION_UP,
32
- MotionEvent.ACTION_CANCEL -> parent?.requestDisallowInterceptTouchEvent(false)
33
- }
34
- }
35
-
36
- override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
37
- updateParentIntercept(ev.actionMasked)
38
- return super.onInterceptTouchEvent(ev)
39
- }
40
-
41
- override fun onTouchEvent(ev: MotionEvent): Boolean {
42
- updateParentIntercept(ev.actionMasked)
43
- return super.onTouchEvent(ev)
44
- }
45
- }
46
-
47
- val editorEditText: EditorEditText
48
- val editorScrollView: EditorScrollView
49
- val blockSurface: NativeBlockEditorSurface
50
- private val remoteSelectionOverlayView: RemoteSelectionOverlayView
51
- private val imageResizeOverlayView: ImageResizeOverlayView
52
-
53
- private var heightBehavior: EditorHeightBehavior = EditorHeightBehavior.FIXED
27
+ private var currentEditorId = 0L
28
+ private var heightBehavior = EditorHeightBehavior.FIXED
54
29
  private var imageResizingEnabled = true
30
+ private var viewportBottomInsetPx = 0
31
+ private var deferEditorUnbindOnDetach = false
55
32
  private var theme: EditorTheme? = null
56
- private var baseBackgroundColor: Int = Color.WHITE
57
- private var viewportBottomInsetPx: Int = 0
58
- internal var appliedCornerRadiusPx: Float = 0f
59
- internal var appliedBackgroundColorForTesting: Int = Color.WHITE
33
+ private var baseBackgroundColor = Color.WHITE
60
34
 
61
- private var currentEditorId: Long = 0
62
- private var deferEditorUnbindOnDetach = false
63
- private var isSyncingBlockSurfaceUpdateToInputAdapter = false
35
+ internal var appliedCornerRadiusPx = 0f
36
+ internal var appliedBackgroundColorForTesting = Color.WHITE
64
37
  internal var onBeforeDetachedFromWindow: (() -> Unit)? = null
38
+ var onSelectionChange: ((Int, Int) -> Unit)? = null
39
+ var onEditorUpdate: ((String) -> Unit)? = null
40
+ var onFocusChange: ((Boolean) -> Unit)? = null
65
41
 
66
- /** Binds or unbinds the Rust editor instance. */
67
42
  var editorId: Long
68
43
  get() = currentEditorId
69
- set(value) {
70
- setEditorId(value, bindEditor = true)
71
- }
44
+ set(value) = bindEditor(value)
45
+
46
+ val activeEditText get() = blockSurface.activeEditText
47
+ val isEditorFocused get() = blockSurface.isEditorFocused
48
+ val editorIsEditable get() = blockSurface.isEditable
49
+ val editorTheme get() = theme
72
50
 
73
51
  init {
74
52
  orientation = VERTICAL
53
+ editorViewport.addView(blockSurface, matchParentParams())
54
+ editorViewport.addView(remoteSelectionOverlayView, matchParentParams())
55
+ editorViewport.addView(imageResizeOverlayView, matchParentParams())
56
+ addView(editorViewport, LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT))
75
57
 
76
- editorEditText = EditorEditText(context)
77
- editorScrollView = EditorScrollView(context).apply {
78
- clipToPadding = false
79
- isFillViewport = false
80
- alpha = 0.01f
81
- }
82
- blockSurface = NativeBlockEditorSurface(context)
83
- editorViewport = FrameLayout(context)
84
- remoteSelectionOverlayView = RemoteSelectionOverlayView(context)
85
- imageResizeOverlayView = ImageResizeOverlayView(context)
86
- editorScrollView.addView(editorEditText, createEditorLayoutParams())
87
- editorViewport.addView(
88
- editorScrollView,
89
- FrameLayout.LayoutParams(
90
- ViewGroup.LayoutParams.MATCH_PARENT,
91
- ViewGroup.LayoutParams.MATCH_PARENT
92
- )
93
- )
94
- editorViewport.addView(
95
- blockSurface,
96
- FrameLayout.LayoutParams(
97
- ViewGroup.LayoutParams.MATCH_PARENT,
98
- ViewGroup.LayoutParams.MATCH_PARENT
99
- )
100
- )
101
- editorViewport.addView(
102
- remoteSelectionOverlayView,
103
- FrameLayout.LayoutParams(
104
- ViewGroup.LayoutParams.MATCH_PARENT,
105
- ViewGroup.LayoutParams.MATCH_PARENT
106
- )
107
- )
108
- editorViewport.addView(
109
- imageResizeOverlayView,
110
- FrameLayout.LayoutParams(
111
- ViewGroup.LayoutParams.MATCH_PARENT,
112
- ViewGroup.LayoutParams.MATCH_PARENT
113
- )
114
- )
115
58
  remoteSelectionOverlayView.bind(this)
116
59
  imageResizeOverlayView.bind(this)
117
- editorScrollView.setOnScrollChangeListener { _, _, _, _, _ ->
118
- refreshOverlays()
119
- }
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
- }
157
-
158
- addView(editorViewport, createContainerLayoutParams())
159
- updateScrollContainerAppearance()
160
- updateScrollContainerInsets()
60
+ blockSurface.onSelectionChange = { anchor, head -> onSelectionChange?.invoke(anchor, head) }
61
+ blockSurface.onEditorUpdate = { update -> onEditorUpdate?.invoke(update) }
62
+ blockSurface.onFocusChange = { focused -> onFocusChange?.invoke(focused) }
63
+ updateAppearance()
161
64
  }
162
65
 
163
66
  fun configure(
@@ -166,257 +69,134 @@ class RichTextEditorView @JvmOverloads constructor(
166
69
  backgroundColor: Int = Color.WHITE
167
70
  ) {
168
71
  baseBackgroundColor = backgroundColor
169
- editorEditText.setBaseStyle(textSizePx, textColor, backgroundColor)
170
72
  blockSurface.configure(textSizePx, textColor, backgroundColor)
171
- updateScrollContainerAppearance()
172
- refreshOverlays()
73
+ updateAppearance()
173
74
  }
174
75
 
175
76
  fun applyTheme(theme: EditorTheme?) {
176
77
  this.theme = theme
177
- val previousScrollY = editorScrollView.scrollY
178
- editorEditText.applyTheme(theme)
179
78
  blockSurface.applyTheme(theme)
180
- updateScrollContainerAppearance()
181
- updateScrollContainerInsets()
182
- if (heightBehavior == EditorHeightBehavior.FIXED) {
183
- editorScrollView.post {
184
- val childHeight = editorScrollView.getChildAt(0)?.height ?: 0
185
- val maxScrollY = maxOf(
186
- 0,
187
- childHeight + editorScrollView.paddingTop + editorScrollView.paddingBottom - editorScrollView.height
188
- )
189
- editorScrollView.scrollTo(0, previousScrollY.coerceIn(0, maxScrollY))
190
- refreshOverlays()
191
- }
192
- }
193
- refreshOverlays()
79
+ updateAppearance()
194
80
  }
195
81
 
196
82
  fun setEditable(editable: Boolean) {
197
- editorEditText.isEditable = editable
198
83
  blockSurface.isEditable = editable
199
- if (!editable) editorEditText.clearFocus()
84
+ if (!editable) blurEditor()
200
85
  }
201
86
 
202
- fun setHeightBehavior(heightBehavior: EditorHeightBehavior) {
203
- if (this.heightBehavior == heightBehavior) return
204
- this.heightBehavior = heightBehavior
205
- editorEditText.setHeightBehavior(heightBehavior)
206
- editorEditText.layoutParams = createEditorLayoutParams()
207
- editorViewport.layoutParams = createContainerLayoutParams()
208
- editorScrollView.isVerticalScrollBarEnabled = heightBehavior == EditorHeightBehavior.FIXED
209
- editorScrollView.overScrollMode = if (heightBehavior == EditorHeightBehavior.FIXED) {
210
- OVER_SCROLL_IF_CONTENT_SCROLLS
211
- } else {
212
- OVER_SCROLL_NEVER
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
- }
221
- updateScrollContainerInsets()
222
- refreshOverlays()
87
+ fun setHeightBehavior(value: EditorHeightBehavior) {
88
+ heightBehavior = value
89
+ blockSurface.isScrollingEnabled = value == EditorHeightBehavior.FIXED
90
+ blockSurface.isVerticalScrollBarEnabled = value == EditorHeightBehavior.FIXED
91
+ layoutParams = createContainerLayoutParams()
223
92
  requestLayout()
224
93
  }
225
94
 
226
95
  fun setImageResizingEnabled(enabled: Boolean) {
227
- if (imageResizingEnabled == enabled) return
228
96
  imageResizingEnabled = enabled
229
- editorEditText.setImageResizingEnabled(enabled)
230
- refreshOverlays()
97
+ imageResizeOverlayView.refresh()
231
98
  }
232
99
 
233
- fun setViewportBottomInsetPx(bottomInsetPx: Int) {
234
- val clampedInset = bottomInsetPx.coerceAtLeast(0)
235
- if (viewportBottomInsetPx == clampedInset) return
236
- viewportBottomInsetPx = clampedInset
237
- updateScrollContainerInsets()
238
- editorEditText.setViewportBottomInsetPx(clampedInset)
239
- refreshOverlays()
100
+ fun setViewportBottomInsetPx(value: Int) {
101
+ viewportBottomInsetPx = value.coerceAtLeast(0)
102
+ blockSurface.setPadding(
103
+ blockSurface.paddingLeft,
104
+ blockSurface.paddingTop,
105
+ blockSurface.paddingRight,
106
+ viewportBottomInsetPx
107
+ )
240
108
  requestLayout()
241
109
  }
242
110
 
243
- internal fun viewportBottomInsetPxForTesting(): Int = viewportBottomInsetPx
244
-
245
- fun setRemoteSelections(selections: List<RemoteSelectionDecoration>) {
111
+ internal fun viewportBottomInsetPxForTesting() = viewportBottomInsetPx
112
+ fun setRemoteSelections(selections: List<RemoteSelectionDecoration>) =
246
113
  remoteSelectionOverlayView.setRemoteSelections(selections)
247
- }
248
-
249
- fun refreshRemoteSelections() {
250
- if (!remoteSelectionOverlayView.hasSelectionsOrCachedGeometry()) return
251
- remoteSelectionOverlayView.refreshGeometry()
252
- }
253
-
254
- fun imageResizeOverlayRectForTesting(): android.graphics.RectF? =
255
- imageResizeOverlayView.visibleRectForTesting()
256
-
257
- fun resizeSelectedImageForTesting(widthPx: Float, heightPx: Float) {
258
- imageResizeOverlayView.simulateResizeForTesting(widthPx, heightPx)
259
- }
260
-
261
- fun remoteSelectionDebugSnapshotsForTesting(): List<RemoteSelectionDebugSnapshot> =
262
- remoteSelectionOverlayView.debugSnapshotsForTesting()
263
-
114
+ fun refreshRemoteSelections() = remoteSelectionOverlayView.refreshGeometry()
115
+ fun remoteSelectionDebugSnapshotsForTesting() = remoteSelectionOverlayView.debugSnapshotsForTesting()
264
116
  fun setRemoteSelectionEditorIdForTesting(editorId: Long) {
265
117
  remoteSelectionOverlayView.editorIdOverrideForTesting = editorId
266
118
  }
119
+ fun imageResizeOverlayRectForTesting() = imageResizeOverlayView.visibleRectForTesting()
120
+ fun resizeSelectedImageForTesting(widthPx: Float, heightPx: Float) =
121
+ imageResizeOverlayView.simulateResizeForTesting(widthPx, heightPx)
267
122
 
268
123
  fun setContent(html: String) {
269
124
  if (editorId == 0L) return
270
- editorSetHtml(editorId.toULong(), html)
271
- applyFullStateToInputAndSurface(editorGetCurrentState(editorId.toULong()))
125
+ applyEditorUpdate(editorSetHtml(editorId.toULong(), html))
272
126
  }
273
127
 
274
128
  fun setContent(json: org.json.JSONObject) {
275
129
  if (editorId == 0L) return
276
- editorSetJson(editorId.toULong(), json.toString())
277
- applyFullStateToInputAndSurface(editorGetCurrentState(editorId.toULong()))
130
+ applyEditorUpdate(editorSetJson(editorId.toULong(), json.toString()))
278
131
  }
279
132
 
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)
292
- }
133
+ fun focusEditor() = blockSurface.focus()
134
+ fun blurEditor() = blockSurface.blur()
135
+ fun hasActiveComposition() = blockSurface.hasActiveComposition()
136
+ fun currentScalarSelection() = blockSurface.currentScalarSelection()
137
+ fun activeDocPositionForUtf16Offset(offset: Int) = blockSurface.activeDocPositionForUtf16Offset(offset)
138
+ fun applyEditorUpdate(updateJSON: String) = blockSurface.applyUpdateJSON(updateJSON, editorId)
139
+ fun setAutoCapitalize(value: String?) = blockSurface.setAutoCapitalize(value)
140
+ fun setAutoCorrect(value: Boolean?) = blockSurface.setAutoCorrect(value)
141
+ fun setKeyboardType(value: String?) = blockSurface.setKeyboardType(value)
293
142
 
294
- internal fun rebindEditorIfNeeded(notifyListener: Boolean = true) {
295
- if (editorId != 0L && editorEditText.editorId != editorId) {
296
- setEditorId(editorId, bindEditor = true, notifyListener = notifyListener)
297
- }
298
- }
299
-
300
- internal fun setEditorIdWhileDetached(value: Long) {
301
- setEditorId(value, bindEditor = false)
302
- }
303
-
304
- internal fun deferEditorUnbindOnNextDetach() {
305
- deferEditorUnbindOnDetach = true
143
+ fun performEditorCommand(command: () -> String): String {
144
+ val update = command()
145
+ applyEditorUpdate(update)
146
+ onEditorUpdate?.invoke(update)
147
+ return update
306
148
  }
307
149
 
308
- internal fun clearDeferredEditorUnbind() {
309
- deferEditorUnbindOnDetach = false
150
+ internal fun rebindEditorIfNeeded(notifyListener: Boolean = true) {
151
+ if (editorId != 0L) applyEditorUpdate(editorGetCurrentState(editorId.toULong()))
310
152
  }
311
153
 
154
+ internal fun setEditorIdWhileDetached(value: Long) = bindEditor(value)
155
+ internal fun deferEditorUnbindOnNextDetach() { deferEditorUnbindOnDetach = true }
156
+ internal fun clearDeferredEditorUnbind() { deferEditorUnbindOnDetach = false }
312
157
  internal fun unbindEditorForDetachedViewIfNeeded() {
313
- if (isAttachedToWindow) return
158
+ if (!deferEditorUnbindOnDetach) blockSurface.bind(0)
314
159
  deferEditorUnbindOnDetach = false
315
- if (editorId != 0L) {
316
- editorEditText.unbindEditor()
317
- }
318
160
  }
319
161
 
320
- private fun setEditorId(value: Long, bindEditor: Boolean, notifyListener: Boolean = true) {
321
- val targetBoundEditorId = if (bindEditor) value else 0L
322
- if (currentEditorId == value && editorEditText.editorId == targetBoundEditorId) return
323
- if (currentEditorId != value || editorEditText.editorId != targetBoundEditorId) {
324
- editorEditText.discardTransientNativeInputForEditorRebind()
325
- }
162
+ private fun bindEditor(value: Long) {
163
+ if (currentEditorId == value) return
326
164
  currentEditorId = value
327
165
  blockSurface.bind(value)
328
- if (bindEditor && value != 0L) {
329
- editorEditText.bindEditor(value, notifyListener = notifyListener)
330
- } else {
331
- editorEditText.unbindEditor()
332
- }
333
- refreshOverlays()
166
+ if (value != 0L) applyEditorUpdate(editorGetCurrentState(value.toULong()))
334
167
  }
335
168
 
336
169
  override fun onAttachedToWindow() {
337
170
  super.onAttachedToWindow()
338
- clearDeferredEditorUnbind()
339
- rebindEditorIfNeeded()
171
+ rebindEditorIfNeeded(false)
340
172
  }
341
173
 
342
174
  override fun onDetachedFromWindow() {
343
175
  onBeforeDetachedFromWindow?.invoke()
176
+ unbindEditorForDetachedViewIfNeeded()
344
177
  super.onDetachedFromWindow()
345
- if (editorId != 0L && !deferEditorUnbindOnDetach) {
346
- editorEditText.unbindEditor()
347
- }
348
178
  }
349
179
 
350
180
  override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
351
- if (heightBehavior != EditorHeightBehavior.AUTO_GROW) {
352
- super.onMeasure(widthMeasureSpec, heightMeasureSpec)
353
- return
354
- }
355
-
356
- val childWidthSpec = getChildMeasureSpec(
357
- widthMeasureSpec,
358
- paddingLeft + paddingRight,
359
- editorViewport.layoutParams.width
360
- )
361
- val childHeightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)
362
- editorViewport.measure(childWidthSpec, childHeightSpec)
363
-
364
- val measuredWidth = resolveSize(
365
- editorViewport.measuredWidth + paddingLeft + paddingRight,
366
- widthMeasureSpec
367
- )
368
- val desiredHeight = editorViewport.measuredHeight + paddingTop + paddingBottom
369
- val measuredHeight = when (MeasureSpec.getMode(heightMeasureSpec)) {
370
- MeasureSpec.AT_MOST -> desiredHeight.coerceAtMost(MeasureSpec.getSize(heightMeasureSpec))
371
- else -> desiredHeight
372
- }
373
- setMeasuredDimension(measuredWidth, measuredHeight)
374
- }
375
-
376
- private fun updateScrollContainerAppearance() {
377
- val cornerRadiusPx = (theme?.borderRadius ?: 0f) * resources.displayMetrics.density
378
- val backgroundColor = theme?.backgroundColor ?: baseBackgroundColor
379
- editorViewport.background = GradientDrawable().apply {
380
- cornerRadius = cornerRadiusPx
381
- setColor(backgroundColor)
382
- }
383
- editorViewport.clipToOutline = cornerRadiusPx > 0f
384
- editorScrollView.setBackgroundColor(Color.TRANSPARENT)
385
- appliedCornerRadiusPx = cornerRadiusPx
386
- appliedBackgroundColorForTesting = backgroundColor
387
- }
388
-
389
- private fun updateScrollContainerInsets() {
390
- if (heightBehavior != EditorHeightBehavior.FIXED) {
391
- editorScrollView.setPadding(0, 0, 0, 0)
392
- return
393
- }
394
-
395
- val density = resources.displayMetrics.density
396
- val topInset = ((theme?.contentInsets?.top ?: 0f) * density).toInt()
397
- val bottomInset = ((theme?.contentInsets?.bottom ?: 0f) * density).toInt()
398
- editorScrollView.setPadding(0, topInset, 0, bottomInset + viewportBottomInsetPx)
399
- }
400
-
401
- private fun createContainerLayoutParams(): LayoutParams =
402
181
  if (heightBehavior == EditorHeightBehavior.AUTO_GROW) {
403
- LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)
182
+ val childWidth = getChildMeasureSpec(widthMeasureSpec, paddingLeft + paddingRight, LayoutParams.MATCH_PARENT)
183
+ val childHeight = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)
184
+ editorViewport.measure(childWidth, childHeight)
185
+ setMeasuredDimension(
186
+ resolveSize(editorViewport.measuredWidth + paddingLeft + paddingRight, widthMeasureSpec),
187
+ editorViewport.measuredHeight + paddingTop + paddingBottom
188
+ )
404
189
  } else {
405
- LayoutParams(LayoutParams.MATCH_PARENT, 0, 1f)
190
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec)
406
191
  }
192
+ }
407
193
 
408
- private fun createEditorLayoutParams(): FrameLayout.LayoutParams =
409
- FrameLayout.LayoutParams(
410
- ViewGroup.LayoutParams.MATCH_PARENT,
411
- ViewGroup.LayoutParams.WRAP_CONTENT
412
- )
413
-
414
- internal fun selectedImageGeometry(): EditorEditText.SelectedImageGeometry? {
194
+ internal fun selectedImageGeometry(): SelectedImageGeometry? {
415
195
  if (!imageResizingEnabled) return null
416
196
  val geometry = blockSurface.selectedImageGeometry() ?: return null
417
- return EditorEditText.SelectedImageGeometry(
418
- docPos = geometry.docPos,
419
- rect = RectF(
197
+ return SelectedImageGeometry(
198
+ geometry.docPos,
199
+ RectF(
420
200
  editorViewport.left + blockSurface.left + geometry.rect.left,
421
201
  editorViewport.top + blockSurface.top + geometry.rect.top,
422
202
  editorViewport.left + blockSurface.left + geometry.rect.right,
@@ -426,7 +206,6 @@ class RichTextEditorView @JvmOverloads constructor(
426
206
  }
427
207
 
428
208
  internal fun caretRect(): RectF? {
429
- blockSurface.isInputFocused = editorEditText.hasFocus()
430
209
  val rect = blockSurface.selectedCaretRect() ?: return null
431
210
  return RectF(
432
211
  editorViewport.left + blockSurface.left + rect.left,
@@ -436,29 +215,37 @@ class RichTextEditorView @JvmOverloads constructor(
436
215
  )
437
216
  }
438
217
 
439
- internal fun maximumImageWidthPx(): Float {
440
- return blockSurface.maximumImageWidthPx()
441
- }
442
-
443
- internal fun clampImageSize(
444
- widthPx: Float,
445
- heightPx: Float,
446
- maximumWidthPx: Float = maximumImageWidthPx()
447
- ): Pair<Float, Float> {
448
- val aspectRatio = maxOf(widthPx / maxOf(heightPx, 1f), 0.1f)
449
- val clampedWidth = minOf(maxOf(48f, maximumWidthPx), maxOf(48f, widthPx))
450
- val clampedHeight = maxOf(48f, clampedWidth / aspectRatio)
451
- return clampedWidth to clampedHeight
218
+ internal fun maximumImageWidthPx() = blockSurface.maximumImageWidthPx()
219
+ internal fun clampImageSize(widthPx: Float, heightPx: Float, maximumWidthPx: Float = maximumImageWidthPx()): Pair<Float, Float> {
220
+ val ratio = maxOf(widthPx / maxOf(heightPx, 1f), 0.1f)
221
+ val width = minOf(maxOf(48f, maximumWidthPx), maxOf(48f, widthPx))
222
+ return width to maxOf(48f, width / ratio)
452
223
  }
453
-
454
224
  internal fun resizeImage(docPos: Int, widthPx: Float, heightPx: Float) {
455
- val (clampedWidth, clampedHeight) = clampImageSize(widthPx, heightPx)
456
- blockSurface.resizeImage(docPos, clampedWidth, clampedHeight)
225
+ val size = clampImageSize(widthPx, heightPx)
226
+ blockSurface.resizeImage(docPos, size.first, size.second)
457
227
  }
458
228
 
459
- private fun refreshOverlays() {
460
- blockSurface.isInputFocused = editorEditText.hasFocus()
461
- remoteSelectionOverlayView.refreshGeometry()
462
- imageResizeOverlayView.refresh()
229
+ private fun updateAppearance() {
230
+ val backgroundColor = theme?.backgroundColor ?: baseBackgroundColor
231
+ val radius = (theme?.borderRadius ?: 0f) * resources.displayMetrics.density
232
+ appliedCornerRadiusPx = radius
233
+ appliedBackgroundColorForTesting = backgroundColor
234
+ background = GradientDrawable().apply {
235
+ shape = GradientDrawable.RECTANGLE
236
+ setColor(backgroundColor)
237
+ cornerRadius = radius
238
+ }
239
+ clipToOutline = radius > 0f
463
240
  }
241
+
242
+ private fun createContainerLayoutParams() = LayoutParams(
243
+ LayoutParams.MATCH_PARENT,
244
+ if (heightBehavior == EditorHeightBehavior.AUTO_GROW) LayoutParams.WRAP_CONTENT else LayoutParams.MATCH_PARENT
245
+ )
246
+
247
+ private fun matchParentParams() = FrameLayout.LayoutParams(
248
+ ViewGroup.LayoutParams.MATCH_PARENT,
249
+ ViewGroup.LayoutParams.MATCH_PARENT
250
+ )
464
251
  }
@@ -69,6 +69,13 @@ export interface EditorCodeBlockTheme {
69
69
  paddingHorizontal?: number;
70
70
  paddingVertical?: number;
71
71
  }
72
+ export interface EditorTableTheme {
73
+ cellBackgroundColor?: string;
74
+ headerBackgroundColor?: string;
75
+ borderColor?: string;
76
+ selectionBackgroundColor?: string;
77
+ selectionBorderColor?: string;
78
+ }
72
79
  export type EditorToolbarAppearance = 'custom' | 'native';
73
80
  export interface EditorToolbarTheme {
74
81
  appearance?: EditorToolbarAppearance;
@@ -99,6 +106,7 @@ export interface EditorTheme {
99
106
  paragraph?: EditorTextStyle;
100
107
  blockquote?: EditorBlockquoteTheme;
101
108
  codeBlock?: EditorCodeBlockTheme;
109
+ table?: EditorTableTheme;
102
110
  headings?: EditorHeadingTheme;
103
111
  list?: EditorListTheme;
104
112
  horizontalRule?: EditorHorizontalRuleTheme;