@apollohg/react-native-rich-text-editor 2.0.1 → 2.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 (31) hide show
  1. package/README.md +8 -116
  2. package/android/src/main/java/com/apollohg/editor/EditorEditTextImages.kt +25 -0
  3. package/android/src/main/java/com/apollohg/editor/EditorV2RenderValidation.kt +1 -0
  4. package/android/src/main/java/com/apollohg/editor/ImageResizeOverlayView.kt +101 -33
  5. package/android/src/main/java/com/apollohg/editor/RenderMediaSpans.kt +15 -3
  6. package/android/src/main/java/com/apollohg/editor/RichTextEditorView.kt +14 -0
  7. package/android/src/main/java/com/apollohg/editor/viewer/AndroidProseLayoutEngine.kt +1 -1
  8. package/dist/EditorStyleSheetNormalization.js +1 -1
  9. package/dist/EditorStyleSheetTypes.d.ts +1 -1
  10. package/ios/EditorCore.xcframework/Info.plist +5 -5
  11. package/ios/EditorCore.xcframework/ios-arm64/libeditor_core.a +0 -0
  12. package/ios/EditorCore.xcframework/ios-arm64_x86_64-simulator/libeditor_core.a +0 -0
  13. package/ios/EditorInteractionOverlays.swift +114 -122
  14. package/ios/EditorLayoutManager.swift +13 -3
  15. package/ios/EditorStyleSheet.swift +12 -1
  16. package/ios/EditorTextView+Images.swift +32 -10
  17. package/ios/EditorTextView.swift +1 -1
  18. package/ios/EditorV2Adapter+RenderValidation.swift +2 -2
  19. package/ios/NativeToolbar+ButtonStyle.swift +17 -5
  20. package/ios/NativeToolbar+Diagnostics.swift +15 -0
  21. package/ios/RenderBridge+Atoms.swift +0 -1
  22. package/ios/RenderBridge+Attachments.swift +0 -29
  23. package/ios/RichTextEditorView.swift +4 -12
  24. package/ios/Viewer/CoreTextProseLayoutEngine+AttributedText.swift +3 -2
  25. package/package.json +1 -1
  26. package/rust/android/arm64-v8a/libeditor_core.so +0 -0
  27. package/rust/android/armeabi-v7a/libeditor_core.so +0 -0
  28. package/rust/android/x86/libeditor_core.so +0 -0
  29. package/rust/android/x86_64/libeditor_core.so +0 -0
  30. package/src/EditorStyleSheetNormalization.ts +1 -1
  31. package/src/EditorStyleSheetTypes.ts +1 -1
