@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.
- package/README.md +8 -116
- package/android/src/main/java/com/apollohg/editor/EditorEditTextImages.kt +25 -0
- package/android/src/main/java/com/apollohg/editor/EditorV2RenderValidation.kt +1 -0
- package/android/src/main/java/com/apollohg/editor/ImageResizeOverlayView.kt +101 -33
- package/android/src/main/java/com/apollohg/editor/RenderMediaSpans.kt +15 -3
- package/android/src/main/java/com/apollohg/editor/RichTextEditorView.kt +14 -0
- package/android/src/main/java/com/apollohg/editor/viewer/AndroidProseLayoutEngine.kt +1 -1
- package/dist/EditorStyleSheetNormalization.js +1 -1
- package/dist/EditorStyleSheetTypes.d.ts +1 -1
- package/ios/EditorCore.xcframework/Info.plist +5 -5
- package/ios/EditorCore.xcframework/ios-arm64/libeditor_core.a +0 -0
- package/ios/EditorCore.xcframework/ios-arm64_x86_64-simulator/libeditor_core.a +0 -0
- package/ios/EditorInteractionOverlays.swift +114 -122
- package/ios/EditorLayoutManager.swift +13 -3
- package/ios/EditorStyleSheet.swift +12 -1
- package/ios/EditorTextView+Images.swift +32 -10
- package/ios/EditorTextView.swift +1 -1
- package/ios/EditorV2Adapter+RenderValidation.swift +2 -2
- package/ios/NativeToolbar+ButtonStyle.swift +17 -5
- package/ios/NativeToolbar+Diagnostics.swift +15 -0
- package/ios/RenderBridge+Atoms.swift +0 -1
- package/ios/RenderBridge+Attachments.swift +0 -29
- package/ios/RichTextEditorView.swift +4 -12
- package/ios/Viewer/CoreTextProseLayoutEngine+AttributedText.swift +3 -2
- package/package.json +1 -1
- package/rust/android/arm64-v8a/libeditor_core.so +0 -0
- package/rust/android/armeabi-v7a/libeditor_core.so +0 -0
- package/rust/android/x86/libeditor_core.so +0 -0
- package/rust/android/x86_64/libeditor_core.so +0 -0
- package/src/EditorStyleSheetNormalization.ts +1 -1
- 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
|
-
<
|
|
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
|
|
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.
|
|
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
|
-
##
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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
|
|
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
|
|
241
|
-
|
|
304
|
+
val projectedScale = 1f +
|
|
305
|
+
((signedDx * originalWidth) + (signedDy * originalHeight)) /
|
|
306
|
+
((originalWidth * originalWidth) + (originalHeight * originalHeight))
|
|
242
307
|
val scale = max(
|
|
243
|
-
|
|
244
|
-
minimumImageSizePx /
|
|
308
|
+
projectedScale,
|
|
309
|
+
max(minimumImageSizePx / originalWidth, minimumImageSizePx / originalHeight)
|
|
245
310
|
)
|
|
246
|
-
val unclampedWidth = max(minimumImageSizePx,
|
|
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
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
Corner.
|
|
263
|
-
Corner.
|
|
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()
|
|
@@ -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-
|
|
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-
|
|
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>
|
|
Binary file
|
|
Binary file
|