@openeditor/react-native-prose-editor 0.0.2

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 (91) hide show
  1. package/LICENSE +160 -0
  2. package/README.md +180 -0
  3. package/android/build.gradle +69 -0
  4. package/android/consumer-rules.pro +18 -0
  5. package/android/src/androidTest/AndroidManifest.xml +8 -0
  6. package/android/src/androidTest/java/com/apollohg/editor/MeasureHeightInstrumentedTest.kt +53 -0
  7. package/android/src/androidTest/java/com/apollohg/editor/NativeDeviceCollaborationInitialSyncTest.kt +241 -0
  8. package/android/src/androidTest/java/com/apollohg/editor/NativeDeviceImeRegressionTest.kt +338 -0
  9. package/android/src/androidTest/java/com/apollohg/editor/NativeDeviceOutsideTapTest.kt +789 -0
  10. package/android/src/androidTest/java/com/apollohg/editor/NativeDevicePerformanceTest.kt +350 -0
  11. package/android/src/androidTest/java/com/apollohg/editor/NativeEditorOutsideTapActivity.kt +5 -0
  12. package/android/src/main/assets/editor-icons/MaterialIcons.json +2236 -0
  13. package/android/src/main/assets/editor-icons/MaterialIcons.ttf +0 -0
  14. package/android/src/main/java/com/apollohg/editor/CaretGeometry.kt +50 -0
  15. package/android/src/main/java/com/apollohg/editor/EditorAddons.kt +135 -0
  16. package/android/src/main/java/com/apollohg/editor/EditorEditText.kt +4527 -0
  17. package/android/src/main/java/com/apollohg/editor/EditorHeightBehavior.kt +14 -0
  18. package/android/src/main/java/com/apollohg/editor/EditorInputConnection.kt +999 -0
  19. package/android/src/main/java/com/apollohg/editor/EditorTheme.kt +474 -0
  20. package/android/src/main/java/com/apollohg/editor/ImageResizeOverlayView.kt +199 -0
  21. package/android/src/main/java/com/apollohg/editor/NativeEditorExpoView.kt +3137 -0
  22. package/android/src/main/java/com/apollohg/editor/NativeEditorModule.kt +561 -0
  23. package/android/src/main/java/com/apollohg/editor/NativeProseViewerExpoView.kt +315 -0
  24. package/android/src/main/java/com/apollohg/editor/NativeToolbar.kt +1413 -0
  25. package/android/src/main/java/com/apollohg/editor/PositionBridge.kt +133 -0
  26. package/android/src/main/java/com/apollohg/editor/RemoteSelectionOverlayView.kt +315 -0
  27. package/android/src/main/java/com/apollohg/editor/RenderBridge.kt +2172 -0
  28. package/android/src/main/java/com/apollohg/editor/RichTextEditorView.kt +399 -0
  29. package/android/src/sharedTest/java/com/apollohg/editor/NativePerformanceSupport.kt +482 -0
  30. package/android/src/test/java/com/apollohg/editor/CaretGeometryTest.kt +137 -0
  31. package/android/src/test/java/com/apollohg/editor/EditorEditTextHardwareKeyTest.kt +321 -0
  32. package/android/src/test/java/com/apollohg/editor/EditorInputConnectionTest.kt +4202 -0
  33. package/android/src/test/java/com/apollohg/editor/NativeEditorExpoViewTest.kt +2581 -0
  34. package/android/src/test/java/com/apollohg/editor/NativeEditorModuleTest.kt +27 -0
  35. package/android/src/test/java/com/apollohg/editor/NativePerformanceTest.kt +197 -0
  36. package/android/src/test/java/com/apollohg/editor/NativeProseViewerExpoViewTest.kt +84 -0
  37. package/android/src/test/java/com/apollohg/editor/NativeToolbarTest.kt +544 -0
  38. package/android/src/test/java/com/apollohg/editor/PositionBridgeTest.kt +461 -0
  39. package/android/src/test/java/com/apollohg/editor/RenderBridgeTest.kt +2054 -0
  40. package/android/src/test/java/com/apollohg/editor/RichTextEditorViewTest.kt +1367 -0
  41. package/app.plugin.js +62 -0
  42. package/dist/EditorTheme.d.ts +113 -0
  43. package/dist/EditorTheme.js +29 -0
  44. package/dist/EditorToolbar.d.ts +191 -0
  45. package/dist/EditorToolbar.js +1095 -0
  46. package/dist/NativeEditorBridge.d.ts +325 -0
  47. package/dist/NativeEditorBridge.js +933 -0
  48. package/dist/NativeProseViewer.d.ts +53 -0
  49. package/dist/NativeProseViewer.js +480 -0
  50. package/dist/NativeRichTextEditor.d.ts +183 -0
  51. package/dist/NativeRichTextEditor.js +2574 -0
  52. package/dist/YjsCollaboration.d.ts +90 -0
  53. package/dist/YjsCollaboration.js +743 -0
  54. package/dist/addons.d.ts +100 -0
  55. package/dist/addons.js +82 -0
  56. package/dist/heightCache.d.ts +6 -0
  57. package/dist/heightCache.js +45 -0
  58. package/dist/index.d.ts +10 -0
  59. package/dist/index.js +30 -0
  60. package/dist/schemas.d.ts +37 -0
  61. package/dist/schemas.js +263 -0
  62. package/dist/useNativeEditor.d.ts +42 -0
  63. package/dist/useNativeEditor.js +124 -0
  64. package/expo-module.config.json +9 -0
  65. package/ios/EditorAddons.swift +327 -0
  66. package/ios/EditorCore.xcframework/Info.plist +44 -0
  67. package/ios/EditorCore.xcframework/ios-arm64/libeditor_core.a +0 -0
  68. package/ios/EditorCore.xcframework/ios-arm64_x86_64-simulator/libeditor_core.a +0 -0
  69. package/ios/EditorLayoutManager.swift +774 -0
  70. package/ios/EditorTheme.swift +515 -0
  71. package/ios/Generated_editor_core.swift +1513 -0
  72. package/ios/NativeEditorExpoView.swift +3593 -0
  73. package/ios/NativeEditorModule.swift +624 -0
  74. package/ios/NativeProseViewerExpoView.swift +276 -0
  75. package/ios/PositionBridge.swift +558 -0
  76. package/ios/ReactNativeProseEditor.podspec +49 -0
  77. package/ios/RenderBridge.swift +1708 -0
  78. package/ios/RichTextEditorView.swift +6150 -0
  79. package/ios/Tests/NativePerformanceTests.swift +687 -0
  80. package/ios/Tests/PositionBridgeTests.swift +706 -0
  81. package/ios/Tests/RenderBridgeTests.swift +2236 -0
  82. package/ios/Tests/RichTextEditorViewTests.swift +6548 -0
  83. package/ios/editor_coreFFI/editor_coreFFI.h +1289 -0
  84. package/ios/editor_coreFFI/module.modulemap +7 -0
  85. package/ios/editor_coreFFI.h +904 -0
  86. package/ios/editor_coreFFI.modulemap +7 -0
  87. package/package.json +74 -0
  88. package/rust/android/arm64-v8a/libeditor_core.so +0 -0
  89. package/rust/android/armeabi-v7a/libeditor_core.so +0 -0
  90. package/rust/android/x86_64/libeditor_core.so +0 -0
  91. package/rust/bindings/kotlin/uniffi/editor_core/editor_core.kt +2563 -0
