@openeditor/react-native-prose-editor 0.0.6 → 0.0.8
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/android/build.gradle +35 -0
- package/android/src/main/java/com/apollohg/editor/CaretGeometry.kt +2 -2
- package/android/src/main/java/com/apollohg/editor/EditorEditText.kt +138 -597
- package/android/src/main/java/com/apollohg/editor/EditorInputConnection.kt +13 -3
- package/android/src/main/java/com/apollohg/editor/InputSnapshotSupport.kt +6 -0
- package/android/src/main/java/com/apollohg/editor/NativeBlockEditorSurface.kt +1086 -0
- package/android/src/main/java/com/apollohg/editor/NativeEditorExpoView.kt +1 -1
- package/android/src/main/java/com/apollohg/editor/NativeEditorModule.kt +26 -50
- package/android/src/main/java/com/apollohg/editor/NativeImageLoader.kt +89 -0
- package/android/src/main/java/com/apollohg/editor/RemoteSelectionOverlayView.kt +31 -66
- package/android/src/main/java/com/apollohg/editor/RichTextEditorView.kt +98 -33
- package/dist/EditorToolbar.d.ts +1 -1
- package/dist/EditorToolbar.js +19 -0
- package/dist/NativeEditorBridge.d.ts +62 -35
- package/dist/NativeEditorBridge.js +122 -79
- package/dist/NativeProseViewer.d.ts +13 -34
- package/dist/NativeProseViewer.js +40 -465
- package/dist/NativeRichTextEditor.d.ts +10 -1
- package/dist/NativeRichTextEditor.js +72 -34
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -1
- package/dist/schemas.d.ts +11 -0
- package/dist/schemas.js +149 -0
- package/dist/useNativeEditor.d.ts +3 -3
- package/dist/useNativeEditor.js +3 -3
- 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/Generated_editor_core.swift +97 -2
- package/ios/NativeBlockEditorSurface.swift +1393 -0
- package/ios/NativeEditorExpoView.swift +3 -3
- package/ios/NativeEditorModule.swift +30 -45
- package/ios/NativeInputSupport.swift +396 -0
- package/ios/PositionBridge.swift +4 -4
- package/ios/RichTextEditorView.swift +333 -786
- package/ios/editor_coreFFI/editor_coreFFI.h +147 -70
- 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_64/libeditor_core.so +0 -0
- package/rust/bindings/kotlin/uniffi/editor_core/editor_core.kt +2087 -1399
- package/android/src/androidTest/AndroidManifest.xml +0 -8
- package/android/src/androidTest/java/com/apollohg/editor/MeasureHeightInstrumentedTest.kt +0 -53
- package/android/src/androidTest/java/com/apollohg/editor/NativeDeviceCollaborationInitialSyncTest.kt +0 -241
- package/android/src/androidTest/java/com/apollohg/editor/NativeDeviceImeRegressionTest.kt +0 -338
- package/android/src/androidTest/java/com/apollohg/editor/NativeDeviceOutsideTapTest.kt +0 -789
- package/android/src/androidTest/java/com/apollohg/editor/NativeDevicePerformanceTest.kt +0 -350
- package/android/src/androidTest/java/com/apollohg/editor/NativeEditorOutsideTapActivity.kt +0 -5
- package/android/src/main/java/com/apollohg/editor/NativeProseViewerExpoView.kt +0 -315
- package/android/src/main/java/com/apollohg/editor/RenderBridge.kt +0 -2172
- package/android/src/sharedTest/java/com/apollohg/editor/NativePerformanceSupport.kt +0 -482
- package/android/src/test/java/com/apollohg/editor/CaretGeometryTest.kt +0 -137
- package/android/src/test/java/com/apollohg/editor/EditorEditTextHardwareKeyTest.kt +0 -321
- package/android/src/test/java/com/apollohg/editor/EditorInputConnectionTest.kt +0 -4202
- package/android/src/test/java/com/apollohg/editor/NativeEditorExpoViewTest.kt +0 -2581
- package/android/src/test/java/com/apollohg/editor/NativeEditorModuleTest.kt +0 -27
- package/android/src/test/java/com/apollohg/editor/NativePerformanceTest.kt +0 -197
- package/android/src/test/java/com/apollohg/editor/NativeProseViewerExpoViewTest.kt +0 -84
- package/android/src/test/java/com/apollohg/editor/NativeToolbarTest.kt +0 -544
- package/android/src/test/java/com/apollohg/editor/PositionBridgeTest.kt +0 -461
- package/android/src/test/java/com/apollohg/editor/RenderBridgeTest.kt +0 -2054
- package/android/src/test/java/com/apollohg/editor/RichTextEditorViewTest.kt +0 -1367
- package/ios/EditorLayoutManager.swift +0 -774
- package/ios/NativeProseViewerExpoView.swift +0 -276
- package/ios/RenderBridge.swift +0 -1708
- package/ios/Tests/NativePerformanceTests.swift +0 -687
- package/ios/Tests/PositionBridgeTests.swift +0 -706
- package/ios/Tests/RenderBridgeTests.swift +0 -2236
- package/ios/Tests/RichTextEditorViewTests.swift +0 -6548
|
@@ -126,7 +126,7 @@ private final class RemoteSelectionOverlayView: UIView {
|
|
|
126
126
|
let color: UIColor
|
|
127
127
|
}
|
|
128
128
|
|
|
129
|
-
weak var
|
|
129
|
+
weak var documentSurface: NativeBlockEditorSurface?
|
|
130
130
|
private var editorId: UInt64 = 0
|
|
131
131
|
private var selections: [RemoteSelectionDecoration] = []
|
|
132
132
|
private var selectionViews: [UIView] = []
|
|
@@ -143,8 +143,8 @@ private final class RemoteSelectionOverlayView: UIView {
|
|
|
143
143
|
return nil
|
|
144
144
|
}
|
|
145
145
|
|
|
146
|
-
func bind(
|
|
147
|
-
self.
|
|
146
|
+
func bind(documentSurface: NativeBlockEditorSurface) {
|
|
147
|
+
self.documentSurface = documentSurface
|
|
148
148
|
}
|
|
149
149
|
|
|
150
150
|
func update(selections: [RemoteSelectionDecoration], editorId: UInt64) {
|
|
@@ -155,7 +155,7 @@ private final class RemoteSelectionOverlayView: UIView {
|
|
|
155
155
|
|
|
156
156
|
func refresh() {
|
|
157
157
|
guard editorId != 0,
|
|
158
|
-
let
|
|
158
|
+
let documentSurface
|
|
159
159
|
else {
|
|
160
160
|
syncSelectionViews(with: [])
|
|
161
161
|
syncCaretViews(with: [])
|
|
@@ -166,7 +166,7 @@ private final class RemoteSelectionOverlayView: UIView {
|
|
|
166
166
|
var caretRects: [ColoredRect] = []
|
|
167
167
|
|
|
168
168
|
for selection in selections {
|
|
169
|
-
let geometry = geometry(for: selection, in:
|
|
169
|
+
let geometry = geometry(for: selection, in: documentSurface)
|
|
170
170
|
for rect in geometry.selectionRects {
|
|
171
171
|
selectionRects.append(
|
|
172
172
|
ColoredRect(
|
|
@@ -209,76 +209,18 @@ private final class RemoteSelectionOverlayView: UIView {
|
|
|
209
209
|
|
|
210
210
|
private func geometry(
|
|
211
211
|
for selection: RemoteSelectionDecoration,
|
|
212
|
-
in
|
|
212
|
+
in documentSurface: NativeBlockEditorSurface
|
|
213
213
|
) -> (selectionRects: [CGRect], caretRect: CGRect?) {
|
|
214
|
-
let
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
)
|
|
222
|
-
|
|
223
|
-
let startPosition = PositionBridge.scalarToTextView(startScalar, in: textView)
|
|
224
|
-
let endPosition = PositionBridge.scalarToTextView(endScalar, in: textView)
|
|
225
|
-
let caretRect = resolvedCaretRect(
|
|
226
|
-
for: endPosition,
|
|
227
|
-
in: textView
|
|
228
|
-
)
|
|
229
|
-
|
|
230
|
-
if startScalar == endScalar {
|
|
231
|
-
return ([], caretRect)
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
guard let range = textView.textRange(from: startPosition, to: endPosition) else {
|
|
235
|
-
return ([], caretRect)
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
let selectionRects = textView.selectionRects(for: range)
|
|
239
|
-
.map(\.rect)
|
|
240
|
-
.filter { !$0.isEmpty && $0.width > 0 && $0.height > 0 }
|
|
241
|
-
.map { textView.convert($0, to: self) }
|
|
242
|
-
|
|
214
|
+
let selectionRects = documentSurface
|
|
215
|
+
.selectionRects(anchor: selection.anchor, head: selection.head)
|
|
216
|
+
.map { documentSurface.convert($0, to: self) }
|
|
217
|
+
let caretRect = selection.isFocused
|
|
218
|
+
? documentSurface.caretRect(forDocPosition: selection.head)
|
|
219
|
+
.map { documentSurface.convert($0, to: self) }
|
|
220
|
+
: nil
|
|
243
221
|
return (selectionRects, caretRect)
|
|
244
222
|
}
|
|
245
223
|
|
|
246
|
-
private func resolvedCaretRect(
|
|
247
|
-
for position: UITextPosition,
|
|
248
|
-
in textView: EditorTextView
|
|
249
|
-
) -> CGRect? {
|
|
250
|
-
let directRect = textView.convert(textView.caretRect(for: position), to: self)
|
|
251
|
-
if directRect.height > 0, directRect.width >= 0 {
|
|
252
|
-
return directRect
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
if let previousPosition = textView.position(from: position, offset: -1),
|
|
256
|
-
let previousRange = textView.textRange(from: previousPosition, to: position),
|
|
257
|
-
let previousRect = textView.selectionRects(for: previousRange)
|
|
258
|
-
.map(\.rect)
|
|
259
|
-
.last(where: { !$0.isEmpty && $0.height > 0 })
|
|
260
|
-
{
|
|
261
|
-
let rect = textView.convert(previousRect, to: self)
|
|
262
|
-
return CGRect(x: rect.maxX, y: rect.minY, width: 2, height: rect.height)
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
if let nextPosition = textView.position(from: position, offset: 1),
|
|
266
|
-
let nextRange = textView.textRange(from: position, to: nextPosition),
|
|
267
|
-
let nextRect = textView.selectionRects(for: nextRange)
|
|
268
|
-
.map(\.rect)
|
|
269
|
-
.first(where: { !$0.isEmpty && $0.height > 0 })
|
|
270
|
-
{
|
|
271
|
-
let rect = textView.convert(nextRect, to: self)
|
|
272
|
-
return CGRect(x: rect.minX, y: rect.minY, width: 2, height: rect.height)
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
if directRect.isEmpty {
|
|
276
|
-
return nil
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
return directRect
|
|
280
|
-
}
|
|
281
|
-
|
|
282
224
|
private func syncSelectionViews(with rects: [ColoredRect]) {
|
|
283
225
|
syncViews(rects, existingViews: &selectionViews) { view, rect in
|
|
284
226
|
view.frame = rect.frame
|
|
@@ -347,15 +289,15 @@ private final class ImageTapOverlayView: UIView {
|
|
|
347
289
|
|
|
348
290
|
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
|
|
349
291
|
guard let editorView else { return false }
|
|
350
|
-
|
|
351
|
-
return editorView.textView.hasImageAttachment(at: pointInTextView)
|
|
292
|
+
return editorView.blockSurfaceHasImage(at: convert(point, to: editorView))
|
|
352
293
|
}
|
|
353
294
|
|
|
354
295
|
@objc
|
|
355
296
|
private func handleTap(_ recognizer: UITapGestureRecognizer) {
|
|
356
297
|
guard recognizer.state == .ended, let editorView else { return }
|
|
357
|
-
|
|
358
|
-
|
|
298
|
+
_ = editorView.selectBlockSurfaceImage(
|
|
299
|
+
at: convert(recognizer.location(in: self), to: editorView)
|
|
300
|
+
)
|
|
359
301
|
}
|
|
360
302
|
|
|
361
303
|
func interceptsPointForTesting(_ point: CGPoint) -> Bool {
|
|
@@ -365,8 +307,7 @@ private final class ImageTapOverlayView: UIView {
|
|
|
365
307
|
@discardableResult
|
|
366
308
|
func handleTapForTesting(_ point: CGPoint) -> Bool {
|
|
367
309
|
guard let editorView else { return false }
|
|
368
|
-
|
|
369
|
-
return editorView.textView.selectImageAttachment(at: pointInTextView)
|
|
310
|
+
return editorView.selectBlockSurfaceImage(at: convert(point, to: editorView))
|
|
370
311
|
}
|
|
371
312
|
}
|
|
372
313
|
|
|
@@ -784,11 +725,11 @@ final class EditorTextViewInternalDelegate: NSObject, UITextViewDelegate {
|
|
|
784
725
|
/// Instead of letting UITextView's internal text storage handle insertions
|
|
785
726
|
/// and deletions, this class captures the user's intent (typing, deleting,
|
|
786
727
|
/// pasting, autocorrect) and sends it to the Rust editor. The Rust editor
|
|
787
|
-
/// returns
|
|
788
|
-
///
|
|
728
|
+
/// returns a canonical plain-text input snapshot, which is authorized and
|
|
729
|
+
/// applied back to this hidden input adapter.
|
|
789
730
|
///
|
|
790
731
|
/// This is the "input interception" pattern: the UITextView is effectively
|
|
791
|
-
///
|
|
732
|
+
/// an IME/selection surface, not the visible block renderer.
|
|
792
733
|
///
|
|
793
734
|
/// ## Composition (IME) Handling
|
|
794
735
|
///
|
|
@@ -848,25 +789,6 @@ final class EditorTextView: UITextView, UIGestureRecognizerDelegate {
|
|
|
848
789
|
let postApplySelectionOrContentCallbackNanos: UInt64
|
|
849
790
|
}
|
|
850
791
|
|
|
851
|
-
private struct PatchApplyTrace {
|
|
852
|
-
let applied: Bool
|
|
853
|
-
let eligibilityNanos: UInt64
|
|
854
|
-
let trimNanos: UInt64
|
|
855
|
-
let metadataNanos: UInt64
|
|
856
|
-
let buildRenderNanos: UInt64
|
|
857
|
-
let applyRenderNanos: UInt64
|
|
858
|
-
let applyRenderReplaceUtf16Length: Int
|
|
859
|
-
let applyRenderReplacementUtf16Length: Int
|
|
860
|
-
let applyRenderTextMutationNanos: UInt64
|
|
861
|
-
let applyRenderBeginEditingNanos: UInt64
|
|
862
|
-
let applyRenderEndEditingNanos: UInt64
|
|
863
|
-
let applyRenderStringMutationNanos: UInt64
|
|
864
|
-
let applyRenderAttributeMutationNanos: UInt64
|
|
865
|
-
let applyRenderAuthorizedTextNanos: UInt64
|
|
866
|
-
let applyRenderCacheInvalidationNanos: UInt64
|
|
867
|
-
let usedSmallPatchTextMutation: Bool
|
|
868
|
-
}
|
|
869
|
-
|
|
870
792
|
private struct ApplyRenderTrace {
|
|
871
793
|
let totalNanos: UInt64
|
|
872
794
|
let replaceUtf16Length: Int
|
|
@@ -901,17 +823,6 @@ final class EditorTextView: UITextView, UIGestureRecognizerDelegate {
|
|
|
901
823
|
let selectionOrContentCallbackNanos: UInt64
|
|
902
824
|
}
|
|
903
825
|
|
|
904
|
-
private struct TopLevelChildMetadata {
|
|
905
|
-
var startOffset: Int
|
|
906
|
-
var containsAttachment: Bool
|
|
907
|
-
var containsPositionAdjustments: Bool
|
|
908
|
-
}
|
|
909
|
-
|
|
910
|
-
private struct TopLevelChildMetadataSlice {
|
|
911
|
-
let startIndex: Int
|
|
912
|
-
let entries: [TopLevelChildMetadata]
|
|
913
|
-
}
|
|
914
|
-
|
|
915
826
|
private struct NativeTextMutation {
|
|
916
827
|
let from: UInt32
|
|
917
828
|
let to: UInt32
|
|
@@ -935,13 +846,6 @@ final class EditorTextView: UITextView, UIGestureRecognizerDelegate {
|
|
|
935
846
|
case rejected
|
|
936
847
|
}
|
|
937
848
|
|
|
938
|
-
private enum PositionCacheUpdate {
|
|
939
|
-
case scan
|
|
940
|
-
case invalidate
|
|
941
|
-
case plainText
|
|
942
|
-
case attributed
|
|
943
|
-
}
|
|
944
|
-
|
|
945
849
|
// MARK: - Properties
|
|
946
850
|
|
|
947
851
|
/// The Rust editor instance ID (from editor_create / editor_create_with_max_length).
|
|
@@ -1027,6 +931,7 @@ final class EditorTextView: UITextView, UIGestureRecognizerDelegate {
|
|
|
1027
931
|
|
|
1028
932
|
/// Delegate for editor events.
|
|
1029
933
|
weak var editorDelegate: EditorTextViewDelegate?
|
|
934
|
+
var onUpdateJSONApplied: ((String) -> Void)?
|
|
1030
935
|
|
|
1031
936
|
/// The plain text from the last Rust render, used by the reconciliation
|
|
1032
937
|
/// fallback to detect unauthorized text storage mutations.
|
|
@@ -1038,8 +943,7 @@ final class EditorTextView: UITextView, UIGestureRecognizerDelegate {
|
|
|
1038
943
|
private(set) var lastRenderAppliedPatchForTesting: Bool = false
|
|
1039
944
|
var captureApplyUpdateTraceForTesting = false
|
|
1040
945
|
private(set) var lastApplyUpdateTraceForTesting: ApplyUpdateTrace?
|
|
1041
|
-
|
|
1042
|
-
private var currentTopLevelChildMetadata: [TopLevelChildMetadata]? = nil
|
|
946
|
+
var pasteboardDataForTesting: [String: Data]?
|
|
1043
947
|
private var renderAppearanceRevision: UInt64 = 1
|
|
1044
948
|
private var lastAppliedRenderAppearanceRevision: UInt64 = 0
|
|
1045
949
|
|
|
@@ -1110,7 +1014,7 @@ final class EditorTextView: UITextView, UIGestureRecognizerDelegate {
|
|
|
1110
1014
|
private var markedTextCompositionText: String?
|
|
1111
1015
|
private var markedTextCompositionIsExplicitlyEmpty = false
|
|
1112
1016
|
|
|
1113
|
-
private let editorLayoutManager:
|
|
1017
|
+
private let editorLayoutManager: NSLayoutManager
|
|
1114
1018
|
|
|
1115
1019
|
private struct InputTraitState {
|
|
1116
1020
|
var autoCapitalize: String?
|
|
@@ -1156,7 +1060,7 @@ final class EditorTextView: UITextView, UIGestureRecognizerDelegate {
|
|
|
1156
1060
|
// MARK: - Initialization
|
|
1157
1061
|
|
|
1158
1062
|
override init(frame: CGRect, textContainer: NSTextContainer?) {
|
|
1159
|
-
let layoutManager =
|
|
1063
|
+
let layoutManager = NSLayoutManager()
|
|
1160
1064
|
let container = textContainer ?? NSTextContainer(size: .zero)
|
|
1161
1065
|
let textStorage = NSTextStorage()
|
|
1162
1066
|
layoutManager.addTextContainer(container)
|
|
@@ -1755,7 +1659,7 @@ final class EditorTextView: UITextView, UIGestureRecognizerDelegate {
|
|
|
1755
1659
|
guard value is NSTextAttachment, range.length > 0 else { return }
|
|
1756
1660
|
|
|
1757
1661
|
let attrs = textStorage.attributes(at: range.location, effectiveRange: nil)
|
|
1758
|
-
guard (attrs[
|
|
1662
|
+
guard (attrs[InputCoordinatorAttributes.voidNodeType] as? String) == "image" else { return }
|
|
1759
1663
|
|
|
1760
1664
|
let glyphRange = layoutManager.glyphRange(
|
|
1761
1665
|
forCharacterRange: range,
|
|
@@ -1801,15 +1705,15 @@ final class EditorTextView: UITextView, UIGestureRecognizerDelegate {
|
|
|
1801
1705
|
}
|
|
1802
1706
|
|
|
1803
1707
|
func blockquoteStripeRectsForTesting() -> [CGRect] {
|
|
1804
|
-
|
|
1708
|
+
[]
|
|
1805
1709
|
}
|
|
1806
1710
|
|
|
1807
1711
|
func resetBlockquoteStripeDrawPassesForTesting() {
|
|
1808
|
-
|
|
1712
|
+
// Blockquote geometry belongs to the structural surface.
|
|
1809
1713
|
}
|
|
1810
1714
|
|
|
1811
1715
|
func blockquoteStripeDrawPassesForTesting() -> [[CGRect]] {
|
|
1812
|
-
|
|
1716
|
+
[]
|
|
1813
1717
|
}
|
|
1814
1718
|
|
|
1815
1719
|
@discardableResult
|
|
@@ -1818,16 +1722,7 @@ final class EditorTextView: UITextView, UIGestureRecognizerDelegate {
|
|
|
1818
1722
|
}
|
|
1819
1723
|
|
|
1820
1724
|
private func taskListMarkerParagraphStart(at location: CGPoint) -> Int? {
|
|
1821
|
-
|
|
1822
|
-
let origin = CGPoint(
|
|
1823
|
-
x: textContainerInset.left - contentOffset.x,
|
|
1824
|
-
y: textContainerInset.top - contentOffset.y
|
|
1825
|
-
)
|
|
1826
|
-
return layoutManager.taskListMarkerParagraphStart(
|
|
1827
|
-
at: location,
|
|
1828
|
-
in: textStorage,
|
|
1829
|
-
textContainerOrigin: origin
|
|
1830
|
-
)
|
|
1725
|
+
nil
|
|
1831
1726
|
}
|
|
1832
1727
|
|
|
1833
1728
|
func imageSelectionTapWouldHandleForTesting(at location: CGPoint) -> Bool {
|
|
@@ -2234,7 +2129,7 @@ final class EditorTextView: UITextView, UIGestureRecognizerDelegate {
|
|
|
2234
2129
|
}
|
|
2235
2130
|
let attrs = textStorage.attributes(at: cursorUtf16Offset, effectiveRange: nil)
|
|
2236
2131
|
guard attrs[.attachment] is NSTextAttachment,
|
|
2237
|
-
attrs[
|
|
2132
|
+
attrs[InputCoordinatorAttributes.voidNodeType] as? String != nil,
|
|
2238
2133
|
cursorScalar < UInt32.max
|
|
2239
2134
|
else {
|
|
2240
2135
|
return nil
|
|
@@ -2280,7 +2175,7 @@ final class EditorTextView: UITextView, UIGestureRecognizerDelegate {
|
|
|
2280
2175
|
}
|
|
2281
2176
|
let attrs = textStorage.attributes(at: utf16Offset, effectiveRange: nil)
|
|
2282
2177
|
guard attrs[.attachment] is NSTextAttachment,
|
|
2283
|
-
attrs[
|
|
2178
|
+
attrs[InputCoordinatorAttributes.voidNodeType] as? String != nil
|
|
2284
2179
|
else {
|
|
2285
2180
|
return nil
|
|
2286
2181
|
}
|
|
@@ -2643,9 +2538,15 @@ final class EditorTextView: UITextView, UIGestureRecognizerDelegate {
|
|
|
2643
2538
|
)
|
|
2644
2539
|
|
|
2645
2540
|
let pasteboard = UIPasteboard.general
|
|
2541
|
+
let pasteboardData: (String) -> Data? = { [pasteboardDataForTesting] type in
|
|
2542
|
+
if let pasteboardDataForTesting {
|
|
2543
|
+
return pasteboardDataForTesting[type]
|
|
2544
|
+
}
|
|
2545
|
+
return pasteboard.data(forPasteboardType: type)
|
|
2546
|
+
}
|
|
2646
2547
|
|
|
2647
2548
|
// Try HTML first for rich paste.
|
|
2648
|
-
if let htmlData =
|
|
2549
|
+
if let htmlData = pasteboardData("public.html"),
|
|
2649
2550
|
let html = String(data: htmlData, encoding: .utf8) {
|
|
2650
2551
|
performInterceptedInput {
|
|
2651
2552
|
pasteHTML(html)
|
|
@@ -2654,7 +2555,7 @@ final class EditorTextView: UITextView, UIGestureRecognizerDelegate {
|
|
|
2654
2555
|
}
|
|
2655
2556
|
|
|
2656
2557
|
// Try attributed string (e.g. from Notes, Pages).
|
|
2657
|
-
if let rtfData =
|
|
2558
|
+
if let rtfData = pasteboardData("public.rtf") {
|
|
2658
2559
|
if let attrStr = try? NSAttributedString(
|
|
2659
2560
|
data: rtfData,
|
|
2660
2561
|
options: [.documentType: NSAttributedString.DocumentType.rtf],
|
|
@@ -2735,7 +2636,7 @@ final class EditorTextView: UITextView, UIGestureRecognizerDelegate {
|
|
|
2735
2636
|
}
|
|
2736
2637
|
|
|
2737
2638
|
let attrs = textStorage.attributes(at: characterRange.location, effectiveRange: nil)
|
|
2738
|
-
guard (attrs[
|
|
2639
|
+
guard (attrs[InputCoordinatorAttributes.voidNodeType] as? String) == "image",
|
|
2739
2640
|
let start = position(from: beginningOfDocument, offset: characterRange.location),
|
|
2740
2641
|
let end = position(from: start, offset: characterRange.length)
|
|
2741
2642
|
else {
|
|
@@ -2920,6 +2821,7 @@ final class EditorTextView: UITextView, UIGestureRecognizerDelegate {
|
|
|
2920
2821
|
editorSetSelectionScalar(id: editorId, scalarAnchor: anchor, scalarHead: head)
|
|
2921
2822
|
recordAuthorizedSelectionIfPossible()
|
|
2922
2823
|
refreshTypingAttributesForSelection()
|
|
2824
|
+
onSelectionOrContentMayChange?()
|
|
2923
2825
|
editorDelegate?.editorTextView(self, selectionDidChange: docAnchor, head: docHead)
|
|
2924
2826
|
}
|
|
2925
2827
|
|
|
@@ -3321,13 +3223,13 @@ final class EditorTextView: UITextView, UIGestureRecognizerDelegate {
|
|
|
3321
3223
|
|
|
3322
3224
|
let attrs = textStorage.attributes(at: utf16Offset, effectiveRange: nil)
|
|
3323
3225
|
return attrs[.attachment] is NSTextAttachment
|
|
3324
|
-
&& (attrs[
|
|
3226
|
+
&& (attrs[InputCoordinatorAttributes.voidNodeType] as? String) == "horizontalRule"
|
|
3325
3227
|
}
|
|
3326
3228
|
|
|
3327
3229
|
private func hardBreakBaselineY(after utf16Offset: Int) -> CGFloat? {
|
|
3328
3230
|
guard utf16Offset > 0, utf16Offset <= textStorage.length else { return nil }
|
|
3329
3231
|
let previousVoidType = textStorage.attribute(
|
|
3330
|
-
|
|
3232
|
+
InputCoordinatorAttributes.voidNodeType,
|
|
3331
3233
|
at: utf16Offset - 1,
|
|
3332
3234
|
effectiveRange: nil
|
|
3333
3235
|
) as? String
|
|
@@ -4102,14 +4004,14 @@ final class EditorTextView: UITextView, UIGestureRecognizerDelegate {
|
|
|
4102
4004
|
}
|
|
4103
4005
|
|
|
4104
4006
|
let attrs = textStorage.attributes(at: selectedRange.location, effectiveRange: nil)
|
|
4105
|
-
guard (attrs[
|
|
4007
|
+
guard (attrs[InputCoordinatorAttributes.voidNodeType] as? String) == "image",
|
|
4106
4008
|
attrs[.attachment] is NSTextAttachment
|
|
4107
4009
|
else {
|
|
4108
4010
|
return nil
|
|
4109
4011
|
}
|
|
4110
4012
|
|
|
4111
|
-
let docPos = (attrs[
|
|
4112
|
-
?? (attrs[
|
|
4013
|
+
let docPos = (attrs[InputCoordinatorAttributes.docPos] as? NSNumber)?.uint32Value
|
|
4014
|
+
?? (attrs[InputCoordinatorAttributes.docPos] as? UInt32)
|
|
4113
4015
|
guard let docPos else { return nil }
|
|
4114
4016
|
return (docPos, selectedRange.location)
|
|
4115
4017
|
}
|
|
@@ -4140,9 +4042,9 @@ final class EditorTextView: UITextView, UIGestureRecognizerDelegate {
|
|
|
4140
4042
|
) { value, range, stop in
|
|
4141
4043
|
guard let attachment = value as? BlockImageAttachment, range.length > 0 else { return }
|
|
4142
4044
|
let attrs = textStorage.attributes(at: range.location, effectiveRange: nil)
|
|
4143
|
-
guard (attrs[
|
|
4144
|
-
let attributeDocPos = (attrs[
|
|
4145
|
-
?? (attrs[
|
|
4045
|
+
guard (attrs[InputCoordinatorAttributes.voidNodeType] as? String) == "image" else { return }
|
|
4046
|
+
let attributeDocPos = (attrs[InputCoordinatorAttributes.docPos] as? NSNumber)?.uint32Value
|
|
4047
|
+
?? (attrs[InputCoordinatorAttributes.docPos] as? UInt32)
|
|
4146
4048
|
guard attributeDocPos == docPos else { return }
|
|
4147
4049
|
resolved = (range, attachment)
|
|
4148
4050
|
stop.pointee = true
|
|
@@ -4264,270 +4166,10 @@ final class EditorTextView: UITextView, UIGestureRecognizerDelegate {
|
|
|
4264
4166
|
|
|
4265
4167
|
// MARK: - Applying Rust State
|
|
4266
4168
|
|
|
4267
|
-
private struct ParsedRenderPatch {
|
|
4268
|
-
let startIndex: Int
|
|
4269
|
-
let deleteCount: Int
|
|
4270
|
-
let renderBlocks: [[[String: Any]]]
|
|
4271
|
-
}
|
|
4272
|
-
|
|
4273
|
-
private enum DerivedRenderPatch {
|
|
4274
|
-
case unchanged
|
|
4275
|
-
case patch(ParsedRenderPatch)
|
|
4276
|
-
}
|
|
4277
|
-
|
|
4278
|
-
private func parseRenderBlocks(_ value: Any?) -> [[[String: Any]]]? {
|
|
4279
|
-
value as? [[[String: Any]]]
|
|
4280
|
-
}
|
|
4281
|
-
|
|
4282
|
-
private func parseRenderPatch(_ value: Any?) -> ParsedRenderPatch? {
|
|
4283
|
-
guard let raw = value as? [String: Any],
|
|
4284
|
-
let startIndex = RenderBridge.jsonInt(raw["startIndex"]),
|
|
4285
|
-
let deleteCount = RenderBridge.jsonInt(raw["deleteCount"]),
|
|
4286
|
-
let renderBlocks = parseRenderBlocks(raw["renderBlocks"])
|
|
4287
|
-
else {
|
|
4288
|
-
return nil
|
|
4289
|
-
}
|
|
4290
|
-
|
|
4291
|
-
return ParsedRenderPatch(
|
|
4292
|
-
startIndex: startIndex,
|
|
4293
|
-
deleteCount: deleteCount,
|
|
4294
|
-
renderBlocks: renderBlocks
|
|
4295
|
-
)
|
|
4296
|
-
}
|
|
4297
|
-
|
|
4298
|
-
private func mergeRenderBlocks(
|
|
4299
|
-
applying patch: ParsedRenderPatch,
|
|
4300
|
-
to current: [[[String: Any]]]
|
|
4301
|
-
) -> [[[String: Any]]]? {
|
|
4302
|
-
guard patch.startIndex >= 0,
|
|
4303
|
-
patch.deleteCount >= 0,
|
|
4304
|
-
patch.startIndex <= current.count,
|
|
4305
|
-
patch.startIndex + patch.deleteCount <= current.count
|
|
4306
|
-
else {
|
|
4307
|
-
return nil
|
|
4308
|
-
}
|
|
4309
|
-
|
|
4310
|
-
var merged = current
|
|
4311
|
-
merged.replaceSubrange(
|
|
4312
|
-
patch.startIndex..<(patch.startIndex + patch.deleteCount),
|
|
4313
|
-
with: patch.renderBlocks
|
|
4314
|
-
)
|
|
4315
|
-
return merged
|
|
4316
|
-
}
|
|
4317
|
-
|
|
4318
|
-
private func renderBlockEquals(
|
|
4319
|
-
_ lhs: [[String: Any]],
|
|
4320
|
-
_ rhs: [[String: Any]]
|
|
4321
|
-
) -> Bool {
|
|
4322
|
-
(lhs as NSArray).isEqual(rhs)
|
|
4323
|
-
}
|
|
4324
|
-
|
|
4325
|
-
private func deriveRenderPatch(
|
|
4326
|
-
from current: [[[String: Any]]],
|
|
4327
|
-
to updated: [[[String: Any]]]
|
|
4328
|
-
) -> DerivedRenderPatch {
|
|
4329
|
-
let sharedCount = min(current.count, updated.count)
|
|
4330
|
-
|
|
4331
|
-
var prefix = 0
|
|
4332
|
-
while prefix < sharedCount, renderBlockEquals(current[prefix], updated[prefix]) {
|
|
4333
|
-
prefix += 1
|
|
4334
|
-
}
|
|
4335
|
-
|
|
4336
|
-
if prefix == current.count, prefix == updated.count {
|
|
4337
|
-
return .unchanged
|
|
4338
|
-
}
|
|
4339
|
-
|
|
4340
|
-
var suffix = 0
|
|
4341
|
-
while suffix < (sharedCount - prefix),
|
|
4342
|
-
renderBlockEquals(
|
|
4343
|
-
current[current.count - suffix - 1],
|
|
4344
|
-
updated[updated.count - suffix - 1]
|
|
4345
|
-
)
|
|
4346
|
-
{
|
|
4347
|
-
suffix += 1
|
|
4348
|
-
}
|
|
4349
|
-
|
|
4350
|
-
let startIndex = prefix
|
|
4351
|
-
let deleteCount = current.count - prefix - suffix
|
|
4352
|
-
let endIndex = updated.count - suffix
|
|
4353
|
-
let replacementBlocks = Array(updated[startIndex..<endIndex])
|
|
4354
|
-
|
|
4355
|
-
return .patch(
|
|
4356
|
-
ParsedRenderPatch(
|
|
4357
|
-
startIndex: startIndex,
|
|
4358
|
-
deleteCount: deleteCount,
|
|
4359
|
-
renderBlocks: replacementBlocks
|
|
4360
|
-
)
|
|
4361
|
-
)
|
|
4362
|
-
}
|
|
4363
|
-
|
|
4364
|
-
private func topLevelChildIndex(from value: Any?) -> Int? {
|
|
4365
|
-
if let number = value as? NSNumber {
|
|
4366
|
-
return number.intValue
|
|
4367
|
-
}
|
|
4368
|
-
return value as? Int
|
|
4369
|
-
}
|
|
4370
|
-
|
|
4371
|
-
private func topLevelChildMetadataSlice(
|
|
4372
|
-
from attributedString: NSAttributedString
|
|
4373
|
-
) -> TopLevelChildMetadataSlice? {
|
|
4374
|
-
guard attributedString.length > 0 else {
|
|
4375
|
-
return TopLevelChildMetadataSlice(startIndex: 0, entries: [])
|
|
4376
|
-
}
|
|
4377
|
-
|
|
4378
|
-
var entriesByIndex: [Int: TopLevelChildMetadata] = [:]
|
|
4379
|
-
var orderedIndexes: [Int] = []
|
|
4380
|
-
|
|
4381
|
-
attributedString.enumerateAttributes(
|
|
4382
|
-
in: NSRange(location: 0, length: attributedString.length),
|
|
4383
|
-
options: []
|
|
4384
|
-
) { attrs, range, _ in
|
|
4385
|
-
guard let index = topLevelChildIndex(from: attrs[RenderBridgeAttributes.topLevelChildIndex]) else {
|
|
4386
|
-
return
|
|
4387
|
-
}
|
|
4388
|
-
if entriesByIndex[index] == nil {
|
|
4389
|
-
entriesByIndex[index] = TopLevelChildMetadata(
|
|
4390
|
-
startOffset: range.location,
|
|
4391
|
-
containsAttachment: false,
|
|
4392
|
-
containsPositionAdjustments: false
|
|
4393
|
-
)
|
|
4394
|
-
orderedIndexes.append(index)
|
|
4395
|
-
}
|
|
4396
|
-
if attrs[.attachment] != nil {
|
|
4397
|
-
entriesByIndex[index]?.containsAttachment = true
|
|
4398
|
-
}
|
|
4399
|
-
if attrs[RenderBridgeAttributes.syntheticPlaceholder] as? Bool == true
|
|
4400
|
-
|| attrs[RenderBridgeAttributes.listMarkerContext] != nil
|
|
4401
|
-
{
|
|
4402
|
-
entriesByIndex[index]?.containsPositionAdjustments = true
|
|
4403
|
-
}
|
|
4404
|
-
}
|
|
4405
|
-
|
|
4406
|
-
guard !orderedIndexes.isEmpty else { return nil }
|
|
4407
|
-
orderedIndexes.sort()
|
|
4408
|
-
guard let startIndex = orderedIndexes.first else { return nil }
|
|
4409
|
-
|
|
4410
|
-
var entries: [TopLevelChildMetadata] = []
|
|
4411
|
-
entries.reserveCapacity(orderedIndexes.count)
|
|
4412
|
-
for (offset, index) in orderedIndexes.enumerated() {
|
|
4413
|
-
guard index == startIndex + offset,
|
|
4414
|
-
let entry = entriesByIndex[index]
|
|
4415
|
-
else {
|
|
4416
|
-
return nil
|
|
4417
|
-
}
|
|
4418
|
-
entries.append(entry)
|
|
4419
|
-
}
|
|
4420
|
-
|
|
4421
|
-
return TopLevelChildMetadataSlice(startIndex: startIndex, entries: entries)
|
|
4422
|
-
}
|
|
4423
|
-
|
|
4424
|
-
private func refreshTopLevelChildMetadata(
|
|
4425
|
-
from attributedString: NSAttributedString
|
|
4426
|
-
) {
|
|
4427
|
-
guard let slice = topLevelChildMetadataSlice(from: attributedString),
|
|
4428
|
-
slice.startIndex == 0
|
|
4429
|
-
else {
|
|
4430
|
-
currentTopLevelChildMetadata = nil
|
|
4431
|
-
return
|
|
4432
|
-
}
|
|
4433
|
-
currentTopLevelChildMetadata = slice.entries
|
|
4434
|
-
}
|
|
4435
|
-
|
|
4436
|
-
private func applyTopLevelChildMetadataPatch(
|
|
4437
|
-
_ patch: ParsedRenderPatch,
|
|
4438
|
-
replaceRange: NSRange,
|
|
4439
|
-
renderedPatchMetadata: TopLevelChildMetadataSlice?,
|
|
4440
|
-
renderedPatchLength: Int
|
|
4441
|
-
) {
|
|
4442
|
-
guard var currentMetadata = currentTopLevelChildMetadata else {
|
|
4443
|
-
currentTopLevelChildMetadata = nil
|
|
4444
|
-
return
|
|
4445
|
-
}
|
|
4446
|
-
|
|
4447
|
-
let newEntries: [TopLevelChildMetadata]
|
|
4448
|
-
if let renderedPatchMetadata,
|
|
4449
|
-
renderedPatchMetadata.entries.isEmpty
|
|
4450
|
-
{
|
|
4451
|
-
newEntries = []
|
|
4452
|
-
} else if let renderedPatchMetadata,
|
|
4453
|
-
renderedPatchMetadata.startIndex == patch.startIndex
|
|
4454
|
-
{
|
|
4455
|
-
newEntries = renderedPatchMetadata.entries.map { entry in
|
|
4456
|
-
TopLevelChildMetadata(
|
|
4457
|
-
startOffset: replaceRange.location + entry.startOffset,
|
|
4458
|
-
containsAttachment: entry.containsAttachment,
|
|
4459
|
-
containsPositionAdjustments: entry.containsPositionAdjustments
|
|
4460
|
-
)
|
|
4461
|
-
}
|
|
4462
|
-
} else {
|
|
4463
|
-
currentTopLevelChildMetadata = nil
|
|
4464
|
-
return
|
|
4465
|
-
}
|
|
4466
|
-
|
|
4467
|
-
guard patch.startIndex >= 0,
|
|
4468
|
-
patch.deleteCount >= 0,
|
|
4469
|
-
patch.startIndex <= currentMetadata.count,
|
|
4470
|
-
patch.startIndex + patch.deleteCount <= currentMetadata.count
|
|
4471
|
-
else {
|
|
4472
|
-
currentTopLevelChildMetadata = nil
|
|
4473
|
-
return
|
|
4474
|
-
}
|
|
4475
|
-
|
|
4476
|
-
currentMetadata.replaceSubrange(
|
|
4477
|
-
patch.startIndex..<(patch.startIndex + patch.deleteCount),
|
|
4478
|
-
with: newEntries
|
|
4479
|
-
)
|
|
4480
|
-
|
|
4481
|
-
let delta = renderedPatchLength - replaceRange.length
|
|
4482
|
-
if delta != 0 {
|
|
4483
|
-
let shiftStart = patch.startIndex + newEntries.count
|
|
4484
|
-
for index in shiftStart..<currentMetadata.count {
|
|
4485
|
-
currentMetadata[index].startOffset += delta
|
|
4486
|
-
}
|
|
4487
|
-
}
|
|
4488
|
-
|
|
4489
|
-
currentTopLevelChildMetadata = currentMetadata
|
|
4490
|
-
}
|
|
4491
|
-
|
|
4492
|
-
private func hasTopLevelChildMetadata() -> Bool {
|
|
4493
|
-
currentTopLevelChildMetadata != nil
|
|
4494
|
-
}
|
|
4495
|
-
|
|
4496
|
-
private func firstCharacterOffset(forTopLevelChildIndex index: Int) -> Int? {
|
|
4497
|
-
guard let currentTopLevelChildMetadata,
|
|
4498
|
-
index >= 0,
|
|
4499
|
-
index < currentTopLevelChildMetadata.count
|
|
4500
|
-
else {
|
|
4501
|
-
return nil
|
|
4502
|
-
}
|
|
4503
|
-
return currentTopLevelChildMetadata[index].startOffset
|
|
4504
|
-
}
|
|
4505
|
-
|
|
4506
|
-
private func replacementRangeForRenderPatch(
|
|
4507
|
-
startIndex: Int,
|
|
4508
|
-
deleteCount: Int
|
|
4509
|
-
) -> NSRange? {
|
|
4510
|
-
let startLocation: Int
|
|
4511
|
-
if let resolvedStart = firstCharacterOffset(forTopLevelChildIndex: startIndex) {
|
|
4512
|
-
startLocation = resolvedStart
|
|
4513
|
-
} else if deleteCount == 0 {
|
|
4514
|
-
startLocation = textStorage.length
|
|
4515
|
-
} else {
|
|
4516
|
-
return nil
|
|
4517
|
-
}
|
|
4518
|
-
|
|
4519
|
-
let endIndexExclusive = startIndex + deleteCount
|
|
4520
|
-
let endLocation = firstCharacterOffset(forTopLevelChildIndex: endIndexExclusive)
|
|
4521
|
-
?? textStorage.length
|
|
4522
|
-
guard startLocation <= endLocation else { return nil }
|
|
4523
|
-
return NSRange(location: startLocation, length: endLocation - startLocation)
|
|
4524
|
-
}
|
|
4525
|
-
|
|
4526
4169
|
private func applyAttributedRender(
|
|
4527
4170
|
_ attrStr: NSAttributedString,
|
|
4528
4171
|
replaceRange: NSRange? = nil,
|
|
4529
4172
|
usedPatch: Bool,
|
|
4530
|
-
positionCacheUpdate: PositionCacheUpdate = .scan,
|
|
4531
4173
|
authorizedReplaceRange: NSRange? = nil,
|
|
4532
4174
|
authorizedReplacementText: String? = nil,
|
|
4533
4175
|
authorizedReplacementAttributedText: NSAttributedString? = nil
|
|
@@ -4604,68 +4246,7 @@ final class EditorTextView: UITextView, UIGestureRecognizerDelegate {
|
|
|
4604
4246
|
let authorizedTextNanos = DispatchTime.now().uptimeNanoseconds - authorizedTextStartedAt
|
|
4605
4247
|
let cacheInvalidationStartedAt = DispatchTime.now().uptimeNanoseconds
|
|
4606
4248
|
lastRenderAppliedPatchForTesting = usedPatch
|
|
4607
|
-
|
|
4608
|
-
case .plainText:
|
|
4609
|
-
guard let replaceRange else {
|
|
4610
|
-
PositionBridge.invalidateCache(for: self)
|
|
4611
|
-
break
|
|
4612
|
-
}
|
|
4613
|
-
let patchedPositionCache = PositionBridge.applyPlainTextPatchIfPossible(
|
|
4614
|
-
for: self,
|
|
4615
|
-
replaceRange: replaceRange,
|
|
4616
|
-
replacementText: attrStr.string
|
|
4617
|
-
)
|
|
4618
|
-
if !patchedPositionCache {
|
|
4619
|
-
PositionBridge.invalidateCache(for: self)
|
|
4620
|
-
}
|
|
4621
|
-
case .attributed:
|
|
4622
|
-
guard let replaceRange else {
|
|
4623
|
-
PositionBridge.invalidateCache(for: self)
|
|
4624
|
-
break
|
|
4625
|
-
}
|
|
4626
|
-
let patchedPositionCache = PositionBridge.applyAttributedPatchIfPossible(
|
|
4627
|
-
for: self,
|
|
4628
|
-
replaceRange: replaceRange,
|
|
4629
|
-
replacement: attrStr
|
|
4630
|
-
)
|
|
4631
|
-
if !patchedPositionCache {
|
|
4632
|
-
PositionBridge.invalidateCache(for: self)
|
|
4633
|
-
}
|
|
4634
|
-
case .invalidate:
|
|
4635
|
-
PositionBridge.invalidateCache(for: self)
|
|
4636
|
-
case .scan:
|
|
4637
|
-
let canPatchPositionCache = if let replaceRange {
|
|
4638
|
-
replaceRange.location >= 0
|
|
4639
|
-
&& !textStorageRangeContainsAttachment(replaceRange)
|
|
4640
|
-
&& !attributedStringContainsAttachment(attrStr)
|
|
4641
|
-
} else {
|
|
4642
|
-
false
|
|
4643
|
-
}
|
|
4644
|
-
if let replaceRange, canPatchPositionCache {
|
|
4645
|
-
let patchedPositionCache: Bool
|
|
4646
|
-
if !textStorageRangeContainsPositionAdjustments(replaceRange),
|
|
4647
|
-
!attributedStringContainsPositionAdjustments(attrStr)
|
|
4648
|
-
{
|
|
4649
|
-
patchedPositionCache = PositionBridge.applyPlainTextPatchIfPossible(
|
|
4650
|
-
for: self,
|
|
4651
|
-
replaceRange: replaceRange,
|
|
4652
|
-
replacementText: attrStr.string
|
|
4653
|
-
)
|
|
4654
|
-
} else {
|
|
4655
|
-
patchedPositionCache = PositionBridge.applyAttributedPatchIfPossible(
|
|
4656
|
-
for: self,
|
|
4657
|
-
replaceRange: replaceRange,
|
|
4658
|
-
replacement: attrStr
|
|
4659
|
-
)
|
|
4660
|
-
}
|
|
4661
|
-
|
|
4662
|
-
if !patchedPositionCache {
|
|
4663
|
-
PositionBridge.invalidateCache(for: self)
|
|
4664
|
-
}
|
|
4665
|
-
} else {
|
|
4666
|
-
PositionBridge.invalidateCache(for: self)
|
|
4667
|
-
}
|
|
4668
|
-
}
|
|
4249
|
+
PositionBridge.invalidateCache(for: self)
|
|
4669
4250
|
let cacheInvalidationNanos = DispatchTime.now().uptimeNanoseconds - cacheInvalidationStartedAt
|
|
4670
4251
|
isApplyingRustState = false
|
|
4671
4252
|
return ApplyRenderTrace(
|
|
@@ -4697,8 +4278,8 @@ final class EditorTextView: UITextView, UIGestureRecognizerDelegate {
|
|
|
4697
4278
|
_ lhs: [NSAttributedString.Key: Any],
|
|
4698
4279
|
_ rhs: [NSAttributedString.Key: Any]
|
|
4699
4280
|
) -> Bool {
|
|
4700
|
-
if let lhsValue = lhs[
|
|
4701
|
-
let rhsValue = rhs[
|
|
4281
|
+
if let lhsValue = lhs[InputCoordinatorAttributes.topLevelChildIndex] as? NSNumber,
|
|
4282
|
+
let rhsValue = rhs[InputCoordinatorAttributes.topLevelChildIndex] as? NSNumber,
|
|
4702
4283
|
lhsValue == rhsValue
|
|
4703
4284
|
{
|
|
4704
4285
|
return NSDictionary(dictionary: lhs).isEqual(to: rhs)
|
|
@@ -4706,8 +4287,8 @@ final class EditorTextView: UITextView, UIGestureRecognizerDelegate {
|
|
|
4706
4287
|
|
|
4707
4288
|
var lhsComparable = lhs
|
|
4708
4289
|
var rhsComparable = rhs
|
|
4709
|
-
lhsComparable.removeValue(forKey:
|
|
4710
|
-
rhsComparable.removeValue(forKey:
|
|
4290
|
+
lhsComparable.removeValue(forKey: InputCoordinatorAttributes.topLevelChildIndex)
|
|
4291
|
+
rhsComparable.removeValue(forKey: InputCoordinatorAttributes.topLevelChildIndex)
|
|
4711
4292
|
return NSDictionary(dictionary: lhsComparable).isEqual(to: rhsComparable)
|
|
4712
4293
|
}
|
|
4713
4294
|
|
|
@@ -4767,8 +4348,8 @@ final class EditorTextView: UITextView, UIGestureRecognizerDelegate {
|
|
|
4767
4348
|
in: NSRange(location: 0, length: attributedString.length),
|
|
4768
4349
|
options: [.longestEffectiveRangeNotRequired]
|
|
4769
4350
|
) { attrs, _, stop in
|
|
4770
|
-
if attrs[
|
|
4771
|
-
|| attrs[
|
|
4351
|
+
if attrs[InputCoordinatorAttributes.syntheticPlaceholder] as? Bool == true
|
|
4352
|
+
|| attrs[InputCoordinatorAttributes.listMarkerContext] != nil
|
|
4772
4353
|
{
|
|
4773
4354
|
hasAdjustments = true
|
|
4774
4355
|
stop.pointee = true
|
|
@@ -4781,7 +4362,7 @@ final class EditorTextView: UITextView, UIGestureRecognizerDelegate {
|
|
|
4781
4362
|
guard attributedString.length > 0 else { return false }
|
|
4782
4363
|
var hasListMarkerContext = false
|
|
4783
4364
|
attributedString.enumerateAttribute(
|
|
4784
|
-
|
|
4365
|
+
InputCoordinatorAttributes.listMarkerContext,
|
|
4785
4366
|
in: NSRange(location: 0, length: attributedString.length),
|
|
4786
4367
|
options: [.longestEffectiveRangeNotRequired]
|
|
4787
4368
|
) { value, _, stop in
|
|
@@ -4806,8 +4387,8 @@ final class EditorTextView: UITextView, UIGestureRecognizerDelegate {
|
|
|
4806
4387
|
in: range,
|
|
4807
4388
|
options: [.longestEffectiveRangeNotRequired]
|
|
4808
4389
|
) { attrs, _, stop in
|
|
4809
|
-
if attrs[
|
|
4810
|
-
|| attrs[
|
|
4390
|
+
if attrs[InputCoordinatorAttributes.syntheticPlaceholder] as? Bool == true
|
|
4391
|
+
|| attrs[InputCoordinatorAttributes.listMarkerContext] != nil
|
|
4811
4392
|
{
|
|
4812
4393
|
hasAdjustments = true
|
|
4813
4394
|
stop.pointee = true
|
|
@@ -4826,7 +4407,7 @@ final class EditorTextView: UITextView, UIGestureRecognizerDelegate {
|
|
|
4826
4407
|
|
|
4827
4408
|
var hasListMarkerContext = false
|
|
4828
4409
|
textStorage.enumerateAttribute(
|
|
4829
|
-
|
|
4410
|
+
InputCoordinatorAttributes.listMarkerContext,
|
|
4830
4411
|
in: range,
|
|
4831
4412
|
options: [.longestEffectiveRangeNotRequired]
|
|
4832
4413
|
) { value, _, stop in
|
|
@@ -4860,36 +4441,6 @@ final class EditorTextView: UITextView, UIGestureRecognizerDelegate {
|
|
|
4860
4441
|
return hasAttachment
|
|
4861
4442
|
}
|
|
4862
4443
|
|
|
4863
|
-
private func topLevelChildrenContainAttachment(
|
|
4864
|
-
startIndex: Int,
|
|
4865
|
-
deleteCount: Int
|
|
4866
|
-
) -> Bool {
|
|
4867
|
-
guard deleteCount > 0,
|
|
4868
|
-
let currentTopLevelChildMetadata,
|
|
4869
|
-
startIndex >= 0,
|
|
4870
|
-
startIndex + deleteCount <= currentTopLevelChildMetadata.count
|
|
4871
|
-
else {
|
|
4872
|
-
return false
|
|
4873
|
-
}
|
|
4874
|
-
return currentTopLevelChildMetadata[startIndex..<(startIndex + deleteCount)]
|
|
4875
|
-
.contains(where: \.containsAttachment)
|
|
4876
|
-
}
|
|
4877
|
-
|
|
4878
|
-
private func topLevelChildrenContainPositionAdjustments(
|
|
4879
|
-
startIndex: Int,
|
|
4880
|
-
deleteCount: Int
|
|
4881
|
-
) -> Bool {
|
|
4882
|
-
guard deleteCount > 0,
|
|
4883
|
-
let currentTopLevelChildMetadata,
|
|
4884
|
-
startIndex >= 0,
|
|
4885
|
-
startIndex + deleteCount <= currentTopLevelChildMetadata.count
|
|
4886
|
-
else {
|
|
4887
|
-
return false
|
|
4888
|
-
}
|
|
4889
|
-
return currentTopLevelChildMetadata[startIndex..<(startIndex + deleteCount)]
|
|
4890
|
-
.contains(where: \.containsPositionAdjustments)
|
|
4891
|
-
}
|
|
4892
|
-
|
|
4893
4444
|
private func trimmedAttributedPatch(
|
|
4894
4445
|
replacing fullReplaceRange: NSRange,
|
|
4895
4446
|
with replacement: NSAttributedString
|
|
@@ -4995,156 +4546,10 @@ final class EditorTextView: UITextView, UIGestureRecognizerDelegate {
|
|
|
4995
4546
|
)
|
|
4996
4547
|
}
|
|
4997
4548
|
|
|
4998
|
-
|
|
4999
|
-
let eligibilityStartedAt = DispatchTime.now().uptimeNanoseconds
|
|
5000
|
-
guard hasTopLevelChildMetadata(),
|
|
5001
|
-
let fullReplaceRange = replacementRangeForRenderPatch(
|
|
5002
|
-
startIndex: patch.startIndex,
|
|
5003
|
-
deleteCount: patch.deleteCount
|
|
5004
|
-
)
|
|
5005
|
-
else {
|
|
5006
|
-
return PatchApplyTrace(
|
|
5007
|
-
applied: false,
|
|
5008
|
-
eligibilityNanos: DispatchTime.now().uptimeNanoseconds - eligibilityStartedAt,
|
|
5009
|
-
trimNanos: 0,
|
|
5010
|
-
metadataNanos: 0,
|
|
5011
|
-
buildRenderNanos: 0,
|
|
5012
|
-
applyRenderNanos: 0,
|
|
5013
|
-
applyRenderReplaceUtf16Length: 0,
|
|
5014
|
-
applyRenderReplacementUtf16Length: 0,
|
|
5015
|
-
applyRenderTextMutationNanos: 0,
|
|
5016
|
-
applyRenderBeginEditingNanos: 0,
|
|
5017
|
-
applyRenderEndEditingNanos: 0,
|
|
5018
|
-
applyRenderStringMutationNanos: 0,
|
|
5019
|
-
applyRenderAttributeMutationNanos: 0,
|
|
5020
|
-
applyRenderAuthorizedTextNanos: 0,
|
|
5021
|
-
applyRenderCacheInvalidationNanos: 0,
|
|
5022
|
-
usedSmallPatchTextMutation: false
|
|
5023
|
-
)
|
|
5024
|
-
}
|
|
5025
|
-
|
|
5026
|
-
let buildStartedAt = DispatchTime.now().uptimeNanoseconds
|
|
5027
|
-
let attrStr = RenderBridge.renderBlocks(
|
|
5028
|
-
fromArray: patch.renderBlocks,
|
|
5029
|
-
startIndex: patch.startIndex,
|
|
5030
|
-
includeLeadingInterBlockSeparator: patch.startIndex > 0,
|
|
5031
|
-
baseFont: baseFont,
|
|
5032
|
-
textColor: baseTextColor,
|
|
5033
|
-
theme: theme
|
|
5034
|
-
)
|
|
5035
|
-
let buildRenderNanos = DispatchTime.now().uptimeNanoseconds - buildStartedAt
|
|
5036
|
-
let renderedPatchMetadata = topLevelChildMetadataSlice(from: attrStr)
|
|
5037
|
-
let renderedPatchContainsAttachment =
|
|
5038
|
-
renderedPatchMetadata?.entries.contains(where: \.containsAttachment)
|
|
5039
|
-
?? attributedStringContainsAttachment(attrStr)
|
|
5040
|
-
let renderedPatchContainsListMarkerContext =
|
|
5041
|
-
attributedStringContainsListMarkerContext(attrStr)
|
|
5042
|
-
let renderedPatchContainsPositionAdjustments =
|
|
5043
|
-
renderedPatchMetadata?.entries.contains(where: \.containsPositionAdjustments)
|
|
5044
|
-
?? attributedStringContainsPositionAdjustments(attrStr)
|
|
5045
|
-
guard !topLevelChildrenContainAttachment(
|
|
5046
|
-
startIndex: patch.startIndex,
|
|
5047
|
-
deleteCount: patch.deleteCount
|
|
5048
|
-
),
|
|
5049
|
-
!renderedPatchContainsAttachment
|
|
5050
|
-
else {
|
|
5051
|
-
return PatchApplyTrace(
|
|
5052
|
-
applied: false,
|
|
5053
|
-
eligibilityNanos: DispatchTime.now().uptimeNanoseconds - eligibilityStartedAt,
|
|
5054
|
-
trimNanos: 0,
|
|
5055
|
-
metadataNanos: 0,
|
|
5056
|
-
buildRenderNanos: buildRenderNanos,
|
|
5057
|
-
applyRenderNanos: 0,
|
|
5058
|
-
applyRenderReplaceUtf16Length: 0,
|
|
5059
|
-
applyRenderReplacementUtf16Length: 0,
|
|
5060
|
-
applyRenderTextMutationNanos: 0,
|
|
5061
|
-
applyRenderBeginEditingNanos: 0,
|
|
5062
|
-
applyRenderEndEditingNanos: 0,
|
|
5063
|
-
applyRenderStringMutationNanos: 0,
|
|
5064
|
-
applyRenderAttributeMutationNanos: 0,
|
|
5065
|
-
applyRenderAuthorizedTextNanos: 0,
|
|
5066
|
-
applyRenderCacheInvalidationNanos: 0,
|
|
5067
|
-
usedSmallPatchTextMutation: false
|
|
5068
|
-
)
|
|
5069
|
-
}
|
|
5070
|
-
guard !textStorageRangeContainsListMarkerContext(fullReplaceRange),
|
|
5071
|
-
!renderedPatchContainsListMarkerContext
|
|
5072
|
-
else {
|
|
5073
|
-
return PatchApplyTrace(
|
|
5074
|
-
applied: false,
|
|
5075
|
-
eligibilityNanos: DispatchTime.now().uptimeNanoseconds - eligibilityStartedAt,
|
|
5076
|
-
trimNanos: 0,
|
|
5077
|
-
metadataNanos: 0,
|
|
5078
|
-
buildRenderNanos: buildRenderNanos,
|
|
5079
|
-
applyRenderNanos: 0,
|
|
5080
|
-
applyRenderReplaceUtf16Length: 0,
|
|
5081
|
-
applyRenderReplacementUtf16Length: 0,
|
|
5082
|
-
applyRenderTextMutationNanos: 0,
|
|
5083
|
-
applyRenderBeginEditingNanos: 0,
|
|
5084
|
-
applyRenderEndEditingNanos: 0,
|
|
5085
|
-
applyRenderStringMutationNanos: 0,
|
|
5086
|
-
applyRenderAttributeMutationNanos: 0,
|
|
5087
|
-
applyRenderAuthorizedTextNanos: 0,
|
|
5088
|
-
applyRenderCacheInvalidationNanos: 0,
|
|
5089
|
-
usedSmallPatchTextMutation: false
|
|
5090
|
-
)
|
|
5091
|
-
}
|
|
5092
|
-
let eligibilityNanos =
|
|
5093
|
-
DispatchTime.now().uptimeNanoseconds - eligibilityStartedAt - buildRenderNanos
|
|
5094
|
-
let positionCacheUpdate: PositionCacheUpdate =
|
|
5095
|
-
if topLevelChildrenContainPositionAdjustments(
|
|
5096
|
-
startIndex: patch.startIndex,
|
|
5097
|
-
deleteCount: patch.deleteCount
|
|
5098
|
-
) || renderedPatchContainsPositionAdjustments
|
|
5099
|
-
{
|
|
5100
|
-
.attributed
|
|
5101
|
-
} else {
|
|
5102
|
-
.plainText
|
|
5103
|
-
}
|
|
5104
|
-
let trimStartedAt = DispatchTime.now().uptimeNanoseconds
|
|
5105
|
-
let patchToApply = trimmedAttributedPatch(replacing: fullReplaceRange, with: attrStr)
|
|
5106
|
-
let trimNanos = DispatchTime.now().uptimeNanoseconds - trimStartedAt
|
|
5107
|
-
let applyTrace = applyAttributedRender(
|
|
5108
|
-
patchToApply.replacement,
|
|
5109
|
-
replaceRange: patchToApply.replaceRange,
|
|
5110
|
-
usedPatch: true,
|
|
5111
|
-
positionCacheUpdate: positionCacheUpdate,
|
|
5112
|
-
authorizedReplaceRange: fullReplaceRange,
|
|
5113
|
-
authorizedReplacementText: attrStr.string,
|
|
5114
|
-
authorizedReplacementAttributedText: attrStr
|
|
5115
|
-
)
|
|
5116
|
-
let metadataStartedAt = DispatchTime.now().uptimeNanoseconds
|
|
5117
|
-
applyTopLevelChildMetadataPatch(
|
|
5118
|
-
patch,
|
|
5119
|
-
replaceRange: fullReplaceRange,
|
|
5120
|
-
renderedPatchMetadata: renderedPatchMetadata,
|
|
5121
|
-
renderedPatchLength: attrStr.length
|
|
5122
|
-
)
|
|
5123
|
-
let metadataNanos = DispatchTime.now().uptimeNanoseconds - metadataStartedAt
|
|
5124
|
-
return PatchApplyTrace(
|
|
5125
|
-
applied: true,
|
|
5126
|
-
eligibilityNanos: eligibilityNanos,
|
|
5127
|
-
trimNanos: trimNanos,
|
|
5128
|
-
metadataNanos: metadataNanos,
|
|
5129
|
-
buildRenderNanos: buildRenderNanos,
|
|
5130
|
-
applyRenderNanos: applyTrace.totalNanos,
|
|
5131
|
-
applyRenderReplaceUtf16Length: applyTrace.replaceUtf16Length,
|
|
5132
|
-
applyRenderReplacementUtf16Length: applyTrace.replacementUtf16Length,
|
|
5133
|
-
applyRenderTextMutationNanos: applyTrace.textMutationNanos,
|
|
5134
|
-
applyRenderBeginEditingNanos: applyTrace.beginEditingNanos,
|
|
5135
|
-
applyRenderEndEditingNanos: applyTrace.endEditingNanos,
|
|
5136
|
-
applyRenderStringMutationNanos: applyTrace.stringMutationNanos,
|
|
5137
|
-
applyRenderAttributeMutationNanos: applyTrace.attributeMutationNanos,
|
|
5138
|
-
applyRenderAuthorizedTextNanos: applyTrace.authorizedTextNanos,
|
|
5139
|
-
applyRenderCacheInvalidationNanos: applyTrace.cacheInvalidationNanos,
|
|
5140
|
-
usedSmallPatchTextMutation: applyTrace.usedSmallPatchTextMutation
|
|
5141
|
-
)
|
|
5142
|
-
}
|
|
5143
|
-
|
|
5144
|
-
/// Apply a full render update from Rust to the text view.
|
|
4549
|
+
/// Apply a structural editor update from Rust to the hidden input text view.
|
|
5145
4550
|
///
|
|
5146
|
-
///
|
|
5147
|
-
///
|
|
4551
|
+
/// The visible editor surface renders the block tree. This text view keeps a
|
|
4552
|
+
/// plain text snapshot for input, selection, and IME routing only.
|
|
5148
4553
|
///
|
|
5149
4554
|
/// - Parameter updateJSON: The JSON string from editor_insert_text, etc.
|
|
5150
4555
|
func applyUpdateJSON(_ updateJSON: String, notifyDelegate: Bool = true) {
|
|
@@ -5157,94 +4562,97 @@ final class EditorTextView: UITextView, UIGestureRecognizerDelegate {
|
|
|
5157
4562
|
let parseNanos = DispatchTime.now().uptimeNanoseconds - parseStartedAt
|
|
5158
4563
|
resetPendingNativeTextMutationState()
|
|
5159
4564
|
|
|
5160
|
-
let
|
|
4565
|
+
let inputSnapshot = update["inputText"] as? String
|
|
5161
4566
|
let selectionFromUpdate = (update["selection"] as? [String: Any])
|
|
5162
4567
|
.map(self.selectionSummary(from:)) ?? "none"
|
|
5163
4568
|
Self.updateLog.debug(
|
|
5164
|
-
"[applyUpdateJSON.begin]
|
|
4569
|
+
"[applyUpdateJSON.begin] structuralInput=\(inputSnapshot != nil, privacy: .public) updateSelection=\(selectionFromUpdate, privacy: .public) before=\(self.textSnapshotSummary(), privacy: .public)"
|
|
5165
4570
|
)
|
|
5166
|
-
let
|
|
5167
|
-
|
|
5168
|
-
|
|
5169
|
-
|
|
5170
|
-
|
|
5171
|
-
|
|
5172
|
-
|
|
5173
|
-
|
|
5174
|
-
|
|
5175
|
-
|
|
5176
|
-
|
|
5177
|
-
|
|
5178
|
-
|
|
5179
|
-
|
|
5180
|
-
|
|
5181
|
-
|
|
5182
|
-
|
|
5183
|
-
|
|
4571
|
+
if inputSnapshot == nil, let selection = update["selection"] as? [String: Any] {
|
|
4572
|
+
let selectionTrace = applySelectionFromJSON(selection)
|
|
4573
|
+
let postApplyTrace = performPostApplyMaintenance()
|
|
4574
|
+
if captureApplyUpdateTraceForTesting {
|
|
4575
|
+
lastApplyUpdateTraceForTesting = ApplyUpdateTrace(
|
|
4576
|
+
attemptedPatch: false,
|
|
4577
|
+
usedPatch: false,
|
|
4578
|
+
usedSmallPatchTextMutation: false,
|
|
4579
|
+
applyRenderReplaceUtf16Length: 0,
|
|
4580
|
+
applyRenderReplacementUtf16Length: 0,
|
|
4581
|
+
parseNanos: parseNanos,
|
|
4582
|
+
resolveRenderBlocksNanos: 0,
|
|
4583
|
+
patchEligibilityNanos: 0,
|
|
4584
|
+
patchTrimNanos: 0,
|
|
4585
|
+
patchMetadataNanos: 0,
|
|
4586
|
+
buildRenderNanos: 0,
|
|
4587
|
+
applyRenderNanos: 0,
|
|
4588
|
+
selectionNanos: selectionTrace.totalNanos,
|
|
4589
|
+
postApplyNanos: postApplyTrace.totalNanos,
|
|
4590
|
+
totalNanos: DispatchTime.now().uptimeNanoseconds - totalStartedAt,
|
|
4591
|
+
applyRenderTextMutationNanos: 0,
|
|
4592
|
+
applyRenderBeginEditingNanos: 0,
|
|
4593
|
+
applyRenderEndEditingNanos: 0,
|
|
4594
|
+
applyRenderStringMutationNanos: 0,
|
|
4595
|
+
applyRenderAttributeMutationNanos: 0,
|
|
4596
|
+
applyRenderAuthorizedTextNanos: 0,
|
|
4597
|
+
applyRenderCacheInvalidationNanos: 0,
|
|
4598
|
+
selectionResolveNanos: selectionTrace.resolveNanos,
|
|
4599
|
+
selectionAssignmentNanos: selectionTrace.assignmentNanos,
|
|
4600
|
+
selectionChromeNanos: selectionTrace.chromeNanos,
|
|
4601
|
+
postApplyTypingAttributesNanos: postApplyTrace.typingAttributesNanos,
|
|
4602
|
+
postApplyHeightNotifyNanos: postApplyTrace.heightNotifyNanos,
|
|
4603
|
+
postApplyHeightNotifyMeasureNanos: postApplyTrace.heightNotifyMeasureNanos,
|
|
4604
|
+
postApplyHeightNotifyCallbackNanos: postApplyTrace.heightNotifyCallbackNanos,
|
|
4605
|
+
postApplyHeightNotifyEnsureLayoutNanos: postApplyTrace.heightNotifyEnsureLayoutNanos,
|
|
4606
|
+
postApplyHeightNotifyUsedRectNanos: postApplyTrace.heightNotifyUsedRectNanos,
|
|
4607
|
+
postApplyHeightNotifyContentSizeNanos: postApplyTrace.heightNotifyContentSizeNanos,
|
|
4608
|
+
postApplyHeightNotifySizeThatFitsNanos: postApplyTrace.heightNotifySizeThatFitsNanos,
|
|
4609
|
+
postApplySelectionOrContentCallbackNanos:
|
|
4610
|
+
postApplyTrace.selectionOrContentCallbackNanos
|
|
4611
|
+
)
|
|
5184
4612
|
}
|
|
5185
|
-
|
|
5186
|
-
|
|
5187
|
-
return patch
|
|
4613
|
+
if notifyDelegate {
|
|
4614
|
+
editorDelegate?.editorTextView(self, didReceiveUpdate: updateJSON)
|
|
5188
4615
|
}
|
|
5189
|
-
|
|
5190
|
-
|
|
5191
|
-
|
|
5192
|
-
|
|
5193
|
-
|
|
5194
|
-
|
|
5195
|
-
|
|
5196
|
-
|
|
5197
|
-
|
|
5198
|
-
|
|
5199
|
-
let appliedPatch =
|
|
5200
|
-
var usedSmallPatchTextMutation =
|
|
5201
|
-
var applyRenderReplaceUtf16Length =
|
|
5202
|
-
var applyRenderReplacementUtf16Length =
|
|
5203
|
-
|
|
5204
|
-
var
|
|
5205
|
-
var
|
|
5206
|
-
var
|
|
5207
|
-
var
|
|
5208
|
-
var
|
|
5209
|
-
var
|
|
5210
|
-
var
|
|
5211
|
-
|
|
5212
|
-
var applyRenderAuthorizedTextNanos = patchTrace?.applyRenderAuthorizedTextNanos ?? 0
|
|
5213
|
-
var applyRenderCacheInvalidationNanos = patchTrace?.applyRenderCacheInvalidationNanos ?? 0
|
|
4616
|
+
onUpdateJSONApplied?(updateJSON)
|
|
4617
|
+
return
|
|
4618
|
+
}
|
|
4619
|
+
let resolveRenderBlocksNanos: UInt64 = 0
|
|
4620
|
+
let shouldSkipRender =
|
|
4621
|
+
inputSnapshot != nil
|
|
4622
|
+
&& textStorage.string == inputSnapshot
|
|
4623
|
+
&& textStorage.string == lastAuthorizedText
|
|
4624
|
+
&& lastAppliedRenderAppearanceRevision == renderAppearanceRevision
|
|
4625
|
+
|
|
4626
|
+
let appliedPatch = false
|
|
4627
|
+
var usedSmallPatchTextMutation = false
|
|
4628
|
+
var applyRenderReplaceUtf16Length = 0
|
|
4629
|
+
var applyRenderReplacementUtf16Length = 0
|
|
4630
|
+
var buildRenderNanos: UInt64 = 0
|
|
4631
|
+
var applyRenderNanos: UInt64 = 0
|
|
4632
|
+
var applyRenderTextMutationNanos: UInt64 = 0
|
|
4633
|
+
var applyRenderBeginEditingNanos: UInt64 = 0
|
|
4634
|
+
var applyRenderEndEditingNanos: UInt64 = 0
|
|
4635
|
+
var applyRenderStringMutationNanos: UInt64 = 0
|
|
4636
|
+
var applyRenderAttributeMutationNanos: UInt64 = 0
|
|
4637
|
+
var applyRenderAuthorizedTextNanos: UInt64 = 0
|
|
4638
|
+
var applyRenderCacheInvalidationNanos: UInt64 = 0
|
|
5214
4639
|
if shouldSkipRender {
|
|
5215
4640
|
lastRenderAppliedPatchForTesting = false
|
|
5216
|
-
|
|
5217
|
-
|
|
5218
|
-
}
|
|
5219
|
-
} else if !appliedPatch {
|
|
4641
|
+
} else {
|
|
4642
|
+
guard let inputSnapshot else { return }
|
|
5220
4643
|
let buildStartedAt = DispatchTime.now().uptimeNanoseconds
|
|
5221
|
-
let attrStr
|
|
5222
|
-
|
|
5223
|
-
|
|
5224
|
-
|
|
5225
|
-
|
|
5226
|
-
|
|
5227
|
-
|
|
5228
|
-
)
|
|
5229
|
-
currentRenderBlocks = resolvedRenderBlocks
|
|
5230
|
-
} else if let renderElements {
|
|
5231
|
-
attrStr = RenderBridge.renderElements(
|
|
5232
|
-
fromArray: renderElements,
|
|
5233
|
-
baseFont: baseFont,
|
|
5234
|
-
textColor: baseTextColor,
|
|
5235
|
-
theme: theme
|
|
5236
|
-
)
|
|
5237
|
-
currentRenderBlocks = nil
|
|
5238
|
-
} else {
|
|
5239
|
-
return
|
|
5240
|
-
}
|
|
4644
|
+
let attrStr = NSAttributedString(
|
|
4645
|
+
string: inputSnapshot,
|
|
4646
|
+
attributes: [
|
|
4647
|
+
.font: baseFont,
|
|
4648
|
+
.foregroundColor: baseTextColor
|
|
4649
|
+
]
|
|
4650
|
+
)
|
|
5241
4651
|
buildRenderNanos = DispatchTime.now().uptimeNanoseconds - buildStartedAt
|
|
5242
4652
|
let applyTrace = applyAttributedRender(
|
|
5243
4653
|
attrStr,
|
|
5244
|
-
usedPatch: false
|
|
5245
|
-
positionCacheUpdate: .invalidate
|
|
4654
|
+
usedPatch: false
|
|
5246
4655
|
)
|
|
5247
|
-
refreshTopLevelChildMetadata(from: attrStr)
|
|
5248
4656
|
applyRenderReplaceUtf16Length = applyTrace.replaceUtf16Length
|
|
5249
4657
|
applyRenderReplacementUtf16Length = applyTrace.replacementUtf16Length
|
|
5250
4658
|
applyRenderNanos = applyTrace.totalNanos
|
|
@@ -5257,14 +4665,11 @@ final class EditorTextView: UITextView, UIGestureRecognizerDelegate {
|
|
|
5257
4665
|
applyRenderCacheInvalidationNanos = applyTrace.cacheInvalidationNanos
|
|
5258
4666
|
usedSmallPatchTextMutation = applyTrace.usedSmallPatchTextMutation
|
|
5259
4667
|
lastAppliedRenderAppearanceRevision = renderAppearanceRevision
|
|
5260
|
-
} else if let resolvedRenderBlocks {
|
|
5261
|
-
currentRenderBlocks = resolvedRenderBlocks
|
|
5262
|
-
lastAppliedRenderAppearanceRevision = renderAppearanceRevision
|
|
5263
4668
|
}
|
|
5264
4669
|
|
|
5265
4670
|
refreshPlaceholderVisibility()
|
|
5266
4671
|
Self.updateLog.debug(
|
|
5267
|
-
"[applyUpdateJSON.rendered] mode
|
|
4672
|
+
"[applyUpdateJSON.rendered] mode=structuralInput after=\(self.textSnapshotSummary(), privacy: .public)"
|
|
5268
4673
|
)
|
|
5269
4674
|
|
|
5270
4675
|
// Apply the selection from the update.
|
|
@@ -5284,16 +4689,16 @@ final class EditorTextView: UITextView, UIGestureRecognizerDelegate {
|
|
|
5284
4689
|
|
|
5285
4690
|
if captureApplyUpdateTraceForTesting {
|
|
5286
4691
|
lastApplyUpdateTraceForTesting = ApplyUpdateTrace(
|
|
5287
|
-
attemptedPatch:
|
|
4692
|
+
attemptedPatch: false,
|
|
5288
4693
|
usedPatch: appliedPatch,
|
|
5289
4694
|
usedSmallPatchTextMutation: usedSmallPatchTextMutation,
|
|
5290
4695
|
applyRenderReplaceUtf16Length: applyRenderReplaceUtf16Length,
|
|
5291
4696
|
applyRenderReplacementUtf16Length: applyRenderReplacementUtf16Length,
|
|
5292
4697
|
parseNanos: parseNanos,
|
|
5293
4698
|
resolveRenderBlocksNanos: resolveRenderBlocksNanos,
|
|
5294
|
-
patchEligibilityNanos:
|
|
5295
|
-
patchTrimNanos:
|
|
5296
|
-
patchMetadataNanos:
|
|
4699
|
+
patchEligibilityNanos: 0,
|
|
4700
|
+
patchTrimNanos: 0,
|
|
4701
|
+
patchMetadataNanos: 0,
|
|
5297
4702
|
buildRenderNanos: buildRenderNanos,
|
|
5298
4703
|
applyRenderNanos: applyRenderNanos,
|
|
5299
4704
|
selectionNanos: selectionTrace.totalNanos,
|
|
@@ -5330,34 +4735,60 @@ final class EditorTextView: UITextView, UIGestureRecognizerDelegate {
|
|
|
5330
4735
|
if notifyDelegate {
|
|
5331
4736
|
editorDelegate?.editorTextView(self, didReceiveUpdate: updateJSON)
|
|
5332
4737
|
}
|
|
4738
|
+
onUpdateJSONApplied?(updateJSON)
|
|
5333
4739
|
}
|
|
5334
4740
|
|
|
5335
|
-
|
|
5336
|
-
|
|
5337
|
-
|
|
5338
|
-
|
|
5339
|
-
|
|
5340
|
-
|
|
5341
|
-
resetPendingNativeTextMutationState()
|
|
5342
|
-
Self.updateLog.debug(
|
|
5343
|
-
"[applyRenderJSON.begin] before=\(self.textSnapshotSummary(), privacy: .public)"
|
|
5344
|
-
)
|
|
5345
|
-
let attrStr = RenderBridge.renderElements(
|
|
5346
|
-
fromJSON: renderJSON,
|
|
5347
|
-
baseFont: baseFont,
|
|
5348
|
-
textColor: baseTextColor,
|
|
5349
|
-
theme: theme
|
|
5350
|
-
)
|
|
5351
|
-
_ = applyAttributedRender(attrStr, usedPatch: false)
|
|
5352
|
-
currentRenderBlocks = nil
|
|
5353
|
-
lastAppliedRenderAppearanceRevision = renderAppearanceRevision
|
|
4741
|
+
private static func plainTextSnapshot(fromRenderTree tree: [String: Any]?) -> String? {
|
|
4742
|
+
guard let root = tree?["root"] as? [String: Any] else { return nil }
|
|
4743
|
+
var text = ""
|
|
4744
|
+
appendInputSnapshotNode(root, to: &text, isRoot: true)
|
|
4745
|
+
return text
|
|
4746
|
+
}
|
|
5354
4747
|
|
|
5355
|
-
|
|
5356
|
-
_
|
|
5357
|
-
|
|
5358
|
-
|
|
5359
|
-
|
|
5360
|
-
|
|
4748
|
+
private static func appendInputSnapshotNode(
|
|
4749
|
+
_ node: [String: Any],
|
|
4750
|
+
to text: inout String,
|
|
4751
|
+
isRoot: Bool
|
|
4752
|
+
) {
|
|
4753
|
+
let kind = node["kind"] as? String ?? "block"
|
|
4754
|
+
let nodeType = node["nodeType"] as? String ?? "unknown"
|
|
4755
|
+
if kind == "text" {
|
|
4756
|
+
text += node["text"] as? String ?? ""
|
|
4757
|
+
return
|
|
4758
|
+
}
|
|
4759
|
+
if kind == "inlineAtom" {
|
|
4760
|
+
if nodeType == "hardBreak" || nodeType == "hard_break" {
|
|
4761
|
+
text += "\n"
|
|
4762
|
+
} else {
|
|
4763
|
+
text += node["label"] as? String ?? "\u{FFFC}"
|
|
4764
|
+
}
|
|
4765
|
+
return
|
|
4766
|
+
}
|
|
4767
|
+
if kind == "blockAtom" {
|
|
4768
|
+
if !text.isEmpty && !text.hasSuffix("\n") {
|
|
4769
|
+
text += "\n"
|
|
4770
|
+
}
|
|
4771
|
+
text += node["label"] as? String ?? "\u{FFFC}"
|
|
4772
|
+
text += "\n"
|
|
4773
|
+
return
|
|
4774
|
+
}
|
|
4775
|
+
|
|
4776
|
+
let children = node["children"] as? [[String: Any]] ?? []
|
|
4777
|
+
let startsBlock = !isRoot && isInputSnapshotBlockBoundary(kind: kind)
|
|
4778
|
+
if startsBlock && !text.isEmpty && !text.hasSuffix("\n") {
|
|
4779
|
+
text += "\n"
|
|
4780
|
+
}
|
|
4781
|
+
for child in children {
|
|
4782
|
+
appendInputSnapshotNode(child, to: &text, isRoot: false)
|
|
4783
|
+
}
|
|
4784
|
+
if startsBlock && !text.isEmpty && !text.hasSuffix("\n") {
|
|
4785
|
+
text += "\n"
|
|
4786
|
+
}
|
|
4787
|
+
}
|
|
4788
|
+
|
|
4789
|
+
private static func isInputSnapshotBlockBoundary(kind: String) -> Bool {
|
|
4790
|
+
kind == "textBlock" || kind == "listItem" || kind == "block" ||
|
|
4791
|
+
kind == "tableCell" || kind == "tableRow" || kind == "table"
|
|
5361
4792
|
}
|
|
5362
4793
|
|
|
5363
4794
|
/// Apply a selection from a parsed JSON selection object.
|
|
@@ -5542,7 +4973,6 @@ extension EditorTextView: NSTextStorageDelegate {
|
|
|
5542
4973
|
// Compare current text storage content against last authorized snapshot.
|
|
5543
4974
|
let currentText = textStorage.string
|
|
5544
4975
|
guard currentText != lastAuthorizedText else { return }
|
|
5545
|
-
currentTopLevelChildMetadata = nil
|
|
5546
4976
|
|
|
5547
4977
|
let allowAfterBlur = canAdoptNativeTextMutationAfterBlur()
|
|
5548
4978
|
if let mutation = nativeTextMutationFromAuthorizedDiff(currentText: currentText),
|
|
@@ -5633,6 +5063,7 @@ final class RichTextEditorView: UIView {
|
|
|
5633
5063
|
|
|
5634
5064
|
/// The editor text view that handles input interception.
|
|
5635
5065
|
let textView: EditorTextView
|
|
5066
|
+
private let blockSurface = NativeBlockEditorSurface()
|
|
5636
5067
|
private let remoteSelectionOverlayView = RemoteSelectionOverlayView()
|
|
5637
5068
|
private let taskListMarkerTapOverlayView = TaskListMarkerTapOverlayView()
|
|
5638
5069
|
private let imageTapOverlayView = ImageTapOverlayView()
|
|
@@ -5642,6 +5073,7 @@ final class RichTextEditorView: UIView {
|
|
|
5642
5073
|
private var cachedAutoGrowMeasuredHeight: CGFloat = 0
|
|
5643
5074
|
private var remoteSelections: [RemoteSelectionDecoration] = []
|
|
5644
5075
|
private var overlayRefreshScheduled = false
|
|
5076
|
+
private var isSyncingBlockSurfaceUpdateToInputAdapter = false
|
|
5645
5077
|
var captureHostedLayoutTraceForTesting = false
|
|
5646
5078
|
private var hostedLayoutTraceNanos = (
|
|
5647
5079
|
intrinsicContentSize: UInt64(0),
|
|
@@ -5674,6 +5106,8 @@ final class RichTextEditorView: UIView {
|
|
|
5674
5106
|
didSet {
|
|
5675
5107
|
guard oldValue != heightBehavior else { return }
|
|
5676
5108
|
textView.heightBehavior = heightBehavior
|
|
5109
|
+
blockSurface.isScrollEnabled = heightBehavior == .fixed
|
|
5110
|
+
blockSurface.alwaysBounceVertical = heightBehavior == .fixed
|
|
5677
5111
|
textView.updateAutoGrowHostHeight(heightBehavior == .autoGrow ? bounds.height : 0)
|
|
5678
5112
|
if heightBehavior != .autoGrow {
|
|
5679
5113
|
cachedAutoGrowMeasuredHeight = 0
|
|
@@ -5702,8 +5136,10 @@ final class RichTextEditorView: UIView {
|
|
|
5702
5136
|
guard oldValue != editorId else { return }
|
|
5703
5137
|
textView.discardTransientNativeInputForEditorRebind()
|
|
5704
5138
|
if editorId != 0 {
|
|
5139
|
+
blockSurface.bind(editorId: editorId)
|
|
5705
5140
|
textView.bindEditor(id: editorId)
|
|
5706
5141
|
} else {
|
|
5142
|
+
blockSurface.bind(editorId: 0)
|
|
5707
5143
|
textView.unbindEditor()
|
|
5708
5144
|
}
|
|
5709
5145
|
remoteSelectionOverlayView.update(
|
|
@@ -5732,7 +5168,7 @@ final class RichTextEditorView: UIView {
|
|
|
5732
5168
|
private func setupView() {
|
|
5733
5169
|
// Add the text view as a subview. These views always track the host bounds,
|
|
5734
5170
|
// so manual layout is cheaper than driving them through Auto Layout.
|
|
5735
|
-
remoteSelectionOverlayView.bind(
|
|
5171
|
+
remoteSelectionOverlayView.bind(documentSurface: blockSurface)
|
|
5736
5172
|
taskListMarkerTapOverlayView.bind(editorView: self)
|
|
5737
5173
|
imageTapOverlayView.bind(editorView: self)
|
|
5738
5174
|
imageResizeOverlayView.bind(editorView: self)
|
|
@@ -5753,9 +5189,46 @@ final class RichTextEditorView: UIView {
|
|
|
5753
5189
|
self?.refreshOverlaysIfNeeded()
|
|
5754
5190
|
}
|
|
5755
5191
|
textView.onSelectionOrContentMayChange = { [weak self] in
|
|
5756
|
-
self
|
|
5192
|
+
guard let self else { return }
|
|
5193
|
+
if self.editorId != 0, !self.isSyncingBlockSurfaceUpdateToInputAdapter {
|
|
5194
|
+
self.blockSurface.applyUpdateJSON(
|
|
5195
|
+
editorGetSelectionState(id: self.editorId),
|
|
5196
|
+
editorId: self.editorId
|
|
5197
|
+
)
|
|
5198
|
+
}
|
|
5199
|
+
self.scheduleRefreshOverlaysIfNeeded()
|
|
5200
|
+
}
|
|
5201
|
+
textView.onUpdateJSONApplied = { [weak self] updateJSON in
|
|
5202
|
+
guard let self else { return }
|
|
5203
|
+
if self.isSyncingBlockSurfaceUpdateToInputAdapter { return }
|
|
5204
|
+
self.blockSurface.applyUpdateJSON(updateJSON, editorId: self.editorId)
|
|
5757
5205
|
}
|
|
5206
|
+
blockSurface.onAppliedUpdate = { [weak self] updateJSON in
|
|
5207
|
+
guard let self else { return }
|
|
5208
|
+
self.isSyncingBlockSurfaceUpdateToInputAdapter = true
|
|
5209
|
+
defer { self.isSyncingBlockSurfaceUpdateToInputAdapter = false }
|
|
5210
|
+
self.textView.applyUpdateJSON(updateJSON, notifyDelegate: true)
|
|
5211
|
+
}
|
|
5212
|
+
blockSurface.onRequestTextInput = { [weak self] docPos in
|
|
5213
|
+
guard let self, self.editorId != 0, self.textView.isEditable else { return }
|
|
5214
|
+
editorSetSelection(id: self.editorId, anchor: docPos, head: docPos)
|
|
5215
|
+
self.isSyncingBlockSurfaceUpdateToInputAdapter = true
|
|
5216
|
+
self.textView.applyUpdateJSON(
|
|
5217
|
+
editorGetSelectionState(id: self.editorId),
|
|
5218
|
+
notifyDelegate: true
|
|
5219
|
+
)
|
|
5220
|
+
self.isSyncingBlockSurfaceUpdateToInputAdapter = false
|
|
5221
|
+
self.textView.becomeFirstResponder()
|
|
5222
|
+
}
|
|
5223
|
+
blockSurface.onContentHeightMayChange = { [weak self] measuredHeight in
|
|
5224
|
+
guard let self, self.heightBehavior == .autoGrow else { return }
|
|
5225
|
+
self.cachedAutoGrowMeasuredHeight = measuredHeight
|
|
5226
|
+
self.invalidateIntrinsicContentSize()
|
|
5227
|
+
self.onHeightMayChange?(measuredHeight)
|
|
5228
|
+
}
|
|
5229
|
+
textView.alpha = 0.01
|
|
5758
5230
|
addSubview(textView)
|
|
5231
|
+
addSubview(blockSurface)
|
|
5759
5232
|
addSubview(remoteSelectionOverlayView)
|
|
5760
5233
|
addSubview(taskListMarkerTapOverlayView)
|
|
5761
5234
|
addSubview(imageTapOverlayView)
|
|
@@ -5821,11 +5294,15 @@ final class RichTextEditorView: UIView {
|
|
|
5821
5294
|
textView.font = font
|
|
5822
5295
|
textView.textColor = textColor
|
|
5823
5296
|
textView.backgroundColor = backgroundColor
|
|
5297
|
+
blockSurface.baseFont = font
|
|
5298
|
+
blockSurface.baseTextColor = textColor
|
|
5299
|
+
blockSurface.baseBackgroundColor = backgroundColor
|
|
5824
5300
|
}
|
|
5825
5301
|
|
|
5826
5302
|
@discardableResult
|
|
5827
5303
|
func applyTheme(_ theme: EditorTheme?) -> Bool {
|
|
5828
5304
|
guard textView.applyTheme(theme) else { return false }
|
|
5305
|
+
blockSurface.applyTheme(theme)
|
|
5829
5306
|
let cornerRadius = theme?.borderRadius ?? 0
|
|
5830
5307
|
layer.cornerRadius = cornerRadius
|
|
5831
5308
|
clipsToBounds = cornerRadius > 0
|
|
@@ -5835,22 +5312,31 @@ final class RichTextEditorView: UIView {
|
|
|
5835
5312
|
|
|
5836
5313
|
func setRemoteSelections(_ selections: [RemoteSelectionDecoration]) {
|
|
5837
5314
|
remoteSelections = selections
|
|
5315
|
+
layoutManagedSubviews()
|
|
5316
|
+
blockSurface.layoutIfNeeded()
|
|
5838
5317
|
remoteSelectionOverlayView.update(
|
|
5839
5318
|
selections: selections,
|
|
5840
5319
|
editorId: editorId
|
|
5841
5320
|
)
|
|
5842
5321
|
}
|
|
5843
5322
|
|
|
5323
|
+
func setEditable(_ editable: Bool) {
|
|
5324
|
+
textView.isEditable = editable
|
|
5325
|
+
blockSurface.isEditable = editable
|
|
5326
|
+
if !editable {
|
|
5327
|
+
textView.resignFirstResponder()
|
|
5328
|
+
}
|
|
5329
|
+
}
|
|
5330
|
+
|
|
5844
5331
|
func refreshRemoteSelections() {
|
|
5845
5332
|
guard remoteSelectionOverlayView.hasSelectionsOrVisibleDecorations else { return }
|
|
5846
5333
|
remoteSelectionOverlayView.refresh()
|
|
5847
5334
|
}
|
|
5848
5335
|
|
|
5849
5336
|
func currentCaretRect() -> CGRect? {
|
|
5850
|
-
|
|
5851
|
-
let rect =
|
|
5852
|
-
|
|
5853
|
-
return textView.convert(rect, to: self)
|
|
5337
|
+
blockSurface.isInputFocused = textView.isFirstResponder
|
|
5338
|
+
guard let rect = blockSurface.selectedCaretRect() else { return nil }
|
|
5339
|
+
return blockSurface.convert(rect, to: self)
|
|
5854
5340
|
}
|
|
5855
5341
|
|
|
5856
5342
|
func remoteSelectionOverlaySubviewsForTesting() -> [UIView] {
|
|
@@ -5899,8 +5385,38 @@ final class RichTextEditorView: UIView {
|
|
|
5899
5385
|
imageResizeOverlayView.visibleRectForTesting
|
|
5900
5386
|
}
|
|
5901
5387
|
|
|
5388
|
+
func blockSurfaceImageRectForTesting() -> CGRect? {
|
|
5389
|
+
guard let geometry = blockSurface.selectedImageGeometry()
|
|
5390
|
+
?? blockSurface.firstImageGeometryForTesting()
|
|
5391
|
+
else { return nil }
|
|
5392
|
+
return blockSurface.convert(geometry.rect, to: self)
|
|
5393
|
+
}
|
|
5394
|
+
|
|
5395
|
+
func blockSurfaceTopLevelViewForTesting(at index: Int) -> UIView? {
|
|
5396
|
+
blockSurface.topLevelViewForTesting(at: index)
|
|
5397
|
+
}
|
|
5398
|
+
|
|
5399
|
+
func blockSurfaceTopLevelViewCountForTesting() -> Int {
|
|
5400
|
+
blockSurface.topLevelViewCountForTesting
|
|
5401
|
+
}
|
|
5402
|
+
|
|
5403
|
+
func blockSurfaceFirstTextAttributesForTesting() -> [NSAttributedString.Key: Any]? {
|
|
5404
|
+
blockSurface.firstTextAttributesForTesting()
|
|
5405
|
+
}
|
|
5406
|
+
|
|
5407
|
+
func blockSurfaceCaretRectForTesting(docPosition: UInt32) -> CGRect? {
|
|
5408
|
+
guard let rect = blockSurface.caretRect(forDocPosition: docPosition) else { return nil }
|
|
5409
|
+
return blockSurface.convert(rect, to: self)
|
|
5410
|
+
}
|
|
5411
|
+
|
|
5412
|
+
func blockSurfaceTopLevelRectForTesting(at index: Int) -> CGRect? {
|
|
5413
|
+
guard let child = blockSurface.topLevelViewForTesting(at: index) else { return nil }
|
|
5414
|
+
return child.convert(child.bounds, to: self)
|
|
5415
|
+
}
|
|
5416
|
+
|
|
5902
5417
|
func imageTapOverlayInterceptsPointForTesting(_ point: CGPoint) -> Bool {
|
|
5903
|
-
imageTapOverlayView.
|
|
5418
|
+
!imageTapOverlayView.isHidden
|
|
5419
|
+
&& imageTapOverlayView.interceptsPointForTesting(convert(point, to: imageTapOverlayView))
|
|
5904
5420
|
}
|
|
5905
5421
|
|
|
5906
5422
|
func taskListMarkerTapOverlayInterceptsPointForTesting(_ point: CGPoint) -> Bool {
|
|
@@ -5934,7 +5450,7 @@ final class RichTextEditorView: UIView {
|
|
|
5934
5450
|
}
|
|
5935
5451
|
|
|
5936
5452
|
func maximumImageWidthForTesting() -> CGFloat {
|
|
5937
|
-
|
|
5453
|
+
blockSurface.maximumImageWidth()
|
|
5938
5454
|
}
|
|
5939
5455
|
|
|
5940
5456
|
func resizeSelectedImageForTesting(width: CGFloat, height: CGFloat) {
|
|
@@ -5955,7 +5471,7 @@ final class RichTextEditorView: UIView {
|
|
|
5955
5471
|
func setContent(html: String) {
|
|
5956
5472
|
guard editorId != 0 else { return }
|
|
5957
5473
|
_ = editorSetHtml(id: editorId, html: html)
|
|
5958
|
-
|
|
5474
|
+
applyFullStateToInputAndSurface(editorGetCurrentState(id: editorId))
|
|
5959
5475
|
}
|
|
5960
5476
|
|
|
5961
5477
|
/// Set initial content from ProseMirror JSON.
|
|
@@ -5964,7 +5480,14 @@ final class RichTextEditorView: UIView {
|
|
|
5964
5480
|
func setContent(json: String) {
|
|
5965
5481
|
guard editorId != 0 else { return }
|
|
5966
5482
|
_ = editorSetJson(id: editorId, json: json)
|
|
5967
|
-
|
|
5483
|
+
applyFullStateToInputAndSurface(editorGetCurrentState(id: editorId))
|
|
5484
|
+
}
|
|
5485
|
+
|
|
5486
|
+
private func applyFullStateToInputAndSurface(_ stateJSON: String) {
|
|
5487
|
+
isSyncingBlockSurfaceUpdateToInputAdapter = true
|
|
5488
|
+
textView.applyUpdateJSON(stateJSON, notifyDelegate: false)
|
|
5489
|
+
isSyncingBlockSurfaceUpdateToInputAdapter = false
|
|
5490
|
+
blockSurface.applyUpdateJSON(stateJSON, editorId: editorId)
|
|
5968
5491
|
}
|
|
5969
5492
|
|
|
5970
5493
|
private func measuredEditorHeight() -> CGFloat {
|
|
@@ -6002,6 +5525,9 @@ final class RichTextEditorView: UIView {
|
|
|
6002
5525
|
if textView.frame != managedFrame {
|
|
6003
5526
|
textView.frame = managedFrame
|
|
6004
5527
|
}
|
|
5528
|
+
if blockSurface.frame != managedFrame {
|
|
5529
|
+
blockSurface.frame = managedFrame
|
|
5530
|
+
}
|
|
6005
5531
|
if remoteSelectionOverlayView.frame != managedFrame {
|
|
6006
5532
|
remoteSelectionOverlayView.frame = managedFrame
|
|
6007
5533
|
}
|
|
@@ -6017,19 +5543,21 @@ final class RichTextEditorView: UIView {
|
|
|
6017
5543
|
}
|
|
6018
5544
|
|
|
6019
5545
|
fileprivate func selectedImageGeometry() -> (docPos: UInt32, rect: CGRect)? {
|
|
6020
|
-
guard
|
|
5546
|
+
guard allowImageResizing, textView.isFirstResponder,
|
|
5547
|
+
let geometry = blockSurface.selectedImageGeometry()
|
|
5548
|
+
else { return nil }
|
|
6021
5549
|
return (
|
|
6022
5550
|
docPos: geometry.docPos,
|
|
6023
|
-
rect:
|
|
5551
|
+
rect: blockSurface.convert(geometry.rect, to: imageResizeOverlayView)
|
|
6024
5552
|
)
|
|
6025
5553
|
}
|
|
6026
5554
|
|
|
6027
5555
|
fileprivate func setImageResizePreviewActive(_ active: Bool) {
|
|
6028
|
-
|
|
5556
|
+
blockSurface.setImagePreviewActive(active)
|
|
6029
5557
|
}
|
|
6030
5558
|
|
|
6031
5559
|
fileprivate func imagePreviewForResize(docPos: UInt32) -> UIImage? {
|
|
6032
|
-
|
|
5560
|
+
blockSurface.imagePreview(docPos: docPos)
|
|
6033
5561
|
}
|
|
6034
5562
|
|
|
6035
5563
|
fileprivate func imageResizePreviewBackgroundColor() -> UIColor {
|
|
@@ -6037,12 +5565,12 @@ final class RichTextEditorView: UIView {
|
|
|
6037
5565
|
}
|
|
6038
5566
|
|
|
6039
5567
|
fileprivate func maximumImageWidthForResizeGesture() -> CGFloat {
|
|
6040
|
-
|
|
5568
|
+
blockSurface.maximumImageWidth()
|
|
6041
5569
|
}
|
|
6042
5570
|
|
|
6043
5571
|
fileprivate func clampedImageSize(_ size: CGSize, maximumWidth: CGFloat? = nil) -> CGSize {
|
|
6044
5572
|
let aspectRatio = max(size.width / max(size.height, 1), 0.1)
|
|
6045
|
-
let maxWidth = max(48, maximumWidth ??
|
|
5573
|
+
let maxWidth = max(48, maximumWidth ?? blockSurface.maximumImageWidth())
|
|
6046
5574
|
let clampedWidth = min(maxWidth, max(48, size.width))
|
|
6047
5575
|
let clampedHeight = max(48, clampedWidth / aspectRatio)
|
|
6048
5576
|
return CGSize(width: clampedWidth, height: clampedHeight)
|
|
@@ -6052,7 +5580,7 @@ final class RichTextEditorView: UIView {
|
|
|
6052
5580
|
let clampedSize = clampedImageSize(size)
|
|
6053
5581
|
let width = max(48, Int(clampedSize.width.rounded()))
|
|
6054
5582
|
let height = max(48, Int(clampedSize.height.rounded()))
|
|
6055
|
-
|
|
5583
|
+
blockSurface.resizeImage(docPos: docPos, width: UInt32(width), height: UInt32(height))
|
|
6056
5584
|
}
|
|
6057
5585
|
|
|
6058
5586
|
private func refreshOverlays() {
|
|
@@ -6063,6 +5591,10 @@ final class RichTextEditorView: UIView {
|
|
|
6063
5591
|
keyPath: .refreshOverlays
|
|
6064
5592
|
)
|
|
6065
5593
|
}
|
|
5594
|
+
blockSurface.isInputFocused = textView.isFirstResponder
|
|
5595
|
+
// Overlay geometry must observe the structural surface's final width;
|
|
5596
|
+
// otherwise a first-pass caret can be cached before text wraps.
|
|
5597
|
+
blockSurface.layoutIfNeeded()
|
|
6066
5598
|
remoteSelectionOverlayView.refresh()
|
|
6067
5599
|
imageResizeOverlayView.refresh()
|
|
6068
5600
|
}
|
|
@@ -6105,12 +5637,27 @@ final class RichTextEditorView: UIView {
|
|
|
6105
5637
|
if imageResizeOverlayView.isOverlayVisible {
|
|
6106
5638
|
return true
|
|
6107
5639
|
}
|
|
6108
|
-
if
|
|
5640
|
+
if blockSurface.selectedImageGeometry() != nil {
|
|
6109
5641
|
return true
|
|
6110
5642
|
}
|
|
6111
5643
|
return false
|
|
6112
5644
|
}
|
|
6113
5645
|
|
|
5646
|
+
fileprivate func blockSurfaceHasImage(at point: CGPoint) -> Bool {
|
|
5647
|
+
blockSurface.hasImage(at: convert(point, to: blockSurface))
|
|
5648
|
+
}
|
|
5649
|
+
|
|
5650
|
+
@discardableResult
|
|
5651
|
+
fileprivate func selectBlockSurfaceImage(at point: CGPoint) -> Bool {
|
|
5652
|
+
guard allowImageResizing else { return false }
|
|
5653
|
+
_ = textView.becomeFirstResponder()
|
|
5654
|
+
let didSelect = blockSurface.selectImage(at: convert(point, to: blockSurface))
|
|
5655
|
+
if didSelect {
|
|
5656
|
+
refreshOverlays()
|
|
5657
|
+
}
|
|
5658
|
+
return didSelect
|
|
5659
|
+
}
|
|
5660
|
+
|
|
6114
5661
|
private enum HostedLayoutTraceKey {
|
|
6115
5662
|
case intrinsicContentSize
|
|
6116
5663
|
case measuredEditorHeight
|