package/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  See the [documentation](https://github.com/apollohg/react-native-rich-text-editor/wiki) for guides and API references. The package is under active development; review the [changelog](./CHANGELOG.md) before upgrading between major versions.
6
6
 
7
- <img src="https://github.com/apollohg/react-native-rich-text-editor/wiki/images/github-banner.png" alt="Example editor on iOS" width="100%" />
7
+ <a href="./assets/github-demo-banner.mp4"><img src="./assets/github-demo-banner.gif" alt="Native iOS editor demo with rich text, nested lists, a code block, and image resizing" width="100%" /></a>
8
8
 
9
9
  ## Highlights
10
10
 
@@ -16,8 +16,6 @@ See the [documentation](https://github.com/apollohg/react-native-rich-text-edito
16
16
  - `RichTextViewer`, an exact-size Fabric renderer for read-only content
17
17
  - Shared document handles for local editing and Yjs collaboration
18
18
 
19
- `NativeRichTextEditor` and `NativeProseViewer`, along with their associated types, remain available as deprecated aliases of `RichTextEditor` and `RichTextViewer`.
20
-
21
19
  ## Requirements
22
20
 
23
21
  The package uses custom native code and Expo Modules. Use a development build or a bare React Native app with Expo Modules configured; it does not run in Expo Go.
@@ -50,7 +48,7 @@ npx expo prebuild
50
48
  npx expo run:ios # or: npx expo run:android
51
49
  ```
52
50
 
53
- See the [Installation Guide](https://github.com/apollohg/react-native-rich-text-editor/wiki/Installation) for bare React Native setup and migration from `@apollohg/react-native-prose-editor`.
51
+ See the [Installation Guide](https://github.com/apollohg/react-native-rich-text-editor/wiki/Installation) for bare React Native setup.
54
52
 
55
53
  ## Editor usage
56
54
 
@@ -91,7 +89,7 @@ See [Getting Started](https://github.com/apollohg/react-native-rich-text-editor/
91
89
 
92
90
  ## Custom atom nodes
93
91
 
94
- Render interactive cards, embeds, and other custom blocks with your own React components. Define an atom with `defineAtomNode`, add it to your document schema, and pass it through the editor or viewer's `atoms` prop.
92
+ Render interactive cards, embeds, and other custom blocks with your own React components.
95
93
 
96
94
  See [Custom Atom Nodes](https://github.com/apollohg/react-native-rich-text-editor/wiki/Custom-Atom-Nodes) for a complete example and API details.
97
95
 
@@ -107,108 +105,13 @@ import { RichTextViewer } from '@apollohg/react-native-rich-text-editor';
107
105
 
108
106
  See the [Viewer Guide](https://github.com/apollohg/react-native-rich-text-editor/wiki/Viewer) for styling, images, interactions, and custom atoms.
109
107
 
110
- ## Element styling and addons
111
-
112
- Version 2 uses a flat, typed stylesheet for both the editor and viewer:
113
-
114
- ```tsx
115
- import { EditorStyleSheet, RichTextEditor } from '@apollohg/react-native-rich-text-editor';
116
-
117
- const theme = EditorStyleSheet.create({
118
- content: { padding: 16, backgroundColor: '#ffffff' },
119
- text: { fontSize: 16, color: '#202124' },
120
- paragraph: { marginBottom: 12 },
121
- link: { color: '#285dcc', textDecorationLine: 'underline' },
122
- blockquote: {
123
- backgroundColor: '#f3f5f8',
124
- padding: 12,
125
- borderWidth: 1,
126
- borderColor: '#dce1e8',
127
- borderLeftWidth: 4,
128
- borderLeftColor: '#285dcc',
129
- borderTopRightRadius: 8,
130
- },
131
- codeBlock: { backgroundColor: '#eff1f5', padding: 12, borderRadius: 8 },
132
- image: { backgroundColor: '#eff1f5', borderRadius: 12, resizeMode: 'cover' },
133
- });
134
-
135
- <RichTextEditor documentHandle={documentHandle} theme={theme} />;
136
- ```
137
-
138
- Each entry accepts a style object or nested, conditional style arrays. Later entries override earlier properties; explicit `undefined` removes an earlier property. Side and corner properties override shorthands. Supported fields are checked by TypeScript and validated before native updates. These are editor styles: layout fields such as `flex`, transforms, and positioning are not accepted.
139
-
140
- Text styling inherits from `text` and enclosing blocks. Backgrounds, spacing, borders, and corner radii belong to each element. Inline entries support typography and backgrounds; mentions also support borders. List containers, items, markers, and checkboxes have separate entries. `toolbar` retains its existing configuration.
141
-
142
- Android editing uses per-block text layout for physical margins, borders, and padding, including RTL content. Per-block justification is supported on Android API 26+; API 24/25 use normal alignment.
143
-
144
- Custom atom components mount inside Android's native scrolling content. Their measured height participates in document layout, and their controls receive normal React Native touch events.
108
+ ## Styling and addons
145
109
 
146
- Addons are a readonly array. Conditional `false`, `null`, and `undefined` entries are allowed; duplicate capabilities are rejected:
147
-
148
- ```tsx
149
- import { createMentionsAddon } from '@apollohg/react-native-rich-text-editor';
150
- import { createCodeHighlightingAddon } from '@apollohg/react-native-rich-text-editor-code-highlighting';
151
-
152
- <RichTextEditor
153
- documentHandle={documentHandle}
154
- theme={theme}
155
- addons={[
156
- createMentionsAddon({ trigger: '@', suggestions }),
157
- enableHighlighting && createCodeHighlightingAddon({ theme: 'base16-ocean.dark' }),
158
- ]}
159
- />;
160
- ```
161
-
162
- Syntax highlighting requires the separately installed [code-highlighting package](./packages/code-highlighting), followed by a native rebuild. The base editor does not include syntect or its grammars. Set a code block's `attrs.language`, for example `{ type: 'codeBlock', attrs: { language: 'typescript' }, content: [...] }`. Missing or unsupported languages keep ordinary code styling. The code block theme controls the panel; the highlighting addon controls token colors and font traits. Mention-enabled editor handles still require `withMentionsSchema` when creating their schema.
163
-
164
- When migrating from version 1, replace `links` with `link`, heading maps with `h1`–`h6`, `spacingAfter` with `marginBottom`, and `contentInsets` with `content.padding*`. Flatten nested quote/code typography into their element entry, and move list appearance into the relevant container/item/marker entries. Addon objects become `[createMentionsAddon(options)]`. The new built-in schema declares code-block language metadata; coordinate schema changes across collaborating clients.
110
+ Customize the editor and viewer with themes, mentions, and syntax highlighting. See [Styling](https://github.com/apollohg/react-native-rich-text-editor/wiki/Styling) and [Addons](https://github.com/apollohg/react-native-rich-text-editor/wiki/Addons).
165
111
 
166
112
  ## Collaboration
167
113
 
168
- `useYjsCollaboration` connects a room-backed document handle to a Yjs sync and awareness server. The editor and collaboration controller must share the same handle.
169
-
170
- ```tsx
171
- import React, { useEffect, useMemo } from 'react';
172
- import {
173
- createNativeEditorDocumentHandle,
174
- RichTextEditor,
175
- useYjsCollaboration,
176
- } from '@apollohg/react-native-rich-text-editor';
177
-
178
- export function CollaborativeEditor({ documentId }: { documentId: string }) {
179
- const documentHandle = useMemo(
180
- () =>
181
- createNativeEditorDocumentHandle({
182
- initialization: {
183
- type: 'room',
184
- documentId,
185
- lineageId: `my-app|${documentId}`,
186
- },
187
- }),
188
- [documentId]
189
- );
190
-
191
- const collaboration = useYjsCollaboration({
192
- documentId,
193
- handle: documentHandle,
194
- transport: {
195
- url: `wss://example.com/collaboration?documentId=${encodeURIComponent(documentId)}`,
196
- connect: true,
197
- },
198
- localAwareness: {
199
- userId: 'user-1',
200
- name: 'Ada',
201
- color: '#0A84FF',
202
- },
203
- });
204
-
205
- useEffect(() => () => documentHandle.destroy(), [documentHandle]);
206
-
207
- return <RichTextEditor {...collaboration.editorBindings} />;
208
- }
209
- ```
210
-
211
- See the [Collaboration Guide](https://github.com/apollohg/react-native-rich-text-editor/wiki/Collaboration) for server requirements, persistence, authentication, and recovery.
114
+ Connect shared documents to a Yjs server for collaborative editing and live cursors. See the [Collaboration Guide](https://github.com/apollohg/react-native-rich-text-editor/wiki/Collaboration).
212
115
 
213
116
  ## Comparison with other React Native editors
214
117
 
@@ -225,14 +128,6 @@ Reviewed on 7 September 2026
225
128
 
226
129
  ## Development
227
130
 
228
- Run `npm run lint:kotlin` to check Kotlin style or `npm run format:kotlin` to
229
- apply automatic fixes. Both commands use ktlint 1.8.0 with Android Studio style
230
- and four-space indentation. Java 17+ is required (`JAVA_HOME` or `java` on PATH).
231
- The first run downloads and verifies the CLI into `.tmp/ktlint`; subsequent runs
232
- use the cached copy. Generated bindings, build output, dependencies, and Expo's
233
- generated native projects are excluded. Additional ktlint options can be passed
234
- after `--`, for example `npm run lint:kotlin -- --reporter=plain-summary`.
235
-
236
131
  See the [example app](./example) to try the editor and viewer, and the [Development Workflow](https://github.com/apollohg/react-native-rich-text-editor/wiki/Development-Workflow) for local setup, native builds, and testing.
237
132
 
238
133
  ## Documentation
@@ -248,12 +143,9 @@ See the [example app](./example) to try the editor and viewer, and the [Developm
248
143
  - [Mentions](https://github.com/apollohg/react-native-rich-text-editor/wiki/Mentions)
249
144
  - [Styling](https://github.com/apollohg/react-native-rich-text-editor/wiki/Styling)
250
145
  - [Production limits and errors](https://github.com/apollohg/react-native-rich-text-editor/wiki/Production-Limits-and-Errors)
146
+ - [Migration guide](https://github.com/apollohg/react-native-rich-text-editor/wiki/Migration-Guide)
251
147
  - [Changelog](./CHANGELOG.md)
252
148
 
253
149
  ## License
254
150
 
255
- [Apache-2.0](./LICENSE) for this project's own code. Dependencies retain their respective licenses.
256
-
257
- [Third-party notices](./THIRD_PARTY_NOTICES.md) cover the bundled Rust libraries, native dependencies, and copied icon assets; [Rust standard-library notices](./RUST-STANDARD-LIBRARY-NOTICES.html) accompany the prebuilt native libraries. The optional highlighting package ships its own notices for syntect, grammars, themes, and its other dependencies.
258
-
259
- When distributing an app, include the applicable notices with the app's open-source acknowledgements or other accompanying materials, including the MPL corresponding-source links. Retain notices for the actual React Native/Expo and native dependency versions resolved by your app as well. An npm package's notices are not automatically copied into the final app by Metro. Refresh notices when dependencies, bundled assets, or the Rust toolchain change.
151
+ [Apache-2.0](./LICENSE). See [third-party notices](./THIRD_PARTY_NOTICES.md) and [Rust standard-library notices](./RUST-STANDARD-LIBRARY-NOTICES.html) for bundled dependencies.
@@ -76,6 +76,31 @@ internal fun EditorEditText.selectedImageGeometryImpl(): SelectedImageGeometry?
76
76
  )
77
77
  }
78
78
 
79
+ internal fun EditorEditText.selectedImageSpanForResize(): BlockImageSpan? {
80
+ val spannable = text as? Spanned ?: return null
81
+ val selection = resolvedSelectedImageRange(spannable) ?: return null
82
+ return spannable.getSpans(
83
+ selection.start,
84
+ selection.end,
85
+ BlockImageSpan::class.java
86
+ ).firstOrNull { span ->
87
+ spannable.getSpanStart(span) == selection.start &&
88
+ spannable.getSpanEnd(span) == selection.end
89
+ }
90
+ }
91
+
92
+ internal fun EditorEditText.relayoutImageResizePreview(span: BlockImageSpan) {
93
+ val content = text ?: return
94
+ val start = content.getSpanStart(span)
95
+ val end = content.getSpanEnd(span)
96
+ if (start < 0 || end <= start) return
97
+ val flags = content.getSpanFlags(span)
98
+ content.setSpan(span, start, end, flags)
99
+ requestLayout()
100
+ invalidate()
101
+ onContentSizeMayChange?.invoke()
102
+ }
103
+
79
104
  internal fun EditorEditText.resizeImageAtDocPosImpl(docPos: Int, widthPx: Float, heightPx: Float) {
80
105
  if (!hasLiveEditor()) return
81
106
  val density = resources.displayMetrics.density
@@ -141,6 +141,7 @@ private fun validMentionNodeStyle(value: Any?): Boolean {
141
141
  listOf("Top", "Right", "Bottom", "Left").map { "border${it}Color" }
142
142
  val nonnegative =
143
143
  listOf("Top", "Right", "Bottom", "Left").map { "border${it}Width" } +
144
+ listOf("Top", "Right", "Bottom", "Left").map { "padding$it" } +
144
145
  listOf("TopLeft", "TopRight", "BottomLeft", "BottomRight").map { "border${it}Radius" }
145
146
  val enums = mapOf(
146
147
  "fontWeight" to
@@ -30,8 +30,15 @@ internal class ImageResizeOverlayView @JvmOverloads constructor(
30
30
  val corner: Corner,
31
31
  val originalRect: RectF,
32
32
  val docPos: Int,
33
+ val span: BlockImageSpan,
34
+ val downX: Float,
35
+ val downY: Float,
36
+ val originalSize: Pair<Int, Int>,
37
+ val fixedWidthPx: Float,
38
+ val fixedHeightPx: Float,
33
39
  val maximumWidthPx: Float,
34
- var previewRect: RectF
40
+ var previewRect: RectF,
41
+ var previewContentSize: Pair<Float, Float>
35
42
  )
36
43
 
37
44
  private var editorView: RichTextEditorView? = null
@@ -73,7 +80,10 @@ internal class ImageResizeOverlayView @JvmOverloads constructor(
73
80
  cancelActiveResize()
74
81
  return
75
82
  }
76
- if (currentGeometry?.docPos != nextGeometry.docPos) cancelActiveResize()
83
+ val selectedSpan = editorView?.selectedImageSpanForResize()
84
+ if (currentGeometry?.docPos != nextGeometry.docPos ||
85
+ dragState?.span?.let { it !== selectedSpan } == true
86
+ ) cancelActiveResize()
77
87
  currentGeometry = nextGeometry
78
88
  visibility = VISIBLE
79
89
  bringToFront()
@@ -82,6 +92,7 @@ internal class ImageResizeOverlayView @JvmOverloads constructor(
82
92
  }
83
93
 
84
94
  fun cancelActiveResize() {
95
+ dragState?.let { state -> editorView?.restoreImageResizePreview(state.span) }
85
96
  dragState = null
86
97
  currentGeometry = null
87
98
  parent?.requestDisallowInterceptTouchEvent(false)
@@ -114,12 +125,20 @@ internal class ImageResizeOverlayView @JvmOverloads constructor(
114
125
  when (event.actionMasked) {
115
126
  MotionEvent.ACTION_DOWN -> {
116
127
  val corner = cornerAt(event.x, event.y, geometry.rect) ?: return false
128
+ val span = editorView?.selectedImageSpanForResize() ?: return false
117
129
  dragState = DragState(
118
130
  corner = corner,
119
131
  originalRect = RectF(geometry.rect),
120
132
  docPos = geometry.docPos,
133
+ span = span,
134
+ downX = event.x,
135
+ downY = event.y,
136
+ originalSize = span.currentSizePx(),
137
+ fixedWidthPx = geometry.rect.width() - span.currentSizePx().first,
138
+ fixedHeightPx = geometry.rect.height() - span.currentSizePx().second,
121
139
  maximumWidthPx = editorView?.maximumImageWidthPx() ?: geometry.rect.width(),
122
- previewRect = RectF(geometry.rect)
140
+ previewRect = RectF(geometry.rect),
141
+ previewContentSize = span.currentSizePx().let { it.first.toFloat() to it.second.toFloat() }
123
142
  )
124
143
  parent?.requestDisallowInterceptTouchEvent(true)
125
144
  return true
@@ -127,29 +146,24 @@ internal class ImageResizeOverlayView @JvmOverloads constructor(
127
146
 
128
147
  MotionEvent.ACTION_MOVE -> {
129
148
  val state = dragState ?: return false
130
- val nextRect = resizedRect(
131
- originalRect = state.originalRect,
132
- corner = state.corner,
133
- deltaX = event.x - handleCenter(state.corner, state.originalRect).x,
134
- deltaY = event.y - handleCenter(state.corner, state.originalRect).y,
135
- maximumWidthPx = state.maximumWidthPx
136
- )
137
- state.previewRect = RectF(nextRect)
138
- currentGeometry = EditorEditText.SelectedImageGeometry(state.docPos, nextRect)
139
- updateSystemGestureExclusionRects()
140
- invalidate()
149
+ updatePreview(state, event.x, event.y)
141
150
  return true
142
151
  }
143
152
 
144
153
  MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
145
154
  val state = dragState ?: return false
146
155
  if (event.actionMasked == MotionEvent.ACTION_UP) {
147
- editorView?.resizeImage(
148
- state.docPos,
149
- state.previewRect.width(),
150
- state.previewRect.height()
151
- )
152
- }
156
+ updatePreview(state, event.x, event.y)
157
+ val previewSize = state.span.currentSizePx()
158
+ if (previewSize != state.originalSize) {
159
+ editorView?.resizeImage(
160
+ state.docPos,
161
+ state.previewContentSize.first,
162
+ state.previewContentSize.second
163
+ )
164
+ }
165
+ editorView?.restoreImageResizePreview(state.span)
166
+ } else editorView?.restoreImageResizePreview(state.span)
153
167
  dragState = null
154
168
  parent?.requestDisallowInterceptTouchEvent(false)
155
169
  post { refresh() }
@@ -160,6 +174,51 @@ internal class ImageResizeOverlayView @JvmOverloads constructor(
160
174
  return false
161
175
  }
162
176
 
177
+ private fun updatePreview(state: DragState, x: Float, y: Float) {
178
+ val deltaX = x - state.downX
179
+ val deltaY = y - state.downY
180
+ if (deltaX == 0f && deltaY == 0f) {
181
+ val originalContentSize = state.originalSize.let {
182
+ it.first.toFloat() to it.second.toFloat()
183
+ }
184
+ if (state.previewContentSize == originalContentSize) return
185
+ val boundEditor = editorView ?: return
186
+ if (!boundEditor.previewImageResize(
187
+ state.span,
188
+ originalContentSize.first,
189
+ originalContentSize.second
190
+ )
191
+ ) return
192
+ state.previewRect = RectF(state.originalRect)
193
+ state.previewContentSize = originalContentSize
194
+ currentGeometry = boundEditor.selectedImageGeometry()
195
+ ?.takeIf { it.docPos == state.docPos }
196
+ ?: EditorEditText.SelectedImageGeometry(state.docPos, state.originalRect)
197
+ updateSystemGestureExclusionRects()
198
+ invalidate()
199
+ return
200
+ }
201
+ val (nextRect, contentSize) = resizedRect(
202
+ originalRect = state.originalRect,
203
+ originalContentSize = state.originalSize,
204
+ fixedWidthPx = state.fixedWidthPx,
205
+ fixedHeightPx = state.fixedHeightPx,
206
+ corner = state.corner,
207
+ deltaX = deltaX,
208
+ deltaY = deltaY,
209
+ maximumWidthPx = state.maximumWidthPx
210
+ )
211
+ val boundEditor = editorView ?: return
212
+ if (!boundEditor.previewImageResize(state.span, contentSize.first, contentSize.second)) return
213
+ state.previewRect = RectF(nextRect)
214
+ state.previewContentSize = contentSize
215
+ currentGeometry = boundEditor.selectedImageGeometry()
216
+ ?.takeIf { it.docPos == state.docPos }
217
+ ?: EditorEditText.SelectedImageGeometry(state.docPos, nextRect)
218
+ updateSystemGestureExclusionRects()
219
+ invalidate()
220
+ }
221
+
163
222
  override fun onDetachedFromWindow() {
164
223
  cancelActiveResize()
165
224
  super.onDetachedFromWindow()
@@ -217,12 +276,17 @@ internal class ImageResizeOverlayView @JvmOverloads constructor(
217
276
 
218
277
  private fun resizedRect(
219
278
  originalRect: RectF,
279
+ originalContentSize: Pair<Int, Int>,
280
+ fixedWidthPx: Float,
281
+ fixedHeightPx: Float,
220
282
  corner: Corner,
221
283
  deltaX: Float,
222
284
  deltaY: Float,
223
285
  maximumWidthPx: Float?
224
- ): RectF {
225
- val aspectRatio = max(originalRect.width() / max(originalRect.height(), 1f), 0.1f)
286
+ ): Pair<RectF, Pair<Float, Float>> {
287
+ val originalWidth = max(originalContentSize.first.toFloat(), 1f)
288
+ val originalHeight = max(originalContentSize.second.toFloat(), 1f)
289
+ val aspectRatio = max(originalWidth / originalHeight, 0.1f)
226
290
  val signedDx = if (corner == Corner.TOP_RIGHT ||
227
291
  corner == Corner.BOTTOM_RIGHT
228
292
  ) {
@@ -237,30 +301,34 @@ internal class ImageResizeOverlayView @JvmOverloads constructor(
237
301
  } else {
238
302
  -deltaY
239
303
  }
240
- val widthScale = (originalRect.width() + signedDx) / max(originalRect.width(), 1f)
241
- val heightScale = (originalRect.height() + signedDy) / max(originalRect.height(), 1f)
304
+ val projectedScale = 1f +
305
+ ((signedDx * originalWidth) + (signedDy * originalHeight)) /
306
+ ((originalWidth * originalWidth) + (originalHeight * originalHeight))
242
307
  val scale = max(
243
- max(widthScale, heightScale),
244
- minimumImageSizePx / max(originalRect.width(), 1f)
308
+ projectedScale,
309
+ max(minimumImageSizePx / originalWidth, minimumImageSizePx / originalHeight)
245
310
  )
246
- val unclampedWidth = max(minimumImageSizePx, originalRect.width() * scale)
311
+ val unclampedWidth = max(minimumImageSizePx, originalWidth * scale)
247
312
  val unclampedHeight = max(minimumImageSizePx / aspectRatio, unclampedWidth / aspectRatio)
248
313
  val (width, height) = editorView?.let { boundEditor ->
249
314
  maximumWidthPx?.let { maxWidth ->
250
315
  boundEditor.clampImageSize(
251
316
  widthPx = unclampedWidth,
252
317
  heightPx = unclampedHeight,
253
- maximumWidthPx = maxWidth
318
+ maximumWidthPx = (maxWidth - fixedWidthPx).coerceAtLeast(minimumImageSizePx)
254
319
  )
255
320
  } ?: boundEditor.clampImageSize(unclampedWidth, unclampedHeight)
256
321
  } ?: (unclampedWidth to unclampedHeight)
257
322
  val anchor = anchorPoint(corner, originalRect)
258
323
 
259
- return when (corner) {
260
- Corner.TOP_LEFT -> RectF(anchor.x - width, anchor.y - height, anchor.x, anchor.y)
261
- Corner.TOP_RIGHT -> RectF(anchor.x, anchor.y - height, anchor.x + width, anchor.y)
262
- Corner.BOTTOM_LEFT -> RectF(anchor.x - width, anchor.y, anchor.x, anchor.y + height)
263
- Corner.BOTTOM_RIGHT -> RectF(anchor.x, anchor.y, anchor.x + width, anchor.y + height)
324
+ val visualWidth = width + fixedWidthPx
325
+ val visualHeight = height + fixedHeightPx
326
+ val rect = when (corner) {
327
+ Corner.TOP_LEFT -> RectF(anchor.x - visualWidth, anchor.y - visualHeight, anchor.x, anchor.y)
328
+ Corner.TOP_RIGHT -> RectF(anchor.x, anchor.y - visualHeight, anchor.x + visualWidth, anchor.y)
329
+ Corner.BOTTOM_LEFT -> RectF(anchor.x - visualWidth, anchor.y, anchor.x, anchor.y + visualHeight)
330
+ Corner.BOTTOM_RIGHT -> RectF(anchor.x, anchor.y, anchor.x + visualWidth, anchor.y + visualHeight)
264
331
  }
332
+ return rect to (width to height)
265
333
  }
266
334
  }
@@ -74,6 +74,7 @@ internal class BlockImageSpan(
74
74
 
75
75
  @Volatile
76
76
  private var lastDrawRect: RectF? = null
77
+ private var temporarySizePx: Pair<Int, Int>? = null
77
78
 
78
79
  init {
79
80
  if (preparedSource != null) {
@@ -228,9 +229,6 @@ internal class BlockImageSpan(
228
229
  paint.color = Color.argb(24, 0, 0, 0)
229
230
  paint.style = Paint.Style.FILL
230
231
  canvas.drawRoundRect(rect, 16f * density, 16f * density, paint)
231
- paint.color = Color.argb(120, 0, 0, 0)
232
- val iconRadius = minOf(rect.width(), rect.height()) * 0.12f
233
- canvas.drawCircle(rect.centerX(), rect.centerY(), iconRadius, paint)
234
232
  paint.color = previousColor
235
233
  paint.style = previousStyle
236
234
  }
@@ -247,6 +245,7 @@ internal class BlockImageSpan(
247
245
  }
248
246
 
249
247
  internal fun currentSizePx(): Pair<Int, Int> {
248
+ temporarySizePx?.let { return it }
250
249
  val maxWidth = resolvedMaxWidth()
251
250
  val loadedBitmap = bitmapLease.get()?.bitmap
252
251
  val fallbackAspectRatio = if (loadedBitmap != null && loadedBitmap.width > 0 &&
@@ -299,6 +298,19 @@ internal class BlockImageSpan(
299
298
 
300
299
  internal fun currentDrawRect(): RectF? = lastDrawRect?.let(::RectF)
301
300
 
301
+ internal fun setTemporarySizePx(widthPx: Float, heightPx: Float): Boolean {
302
+ val next = checkedPositiveInt(widthPx) to checkedPositiveInt(heightPx)
303
+ if (temporarySizePx == next) return false
304
+ temporarySizePx = next
305
+ return true
306
+ }
307
+
308
+ internal fun clearTemporarySize(): Boolean {
309
+ if (temporarySizePx == null) return false
310
+ temporarySizePx = null
311
+ return true
312
+ }
313
+
302
314
  private fun resolvedMaxWidth(): Float {
303
315
  val host = hostRef.get()
304
316
  val hostWidth = host?.let {
@@ -943,6 +943,20 @@ class RichTextEditorView @JvmOverloads constructor(
943
943
  editorEditText.resizeImageAtDocPos(docPos, clampedWidth, clampedHeight)
944
944
  }
945
945
 
946
+ internal fun selectedImageSpanForResize(): BlockImageSpan? =
947
+ editorEditText.selectedImageSpanForResize()
948
+
949
+ internal fun previewImageResize(span: BlockImageSpan, widthPx: Float, heightPx: Float): Boolean {
950
+ if (!span.setTemporarySizePx(widthPx, heightPx)) return false
951
+ editorEditText.relayoutImageResizePreview(span)
952
+ return true
953
+ }
954
+
955
+ internal fun restoreImageResizePreview(span: BlockImageSpan) {
956
+ if (!span.clearTemporarySize()) return
957
+ editorEditText.relayoutImageResizePreview(span)
958
+ }
959
+
946
960
  private fun refreshOverlays() {
947
961
  layoutAtomHostViews()
948
962
  remoteSelectionOverlayView.refreshGeometry()
@@ -1182,7 +1182,7 @@ internal class StaticLayoutAndroidProseLayoutEngine : AndroidProseLayoutEngine {
1182
1182
  )
1183
1183
  val width =
1184
1184
  max(
1185
- base.sizePx.toInt(),
1185
+ if (inline.nodeType == "mention") 0 else base.sizePx.toInt(),
1186
1186
  ceil(
1187
1187
  labelPaint.measureText(label) + atomInset.left + atomInset.right
1188
1188
  ).toInt()
@@ -63,7 +63,7 @@ const fields = {
63
63
  italic: inline,
64
64
  underline: inline,
65
65
  strike: inline,
66
- mention: [...inline, ...border],
66
+ mention: [...inline, ...border, ...spacing('padding')],
67
67
  placeholder: typography,
68
68
  };
69
69
  const enums = {
@@ -91,7 +91,7 @@ export interface EditorHorizontalRuleStyle extends EditorBorderStyle, EditorMarg
91
91
  backgroundColor?: string;
92
92
  height?: number;
93
93
  }
94
- export interface EditorMentionStyle extends EditorInlineStyle, EditorBorderStyle {
94
+ export interface EditorMentionStyle extends EditorInlineStyle, EditorBorderStyle, EditorPaddingStyle {
95
95
  }
96
96
  export interface EditorStyleMap {
97
97
  content: EditorSurfaceStyle;
@@ -8,32 +8,32 @@
8
8
  <key>BinaryPath</key>
9
9
  <string>libeditor_core.a</string>
10
10
  <key>LibraryIdentifier</key>
11
- <string>ios-arm64_x86_64-simulator</string>
11
+ <string>ios-arm64</string>
12
12
  <key>LibraryPath</key>
13
13
  <string>libeditor_core.a</string>
14
14
  <key>SupportedArchitectures</key>
15
15
  <array>
16
16
  <string>arm64</string>
17
- <string>x86_64</string>
18
17
  </array>
19
18
  <key>SupportedPlatform</key>
20
19
  <string>ios</string>
21
- <key>SupportedPlatformVariant</key>
22
- <string>simulator</string>
23
20
  </dict>
24
21
  <dict>
25
22
  <key>BinaryPath</key>
26
23
  <string>libeditor_core.a</string>
27
24
  <key>LibraryIdentifier</key>
28
- <string>ios-arm64</string>
25
+ <string>ios-arm64_x86_64-simulator</string>
29
26
  <key>LibraryPath</key>
30
27
  <string>libeditor_core.a</string>
31
28
  <key>SupportedArchitectures</key>
32
29
  <array>
33
30
  <string>arm64</string>
31
+ <string>x86_64</string>
34
32
  </array>
35
33
  <key>SupportedPlatform</key>
36
34
  <string>ios</string>
35
+ <key>SupportedPlatformVariant</key>
36
+ <string>simulator</string>
37
37
  </dict>
38
38
  </array>
39
39
  <key>CFBundlePackageType</key>