@@ -0,0 +1,399 @@
1
+ package com.openeditor.editor
2
+
3
+ import android.content.Context
4
+ import android.graphics.Color
5
+ import android.graphics.RectF
6
+ import android.graphics.drawable.GradientDrawable
7
+ import android.util.AttributeSet
8
+ import android.view.MotionEvent
9
+ import android.view.ViewGroup
10
+ import android.widget.FrameLayout
11
+ import android.widget.LinearLayout
12
+ import android.widget.ScrollView
13
+ import uniffi.editor_core.*
14
+
15
+ /** Container view that owns the native editor text field. */
16
+ class RichTextEditorView @JvmOverloads constructor(
17
+ context: Context,
18
+ attrs: AttributeSet? = null,
19
+ defStyleAttr: Int = 0
20
+ ) : LinearLayout(context, attrs, defStyleAttr) {
21
+ val editorViewport: FrameLayout
22
+
23
+ private class EditorScrollView(context: Context) : ScrollView(context) {
24
+ private fun updateParentIntercept(action: Int) {
25
+ val canScroll = canScrollVertically(-1) || canScrollVertically(1)
26
+ if (!canScroll) return
27
+ when (action) {
28
+ MotionEvent.ACTION_DOWN,
29
+ MotionEvent.ACTION_MOVE -> parent?.requestDisallowInterceptTouchEvent(true)
30
+ MotionEvent.ACTION_UP,
31
+ MotionEvent.ACTION_CANCEL -> parent?.requestDisallowInterceptTouchEvent(false)
32
+ }
33
+ }
34
+
35
+ override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
36
+ updateParentIntercept(ev.actionMasked)
37
+ return super.onInterceptTouchEvent(ev)
38
+ }
39
+
40
+ override fun onTouchEvent(ev: MotionEvent): Boolean {
41
+ updateParentIntercept(ev.actionMasked)
42
+ return super.onTouchEvent(ev)
43
+ }
44
+ }
45
+
46
+ val editorEditText: EditorEditText
47
+ val editorScrollView: ScrollView
48
+ private val remoteSelectionOverlayView: RemoteSelectionOverlayView
49
+ private val imageResizeOverlayView: ImageResizeOverlayView
50
+
51
+ private var heightBehavior: EditorHeightBehavior = EditorHeightBehavior.FIXED
52
+ private var imageResizingEnabled = true
53
+ private var theme: EditorTheme? = null
54
+ private var baseBackgroundColor: Int = Color.WHITE
55
+ private var viewportBottomInsetPx: Int = 0
56
+ internal var appliedCornerRadiusPx: Float = 0f
57
+ internal var appliedBackgroundColorForTesting: Int = Color.WHITE
58
+
59
+ private var currentEditorId: Long = 0
60
+ private var deferEditorUnbindOnDetach = false
61
+ internal var onBeforeDetachedFromWindow: (() -> Unit)? = null
62
+
63
+ /** Binds or unbinds the Rust editor instance. */
64
+ var editorId: Long
65
+ get() = currentEditorId
66
+ set(value) {
67
+ setEditorId(value, bindEditor = true)
68
+ }
69
+
70
+ init {
71
+ orientation = VERTICAL
72
+
73
+ editorEditText = EditorEditText(context)
74
+ editorScrollView = EditorScrollView(context).apply {
75
+ clipToPadding = false
76
+ isFillViewport = false
77
+ }
78
+ editorViewport = FrameLayout(context)
79
+ remoteSelectionOverlayView = RemoteSelectionOverlayView(context)
80
+ imageResizeOverlayView = ImageResizeOverlayView(context)
81
+ editorScrollView.addView(editorEditText, createEditorLayoutParams())
82
+ editorViewport.addView(
83
+ editorScrollView,
84
+ FrameLayout.LayoutParams(
85
+ ViewGroup.LayoutParams.MATCH_PARENT,
86
+ ViewGroup.LayoutParams.MATCH_PARENT
87
+ )
88
+ )
89
+ editorViewport.addView(
90
+ remoteSelectionOverlayView,
91
+ FrameLayout.LayoutParams(
92
+ ViewGroup.LayoutParams.MATCH_PARENT,
93
+ ViewGroup.LayoutParams.MATCH_PARENT
94
+ )
95
+ )
96
+ editorViewport.addView(
97
+ imageResizeOverlayView,
98
+ FrameLayout.LayoutParams(
99
+ ViewGroup.LayoutParams.MATCH_PARENT,
100
+ ViewGroup.LayoutParams.MATCH_PARENT
101
+ )
102
+ )
103
+ remoteSelectionOverlayView.bind(this)
104
+ imageResizeOverlayView.bind(this)
105
+ editorScrollView.setOnScrollChangeListener { _, _, _, _, _ ->
106
+ refreshOverlays()
107
+ }
108
+ editorEditText.onSelectionOrContentMayChange = { refreshOverlays() }
109
+
110
+ addView(editorViewport, createContainerLayoutParams())
111
+ updateScrollContainerAppearance()
112
+ updateScrollContainerInsets()
113
+ }
114
+
115
+ fun configure(
116
+ textSizePx: Float = 16f * resources.displayMetrics.density,
117
+ textColor: Int = Color.BLACK,
118
+ backgroundColor: Int = Color.WHITE
119
+ ) {
120
+ baseBackgroundColor = backgroundColor
121
+ editorEditText.setBaseStyle(textSizePx, textColor, backgroundColor)
122
+ updateScrollContainerAppearance()
123
+ refreshOverlays()
124
+ }
125
+
126
+ fun applyTheme(theme: EditorTheme?) {
127
+ this.theme = theme
128
+ val previousScrollY = editorScrollView.scrollY
129
+ editorEditText.applyTheme(theme)
130
+ updateScrollContainerAppearance()
131
+ updateScrollContainerInsets()
132
+ if (heightBehavior == EditorHeightBehavior.FIXED) {
133
+ editorScrollView.post {
134
+ val childHeight = editorScrollView.getChildAt(0)?.height ?: 0
135
+ val maxScrollY = maxOf(
136
+ 0,
137
+ childHeight + editorScrollView.paddingTop + editorScrollView.paddingBottom - editorScrollView.height
138
+ )
139
+ editorScrollView.scrollTo(0, previousScrollY.coerceIn(0, maxScrollY))
140
+ refreshOverlays()
141
+ }
142
+ }
143
+ refreshOverlays()
144
+ }
145
+
146
+ fun setHeightBehavior(heightBehavior: EditorHeightBehavior) {
147
+ if (this.heightBehavior == heightBehavior) return
148
+ this.heightBehavior = heightBehavior
149
+ editorEditText.setHeightBehavior(heightBehavior)
150
+ editorEditText.layoutParams = createEditorLayoutParams()
151
+ editorViewport.layoutParams = createContainerLayoutParams()
152
+ editorScrollView.isVerticalScrollBarEnabled = heightBehavior == EditorHeightBehavior.FIXED
153
+ editorScrollView.overScrollMode = if (heightBehavior == EditorHeightBehavior.FIXED) {
154
+ OVER_SCROLL_IF_CONTENT_SCROLLS
155
+ } else {
156
+ OVER_SCROLL_NEVER
157
+ }
158
+ updateScrollContainerInsets()
159
+ refreshOverlays()
160
+ requestLayout()
161
+ }
162
+
163
+ fun setImageResizingEnabled(enabled: Boolean) {
164
+ if (imageResizingEnabled == enabled) return
165
+ imageResizingEnabled = enabled
166
+ editorEditText.setImageResizingEnabled(enabled)
167
+ refreshOverlays()
168
+ }
169
+
170
+ fun setViewportBottomInsetPx(bottomInsetPx: Int) {
171
+ val clampedInset = bottomInsetPx.coerceAtLeast(0)
172
+ if (viewportBottomInsetPx == clampedInset) return
173
+ viewportBottomInsetPx = clampedInset
174
+ updateScrollContainerInsets()
175
+ editorEditText.setViewportBottomInsetPx(clampedInset)
176
+ refreshOverlays()
177
+ requestLayout()
178
+ }
179
+
180
+ internal fun viewportBottomInsetPxForTesting(): Int = viewportBottomInsetPx
181
+
182
+ fun setRemoteSelections(selections: List<RemoteSelectionDecoration>) {
183
+ remoteSelectionOverlayView.setRemoteSelections(selections)
184
+ }
185
+
186
+ fun refreshRemoteSelections() {
187
+ if (!remoteSelectionOverlayView.hasSelectionsOrCachedGeometry()) return
188
+ remoteSelectionOverlayView.refreshGeometry()
189
+ }
190
+
191
+ fun imageResizeOverlayRectForTesting(): android.graphics.RectF? =
192
+ imageResizeOverlayView.visibleRectForTesting()
193
+
194
+ fun resizeSelectedImageForTesting(widthPx: Float, heightPx: Float) {
195
+ imageResizeOverlayView.simulateResizeForTesting(widthPx, heightPx)
196
+ }
197
+
198
+ fun remoteSelectionDebugSnapshotsForTesting(): List<RemoteSelectionDebugSnapshot> =
199
+ remoteSelectionOverlayView.debugSnapshotsForTesting()
200
+
201
+ fun setRemoteSelectionScalarResolverForTesting(resolver: (Long, Int) -> Int) {
202
+ remoteSelectionOverlayView.docToScalarResolver = resolver
203
+ }
204
+
205
+ fun setRemoteSelectionEditorIdForTesting(editorId: Long) {
206
+ remoteSelectionOverlayView.editorIdOverrideForTesting = editorId
207
+ }
208
+
209
+ fun setContent(html: String) {
210
+ if (editorId == 0L) return
211
+ editorSetHtml(editorId.toULong(), html)
212
+ editorEditText.applyUpdateJSON(
213
+ editorGetCurrentState(editorId.toULong()),
214
+ notifyListener = false,
215
+ refreshInputConnectionForExternalUpdate = true
216
+ )
217
+ }
218
+
219
+ fun setContent(json: org.json.JSONObject) {
220
+ if (editorId == 0L) return
221
+ editorSetJson(editorId.toULong(), json.toString())
222
+ editorEditText.applyUpdateJSON(
223
+ editorGetCurrentState(editorId.toULong()),
224
+ notifyListener = false,
225
+ refreshInputConnectionForExternalUpdate = true
226
+ )
227
+ }
228
+
229
+ internal fun rebindEditorIfNeeded(notifyListener: Boolean = true) {
230
+ if (editorId != 0L && editorEditText.editorId != editorId) {
231
+ setEditorId(editorId, bindEditor = true, notifyListener = notifyListener)
232
+ }
233
+ }
234
+
235
+ internal fun setEditorIdWhileDetached(value: Long) {
236
+ setEditorId(value, bindEditor = false)
237
+ }
238
+
239
+ internal fun deferEditorUnbindOnNextDetach() {
240
+ deferEditorUnbindOnDetach = true
241
+ }
242
+
243
+ internal fun clearDeferredEditorUnbind() {
244
+ deferEditorUnbindOnDetach = false
245
+ }
246
+
247
+ internal fun unbindEditorForDetachedViewIfNeeded() {
248
+ if (isAttachedToWindow) return
249
+ deferEditorUnbindOnDetach = false
250
+ if (editorId != 0L) {
251
+ editorEditText.unbindEditor()
252
+ }
253
+ }
254
+
255
+ private fun setEditorId(value: Long, bindEditor: Boolean, notifyListener: Boolean = true) {
256
+ val targetBoundEditorId = if (bindEditor) value else 0L
257
+ if (currentEditorId == value && editorEditText.editorId == targetBoundEditorId) return
258
+ if (currentEditorId != value || editorEditText.editorId != targetBoundEditorId) {
259
+ editorEditText.discardTransientNativeInputForEditorRebind()
260
+ }
261
+ currentEditorId = value
262
+ if (bindEditor && value != 0L) {
263
+ editorEditText.bindEditor(value, notifyListener = notifyListener)
264
+ } else {
265
+ editorEditText.unbindEditor()
266
+ }
267
+ refreshOverlays()
268
+ }
269
+
270
+ override fun onAttachedToWindow() {
271
+ super.onAttachedToWindow()
272
+ clearDeferredEditorUnbind()
273
+ rebindEditorIfNeeded()
274
+ }
275
+
276
+ override fun onDetachedFromWindow() {
277
+ onBeforeDetachedFromWindow?.invoke()
278
+ super.onDetachedFromWindow()
279
+ if (editorId != 0L && !deferEditorUnbindOnDetach) {
280
+ editorEditText.unbindEditor()
281
+ }
282
+ }
283
+
284
+ override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
285
+ if (heightBehavior != EditorHeightBehavior.AUTO_GROW) {
286
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec)
287
+ return
288
+ }
289
+
290
+ val childWidthSpec = getChildMeasureSpec(
291
+ widthMeasureSpec,
292
+ paddingLeft + paddingRight,
293
+ editorViewport.layoutParams.width
294
+ )
295
+ val childHeightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)
296
+ editorViewport.measure(childWidthSpec, childHeightSpec)
297
+
298
+ val measuredWidth = resolveSize(
299
+ editorViewport.measuredWidth + paddingLeft + paddingRight,
300
+ widthMeasureSpec
301
+ )
302
+ val desiredHeight = editorViewport.measuredHeight + paddingTop + paddingBottom
303
+ val measuredHeight = when (MeasureSpec.getMode(heightMeasureSpec)) {
304
+ MeasureSpec.AT_MOST -> desiredHeight.coerceAtMost(MeasureSpec.getSize(heightMeasureSpec))
305
+ else -> desiredHeight
306
+ }
307
+ setMeasuredDimension(measuredWidth, measuredHeight)
308
+ }
309
+
310
+ private fun updateScrollContainerAppearance() {
311
+ val cornerRadiusPx = (theme?.borderRadius ?: 0f) * resources.displayMetrics.density
312
+ val backgroundColor = theme?.backgroundColor ?: baseBackgroundColor
313
+ editorViewport.background = GradientDrawable().apply {
314
+ cornerRadius = cornerRadiusPx
315
+ setColor(backgroundColor)
316
+ }
317
+ editorViewport.clipToOutline = cornerRadiusPx > 0f
318
+ editorScrollView.setBackgroundColor(Color.TRANSPARENT)
319
+ appliedCornerRadiusPx = cornerRadiusPx
320
+ appliedBackgroundColorForTesting = backgroundColor
321
+ }
322
+
323
+ private fun updateScrollContainerInsets() {
324
+ if (heightBehavior != EditorHeightBehavior.FIXED) {
325
+ editorScrollView.setPadding(0, 0, 0, 0)
326
+ return
327
+ }
328
+
329
+ val density = resources.displayMetrics.density
330
+ val topInset = ((theme?.contentInsets?.top ?: 0f) * density).toInt()
331
+ val bottomInset = ((theme?.contentInsets?.bottom ?: 0f) * density).toInt()
332
+ editorScrollView.setPadding(0, topInset, 0, bottomInset + viewportBottomInsetPx)
333
+ }
334
+
335
+ private fun createContainerLayoutParams(): LayoutParams =
336
+ if (heightBehavior == EditorHeightBehavior.AUTO_GROW) {
337
+ LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)
338
+ } else {
339
+ LayoutParams(LayoutParams.MATCH_PARENT, 0, 1f)
340
+ }
341
+
342
+ private fun createEditorLayoutParams(): FrameLayout.LayoutParams =
343
+ FrameLayout.LayoutParams(
344
+ ViewGroup.LayoutParams.MATCH_PARENT,
345
+ ViewGroup.LayoutParams.WRAP_CONTENT
346
+ )
347
+
348
+ internal fun selectedImageGeometry(): EditorEditText.SelectedImageGeometry? {
349
+ val geometry = editorEditText.selectedImageGeometry() ?: return null
350
+ return EditorEditText.SelectedImageGeometry(
351
+ docPos = geometry.docPos,
352
+ 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
357
+ )
358
+ )
359
+ }
360
+
361
+ internal fun caretRect(): RectF? {
362
+ val rect = editorEditText.caretRect() ?: return null
363
+ 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
368
+ )
369
+ }
370
+
371
+ 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()
377
+ }
378
+
379
+ internal fun clampImageSize(
380
+ widthPx: Float,
381
+ heightPx: Float,
382
+ maximumWidthPx: Float = maximumImageWidthPx()
383
+ ): Pair<Float, Float> {
384
+ val aspectRatio = maxOf(widthPx / maxOf(heightPx, 1f), 0.1f)
385
+ val clampedWidth = minOf(maxOf(48f, maximumWidthPx), maxOf(48f, widthPx))
386
+ val clampedHeight = maxOf(48f, clampedWidth / aspectRatio)
387
+ return clampedWidth to clampedHeight
388
+ }
389
+
390
+ internal fun resizeImage(docPos: Int, widthPx: Float, heightPx: Float) {
391
+ val (clampedWidth, clampedHeight) = clampImageSize(widthPx, heightPx)
392
+ editorEditText.resizeImageAtDocPos(docPos, clampedWidth, clampedHeight)
393
+ }
394
+
395
+ private fun refreshOverlays() {
396
+ remoteSelectionOverlayView.refreshGeometry()
397
+ imageResizeOverlayView.refresh()
398
+ }
399
+ }