@apollohg/react-native-prose-editor 0.4.0 → 0.4.1
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 +18 -0
- package/android/build.gradle +23 -0
- package/android/src/main/java/com/apollohg/editor/EditorEditText.kt +502 -39
- package/android/src/main/java/com/apollohg/editor/NativeEditorExpoView.kt +56 -28
- package/android/src/main/java/com/apollohg/editor/NativeEditorModule.kt +6 -0
- package/android/src/main/java/com/apollohg/editor/PositionBridge.kt +57 -27
- package/android/src/main/java/com/apollohg/editor/RemoteSelectionOverlayView.kt +147 -78
- package/android/src/main/java/com/apollohg/editor/RenderBridge.kt +249 -71
- package/android/src/main/java/com/apollohg/editor/RichTextEditorView.kt +7 -6
- package/dist/NativeEditorBridge.d.ts +36 -1
- package/dist/NativeEditorBridge.js +173 -94
- package/dist/NativeRichTextEditor.d.ts +2 -0
- package/dist/NativeRichTextEditor.js +160 -53
- package/dist/YjsCollaboration.d.ts +2 -0
- package/dist/YjsCollaboration.js +142 -20
- 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/EditorLayoutManager.swift +3 -3
- package/ios/Generated_editor_core.swift +41 -0
- package/ios/NativeEditorExpoView.swift +43 -11
- package/ios/NativeEditorModule.swift +6 -0
- package/ios/PositionBridge.swift +310 -75
- package/ios/RenderBridge.swift +362 -27
- package/ios/RichTextEditorView.swift +1983 -187
- package/ios/editor_coreFFI/editor_coreFFI.h +33 -0
- package/package.json +11 -2
- 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 +63 -0
|
@@ -120,9 +120,16 @@ private final class RemoteSelectionBadgeLabel: UILabel {
|
|
|
120
120
|
}
|
|
121
121
|
|
|
122
122
|
private final class RemoteSelectionOverlayView: UIView {
|
|
123
|
+
private struct ColoredRect {
|
|
124
|
+
let frame: CGRect
|
|
125
|
+
let color: UIColor
|
|
126
|
+
}
|
|
127
|
+
|
|
123
128
|
weak var textView: EditorTextView?
|
|
124
129
|
private var editorId: UInt64 = 0
|
|
125
130
|
private var selections: [RemoteSelectionDecoration] = []
|
|
131
|
+
private var selectionViews: [UIView] = []
|
|
132
|
+
private var caretViews: [UIView] = []
|
|
126
133
|
|
|
127
134
|
override init(frame: CGRect) {
|
|
128
135
|
super.init(frame: frame)
|
|
@@ -146,20 +153,26 @@ private final class RemoteSelectionOverlayView: UIView {
|
|
|
146
153
|
}
|
|
147
154
|
|
|
148
155
|
func refresh() {
|
|
149
|
-
subviews.forEach { $0.removeFromSuperview() }
|
|
150
156
|
guard editorId != 0,
|
|
151
157
|
let textView
|
|
152
158
|
else {
|
|
159
|
+
syncSelectionViews(with: [])
|
|
160
|
+
syncCaretViews(with: [])
|
|
153
161
|
return
|
|
154
162
|
}
|
|
155
163
|
|
|
164
|
+
var selectionRects: [ColoredRect] = []
|
|
165
|
+
var caretRects: [ColoredRect] = []
|
|
166
|
+
|
|
156
167
|
for selection in selections {
|
|
157
168
|
let geometry = geometry(for: selection, in: textView)
|
|
158
169
|
for rect in geometry.selectionRects {
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
170
|
+
selectionRects.append(
|
|
171
|
+
ColoredRect(
|
|
172
|
+
frame: rect.integral,
|
|
173
|
+
color: selection.color.withAlphaComponent(0.18)
|
|
174
|
+
)
|
|
175
|
+
)
|
|
163
176
|
}
|
|
164
177
|
|
|
165
178
|
guard selection.isFocused,
|
|
@@ -168,16 +181,29 @@ private final class RemoteSelectionOverlayView: UIView {
|
|
|
168
181
|
continue
|
|
169
182
|
}
|
|
170
183
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
184
|
+
caretRects.append(
|
|
185
|
+
ColoredRect(
|
|
186
|
+
frame: CGRect(
|
|
187
|
+
x: round(caretRect.minX),
|
|
188
|
+
y: round(caretRect.minY),
|
|
189
|
+
width: max(2, round(caretRect.width)),
|
|
190
|
+
height: round(caretRect.height)
|
|
191
|
+
),
|
|
192
|
+
color: selection.color
|
|
193
|
+
)
|
|
194
|
+
)
|
|
180
195
|
}
|
|
196
|
+
|
|
197
|
+
syncSelectionViews(with: selectionRects)
|
|
198
|
+
syncCaretViews(with: caretRects)
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
var hasVisibleDecorations: Bool {
|
|
202
|
+
selectionViews.contains { !$0.isHidden } || caretViews.contains { !$0.isHidden }
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
var hasSelectionsOrVisibleDecorations: Bool {
|
|
206
|
+
!selections.isEmpty || hasVisibleDecorations
|
|
181
207
|
}
|
|
182
208
|
|
|
183
209
|
private func geometry(
|
|
@@ -251,6 +277,49 @@ private final class RemoteSelectionOverlayView: UIView {
|
|
|
251
277
|
|
|
252
278
|
return directRect
|
|
253
279
|
}
|
|
280
|
+
|
|
281
|
+
private func syncSelectionViews(with rects: [ColoredRect]) {
|
|
282
|
+
syncViews(rects, existingViews: &selectionViews) { view, rect in
|
|
283
|
+
view.frame = rect.frame
|
|
284
|
+
view.backgroundColor = rect.color
|
|
285
|
+
view.layer.cornerRadius = 3
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
private func syncCaretViews(with rects: [ColoredRect]) {
|
|
290
|
+
syncViews(rects, existingViews: &caretViews) { view, rect in
|
|
291
|
+
view.frame = rect.frame
|
|
292
|
+
view.backgroundColor = rect.color
|
|
293
|
+
view.layer.cornerRadius = view.bounds.width / 2
|
|
294
|
+
bringSubviewToFront(view)
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
private func syncViews(
|
|
299
|
+
_ rects: [ColoredRect],
|
|
300
|
+
existingViews: inout [UIView],
|
|
301
|
+
configure: (UIView, ColoredRect) -> Void
|
|
302
|
+
) {
|
|
303
|
+
while existingViews.count < rects.count {
|
|
304
|
+
let view = UIView(frame: .zero)
|
|
305
|
+
view.isUserInteractionEnabled = false
|
|
306
|
+
addSubview(view)
|
|
307
|
+
existingViews.append(view)
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
for (index, rect) in rects.enumerated() {
|
|
311
|
+
let view = existingViews[index]
|
|
312
|
+
view.isHidden = false
|
|
313
|
+
configure(view, rect)
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
if existingViews.count > rects.count {
|
|
317
|
+
for view in existingViews[rects.count...] {
|
|
318
|
+
view.isHidden = true
|
|
319
|
+
view.frame = .zero
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
254
323
|
}
|
|
255
324
|
|
|
256
325
|
private final class ImageTapOverlayView: UIView {
|
|
@@ -427,6 +496,10 @@ private final class ImageResizeOverlayView: UIView {
|
|
|
427
496
|
isHidden ? nil : currentRect
|
|
428
497
|
}
|
|
429
498
|
|
|
499
|
+
var isOverlayVisible: Bool {
|
|
500
|
+
!isHidden
|
|
501
|
+
}
|
|
502
|
+
|
|
430
503
|
var previewHasImageForTesting: Bool {
|
|
431
504
|
!previewImageView.isHidden && previewImageView.image != nil
|
|
432
505
|
}
|
|
@@ -640,7 +713,117 @@ private final class ImageResizeOverlayView: UIView {
|
|
|
640
713
|
/// fast enough for main-thread use. If profiling shows otherwise, we can
|
|
641
714
|
/// dispatch to a serial queue and batch updates.
|
|
642
715
|
final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerDelegate {
|
|
643
|
-
private static let emptyBlockPlaceholderScalar = UnicodeScalar(0x200B)
|
|
716
|
+
private static let emptyBlockPlaceholderScalar = UnicodeScalar(0x200B)!
|
|
717
|
+
|
|
718
|
+
override var undoManager: UndoManager? { nil }
|
|
719
|
+
|
|
720
|
+
struct ApplyUpdateTrace {
|
|
721
|
+
let attemptedPatch: Bool
|
|
722
|
+
let usedPatch: Bool
|
|
723
|
+
let usedSmallPatchTextMutation: Bool
|
|
724
|
+
let applyRenderReplaceUtf16Length: Int
|
|
725
|
+
let applyRenderReplacementUtf16Length: Int
|
|
726
|
+
let parseNanos: UInt64
|
|
727
|
+
let resolveRenderBlocksNanos: UInt64
|
|
728
|
+
let patchEligibilityNanos: UInt64
|
|
729
|
+
let patchTrimNanos: UInt64
|
|
730
|
+
let patchMetadataNanos: UInt64
|
|
731
|
+
let buildRenderNanos: UInt64
|
|
732
|
+
let applyRenderNanos: UInt64
|
|
733
|
+
let selectionNanos: UInt64
|
|
734
|
+
let postApplyNanos: UInt64
|
|
735
|
+
let totalNanos: UInt64
|
|
736
|
+
let applyRenderTextMutationNanos: UInt64
|
|
737
|
+
let applyRenderBeginEditingNanos: UInt64
|
|
738
|
+
let applyRenderEndEditingNanos: UInt64
|
|
739
|
+
let applyRenderStringMutationNanos: UInt64
|
|
740
|
+
let applyRenderAttributeMutationNanos: UInt64
|
|
741
|
+
let applyRenderAuthorizedTextNanos: UInt64
|
|
742
|
+
let applyRenderCacheInvalidationNanos: UInt64
|
|
743
|
+
let selectionResolveNanos: UInt64
|
|
744
|
+
let selectionAssignmentNanos: UInt64
|
|
745
|
+
let selectionChromeNanos: UInt64
|
|
746
|
+
let postApplyTypingAttributesNanos: UInt64
|
|
747
|
+
let postApplyHeightNotifyNanos: UInt64
|
|
748
|
+
let postApplyHeightNotifyMeasureNanos: UInt64
|
|
749
|
+
let postApplyHeightNotifyCallbackNanos: UInt64
|
|
750
|
+
let postApplyHeightNotifyEnsureLayoutNanos: UInt64
|
|
751
|
+
let postApplyHeightNotifyUsedRectNanos: UInt64
|
|
752
|
+
let postApplyHeightNotifyContentSizeNanos: UInt64
|
|
753
|
+
let postApplyHeightNotifySizeThatFitsNanos: UInt64
|
|
754
|
+
let postApplySelectionOrContentCallbackNanos: UInt64
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
private struct PatchApplyTrace {
|
|
758
|
+
let applied: Bool
|
|
759
|
+
let eligibilityNanos: UInt64
|
|
760
|
+
let trimNanos: UInt64
|
|
761
|
+
let metadataNanos: UInt64
|
|
762
|
+
let buildRenderNanos: UInt64
|
|
763
|
+
let applyRenderNanos: UInt64
|
|
764
|
+
let applyRenderReplaceUtf16Length: Int
|
|
765
|
+
let applyRenderReplacementUtf16Length: Int
|
|
766
|
+
let applyRenderTextMutationNanos: UInt64
|
|
767
|
+
let applyRenderBeginEditingNanos: UInt64
|
|
768
|
+
let applyRenderEndEditingNanos: UInt64
|
|
769
|
+
let applyRenderStringMutationNanos: UInt64
|
|
770
|
+
let applyRenderAttributeMutationNanos: UInt64
|
|
771
|
+
let applyRenderAuthorizedTextNanos: UInt64
|
|
772
|
+
let applyRenderCacheInvalidationNanos: UInt64
|
|
773
|
+
let usedSmallPatchTextMutation: Bool
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
private struct ApplyRenderTrace {
|
|
777
|
+
let totalNanos: UInt64
|
|
778
|
+
let replaceUtf16Length: Int
|
|
779
|
+
let replacementUtf16Length: Int
|
|
780
|
+
let textMutationNanos: UInt64
|
|
781
|
+
let beginEditingNanos: UInt64
|
|
782
|
+
let endEditingNanos: UInt64
|
|
783
|
+
let stringMutationNanos: UInt64
|
|
784
|
+
let attributeMutationNanos: UInt64
|
|
785
|
+
let authorizedTextNanos: UInt64
|
|
786
|
+
let cacheInvalidationNanos: UInt64
|
|
787
|
+
let usedSmallPatchTextMutation: Bool
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
private struct SelectionApplyTrace {
|
|
791
|
+
let totalNanos: UInt64
|
|
792
|
+
let resolveNanos: UInt64
|
|
793
|
+
let assignmentNanos: UInt64
|
|
794
|
+
let chromeNanos: UInt64
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
private struct PostApplyTrace {
|
|
798
|
+
let totalNanos: UInt64
|
|
799
|
+
let typingAttributesNanos: UInt64
|
|
800
|
+
let heightNotifyNanos: UInt64
|
|
801
|
+
let heightNotifyMeasureNanos: UInt64
|
|
802
|
+
let heightNotifyCallbackNanos: UInt64
|
|
803
|
+
let heightNotifyEnsureLayoutNanos: UInt64
|
|
804
|
+
let heightNotifyUsedRectNanos: UInt64
|
|
805
|
+
let heightNotifyContentSizeNanos: UInt64
|
|
806
|
+
let heightNotifySizeThatFitsNanos: UInt64
|
|
807
|
+
let selectionOrContentCallbackNanos: UInt64
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
private struct TopLevelChildMetadata {
|
|
811
|
+
var startOffset: Int
|
|
812
|
+
var containsAttachment: Bool
|
|
813
|
+
var containsPositionAdjustments: Bool
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
private struct TopLevelChildMetadataSlice {
|
|
817
|
+
let startIndex: Int
|
|
818
|
+
let entries: [TopLevelChildMetadata]
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
private enum PositionCacheUpdate {
|
|
822
|
+
case scan
|
|
823
|
+
case invalidate
|
|
824
|
+
case plainText
|
|
825
|
+
case attributed
|
|
826
|
+
}
|
|
644
827
|
|
|
645
828
|
// MARK: - Properties
|
|
646
829
|
|
|
@@ -657,10 +840,20 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
657
840
|
var allowImageResizing = true
|
|
658
841
|
|
|
659
842
|
/// The base font used for unstyled text. Configurable from React props.
|
|
660
|
-
var baseFont: UIFont = .systemFont(ofSize: 16)
|
|
843
|
+
var baseFont: UIFont = .systemFont(ofSize: 16) {
|
|
844
|
+
didSet {
|
|
845
|
+
placeholderLabel.font = resolvedDefaultFont()
|
|
846
|
+
renderAppearanceRevision &+= 1
|
|
847
|
+
invalidateAutoGrowHeightMeasurement()
|
|
848
|
+
}
|
|
849
|
+
}
|
|
661
850
|
|
|
662
851
|
/// The base text color. Configurable from React props.
|
|
663
|
-
var baseTextColor: UIColor = .label
|
|
852
|
+
var baseTextColor: UIColor = .label {
|
|
853
|
+
didSet {
|
|
854
|
+
renderAppearanceRevision &+= 1
|
|
855
|
+
}
|
|
856
|
+
}
|
|
664
857
|
|
|
665
858
|
/// The base background color before theme overrides.
|
|
666
859
|
var baseBackgroundColor: UIColor = .systemBackground
|
|
@@ -669,6 +862,7 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
669
862
|
/// Optional render theme supplied by React.
|
|
670
863
|
var theme: EditorTheme? {
|
|
671
864
|
didSet {
|
|
865
|
+
renderAppearanceRevision &+= 1
|
|
672
866
|
placeholderLabel.font = resolvedDefaultFont()
|
|
673
867
|
backgroundColor = theme?.backgroundColor ?? baseBackgroundColor
|
|
674
868
|
if let contentInsets = theme?.contentInsets {
|
|
@@ -681,6 +875,7 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
681
875
|
} else {
|
|
682
876
|
textContainerInset = baseTextContainerInset
|
|
683
877
|
}
|
|
878
|
+
invalidateAutoGrowHeightMeasurement()
|
|
684
879
|
setNeedsLayout()
|
|
685
880
|
}
|
|
686
881
|
}
|
|
@@ -689,22 +884,42 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
689
884
|
didSet {
|
|
690
885
|
guard oldValue != heightBehavior else { return }
|
|
691
886
|
isScrollEnabled = heightBehavior == .fixed
|
|
887
|
+
invalidateAutoGrowHeightMeasurement()
|
|
692
888
|
invalidateIntrinsicContentSize()
|
|
693
889
|
notifyHeightChangeIfNeeded(force: true)
|
|
694
890
|
}
|
|
695
891
|
}
|
|
696
892
|
|
|
697
|
-
var onHeightMayChange: (() -> Void)?
|
|
893
|
+
var onHeightMayChange: ((CGFloat) -> Void)?
|
|
698
894
|
var onViewportMayChange: (() -> Void)?
|
|
699
895
|
var onSelectionOrContentMayChange: (() -> Void)?
|
|
700
896
|
private var lastAutoGrowMeasuredHeight: CGFloat = 0
|
|
897
|
+
private var lastAutoGrowMeasuredWidth: CGFloat = 0
|
|
898
|
+
private var autoGrowHostHeight: CGFloat = 0
|
|
899
|
+
private var autoGrowHeightCheckIsDirty = true
|
|
900
|
+
private var lastHeightNotifyMeasureNanosForTesting: UInt64 = 0
|
|
901
|
+
private var lastHeightNotifyCallbackNanosForTesting: UInt64 = 0
|
|
902
|
+
private var lastHeightNotifyEnsureLayoutNanosForTesting: UInt64 = 0
|
|
903
|
+
private var lastHeightNotifyUsedRectNanosForTesting: UInt64 = 0
|
|
904
|
+
private var lastHeightNotifyContentSizeNanosForTesting: UInt64 = 0
|
|
905
|
+
private var lastHeightNotifySizeThatFitsNanosForTesting: UInt64 = 0
|
|
701
906
|
|
|
702
907
|
/// Delegate for editor events.
|
|
703
908
|
weak var editorDelegate: EditorTextViewDelegate?
|
|
704
909
|
|
|
705
910
|
/// The plain text from the last Rust render, used by the reconciliation
|
|
706
911
|
/// fallback to detect unauthorized text storage mutations.
|
|
707
|
-
private
|
|
912
|
+
private var lastAuthorizedTextStorage = NSMutableString()
|
|
913
|
+
private var lastAuthorizedText: String {
|
|
914
|
+
lastAuthorizedTextStorage as String
|
|
915
|
+
}
|
|
916
|
+
private(set) var lastRenderAppliedPatchForTesting: Bool = false
|
|
917
|
+
var captureApplyUpdateTraceForTesting = false
|
|
918
|
+
private(set) var lastApplyUpdateTraceForTesting: ApplyUpdateTrace?
|
|
919
|
+
private var currentRenderBlocks: [[[String: Any]]]? = nil
|
|
920
|
+
private var currentTopLevelChildMetadata: [TopLevelChildMetadata]? = nil
|
|
921
|
+
private var renderAppearanceRevision: UInt64 = 1
|
|
922
|
+
private var lastAppliedRenderAppearanceRevision: UInt64 = 0
|
|
708
923
|
|
|
709
924
|
/// Number of times the reconciliation fallback has fired. Exposed for
|
|
710
925
|
/// monitoring / kill-condition telemetry.
|
|
@@ -795,7 +1010,9 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
795
1010
|
|
|
796
1011
|
private func commonInit() {
|
|
797
1012
|
textContainer.widthTracksTextView = true
|
|
798
|
-
|
|
1013
|
+
// Large documents edit more smoothly when TextKit can invalidate and
|
|
1014
|
+
// relayout only the touched region instead of forcing contiguous layout.
|
|
1015
|
+
editorLayoutManager.allowsNonContiguousLayout = true
|
|
799
1016
|
NotificationCenter.default.addObserver(
|
|
800
1017
|
self,
|
|
801
1018
|
selector: #selector(handleImageAttachmentDidLoad(_:)),
|
|
@@ -855,6 +1072,7 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
855
1072
|
textStorage.beginEditing()
|
|
856
1073
|
textStorage.edited(.editedAttributes, range: NSRange(location: 0, length: textStorage.length), changeInLength: 0)
|
|
857
1074
|
textStorage.endEditing()
|
|
1075
|
+
invalidateAutoGrowHeightMeasurement()
|
|
858
1076
|
setNeedsLayout()
|
|
859
1077
|
invalidateIntrinsicContentSize()
|
|
860
1078
|
onSelectionOrContentMayChange?()
|
|
@@ -880,28 +1098,43 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
880
1098
|
|
|
881
1099
|
override func layoutSubviews() {
|
|
882
1100
|
super.layoutSubviews()
|
|
883
|
-
installImageSelectionTapDependencies()
|
|
884
1101
|
let placeholderX = textContainerInset.left + textContainer.lineFragmentPadding
|
|
885
1102
|
let placeholderY = textContainerInset.top
|
|
886
1103
|
let placeholderWidth = max(
|
|
887
1104
|
0,
|
|
888
1105
|
bounds.width - textContainerInset.left - textContainerInset.right - 2 * textContainer.lineFragmentPadding
|
|
889
1106
|
)
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
1107
|
+
if placeholderLabel.isHidden {
|
|
1108
|
+
placeholderLabel.frame = CGRect(
|
|
1109
|
+
x: placeholderX,
|
|
1110
|
+
y: placeholderY,
|
|
1111
|
+
width: placeholderWidth,
|
|
1112
|
+
height: 0
|
|
1113
|
+
)
|
|
1114
|
+
} else {
|
|
1115
|
+
let maxPlaceholderHeight = max(
|
|
1116
|
+
0,
|
|
1117
|
+
bounds.height - textContainerInset.top - textContainerInset.bottom
|
|
1118
|
+
)
|
|
1119
|
+
let fittedHeight = placeholderLabel.sizeThatFits(
|
|
1120
|
+
CGSize(width: placeholderWidth, height: CGFloat.greatestFiniteMagnitude)
|
|
1121
|
+
).height
|
|
1122
|
+
placeholderLabel.frame = CGRect(
|
|
1123
|
+
x: placeholderX,
|
|
1124
|
+
y: placeholderY,
|
|
1125
|
+
width: placeholderWidth,
|
|
1126
|
+
height: min(maxPlaceholderHeight, ceil(fittedHeight))
|
|
1127
|
+
)
|
|
1128
|
+
}
|
|
903
1129
|
if heightBehavior == .autoGrow, !isPreviewingImageResize {
|
|
904
|
-
|
|
1130
|
+
let currentWidth = ceil(bounds.width)
|
|
1131
|
+
if abs(currentWidth - lastAutoGrowMeasuredWidth) > 0.5 {
|
|
1132
|
+
autoGrowHeightCheckIsDirty = true
|
|
1133
|
+
lastAutoGrowMeasuredWidth = currentWidth
|
|
1134
|
+
}
|
|
1135
|
+
if autoGrowHeightCheckIsDirty {
|
|
1136
|
+
notifyHeightChangeIfNeeded()
|
|
1137
|
+
}
|
|
905
1138
|
}
|
|
906
1139
|
if !isPreviewingImageResize {
|
|
907
1140
|
onViewportMayChange?()
|
|
@@ -1031,7 +1264,11 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
1031
1264
|
let fullRange = NSRange(location: 0, length: textStorage.length)
|
|
1032
1265
|
var resolvedRange: NSRange?
|
|
1033
1266
|
|
|
1034
|
-
textStorage.enumerateAttribute(
|
|
1267
|
+
textStorage.enumerateAttribute(
|
|
1268
|
+
.attachment,
|
|
1269
|
+
in: fullRange,
|
|
1270
|
+
options: [.longestEffectiveRangeNotRequired]
|
|
1271
|
+
) { value, range, stop in
|
|
1035
1272
|
guard value is NSTextAttachment, range.length > 0 else { return }
|
|
1036
1273
|
|
|
1037
1274
|
let attrs = textStorage.attributes(at: range.location, effectiveRange: nil)
|
|
@@ -1064,6 +1301,14 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
1064
1301
|
placeholderLabel.frame
|
|
1065
1302
|
}
|
|
1066
1303
|
|
|
1304
|
+
func lastRenderAppliedPatch() -> Bool {
|
|
1305
|
+
lastRenderAppliedPatchForTesting
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
func lastApplyUpdateTrace() -> ApplyUpdateTrace? {
|
|
1309
|
+
lastApplyUpdateTraceForTesting
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1067
1312
|
func blockquoteStripeRectsForTesting() -> [CGRect] {
|
|
1068
1313
|
editorLayoutManager.blockquoteStripeRectsForTesting(in: textStorage)
|
|
1069
1314
|
}
|
|
@@ -1158,8 +1403,18 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
1158
1403
|
)
|
|
1159
1404
|
}
|
|
1160
1405
|
|
|
1406
|
+
func measuredAutoGrowHeightForTesting(width: CGFloat) -> CGFloat {
|
|
1407
|
+
measuredAutoGrowHeight(forWidth: width)
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1161
1410
|
private func resolvedCaretReferenceRect(for position: UITextPosition) -> CGRect {
|
|
1162
1411
|
let directRect = super.caretRect(for: position)
|
|
1412
|
+
if let horizontalRuleRect = resolvedHorizontalRuleAdjacentCaretRect(
|
|
1413
|
+
for: position,
|
|
1414
|
+
directRect: directRect
|
|
1415
|
+
) {
|
|
1416
|
+
return horizontalRuleRect
|
|
1417
|
+
}
|
|
1163
1418
|
guard directRect.height <= 0 || directRect.isEmpty else {
|
|
1164
1419
|
return directRect
|
|
1165
1420
|
}
|
|
@@ -1197,6 +1452,48 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
1197
1452
|
return directRect
|
|
1198
1453
|
}
|
|
1199
1454
|
|
|
1455
|
+
private func resolvedHorizontalRuleAdjacentCaretRect(
|
|
1456
|
+
for position: UITextPosition,
|
|
1457
|
+
directRect: CGRect
|
|
1458
|
+
) -> CGRect? {
|
|
1459
|
+
guard textStorage.length > 0 else { return nil }
|
|
1460
|
+
|
|
1461
|
+
let utf16Offset = offset(from: beginningOfDocument, to: position)
|
|
1462
|
+
let caretWidth = max(directRect.width, 2)
|
|
1463
|
+
|
|
1464
|
+
if isHorizontalRuleAttachment(at: utf16Offset),
|
|
1465
|
+
let previousCharacterIndex = nearestVisibleCharacterIndex(
|
|
1466
|
+
from: utf16Offset - 1,
|
|
1467
|
+
direction: -1
|
|
1468
|
+
),
|
|
1469
|
+
let previousRect = visibleSelectionRect(forCharacterAt: previousCharacterIndex)
|
|
1470
|
+
{
|
|
1471
|
+
return CGRect(
|
|
1472
|
+
x: previousRect.maxX,
|
|
1473
|
+
y: previousRect.minY,
|
|
1474
|
+
width: caretWidth,
|
|
1475
|
+
height: max(directRect.height, previousRect.height)
|
|
1476
|
+
)
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1479
|
+
if isHorizontalRuleAttachment(at: utf16Offset - 1),
|
|
1480
|
+
let nextCharacterIndex = nearestVisibleCharacterIndex(
|
|
1481
|
+
from: utf16Offset,
|
|
1482
|
+
direction: 1
|
|
1483
|
+
),
|
|
1484
|
+
let nextRect = visibleSelectionRect(forCharacterAt: nextCharacterIndex)
|
|
1485
|
+
{
|
|
1486
|
+
return CGRect(
|
|
1487
|
+
x: nextRect.minX,
|
|
1488
|
+
y: nextRect.minY,
|
|
1489
|
+
width: caretWidth,
|
|
1490
|
+
height: max(directRect.height, nextRect.height)
|
|
1491
|
+
)
|
|
1492
|
+
}
|
|
1493
|
+
|
|
1494
|
+
return nil
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1200
1497
|
// MARK: - Editor Binding
|
|
1201
1498
|
|
|
1202
1499
|
/// Bind this text view to a Rust editor instance and apply initial content.
|
|
@@ -1208,12 +1505,13 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
1208
1505
|
editorId = id
|
|
1209
1506
|
|
|
1210
1507
|
if let html = initialHTML, !html.isEmpty {
|
|
1211
|
-
|
|
1212
|
-
|
|
1508
|
+
_ = editorSetHtml(id: editorId, html: html)
|
|
1509
|
+
let stateJSON = editorGetCurrentState(id: editorId)
|
|
1510
|
+
applyUpdateJSON(stateJSON, notifyDelegate: false)
|
|
1213
1511
|
} else {
|
|
1214
1512
|
// Pull current state from Rust (content may already be loaded via bridge).
|
|
1215
1513
|
let stateJSON = editorGetCurrentState(id: editorId)
|
|
1216
|
-
applyUpdateJSON(stateJSON)
|
|
1514
|
+
applyUpdateJSON(stateJSON, notifyDelegate: false)
|
|
1217
1515
|
}
|
|
1218
1516
|
}
|
|
1219
1517
|
|
|
@@ -1338,7 +1636,12 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
1338
1636
|
} else {
|
|
1339
1637
|
// Cursor: delete one grapheme cluster backward.
|
|
1340
1638
|
let cursorPos = PositionBridge.textViewToScalar(selectedRange.start, in: self)
|
|
1341
|
-
|
|
1639
|
+
if cursorPos == 0 {
|
|
1640
|
+
performInterceptedInput {
|
|
1641
|
+
deleteBackwardAtSelectionScalarInRust(anchor: cursorPos, head: cursorPos)
|
|
1642
|
+
}
|
|
1643
|
+
return
|
|
1644
|
+
}
|
|
1342
1645
|
|
|
1343
1646
|
let cursorUtf16Offset = offset(from: beginningOfDocument, to: selectedRange.start)
|
|
1344
1647
|
if let marker = PositionBridge.virtualListMarker(
|
|
@@ -1380,7 +1683,11 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
1380
1683
|
let prevScalar = PositionBridge.textViewToScalar(prevPos, in: self)
|
|
1381
1684
|
|
|
1382
1685
|
performInterceptedInput {
|
|
1383
|
-
|
|
1686
|
+
if prevScalar < cursorPos {
|
|
1687
|
+
deleteScalarRangeInRust(from: prevScalar, to: cursorPos)
|
|
1688
|
+
} else {
|
|
1689
|
+
deleteBackwardAtSelectionScalarInRust(anchor: cursorPos, head: cursorPos)
|
|
1690
|
+
}
|
|
1384
1691
|
}
|
|
1385
1692
|
}
|
|
1386
1693
|
}
|
|
@@ -1639,6 +1946,7 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
1639
1946
|
/// internally during tap handling and word-boundary resolution.
|
|
1640
1947
|
func textViewDidChangeSelection(_ textView: UITextView) {
|
|
1641
1948
|
guard textView === self else { return }
|
|
1949
|
+
guard !isApplyingRustState else { return }
|
|
1642
1950
|
refreshNativeSelectionChromeVisibility()
|
|
1643
1951
|
onSelectionOrContentMayChange?()
|
|
1644
1952
|
scheduleSelectionSync()
|
|
@@ -1776,13 +2084,20 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
1776
2084
|
}
|
|
1777
2085
|
|
|
1778
2086
|
private func refreshNativeSelectionChromeVisibility() {
|
|
1779
|
-
let hidden =
|
|
2087
|
+
let hidden = selectedImageSelectionState() != nil
|
|
1780
2088
|
if !hidden, tintColor.cgColor.alpha > 0 {
|
|
1781
2089
|
visibleSelectionTintColor = tintColor
|
|
1782
2090
|
}
|
|
1783
2091
|
setNativeSelectionChromeHidden(hidden)
|
|
1784
2092
|
}
|
|
1785
2093
|
|
|
2094
|
+
private func showNativeSelectionChromeIfNeeded() {
|
|
2095
|
+
if tintColor.cgColor.alpha > 0 {
|
|
2096
|
+
visibleSelectionTintColor = tintColor
|
|
2097
|
+
}
|
|
2098
|
+
setNativeSelectionChromeHidden(false)
|
|
2099
|
+
}
|
|
2100
|
+
|
|
1786
2101
|
func refreshSelectionVisualState() {
|
|
1787
2102
|
refreshNativeSelectionChromeVisibility()
|
|
1788
2103
|
onSelectionOrContentMayChange?()
|
|
@@ -1877,12 +2192,143 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
1877
2192
|
guard heightBehavior == .autoGrow else { return }
|
|
1878
2193
|
let width = bounds.width > 0 ? bounds.width : UIScreen.main.bounds.width
|
|
1879
2194
|
guard width > 0 else { return }
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
2195
|
+
if !force {
|
|
2196
|
+
let measuredWidth = ceil(width)
|
|
2197
|
+
if !autoGrowHeightCheckIsDirty && abs(measuredWidth - lastAutoGrowMeasuredWidth) <= 0.5 {
|
|
2198
|
+
return
|
|
2199
|
+
}
|
|
2200
|
+
}
|
|
2201
|
+
lastHeightNotifyEnsureLayoutNanosForTesting = 0
|
|
2202
|
+
lastHeightNotifyUsedRectNanosForTesting = 0
|
|
2203
|
+
lastHeightNotifyContentSizeNanosForTesting = 0
|
|
2204
|
+
lastHeightNotifySizeThatFitsNanosForTesting = 0
|
|
2205
|
+
let measurementStartedAt = DispatchTime.now().uptimeNanoseconds
|
|
2206
|
+
let measuredHeight = measuredAutoGrowHeight(forWidth: width)
|
|
2207
|
+
lastHeightNotifyMeasureNanosForTesting =
|
|
2208
|
+
DispatchTime.now().uptimeNanoseconds - measurementStartedAt
|
|
2209
|
+
autoGrowHeightCheckIsDirty = false
|
|
2210
|
+
lastAutoGrowMeasuredWidth = ceil(width)
|
|
1883
2211
|
guard force || abs(measuredHeight - lastAutoGrowMeasuredHeight) > 0.5 else { return }
|
|
1884
2212
|
lastAutoGrowMeasuredHeight = measuredHeight
|
|
1885
|
-
|
|
2213
|
+
let callbackStartedAt = DispatchTime.now().uptimeNanoseconds
|
|
2214
|
+
onHeightMayChange?(measuredHeight)
|
|
2215
|
+
lastHeightNotifyCallbackNanosForTesting =
|
|
2216
|
+
DispatchTime.now().uptimeNanoseconds - callbackStartedAt
|
|
2217
|
+
}
|
|
2218
|
+
|
|
2219
|
+
private func measuredAutoGrowHeight(forWidth width: CGFloat) -> CGFloat {
|
|
2220
|
+
guard width > 0 else { return 0 }
|
|
2221
|
+
|
|
2222
|
+
if abs(bounds.width - width) <= 0.5 {
|
|
2223
|
+
let currentHeight = ceil(bounds.height)
|
|
2224
|
+
let ensureLayoutStartedAt = DispatchTime.now().uptimeNanoseconds
|
|
2225
|
+
editorLayoutManager.ensureLayout(for: textContainer)
|
|
2226
|
+
lastHeightNotifyEnsureLayoutNanosForTesting =
|
|
2227
|
+
DispatchTime.now().uptimeNanoseconds - ensureLayoutStartedAt
|
|
2228
|
+
|
|
2229
|
+
let usedRectStartedAt = DispatchTime.now().uptimeNanoseconds
|
|
2230
|
+
var usedRect = editorLayoutManager.usedRect(for: textContainer)
|
|
2231
|
+
let extraLineFragmentRect = editorLayoutManager.extraLineFragmentRect
|
|
2232
|
+
if !extraLineFragmentRect.isEmpty {
|
|
2233
|
+
usedRect = usedRect.union(extraLineFragmentRect)
|
|
2234
|
+
}
|
|
2235
|
+
lastHeightNotifyUsedRectNanosForTesting =
|
|
2236
|
+
DispatchTime.now().uptimeNanoseconds - usedRectStartedAt
|
|
2237
|
+
let layoutHeight = ceil(usedRect.height + textContainerInset.top + textContainerInset.bottom)
|
|
2238
|
+
|
|
2239
|
+
let contentSizeStartedAt = DispatchTime.now().uptimeNanoseconds
|
|
2240
|
+
let contentHeight = ceil(contentSize.height)
|
|
2241
|
+
lastHeightNotifyContentSizeNanosForTesting =
|
|
2242
|
+
DispatchTime.now().uptimeNanoseconds - contentSizeStartedAt
|
|
2243
|
+
if currentHeight > 0 {
|
|
2244
|
+
if layoutHeight > currentHeight + 0.5 {
|
|
2245
|
+
return layoutHeight
|
|
2246
|
+
}
|
|
2247
|
+
let hostIsTrackingMeasuredHeight =
|
|
2248
|
+
autoGrowHostHeight > 0
|
|
2249
|
+
&& abs(currentHeight - ceil(autoGrowHostHeight)) <= 1.0
|
|
2250
|
+
guard hostIsTrackingMeasuredHeight else {
|
|
2251
|
+
return layoutHeight
|
|
2252
|
+
}
|
|
2253
|
+
let measuredFromLayout = max(layoutHeight, contentHeight)
|
|
2254
|
+
if measuredFromLayout > currentHeight + 0.5 {
|
|
2255
|
+
return measuredFromLayout
|
|
2256
|
+
}
|
|
2257
|
+
let sizeThatFitsStartedAt = DispatchTime.now().uptimeNanoseconds
|
|
2258
|
+
let fittedHeight = ceil(
|
|
2259
|
+
sizeThatFits(
|
|
2260
|
+
CGSize(width: width, height: CGFloat.greatestFiniteMagnitude)
|
|
2261
|
+
).height
|
|
2262
|
+
)
|
|
2263
|
+
lastHeightNotifySizeThatFitsNanosForTesting =
|
|
2264
|
+
DispatchTime.now().uptimeNanoseconds - sizeThatFitsStartedAt
|
|
2265
|
+
if fittedHeight > currentHeight + 0.5 {
|
|
2266
|
+
return max(measuredFromLayout, fittedHeight)
|
|
2267
|
+
}
|
|
2268
|
+
return layoutHeight
|
|
2269
|
+
}
|
|
2270
|
+
return max(layoutHeight, contentHeight)
|
|
2271
|
+
}
|
|
2272
|
+
|
|
2273
|
+
let sizeThatFitsStartedAt = DispatchTime.now().uptimeNanoseconds
|
|
2274
|
+
let fittedHeight = ceil(
|
|
2275
|
+
sizeThatFits(CGSize(width: width, height: CGFloat.greatestFiniteMagnitude)).height
|
|
2276
|
+
)
|
|
2277
|
+
lastHeightNotifySizeThatFitsNanosForTesting =
|
|
2278
|
+
DispatchTime.now().uptimeNanoseconds - sizeThatFitsStartedAt
|
|
2279
|
+
return fittedHeight
|
|
2280
|
+
}
|
|
2281
|
+
|
|
2282
|
+
func updateAutoGrowHostHeight(_ height: CGFloat) {
|
|
2283
|
+
autoGrowHostHeight = max(0, ceil(height))
|
|
2284
|
+
}
|
|
2285
|
+
|
|
2286
|
+
private func invalidateAutoGrowHeightMeasurement() {
|
|
2287
|
+
autoGrowHeightCheckIsDirty = true
|
|
2288
|
+
lastAutoGrowMeasuredWidth = 0
|
|
2289
|
+
}
|
|
2290
|
+
|
|
2291
|
+
private func performPostApplyMaintenance(forceHeightNotify: Bool = false) -> PostApplyTrace {
|
|
2292
|
+
let totalStartedAt = DispatchTime.now().uptimeNanoseconds
|
|
2293
|
+
|
|
2294
|
+
let typingAttributesStartedAt = totalStartedAt
|
|
2295
|
+
refreshTypingAttributesForSelection()
|
|
2296
|
+
let typingAttributesNanos = DispatchTime.now().uptimeNanoseconds - typingAttributesStartedAt
|
|
2297
|
+
|
|
2298
|
+
let heightNotifyStartedAt = DispatchTime.now().uptimeNanoseconds
|
|
2299
|
+
lastHeightNotifyMeasureNanosForTesting = 0
|
|
2300
|
+
lastHeightNotifyCallbackNanosForTesting = 0
|
|
2301
|
+
lastHeightNotifyEnsureLayoutNanosForTesting = 0
|
|
2302
|
+
lastHeightNotifyUsedRectNanosForTesting = 0
|
|
2303
|
+
lastHeightNotifyContentSizeNanosForTesting = 0
|
|
2304
|
+
lastHeightNotifySizeThatFitsNanosForTesting = 0
|
|
2305
|
+
if heightBehavior == .autoGrow {
|
|
2306
|
+
invalidateAutoGrowHeightMeasurement()
|
|
2307
|
+
if forceHeightNotify || window == nil {
|
|
2308
|
+
notifyHeightChangeIfNeeded(force: forceHeightNotify)
|
|
2309
|
+
} else {
|
|
2310
|
+
setNeedsLayout()
|
|
2311
|
+
}
|
|
2312
|
+
}
|
|
2313
|
+
let heightNotifyNanos = DispatchTime.now().uptimeNanoseconds - heightNotifyStartedAt
|
|
2314
|
+
|
|
2315
|
+
let selectionOrContentStartedAt = DispatchTime.now().uptimeNanoseconds
|
|
2316
|
+
onSelectionOrContentMayChange?()
|
|
2317
|
+
let selectionOrContentCallbackNanos =
|
|
2318
|
+
DispatchTime.now().uptimeNanoseconds - selectionOrContentStartedAt
|
|
2319
|
+
|
|
2320
|
+
return PostApplyTrace(
|
|
2321
|
+
totalNanos: DispatchTime.now().uptimeNanoseconds - totalStartedAt,
|
|
2322
|
+
typingAttributesNanos: typingAttributesNanos,
|
|
2323
|
+
heightNotifyNanos: heightNotifyNanos,
|
|
2324
|
+
heightNotifyMeasureNanos: lastHeightNotifyMeasureNanosForTesting,
|
|
2325
|
+
heightNotifyCallbackNanos: lastHeightNotifyCallbackNanosForTesting,
|
|
2326
|
+
heightNotifyEnsureLayoutNanos: lastHeightNotifyEnsureLayoutNanosForTesting,
|
|
2327
|
+
heightNotifyUsedRectNanos: lastHeightNotifyUsedRectNanosForTesting,
|
|
2328
|
+
heightNotifyContentSizeNanos: lastHeightNotifyContentSizeNanosForTesting,
|
|
2329
|
+
heightNotifySizeThatFitsNanos: lastHeightNotifySizeThatFitsNanosForTesting,
|
|
2330
|
+
selectionOrContentCallbackNanos: selectionOrContentCallbackNanos
|
|
2331
|
+
)
|
|
1886
2332
|
}
|
|
1887
2333
|
|
|
1888
2334
|
static func adjustedCaretRect(
|
|
@@ -1947,6 +2393,10 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
1947
2393
|
let rawOffset = offset(from: beginningOfDocument, to: position)
|
|
1948
2394
|
let clampedOffset = min(max(rawOffset, 0), textStorage.length)
|
|
1949
2395
|
|
|
2396
|
+
if let horizontalRuleBaselineY = horizontalRuleAdjacentBaselineY(at: clampedOffset) {
|
|
2397
|
+
return horizontalRuleBaselineY
|
|
2398
|
+
}
|
|
2399
|
+
|
|
1950
2400
|
if let hardBreakBaselineY = hardBreakBaselineY(after: clampedOffset) {
|
|
1951
2401
|
return hardBreakBaselineY
|
|
1952
2402
|
}
|
|
@@ -1992,6 +2442,91 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
1992
2442
|
return bestMatch?.baselineY
|
|
1993
2443
|
}
|
|
1994
2444
|
|
|
2445
|
+
private func horizontalRuleAdjacentBaselineY(at utf16Offset: Int) -> CGFloat? {
|
|
2446
|
+
guard textStorage.length > 0 else { return nil }
|
|
2447
|
+
|
|
2448
|
+
if isHorizontalRuleAttachment(at: utf16Offset),
|
|
2449
|
+
let previousCharacterIndex = nearestVisibleCharacterIndex(
|
|
2450
|
+
from: utf16Offset - 1,
|
|
2451
|
+
direction: -1
|
|
2452
|
+
)
|
|
2453
|
+
{
|
|
2454
|
+
return baselineY(forCharacterAt: previousCharacterIndex)
|
|
2455
|
+
}
|
|
2456
|
+
|
|
2457
|
+
if isHorizontalRuleAttachment(at: utf16Offset - 1),
|
|
2458
|
+
let nextCharacterIndex = nearestVisibleCharacterIndex(
|
|
2459
|
+
from: utf16Offset,
|
|
2460
|
+
direction: 1
|
|
2461
|
+
)
|
|
2462
|
+
{
|
|
2463
|
+
return baselineY(forCharacterAt: nextCharacterIndex)
|
|
2464
|
+
}
|
|
2465
|
+
|
|
2466
|
+
return nil
|
|
2467
|
+
}
|
|
2468
|
+
|
|
2469
|
+
private func baselineY(forCharacterAt characterIndex: Int) -> CGFloat? {
|
|
2470
|
+
guard characterIndex >= 0, characterIndex < textStorage.length else { return nil }
|
|
2471
|
+
|
|
2472
|
+
let glyphIndex = layoutManager.glyphIndexForCharacter(at: characterIndex)
|
|
2473
|
+
guard glyphIndex < layoutManager.numberOfGlyphs else { return nil }
|
|
2474
|
+
|
|
2475
|
+
let lineFragmentRect = layoutManager.lineFragmentRect(
|
|
2476
|
+
forGlyphAt: glyphIndex,
|
|
2477
|
+
effectiveRange: nil
|
|
2478
|
+
)
|
|
2479
|
+
let glyphLocation = layoutManager.location(forGlyphAt: glyphIndex)
|
|
2480
|
+
return textContainerInset.top + lineFragmentRect.minY + glyphLocation.y
|
|
2481
|
+
}
|
|
2482
|
+
|
|
2483
|
+
private func visibleSelectionRect(forCharacterAt characterIndex: Int) -> CGRect? {
|
|
2484
|
+
guard characterIndex >= 0, characterIndex < textStorage.length else { return nil }
|
|
2485
|
+
guard let start = position(from: beginningOfDocument, offset: characterIndex),
|
|
2486
|
+
let end = position(from: start, offset: 1),
|
|
2487
|
+
let range = textRange(from: start, to: end)
|
|
2488
|
+
else {
|
|
2489
|
+
return nil
|
|
2490
|
+
}
|
|
2491
|
+
|
|
2492
|
+
return selectionRects(for: range)
|
|
2493
|
+
.map(\.rect)
|
|
2494
|
+
.first(where: { !$0.isEmpty && $0.width > 0 && $0.height > 0 })
|
|
2495
|
+
}
|
|
2496
|
+
|
|
2497
|
+
private func nearestVisibleCharacterIndex(from startIndex: Int, direction: Int) -> Int? {
|
|
2498
|
+
guard direction == -1 || direction == 1 else { return nil }
|
|
2499
|
+
guard textStorage.length > 0 else { return nil }
|
|
2500
|
+
|
|
2501
|
+
let text = textStorage.string as NSString
|
|
2502
|
+
var index = startIndex
|
|
2503
|
+
|
|
2504
|
+
while index >= 0, index < text.length {
|
|
2505
|
+
let attrs = textStorage.attributes(at: index, effectiveRange: nil)
|
|
2506
|
+
let character = text.substring(with: NSRange(location: index, length: 1))
|
|
2507
|
+
|
|
2508
|
+
if attrs[.attachment] == nil,
|
|
2509
|
+
character != "\n",
|
|
2510
|
+
character != "\r",
|
|
2511
|
+
visibleSelectionRect(forCharacterAt: index) != nil
|
|
2512
|
+
{
|
|
2513
|
+
return index
|
|
2514
|
+
}
|
|
2515
|
+
|
|
2516
|
+
index += direction
|
|
2517
|
+
}
|
|
2518
|
+
|
|
2519
|
+
return nil
|
|
2520
|
+
}
|
|
2521
|
+
|
|
2522
|
+
private func isHorizontalRuleAttachment(at utf16Offset: Int) -> Bool {
|
|
2523
|
+
guard utf16Offset >= 0, utf16Offset < textStorage.length else { return false }
|
|
2524
|
+
|
|
2525
|
+
let attrs = textStorage.attributes(at: utf16Offset, effectiveRange: nil)
|
|
2526
|
+
return attrs[.attachment] is NSTextAttachment
|
|
2527
|
+
&& (attrs[RenderBridgeAttributes.voidNodeType] as? String) == "horizontalRule"
|
|
2528
|
+
}
|
|
2529
|
+
|
|
1995
2530
|
private func hardBreakBaselineY(after utf16Offset: Int) -> CGFloat? {
|
|
1996
2531
|
guard utf16Offset > 0, utf16Offset <= textStorage.length else { return nil }
|
|
1997
2532
|
let previousVoidType = textStorage.attribute(
|
|
@@ -2197,6 +2732,18 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
2197
2732
|
applyUpdateJSON(updateJSON)
|
|
2198
2733
|
}
|
|
2199
2734
|
|
|
2735
|
+
private func deleteBackwardAtSelectionScalarInRust(anchor: UInt32, head: UInt32) {
|
|
2736
|
+
Self.inputLog.debug(
|
|
2737
|
+
"[rust.deleteBackwardAtSelectionScalar] scalar=\(anchor)-\(head) selection=\(self.selectionSummary(), privacy: .public)"
|
|
2738
|
+
)
|
|
2739
|
+
let updateJSON = editorDeleteBackwardAtSelectionScalar(
|
|
2740
|
+
id: editorId,
|
|
2741
|
+
scalarAnchor: anchor,
|
|
2742
|
+
scalarHead: head
|
|
2743
|
+
)
|
|
2744
|
+
applyUpdateJSON(updateJSON)
|
|
2745
|
+
}
|
|
2746
|
+
|
|
2200
2747
|
/// Delete a document-position range via the Rust editor.
|
|
2201
2748
|
private func deleteRangeInRust(from: UInt32, to: UInt32) {
|
|
2202
2749
|
guard from < to else { return }
|
|
@@ -2213,35 +2760,35 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
2213
2760
|
return (anchor: scalarRange.from, head: scalarRange.to)
|
|
2214
2761
|
}
|
|
2215
2762
|
|
|
2216
|
-
func
|
|
2763
|
+
private func selectedImageSelectionState() -> (docPos: UInt32, utf16Offset: Int)? {
|
|
2217
2764
|
guard allowImageResizing else { return nil }
|
|
2218
2765
|
guard isFirstResponder else { return nil }
|
|
2219
|
-
guard let selectedRange =
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2766
|
+
guard let selectedRange = selectedUtf16Range(),
|
|
2767
|
+
selectedRange.length == 1,
|
|
2768
|
+
selectedRange.location >= 0,
|
|
2769
|
+
selectedRange.location < textStorage.length
|
|
2770
|
+
else {
|
|
2224
2771
|
return nil
|
|
2225
2772
|
}
|
|
2226
2773
|
|
|
2227
|
-
let attrs = textStorage.attributes(at:
|
|
2774
|
+
let attrs = textStorage.attributes(at: selectedRange.location, effectiveRange: nil)
|
|
2228
2775
|
guard (attrs[RenderBridgeAttributes.voidNodeType] as? String) == "image",
|
|
2229
2776
|
attrs[.attachment] is NSTextAttachment
|
|
2230
2777
|
else {
|
|
2231
2778
|
return nil
|
|
2232
2779
|
}
|
|
2233
2780
|
|
|
2234
|
-
let docPos
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
}
|
|
2781
|
+
let docPos = (attrs[RenderBridgeAttributes.docPos] as? NSNumber)?.uint32Value
|
|
2782
|
+
?? (attrs[RenderBridgeAttributes.docPos] as? UInt32)
|
|
2783
|
+
guard let docPos else { return nil }
|
|
2784
|
+
return (docPos, selectedRange.location)
|
|
2785
|
+
}
|
|
2786
|
+
|
|
2787
|
+
func selectedImageGeometry() -> (docPos: UInt32, rect: CGRect)? {
|
|
2788
|
+
guard let selectionState = selectedImageSelectionState() else { return nil }
|
|
2242
2789
|
|
|
2243
2790
|
let glyphRange = layoutManager.glyphRange(
|
|
2244
|
-
forCharacterRange: NSRange(location:
|
|
2791
|
+
forCharacterRange: NSRange(location: selectionState.utf16Offset, length: 1),
|
|
2245
2792
|
actualCharacterRange: nil
|
|
2246
2793
|
)
|
|
2247
2794
|
guard glyphRange.length > 0 else { return nil }
|
|
@@ -2250,13 +2797,17 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
2250
2797
|
rect.origin.x += textContainerInset.left
|
|
2251
2798
|
rect.origin.y += textContainerInset.top
|
|
2252
2799
|
guard rect.width > 0, rect.height > 0 else { return nil }
|
|
2253
|
-
return (docPos, rect)
|
|
2800
|
+
return (selectionState.docPos, rect)
|
|
2254
2801
|
}
|
|
2255
2802
|
|
|
2256
2803
|
private func blockImageAttachment(docPos: UInt32) -> (range: NSRange, attachment: BlockImageAttachment)? {
|
|
2257
2804
|
let fullRange = NSRange(location: 0, length: textStorage.length)
|
|
2258
2805
|
var resolved: (range: NSRange, attachment: BlockImageAttachment)?
|
|
2259
|
-
textStorage.enumerateAttribute(
|
|
2806
|
+
textStorage.enumerateAttribute(
|
|
2807
|
+
.attachment,
|
|
2808
|
+
in: fullRange,
|
|
2809
|
+
options: [.longestEffectiveRangeNotRequired]
|
|
2810
|
+
) { value, range, stop in
|
|
2260
2811
|
guard let attachment = value as? BlockImageAttachment, range.length > 0 else { return }
|
|
2261
2812
|
let attrs = textStorage.attributes(at: range.location, effectiveRange: nil)
|
|
2262
2813
|
guard (attrs[RenderBridgeAttributes.voidNodeType] as? String) == "image" else { return }
|
|
@@ -2371,93 +2922,1059 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
2371
2922
|
|
|
2372
2923
|
// MARK: - Applying Rust State
|
|
2373
2924
|
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
/// - Parameter updateJSON: The JSON string from editor_insert_text, etc.
|
|
2380
|
-
func applyUpdateJSON(_ updateJSON: String, notifyDelegate: Bool = true) {
|
|
2381
|
-
guard let data = updateJSON.data(using: .utf8),
|
|
2382
|
-
let update = try? JSONSerialization.jsonObject(with: data) as? [String: Any]
|
|
2383
|
-
else { return }
|
|
2925
|
+
private struct ParsedRenderPatch {
|
|
2926
|
+
let startIndex: Int
|
|
2927
|
+
let deleteCount: Int
|
|
2928
|
+
let renderBlocks: [[[String: Any]]]
|
|
2929
|
+
}
|
|
2384
2930
|
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
Self.updateLog.debug(
|
|
2390
|
-
"[applyUpdateJSON.begin] renderCount=\(renderElements.count) updateSelection=\(selectionFromUpdate, privacy: .public) before=\(self.textSnapshotSummary(), privacy: .public)"
|
|
2391
|
-
)
|
|
2931
|
+
private enum DerivedRenderPatch {
|
|
2932
|
+
case unchanged
|
|
2933
|
+
case patch(ParsedRenderPatch)
|
|
2934
|
+
}
|
|
2392
2935
|
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2936
|
+
private func parseRenderBlocks(_ value: Any?) -> [[[String: Any]]]? {
|
|
2937
|
+
value as? [[[String: Any]]]
|
|
2938
|
+
}
|
|
2939
|
+
|
|
2940
|
+
private func parseRenderPatch(_ value: Any?) -> ParsedRenderPatch? {
|
|
2941
|
+
guard let raw = value as? [String: Any],
|
|
2942
|
+
let startIndex = RenderBridge.jsonInt(raw["startIndex"]),
|
|
2943
|
+
let deleteCount = RenderBridge.jsonInt(raw["deleteCount"]),
|
|
2944
|
+
let renderBlocks = parseRenderBlocks(raw["renderBlocks"])
|
|
2945
|
+
else {
|
|
2946
|
+
return nil
|
|
2947
|
+
}
|
|
2948
|
+
|
|
2949
|
+
return ParsedRenderPatch(
|
|
2950
|
+
startIndex: startIndex,
|
|
2951
|
+
deleteCount: deleteCount,
|
|
2952
|
+
renderBlocks: renderBlocks
|
|
2398
2953
|
)
|
|
2954
|
+
}
|
|
2399
2955
|
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2956
|
+
private func mergeRenderBlocks(
|
|
2957
|
+
applying patch: ParsedRenderPatch,
|
|
2958
|
+
to current: [[[String: Any]]]
|
|
2959
|
+
) -> [[[String: Any]]]? {
|
|
2960
|
+
guard patch.startIndex >= 0,
|
|
2961
|
+
patch.deleteCount >= 0,
|
|
2962
|
+
patch.startIndex <= current.count,
|
|
2963
|
+
patch.startIndex + patch.deleteCount <= current.count
|
|
2964
|
+
else {
|
|
2965
|
+
return nil
|
|
2966
|
+
}
|
|
2407
2967
|
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2968
|
+
var merged = current
|
|
2969
|
+
merged.replaceSubrange(
|
|
2970
|
+
patch.startIndex..<(patch.startIndex + patch.deleteCount),
|
|
2971
|
+
with: patch.renderBlocks
|
|
2411
2972
|
)
|
|
2973
|
+
return merged
|
|
2974
|
+
}
|
|
2412
2975
|
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2976
|
+
private func renderBlockEquals(
|
|
2977
|
+
_ lhs: [[String: Any]],
|
|
2978
|
+
_ rhs: [[String: Any]]
|
|
2979
|
+
) -> Bool {
|
|
2980
|
+
(lhs as NSArray).isEqual(rhs)
|
|
2981
|
+
}
|
|
2982
|
+
|
|
2983
|
+
private func deriveRenderPatch(
|
|
2984
|
+
from current: [[[String: Any]]],
|
|
2985
|
+
to updated: [[[String: Any]]]
|
|
2986
|
+
) -> DerivedRenderPatch {
|
|
2987
|
+
let sharedCount = min(current.count, updated.count)
|
|
2988
|
+
|
|
2989
|
+
var prefix = 0
|
|
2990
|
+
while prefix < sharedCount, renderBlockEquals(current[prefix], updated[prefix]) {
|
|
2991
|
+
prefix += 1
|
|
2416
2992
|
}
|
|
2417
|
-
|
|
2418
|
-
if
|
|
2419
|
-
|
|
2993
|
+
|
|
2994
|
+
if prefix == current.count, prefix == updated.count {
|
|
2995
|
+
return .unchanged
|
|
2420
2996
|
}
|
|
2421
|
-
onSelectionOrContentMayChange?()
|
|
2422
2997
|
|
|
2423
|
-
|
|
2424
|
-
|
|
2998
|
+
var suffix = 0
|
|
2999
|
+
while suffix < (sharedCount - prefix),
|
|
3000
|
+
renderBlockEquals(
|
|
3001
|
+
current[current.count - suffix - 1],
|
|
3002
|
+
updated[updated.count - suffix - 1]
|
|
3003
|
+
)
|
|
3004
|
+
{
|
|
3005
|
+
suffix += 1
|
|
3006
|
+
}
|
|
3007
|
+
|
|
3008
|
+
let startIndex = prefix
|
|
3009
|
+
let deleteCount = current.count - prefix - suffix
|
|
3010
|
+
let endIndex = updated.count - suffix
|
|
3011
|
+
let replacementBlocks = Array(updated[startIndex..<endIndex])
|
|
3012
|
+
|
|
3013
|
+
return .patch(
|
|
3014
|
+
ParsedRenderPatch(
|
|
3015
|
+
startIndex: startIndex,
|
|
3016
|
+
deleteCount: deleteCount,
|
|
3017
|
+
renderBlocks: replacementBlocks
|
|
3018
|
+
)
|
|
2425
3019
|
)
|
|
3020
|
+
}
|
|
2426
3021
|
|
|
2427
|
-
|
|
2428
|
-
if
|
|
2429
|
-
|
|
3022
|
+
private func topLevelChildIndex(from value: Any?) -> Int? {
|
|
3023
|
+
if let number = value as? NSNumber {
|
|
3024
|
+
return number.intValue
|
|
2430
3025
|
}
|
|
3026
|
+
return value as? Int
|
|
2431
3027
|
}
|
|
2432
3028
|
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
3029
|
+
private func topLevelChildMetadataSlice(
|
|
3030
|
+
from attributedString: NSAttributedString
|
|
3031
|
+
) -> TopLevelChildMetadataSlice? {
|
|
3032
|
+
guard attributedString.length > 0 else {
|
|
3033
|
+
return TopLevelChildMetadataSlice(startIndex: 0, entries: [])
|
|
3034
|
+
}
|
|
3035
|
+
|
|
3036
|
+
var entriesByIndex: [Int: TopLevelChildMetadata] = [:]
|
|
3037
|
+
var orderedIndexes: [Int] = []
|
|
3038
|
+
|
|
3039
|
+
attributedString.enumerateAttributes(
|
|
3040
|
+
in: NSRange(location: 0, length: attributedString.length),
|
|
3041
|
+
options: []
|
|
3042
|
+
) { attrs, range, _ in
|
|
3043
|
+
guard let index = topLevelChildIndex(from: attrs[RenderBridgeAttributes.topLevelChildIndex]) else {
|
|
3044
|
+
return
|
|
3045
|
+
}
|
|
3046
|
+
if entriesByIndex[index] == nil {
|
|
3047
|
+
entriesByIndex[index] = TopLevelChildMetadata(
|
|
3048
|
+
startOffset: range.location,
|
|
3049
|
+
containsAttachment: false,
|
|
3050
|
+
containsPositionAdjustments: false
|
|
3051
|
+
)
|
|
3052
|
+
orderedIndexes.append(index)
|
|
3053
|
+
}
|
|
3054
|
+
if attrs[.attachment] != nil {
|
|
3055
|
+
entriesByIndex[index]?.containsAttachment = true
|
|
3056
|
+
}
|
|
3057
|
+
if attrs[RenderBridgeAttributes.syntheticPlaceholder] as? Bool == true
|
|
3058
|
+
|| attrs[RenderBridgeAttributes.listMarkerContext] != nil
|
|
3059
|
+
{
|
|
3060
|
+
entriesByIndex[index]?.containsPositionAdjustments = true
|
|
3061
|
+
}
|
|
3062
|
+
}
|
|
3063
|
+
|
|
3064
|
+
guard !orderedIndexes.isEmpty else { return nil }
|
|
3065
|
+
orderedIndexes.sort()
|
|
3066
|
+
guard let startIndex = orderedIndexes.first else { return nil }
|
|
3067
|
+
|
|
3068
|
+
var entries: [TopLevelChildMetadata] = []
|
|
3069
|
+
entries.reserveCapacity(orderedIndexes.count)
|
|
3070
|
+
for (offset, index) in orderedIndexes.enumerated() {
|
|
3071
|
+
guard index == startIndex + offset,
|
|
3072
|
+
let entry = entriesByIndex[index]
|
|
3073
|
+
else {
|
|
3074
|
+
return nil
|
|
3075
|
+
}
|
|
3076
|
+
entries.append(entry)
|
|
3077
|
+
}
|
|
3078
|
+
|
|
3079
|
+
return TopLevelChildMetadataSlice(startIndex: startIndex, entries: entries)
|
|
3080
|
+
}
|
|
3081
|
+
|
|
3082
|
+
private func refreshTopLevelChildMetadata(
|
|
3083
|
+
from attributedString: NSAttributedString
|
|
3084
|
+
) {
|
|
3085
|
+
guard let slice = topLevelChildMetadataSlice(from: attributedString),
|
|
3086
|
+
slice.startIndex == 0
|
|
3087
|
+
else {
|
|
3088
|
+
currentTopLevelChildMetadata = nil
|
|
3089
|
+
return
|
|
3090
|
+
}
|
|
3091
|
+
currentTopLevelChildMetadata = slice.entries
|
|
3092
|
+
}
|
|
3093
|
+
|
|
3094
|
+
private func applyTopLevelChildMetadataPatch(
|
|
3095
|
+
_ patch: ParsedRenderPatch,
|
|
3096
|
+
replaceRange: NSRange,
|
|
3097
|
+
renderedPatchMetadata: TopLevelChildMetadataSlice?,
|
|
3098
|
+
renderedPatchLength: Int
|
|
3099
|
+
) {
|
|
3100
|
+
guard var currentMetadata = currentTopLevelChildMetadata else {
|
|
3101
|
+
currentTopLevelChildMetadata = nil
|
|
3102
|
+
return
|
|
3103
|
+
}
|
|
3104
|
+
|
|
3105
|
+
let newEntries: [TopLevelChildMetadata]
|
|
3106
|
+
if let renderedPatchMetadata,
|
|
3107
|
+
renderedPatchMetadata.entries.isEmpty
|
|
3108
|
+
{
|
|
3109
|
+
newEntries = []
|
|
3110
|
+
} else if let renderedPatchMetadata,
|
|
3111
|
+
renderedPatchMetadata.startIndex == patch.startIndex
|
|
3112
|
+
{
|
|
3113
|
+
newEntries = renderedPatchMetadata.entries.map { entry in
|
|
3114
|
+
TopLevelChildMetadata(
|
|
3115
|
+
startOffset: replaceRange.location + entry.startOffset,
|
|
3116
|
+
containsAttachment: entry.containsAttachment,
|
|
3117
|
+
containsPositionAdjustments: entry.containsPositionAdjustments
|
|
3118
|
+
)
|
|
3119
|
+
}
|
|
3120
|
+
} else {
|
|
3121
|
+
currentTopLevelChildMetadata = nil
|
|
3122
|
+
return
|
|
3123
|
+
}
|
|
3124
|
+
|
|
3125
|
+
guard patch.startIndex >= 0,
|
|
3126
|
+
patch.deleteCount >= 0,
|
|
3127
|
+
patch.startIndex <= currentMetadata.count,
|
|
3128
|
+
patch.startIndex + patch.deleteCount <= currentMetadata.count
|
|
3129
|
+
else {
|
|
3130
|
+
currentTopLevelChildMetadata = nil
|
|
3131
|
+
return
|
|
3132
|
+
}
|
|
3133
|
+
|
|
3134
|
+
currentMetadata.replaceSubrange(
|
|
3135
|
+
patch.startIndex..<(patch.startIndex + patch.deleteCount),
|
|
3136
|
+
with: newEntries
|
|
2446
3137
|
)
|
|
2447
3138
|
|
|
3139
|
+
let delta = renderedPatchLength - replaceRange.length
|
|
3140
|
+
if delta != 0 {
|
|
3141
|
+
let shiftStart = patch.startIndex + newEntries.count
|
|
3142
|
+
for index in shiftStart..<currentMetadata.count {
|
|
3143
|
+
currentMetadata[index].startOffset += delta
|
|
3144
|
+
}
|
|
3145
|
+
}
|
|
3146
|
+
|
|
3147
|
+
currentTopLevelChildMetadata = currentMetadata
|
|
3148
|
+
}
|
|
3149
|
+
|
|
3150
|
+
private func hasTopLevelChildMetadata() -> Bool {
|
|
3151
|
+
currentTopLevelChildMetadata != nil
|
|
3152
|
+
}
|
|
3153
|
+
|
|
3154
|
+
private func firstCharacterOffset(forTopLevelChildIndex index: Int) -> Int? {
|
|
3155
|
+
guard let currentTopLevelChildMetadata,
|
|
3156
|
+
index >= 0,
|
|
3157
|
+
index < currentTopLevelChildMetadata.count
|
|
3158
|
+
else {
|
|
3159
|
+
return nil
|
|
3160
|
+
}
|
|
3161
|
+
return currentTopLevelChildMetadata[index].startOffset
|
|
3162
|
+
}
|
|
3163
|
+
|
|
3164
|
+
private func replacementRangeForRenderPatch(
|
|
3165
|
+
startIndex: Int,
|
|
3166
|
+
deleteCount: Int
|
|
3167
|
+
) -> NSRange? {
|
|
3168
|
+
let startLocation: Int
|
|
3169
|
+
if let resolvedStart = firstCharacterOffset(forTopLevelChildIndex: startIndex) {
|
|
3170
|
+
startLocation = resolvedStart
|
|
3171
|
+
} else if deleteCount == 0 {
|
|
3172
|
+
startLocation = textStorage.length
|
|
3173
|
+
} else {
|
|
3174
|
+
return nil
|
|
3175
|
+
}
|
|
3176
|
+
|
|
3177
|
+
let endIndexExclusive = startIndex + deleteCount
|
|
3178
|
+
let endLocation = firstCharacterOffset(forTopLevelChildIndex: endIndexExclusive)
|
|
3179
|
+
?? textStorage.length
|
|
3180
|
+
guard startLocation <= endLocation else { return nil }
|
|
3181
|
+
return NSRange(location: startLocation, length: endLocation - startLocation)
|
|
3182
|
+
}
|
|
3183
|
+
|
|
3184
|
+
private func applyAttributedRender(
|
|
3185
|
+
_ attrStr: NSAttributedString,
|
|
3186
|
+
replaceRange: NSRange? = nil,
|
|
3187
|
+
usedPatch: Bool,
|
|
3188
|
+
positionCacheUpdate: PositionCacheUpdate = .scan
|
|
3189
|
+
) -> ApplyRenderTrace {
|
|
3190
|
+
let totalStartedAt = DispatchTime.now().uptimeNanoseconds
|
|
3191
|
+
let replaceUtf16Length = replaceRange?.length ?? textStorage.length
|
|
3192
|
+
let replacementUtf16Length = attrStr.length
|
|
3193
|
+
let shouldUseSmallPatchTextMutation =
|
|
3194
|
+
replaceRange != nil && shouldUseSmallPatchTextMutation(for: attrStr, replaceRange: replaceRange)
|
|
2448
3195
|
isApplyingRustState = true
|
|
3196
|
+
let textMutationStartedAt = DispatchTime.now().uptimeNanoseconds
|
|
3197
|
+
let beginEditingStartedAt = DispatchTime.now().uptimeNanoseconds
|
|
2449
3198
|
textStorage.beginEditing()
|
|
2450
|
-
|
|
3199
|
+
let beginEditingNanos = DispatchTime.now().uptimeNanoseconds - beginEditingStartedAt
|
|
3200
|
+
var stringMutationNanos: UInt64 = 0
|
|
3201
|
+
var attributeMutationNanos: UInt64 = 0
|
|
3202
|
+
let previousTextStorageDelegate = textStorage.delegate
|
|
3203
|
+
let previousTextViewDelegate = delegate
|
|
3204
|
+
textStorage.delegate = nil
|
|
3205
|
+
delegate = nil
|
|
3206
|
+
defer {
|
|
3207
|
+
textStorage.delegate = previousTextStorageDelegate
|
|
3208
|
+
delegate = previousTextViewDelegate
|
|
3209
|
+
}
|
|
3210
|
+
if let replaceRange {
|
|
3211
|
+
if shouldUseSmallPatchTextMutation {
|
|
3212
|
+
let stringMutationStartedAt = DispatchTime.now().uptimeNanoseconds
|
|
3213
|
+
textStorage.replaceCharacters(in: replaceRange, with: attrStr.string)
|
|
3214
|
+
stringMutationNanos =
|
|
3215
|
+
DispatchTime.now().uptimeNanoseconds - stringMutationStartedAt
|
|
3216
|
+
let destinationRange = NSRange(location: replaceRange.location, length: attrStr.length)
|
|
3217
|
+
let attributeMutationStartedAt = DispatchTime.now().uptimeNanoseconds
|
|
3218
|
+
applyAttributes(from: attrStr, to: destinationRange)
|
|
3219
|
+
attributeMutationNanos =
|
|
3220
|
+
DispatchTime.now().uptimeNanoseconds - attributeMutationStartedAt
|
|
3221
|
+
} else {
|
|
3222
|
+
let stringMutationStartedAt = DispatchTime.now().uptimeNanoseconds
|
|
3223
|
+
textStorage.replaceCharacters(in: replaceRange, with: attrStr)
|
|
3224
|
+
stringMutationNanos =
|
|
3225
|
+
DispatchTime.now().uptimeNanoseconds - stringMutationStartedAt
|
|
3226
|
+
}
|
|
3227
|
+
} else {
|
|
3228
|
+
let stringMutationStartedAt = DispatchTime.now().uptimeNanoseconds
|
|
3229
|
+
textStorage.setAttributedString(attrStr)
|
|
3230
|
+
stringMutationNanos =
|
|
3231
|
+
DispatchTime.now().uptimeNanoseconds - stringMutationStartedAt
|
|
3232
|
+
}
|
|
3233
|
+
let endEditingStartedAt = DispatchTime.now().uptimeNanoseconds
|
|
2451
3234
|
textStorage.endEditing()
|
|
2452
|
-
|
|
3235
|
+
let endEditingNanos = DispatchTime.now().uptimeNanoseconds - endEditingStartedAt
|
|
3236
|
+
let textMutationNanos = DispatchTime.now().uptimeNanoseconds - textMutationStartedAt
|
|
3237
|
+
let authorizedTextStartedAt = DispatchTime.now().uptimeNanoseconds
|
|
3238
|
+
if let replaceRange,
|
|
3239
|
+
replaceRange.location >= 0,
|
|
3240
|
+
replaceRange.location + replaceRange.length <= lastAuthorizedTextStorage.length
|
|
3241
|
+
{
|
|
3242
|
+
lastAuthorizedTextStorage.replaceCharacters(in: replaceRange, with: attrStr.string)
|
|
3243
|
+
} else {
|
|
3244
|
+
lastAuthorizedTextStorage.setString(attrStr.string)
|
|
3245
|
+
}
|
|
3246
|
+
let authorizedTextNanos = DispatchTime.now().uptimeNanoseconds - authorizedTextStartedAt
|
|
3247
|
+
let cacheInvalidationStartedAt = DispatchTime.now().uptimeNanoseconds
|
|
3248
|
+
lastRenderAppliedPatchForTesting = usedPatch
|
|
3249
|
+
switch positionCacheUpdate {
|
|
3250
|
+
case .plainText:
|
|
3251
|
+
guard let replaceRange else {
|
|
3252
|
+
PositionBridge.invalidateCache(for: self)
|
|
3253
|
+
break
|
|
3254
|
+
}
|
|
3255
|
+
let patchedPositionCache = PositionBridge.applyPlainTextPatchIfPossible(
|
|
3256
|
+
for: self,
|
|
3257
|
+
replaceRange: replaceRange,
|
|
3258
|
+
replacementText: attrStr.string
|
|
3259
|
+
)
|
|
3260
|
+
if !patchedPositionCache {
|
|
3261
|
+
PositionBridge.invalidateCache(for: self)
|
|
3262
|
+
}
|
|
3263
|
+
case .attributed:
|
|
3264
|
+
guard let replaceRange else {
|
|
3265
|
+
PositionBridge.invalidateCache(for: self)
|
|
3266
|
+
break
|
|
3267
|
+
}
|
|
3268
|
+
let patchedPositionCache = PositionBridge.applyAttributedPatchIfPossible(
|
|
3269
|
+
for: self,
|
|
3270
|
+
replaceRange: replaceRange,
|
|
3271
|
+
replacement: attrStr
|
|
3272
|
+
)
|
|
3273
|
+
if !patchedPositionCache {
|
|
3274
|
+
PositionBridge.invalidateCache(for: self)
|
|
3275
|
+
}
|
|
3276
|
+
case .invalidate:
|
|
3277
|
+
PositionBridge.invalidateCache(for: self)
|
|
3278
|
+
case .scan:
|
|
3279
|
+
let canPatchPositionCache = if let replaceRange {
|
|
3280
|
+
replaceRange.location >= 0
|
|
3281
|
+
&& !textStorageRangeContainsAttachment(replaceRange)
|
|
3282
|
+
&& !attributedStringContainsAttachment(attrStr)
|
|
3283
|
+
} else {
|
|
3284
|
+
false
|
|
3285
|
+
}
|
|
3286
|
+
if let replaceRange, canPatchPositionCache {
|
|
3287
|
+
let patchedPositionCache: Bool
|
|
3288
|
+
if !textStorageRangeContainsPositionAdjustments(replaceRange),
|
|
3289
|
+
!attributedStringContainsPositionAdjustments(attrStr)
|
|
3290
|
+
{
|
|
3291
|
+
patchedPositionCache = PositionBridge.applyPlainTextPatchIfPossible(
|
|
3292
|
+
for: self,
|
|
3293
|
+
replaceRange: replaceRange,
|
|
3294
|
+
replacementText: attrStr.string
|
|
3295
|
+
)
|
|
3296
|
+
} else {
|
|
3297
|
+
patchedPositionCache = PositionBridge.applyAttributedPatchIfPossible(
|
|
3298
|
+
for: self,
|
|
3299
|
+
replaceRange: replaceRange,
|
|
3300
|
+
replacement: attrStr
|
|
3301
|
+
)
|
|
3302
|
+
}
|
|
3303
|
+
|
|
3304
|
+
if !patchedPositionCache {
|
|
3305
|
+
PositionBridge.invalidateCache(for: self)
|
|
3306
|
+
}
|
|
3307
|
+
} else {
|
|
3308
|
+
PositionBridge.invalidateCache(for: self)
|
|
3309
|
+
}
|
|
3310
|
+
}
|
|
3311
|
+
let cacheInvalidationNanos = DispatchTime.now().uptimeNanoseconds - cacheInvalidationStartedAt
|
|
2453
3312
|
isApplyingRustState = false
|
|
3313
|
+
return ApplyRenderTrace(
|
|
3314
|
+
totalNanos: DispatchTime.now().uptimeNanoseconds - totalStartedAt,
|
|
3315
|
+
replaceUtf16Length: replaceUtf16Length,
|
|
3316
|
+
replacementUtf16Length: replacementUtf16Length,
|
|
3317
|
+
textMutationNanos: textMutationNanos,
|
|
3318
|
+
beginEditingNanos: beginEditingNanos,
|
|
3319
|
+
endEditingNanos: endEditingNanos,
|
|
3320
|
+
stringMutationNanos: stringMutationNanos,
|
|
3321
|
+
attributeMutationNanos: attributeMutationNanos,
|
|
3322
|
+
authorizedTextNanos: authorizedTextNanos,
|
|
3323
|
+
cacheInvalidationNanos: cacheInvalidationNanos,
|
|
3324
|
+
usedSmallPatchTextMutation: shouldUseSmallPatchTextMutation
|
|
3325
|
+
)
|
|
3326
|
+
}
|
|
3327
|
+
|
|
3328
|
+
private func shouldUseSmallPatchTextMutation(
|
|
3329
|
+
for attributedString: NSAttributedString,
|
|
3330
|
+
replaceRange: NSRange?
|
|
3331
|
+
) -> Bool {
|
|
3332
|
+
attributedString.length > 0
|
|
3333
|
+
&& attributedString.length <= 512
|
|
3334
|
+
&& (replaceRange?.length ?? 0) <= 512
|
|
3335
|
+
&& !attributedStringContainsAttachment(attributedString)
|
|
3336
|
+
}
|
|
3337
|
+
|
|
3338
|
+
private func attributesEqualForPatchTrimming(
|
|
3339
|
+
_ lhs: [NSAttributedString.Key: Any],
|
|
3340
|
+
_ rhs: [NSAttributedString.Key: Any]
|
|
3341
|
+
) -> Bool {
|
|
3342
|
+
if let lhsValue = lhs[RenderBridgeAttributes.topLevelChildIndex] as? NSNumber,
|
|
3343
|
+
let rhsValue = rhs[RenderBridgeAttributes.topLevelChildIndex] as? NSNumber,
|
|
3344
|
+
lhsValue == rhsValue
|
|
3345
|
+
{
|
|
3346
|
+
return NSDictionary(dictionary: lhs).isEqual(to: rhs)
|
|
3347
|
+
}
|
|
3348
|
+
|
|
3349
|
+
var lhsComparable = lhs
|
|
3350
|
+
var rhsComparable = rhs
|
|
3351
|
+
lhsComparable.removeValue(forKey: RenderBridgeAttributes.topLevelChildIndex)
|
|
3352
|
+
rhsComparable.removeValue(forKey: RenderBridgeAttributes.topLevelChildIndex)
|
|
3353
|
+
return NSDictionary(dictionary: lhsComparable).isEqual(to: rhsComparable)
|
|
3354
|
+
}
|
|
3355
|
+
|
|
3356
|
+
private func applyAttributes(from attributedString: NSAttributedString, to destinationRange: NSRange) {
|
|
3357
|
+
guard attributedString.length == destinationRange.length else { return }
|
|
3358
|
+
if let uniformAttributes = uniformAttributes(in: attributedString) {
|
|
3359
|
+
textStorage.setAttributes(uniformAttributes, range: destinationRange)
|
|
3360
|
+
return
|
|
3361
|
+
}
|
|
3362
|
+
let sourceRange = NSRange(location: 0, length: attributedString.length)
|
|
3363
|
+
attributedString.enumerateAttributes(
|
|
3364
|
+
in: sourceRange,
|
|
3365
|
+
options: [.longestEffectiveRangeNotRequired]
|
|
3366
|
+
) { attrs, range, _ in
|
|
3367
|
+
let targetRange = NSRange(location: destinationRange.location + range.location, length: range.length)
|
|
3368
|
+
textStorage.setAttributes(attrs, range: targetRange)
|
|
3369
|
+
}
|
|
3370
|
+
}
|
|
3371
|
+
|
|
3372
|
+
private func uniformAttributes(in attributedString: NSAttributedString) -> [NSAttributedString.Key: Any]? {
|
|
3373
|
+
guard attributedString.length > 0 else { return [:] }
|
|
3374
|
+
let firstAttributes = attributedString.attributes(at: 0, effectiveRange: nil)
|
|
3375
|
+
var isUniform = true
|
|
3376
|
+
attributedString.enumerateAttributes(
|
|
3377
|
+
in: NSRange(location: 0, length: attributedString.length),
|
|
3378
|
+
options: [.longestEffectiveRangeNotRequired]
|
|
3379
|
+
) { attrs, _, stop in
|
|
3380
|
+
guard (attrs as NSDictionary).isEqual(firstAttributes) else {
|
|
3381
|
+
isUniform = false
|
|
3382
|
+
stop.pointee = true
|
|
3383
|
+
return
|
|
3384
|
+
}
|
|
3385
|
+
}
|
|
3386
|
+
return isUniform ? firstAttributes : nil
|
|
3387
|
+
}
|
|
3388
|
+
|
|
3389
|
+
private func attributedStringContainsAttachment(_ attributedString: NSAttributedString) -> Bool {
|
|
3390
|
+
guard attributedString.length > 0 else { return false }
|
|
3391
|
+
var hasAttachment = false
|
|
3392
|
+
attributedString.enumerateAttribute(
|
|
3393
|
+
.attachment,
|
|
3394
|
+
in: NSRange(location: 0, length: attributedString.length),
|
|
3395
|
+
options: [.longestEffectiveRangeNotRequired]
|
|
3396
|
+
) { value, _, stop in
|
|
3397
|
+
if value != nil {
|
|
3398
|
+
hasAttachment = true
|
|
3399
|
+
stop.pointee = true
|
|
3400
|
+
}
|
|
3401
|
+
}
|
|
3402
|
+
return hasAttachment
|
|
3403
|
+
}
|
|
3404
|
+
|
|
3405
|
+
private func attributedStringContainsPositionAdjustments(_ attributedString: NSAttributedString) -> Bool {
|
|
3406
|
+
guard attributedString.length > 0 else { return false }
|
|
3407
|
+
var hasAdjustments = false
|
|
3408
|
+
attributedString.enumerateAttributes(
|
|
3409
|
+
in: NSRange(location: 0, length: attributedString.length),
|
|
3410
|
+
options: [.longestEffectiveRangeNotRequired]
|
|
3411
|
+
) { attrs, _, stop in
|
|
3412
|
+
if attrs[RenderBridgeAttributes.syntheticPlaceholder] as? Bool == true
|
|
3413
|
+
|| attrs[RenderBridgeAttributes.listMarkerContext] != nil
|
|
3414
|
+
{
|
|
3415
|
+
hasAdjustments = true
|
|
3416
|
+
stop.pointee = true
|
|
3417
|
+
}
|
|
3418
|
+
}
|
|
3419
|
+
return hasAdjustments
|
|
3420
|
+
}
|
|
3421
|
+
|
|
3422
|
+
private func attributedStringContainsListMarkerContext(_ attributedString: NSAttributedString) -> Bool {
|
|
3423
|
+
guard attributedString.length > 0 else { return false }
|
|
3424
|
+
var hasListMarkerContext = false
|
|
3425
|
+
attributedString.enumerateAttribute(
|
|
3426
|
+
RenderBridgeAttributes.listMarkerContext,
|
|
3427
|
+
in: NSRange(location: 0, length: attributedString.length),
|
|
3428
|
+
options: [.longestEffectiveRangeNotRequired]
|
|
3429
|
+
) { value, _, stop in
|
|
3430
|
+
if value != nil {
|
|
3431
|
+
hasListMarkerContext = true
|
|
3432
|
+
stop.pointee = true
|
|
3433
|
+
}
|
|
3434
|
+
}
|
|
3435
|
+
return hasListMarkerContext
|
|
3436
|
+
}
|
|
3437
|
+
|
|
3438
|
+
private func textStorageRangeContainsPositionAdjustments(_ range: NSRange) -> Bool {
|
|
3439
|
+
guard range.length > 0,
|
|
3440
|
+
range.location >= 0,
|
|
3441
|
+
range.location + range.length <= textStorage.length
|
|
3442
|
+
else {
|
|
3443
|
+
return false
|
|
3444
|
+
}
|
|
3445
|
+
|
|
3446
|
+
var hasAdjustments = false
|
|
3447
|
+
textStorage.enumerateAttributes(
|
|
3448
|
+
in: range,
|
|
3449
|
+
options: [.longestEffectiveRangeNotRequired]
|
|
3450
|
+
) { attrs, _, stop in
|
|
3451
|
+
if attrs[RenderBridgeAttributes.syntheticPlaceholder] as? Bool == true
|
|
3452
|
+
|| attrs[RenderBridgeAttributes.listMarkerContext] != nil
|
|
3453
|
+
{
|
|
3454
|
+
hasAdjustments = true
|
|
3455
|
+
stop.pointee = true
|
|
3456
|
+
}
|
|
3457
|
+
}
|
|
3458
|
+
return hasAdjustments
|
|
3459
|
+
}
|
|
3460
|
+
|
|
3461
|
+
private func textStorageRangeContainsListMarkerContext(_ range: NSRange) -> Bool {
|
|
3462
|
+
guard range.length > 0,
|
|
3463
|
+
range.location >= 0,
|
|
3464
|
+
range.location + range.length <= textStorage.length
|
|
3465
|
+
else {
|
|
3466
|
+
return false
|
|
3467
|
+
}
|
|
3468
|
+
|
|
3469
|
+
var hasListMarkerContext = false
|
|
3470
|
+
textStorage.enumerateAttribute(
|
|
3471
|
+
RenderBridgeAttributes.listMarkerContext,
|
|
3472
|
+
in: range,
|
|
3473
|
+
options: [.longestEffectiveRangeNotRequired]
|
|
3474
|
+
) { value, _, stop in
|
|
3475
|
+
if value != nil {
|
|
3476
|
+
hasListMarkerContext = true
|
|
3477
|
+
stop.pointee = true
|
|
3478
|
+
}
|
|
3479
|
+
}
|
|
3480
|
+
return hasListMarkerContext
|
|
3481
|
+
}
|
|
3482
|
+
|
|
3483
|
+
private func textStorageRangeContainsAttachment(_ range: NSRange) -> Bool {
|
|
3484
|
+
guard range.length > 0,
|
|
3485
|
+
range.location >= 0,
|
|
3486
|
+
range.location + range.length <= textStorage.length
|
|
3487
|
+
else {
|
|
3488
|
+
return false
|
|
3489
|
+
}
|
|
3490
|
+
|
|
3491
|
+
var hasAttachment = false
|
|
3492
|
+
textStorage.enumerateAttribute(
|
|
3493
|
+
.attachment,
|
|
3494
|
+
in: range,
|
|
3495
|
+
options: [.longestEffectiveRangeNotRequired]
|
|
3496
|
+
) { value, _, stop in
|
|
3497
|
+
if value != nil {
|
|
3498
|
+
hasAttachment = true
|
|
3499
|
+
stop.pointee = true
|
|
3500
|
+
}
|
|
3501
|
+
}
|
|
3502
|
+
return hasAttachment
|
|
3503
|
+
}
|
|
3504
|
+
|
|
3505
|
+
private func topLevelChildrenContainAttachment(
|
|
3506
|
+
startIndex: Int,
|
|
3507
|
+
deleteCount: Int
|
|
3508
|
+
) -> Bool {
|
|
3509
|
+
guard deleteCount > 0,
|
|
3510
|
+
let currentTopLevelChildMetadata,
|
|
3511
|
+
startIndex >= 0,
|
|
3512
|
+
startIndex + deleteCount <= currentTopLevelChildMetadata.count
|
|
3513
|
+
else {
|
|
3514
|
+
return false
|
|
3515
|
+
}
|
|
3516
|
+
return currentTopLevelChildMetadata[startIndex..<(startIndex + deleteCount)]
|
|
3517
|
+
.contains(where: \.containsAttachment)
|
|
3518
|
+
}
|
|
3519
|
+
|
|
3520
|
+
private func topLevelChildrenContainPositionAdjustments(
|
|
3521
|
+
startIndex: Int,
|
|
3522
|
+
deleteCount: Int
|
|
3523
|
+
) -> Bool {
|
|
3524
|
+
guard deleteCount > 0,
|
|
3525
|
+
let currentTopLevelChildMetadata,
|
|
3526
|
+
startIndex >= 0,
|
|
3527
|
+
startIndex + deleteCount <= currentTopLevelChildMetadata.count
|
|
3528
|
+
else {
|
|
3529
|
+
return false
|
|
3530
|
+
}
|
|
3531
|
+
return currentTopLevelChildMetadata[startIndex..<(startIndex + deleteCount)]
|
|
3532
|
+
.contains(where: \.containsPositionAdjustments)
|
|
3533
|
+
}
|
|
3534
|
+
|
|
3535
|
+
private func trimmedAttributedPatch(
|
|
3536
|
+
replacing fullReplaceRange: NSRange,
|
|
3537
|
+
with replacement: NSAttributedString
|
|
3538
|
+
) -> (replaceRange: NSRange, replacement: NSAttributedString) {
|
|
3539
|
+
guard fullReplaceRange.length > 0 else {
|
|
3540
|
+
return (fullReplaceRange, replacement)
|
|
3541
|
+
}
|
|
3542
|
+
|
|
3543
|
+
let existing = textStorage.attributedSubstring(from: fullReplaceRange)
|
|
3544
|
+
let existingString = existing.string as NSString
|
|
3545
|
+
let replacementString = replacement.string as NSString
|
|
3546
|
+
let sharedLength = min(existing.length, replacement.length)
|
|
3547
|
+
|
|
3548
|
+
var prefix = 0
|
|
3549
|
+
while prefix < sharedLength {
|
|
3550
|
+
var existingRange = NSRange()
|
|
3551
|
+
let existingAttrs = existing.attributes(
|
|
3552
|
+
at: prefix,
|
|
3553
|
+
longestEffectiveRange: &existingRange,
|
|
3554
|
+
in: NSRange(location: prefix, length: sharedLength - prefix)
|
|
3555
|
+
)
|
|
3556
|
+
var replacementRange = NSRange()
|
|
3557
|
+
let replacementAttrs = replacement.attributes(
|
|
3558
|
+
at: prefix,
|
|
3559
|
+
longestEffectiveRange: &replacementRange,
|
|
3560
|
+
in: NSRange(location: prefix, length: sharedLength - prefix)
|
|
3561
|
+
)
|
|
3562
|
+
guard attributesEqualForPatchTrimming(existingAttrs, replacementAttrs) else { break }
|
|
3563
|
+
let runEnd = min(NSMaxRange(existingRange), NSMaxRange(replacementRange), sharedLength)
|
|
3564
|
+
while prefix < runEnd,
|
|
3565
|
+
existingString.character(at: prefix) == replacementString.character(at: prefix)
|
|
3566
|
+
{
|
|
3567
|
+
prefix += 1
|
|
3568
|
+
}
|
|
3569
|
+
if prefix < runEnd {
|
|
3570
|
+
break
|
|
3571
|
+
}
|
|
3572
|
+
}
|
|
3573
|
+
|
|
3574
|
+
var suffix = 0
|
|
3575
|
+
while suffix < (sharedLength - prefix) {
|
|
3576
|
+
let existingIndex = existing.length - suffix - 1
|
|
3577
|
+
let replacementIndex = replacement.length - suffix - 1
|
|
3578
|
+
var existingRange = NSRange()
|
|
3579
|
+
let existingAttrs = existing.attributes(
|
|
3580
|
+
at: existingIndex,
|
|
3581
|
+
longestEffectiveRange: &existingRange,
|
|
3582
|
+
in: NSRange(location: prefix, length: existingIndex - prefix + 1)
|
|
3583
|
+
)
|
|
3584
|
+
var replacementRange = NSRange()
|
|
3585
|
+
let replacementAttrs = replacement.attributes(
|
|
3586
|
+
at: replacementIndex,
|
|
3587
|
+
longestEffectiveRange: &replacementRange,
|
|
3588
|
+
in: NSRange(location: prefix, length: replacementIndex - prefix + 1)
|
|
3589
|
+
)
|
|
3590
|
+
guard attributesEqualForPatchTrimming(existingAttrs, replacementAttrs) else { break }
|
|
3591
|
+
let maxComparableLength = min(
|
|
3592
|
+
existingIndex - max(existingRange.location, prefix) + 1,
|
|
3593
|
+
replacementIndex - max(replacementRange.location, prefix) + 1,
|
|
3594
|
+
sharedLength - prefix - suffix
|
|
3595
|
+
)
|
|
3596
|
+
var matchedLength = 0
|
|
3597
|
+
while matchedLength < maxComparableLength,
|
|
3598
|
+
existingString.character(at: existingIndex - matchedLength)
|
|
3599
|
+
== replacementString.character(at: replacementIndex - matchedLength)
|
|
3600
|
+
{
|
|
3601
|
+
matchedLength += 1
|
|
3602
|
+
}
|
|
3603
|
+
suffix += matchedLength
|
|
3604
|
+
if matchedLength < maxComparableLength {
|
|
3605
|
+
break
|
|
3606
|
+
}
|
|
3607
|
+
}
|
|
3608
|
+
|
|
3609
|
+
guard prefix > 0 || suffix > 0 else {
|
|
3610
|
+
return (fullReplaceRange, replacement)
|
|
3611
|
+
}
|
|
3612
|
+
|
|
3613
|
+
let trimmedReplaceRange = NSRange(
|
|
3614
|
+
location: fullReplaceRange.location + prefix,
|
|
3615
|
+
length: fullReplaceRange.length - prefix - suffix
|
|
3616
|
+
)
|
|
3617
|
+
let trimmedReplacementRange = NSRange(
|
|
3618
|
+
location: prefix,
|
|
3619
|
+
length: replacement.length - prefix - suffix
|
|
3620
|
+
)
|
|
3621
|
+
return (
|
|
3622
|
+
trimmedReplaceRange,
|
|
3623
|
+
replacement.attributedSubstring(from: trimmedReplacementRange)
|
|
3624
|
+
)
|
|
3625
|
+
}
|
|
3626
|
+
|
|
3627
|
+
private func applyRenderPatchIfPossible(_ patch: ParsedRenderPatch) -> PatchApplyTrace {
|
|
3628
|
+
let eligibilityStartedAt = DispatchTime.now().uptimeNanoseconds
|
|
3629
|
+
guard hasTopLevelChildMetadata(),
|
|
3630
|
+
let fullReplaceRange = replacementRangeForRenderPatch(
|
|
3631
|
+
startIndex: patch.startIndex,
|
|
3632
|
+
deleteCount: patch.deleteCount
|
|
3633
|
+
)
|
|
3634
|
+
else {
|
|
3635
|
+
return PatchApplyTrace(
|
|
3636
|
+
applied: false,
|
|
3637
|
+
eligibilityNanos: DispatchTime.now().uptimeNanoseconds - eligibilityStartedAt,
|
|
3638
|
+
trimNanos: 0,
|
|
3639
|
+
metadataNanos: 0,
|
|
3640
|
+
buildRenderNanos: 0,
|
|
3641
|
+
applyRenderNanos: 0,
|
|
3642
|
+
applyRenderReplaceUtf16Length: 0,
|
|
3643
|
+
applyRenderReplacementUtf16Length: 0,
|
|
3644
|
+
applyRenderTextMutationNanos: 0,
|
|
3645
|
+
applyRenderBeginEditingNanos: 0,
|
|
3646
|
+
applyRenderEndEditingNanos: 0,
|
|
3647
|
+
applyRenderStringMutationNanos: 0,
|
|
3648
|
+
applyRenderAttributeMutationNanos: 0,
|
|
3649
|
+
applyRenderAuthorizedTextNanos: 0,
|
|
3650
|
+
applyRenderCacheInvalidationNanos: 0,
|
|
3651
|
+
usedSmallPatchTextMutation: false
|
|
3652
|
+
)
|
|
3653
|
+
}
|
|
3654
|
+
|
|
3655
|
+
let buildStartedAt = DispatchTime.now().uptimeNanoseconds
|
|
3656
|
+
let attrStr = RenderBridge.renderBlocks(
|
|
3657
|
+
fromArray: patch.renderBlocks,
|
|
3658
|
+
startIndex: patch.startIndex,
|
|
3659
|
+
includeLeadingInterBlockSeparator: patch.startIndex > 0,
|
|
3660
|
+
baseFont: baseFont,
|
|
3661
|
+
textColor: baseTextColor,
|
|
3662
|
+
theme: theme
|
|
3663
|
+
)
|
|
3664
|
+
let buildRenderNanos = DispatchTime.now().uptimeNanoseconds - buildStartedAt
|
|
3665
|
+
let renderedPatchMetadata = topLevelChildMetadataSlice(from: attrStr)
|
|
3666
|
+
let renderedPatchContainsAttachment =
|
|
3667
|
+
renderedPatchMetadata?.entries.contains(where: \.containsAttachment)
|
|
3668
|
+
?? attributedStringContainsAttachment(attrStr)
|
|
3669
|
+
let renderedPatchContainsListMarkerContext =
|
|
3670
|
+
attributedStringContainsListMarkerContext(attrStr)
|
|
3671
|
+
let renderedPatchContainsPositionAdjustments =
|
|
3672
|
+
renderedPatchMetadata?.entries.contains(where: \.containsPositionAdjustments)
|
|
3673
|
+
?? attributedStringContainsPositionAdjustments(attrStr)
|
|
3674
|
+
guard !topLevelChildrenContainAttachment(
|
|
3675
|
+
startIndex: patch.startIndex,
|
|
3676
|
+
deleteCount: patch.deleteCount
|
|
3677
|
+
),
|
|
3678
|
+
!renderedPatchContainsAttachment
|
|
3679
|
+
else {
|
|
3680
|
+
return PatchApplyTrace(
|
|
3681
|
+
applied: false,
|
|
3682
|
+
eligibilityNanos: DispatchTime.now().uptimeNanoseconds - eligibilityStartedAt,
|
|
3683
|
+
trimNanos: 0,
|
|
3684
|
+
metadataNanos: 0,
|
|
3685
|
+
buildRenderNanos: buildRenderNanos,
|
|
3686
|
+
applyRenderNanos: 0,
|
|
3687
|
+
applyRenderReplaceUtf16Length: 0,
|
|
3688
|
+
applyRenderReplacementUtf16Length: 0,
|
|
3689
|
+
applyRenderTextMutationNanos: 0,
|
|
3690
|
+
applyRenderBeginEditingNanos: 0,
|
|
3691
|
+
applyRenderEndEditingNanos: 0,
|
|
3692
|
+
applyRenderStringMutationNanos: 0,
|
|
3693
|
+
applyRenderAttributeMutationNanos: 0,
|
|
3694
|
+
applyRenderAuthorizedTextNanos: 0,
|
|
3695
|
+
applyRenderCacheInvalidationNanos: 0,
|
|
3696
|
+
usedSmallPatchTextMutation: false
|
|
3697
|
+
)
|
|
3698
|
+
}
|
|
3699
|
+
guard !textStorageRangeContainsListMarkerContext(fullReplaceRange),
|
|
3700
|
+
!renderedPatchContainsListMarkerContext
|
|
3701
|
+
else {
|
|
3702
|
+
return PatchApplyTrace(
|
|
3703
|
+
applied: false,
|
|
3704
|
+
eligibilityNanos: DispatchTime.now().uptimeNanoseconds - eligibilityStartedAt,
|
|
3705
|
+
trimNanos: 0,
|
|
3706
|
+
metadataNanos: 0,
|
|
3707
|
+
buildRenderNanos: buildRenderNanos,
|
|
3708
|
+
applyRenderNanos: 0,
|
|
3709
|
+
applyRenderReplaceUtf16Length: 0,
|
|
3710
|
+
applyRenderReplacementUtf16Length: 0,
|
|
3711
|
+
applyRenderTextMutationNanos: 0,
|
|
3712
|
+
applyRenderBeginEditingNanos: 0,
|
|
3713
|
+
applyRenderEndEditingNanos: 0,
|
|
3714
|
+
applyRenderStringMutationNanos: 0,
|
|
3715
|
+
applyRenderAttributeMutationNanos: 0,
|
|
3716
|
+
applyRenderAuthorizedTextNanos: 0,
|
|
3717
|
+
applyRenderCacheInvalidationNanos: 0,
|
|
3718
|
+
usedSmallPatchTextMutation: false
|
|
3719
|
+
)
|
|
3720
|
+
}
|
|
3721
|
+
let eligibilityNanos =
|
|
3722
|
+
DispatchTime.now().uptimeNanoseconds - eligibilityStartedAt - buildRenderNanos
|
|
3723
|
+
let positionCacheUpdate: PositionCacheUpdate =
|
|
3724
|
+
if topLevelChildrenContainPositionAdjustments(
|
|
3725
|
+
startIndex: patch.startIndex,
|
|
3726
|
+
deleteCount: patch.deleteCount
|
|
3727
|
+
) || renderedPatchContainsPositionAdjustments
|
|
3728
|
+
{
|
|
3729
|
+
.attributed
|
|
3730
|
+
} else {
|
|
3731
|
+
.plainText
|
|
3732
|
+
}
|
|
3733
|
+
let trimStartedAt = DispatchTime.now().uptimeNanoseconds
|
|
3734
|
+
let patchToApply = trimmedAttributedPatch(replacing: fullReplaceRange, with: attrStr)
|
|
3735
|
+
let trimNanos = DispatchTime.now().uptimeNanoseconds - trimStartedAt
|
|
3736
|
+
let applyTrace = applyAttributedRender(
|
|
3737
|
+
patchToApply.replacement,
|
|
3738
|
+
replaceRange: patchToApply.replaceRange,
|
|
3739
|
+
usedPatch: true,
|
|
3740
|
+
positionCacheUpdate: positionCacheUpdate
|
|
3741
|
+
)
|
|
3742
|
+
let metadataStartedAt = DispatchTime.now().uptimeNanoseconds
|
|
3743
|
+
applyTopLevelChildMetadataPatch(
|
|
3744
|
+
patch,
|
|
3745
|
+
replaceRange: fullReplaceRange,
|
|
3746
|
+
renderedPatchMetadata: renderedPatchMetadata,
|
|
3747
|
+
renderedPatchLength: attrStr.length
|
|
3748
|
+
)
|
|
3749
|
+
let metadataNanos = DispatchTime.now().uptimeNanoseconds - metadataStartedAt
|
|
3750
|
+
return PatchApplyTrace(
|
|
3751
|
+
applied: true,
|
|
3752
|
+
eligibilityNanos: eligibilityNanos,
|
|
3753
|
+
trimNanos: trimNanos,
|
|
3754
|
+
metadataNanos: metadataNanos,
|
|
3755
|
+
buildRenderNanos: buildRenderNanos,
|
|
3756
|
+
applyRenderNanos: applyTrace.totalNanos,
|
|
3757
|
+
applyRenderReplaceUtf16Length: applyTrace.replaceUtf16Length,
|
|
3758
|
+
applyRenderReplacementUtf16Length: applyTrace.replacementUtf16Length,
|
|
3759
|
+
applyRenderTextMutationNanos: applyTrace.textMutationNanos,
|
|
3760
|
+
applyRenderBeginEditingNanos: applyTrace.beginEditingNanos,
|
|
3761
|
+
applyRenderEndEditingNanos: applyTrace.endEditingNanos,
|
|
3762
|
+
applyRenderStringMutationNanos: applyTrace.stringMutationNanos,
|
|
3763
|
+
applyRenderAttributeMutationNanos: applyTrace.attributeMutationNanos,
|
|
3764
|
+
applyRenderAuthorizedTextNanos: applyTrace.authorizedTextNanos,
|
|
3765
|
+
applyRenderCacheInvalidationNanos: applyTrace.cacheInvalidationNanos,
|
|
3766
|
+
usedSmallPatchTextMutation: applyTrace.usedSmallPatchTextMutation
|
|
3767
|
+
)
|
|
3768
|
+
}
|
|
3769
|
+
|
|
3770
|
+
/// Apply a full render update from Rust to the text view.
|
|
3771
|
+
///
|
|
3772
|
+
/// Parses the update JSON, converts render elements to NSAttributedString
|
|
3773
|
+
/// via RenderBridge, and replaces the text view's content.
|
|
3774
|
+
///
|
|
3775
|
+
/// - Parameter updateJSON: The JSON string from editor_insert_text, etc.
|
|
3776
|
+
func applyUpdateJSON(_ updateJSON: String, notifyDelegate: Bool = true) {
|
|
3777
|
+
let totalStartedAt = DispatchTime.now().uptimeNanoseconds
|
|
3778
|
+
let parseStartedAt = totalStartedAt
|
|
3779
|
+
guard let data = updateJSON.data(using: .utf8),
|
|
3780
|
+
let update = try? JSONSerialization.jsonObject(with: data) as? [String: Any]
|
|
3781
|
+
else { return }
|
|
3782
|
+
let parseNanos = DispatchTime.now().uptimeNanoseconds - parseStartedAt
|
|
3783
|
+
|
|
3784
|
+
let renderElements = update["renderElements"] as? [[String: Any]]
|
|
3785
|
+
let selectionFromUpdate = (update["selection"] as? [String: Any])
|
|
3786
|
+
.map(self.selectionSummary(from:)) ?? "none"
|
|
3787
|
+
Self.updateLog.debug(
|
|
3788
|
+
"[applyUpdateJSON.begin] renderCount=\(renderElements?.count ?? 0) updateSelection=\(selectionFromUpdate, privacy: .public) before=\(self.textSnapshotSummary(), privacy: .public)"
|
|
3789
|
+
)
|
|
3790
|
+
let resolveRenderBlocksStartedAt = DispatchTime.now().uptimeNanoseconds
|
|
3791
|
+
let renderBlocks = parseRenderBlocks(update["renderBlocks"])
|
|
3792
|
+
let explicitRenderPatch = parseRenderPatch(update["renderPatch"])
|
|
3793
|
+
let resolvedRenderBlocks = renderBlocks
|
|
3794
|
+
?? explicitRenderPatch.flatMap { patch in
|
|
3795
|
+
currentRenderBlocks.flatMap { mergeRenderBlocks(applying: patch, to: $0) }
|
|
3796
|
+
}
|
|
3797
|
+
let resolveRenderBlocksNanos =
|
|
3798
|
+
DispatchTime.now().uptimeNanoseconds - resolveRenderBlocksStartedAt
|
|
3799
|
+
|
|
3800
|
+
let derivedRenderPatch: DerivedRenderPatch? =
|
|
3801
|
+
if explicitRenderPatch == nil,
|
|
3802
|
+
let currentRenderBlocks,
|
|
3803
|
+
let resolvedRenderBlocks
|
|
3804
|
+
{
|
|
3805
|
+
deriveRenderPatch(from: currentRenderBlocks, to: resolvedRenderBlocks)
|
|
3806
|
+
} else {
|
|
3807
|
+
nil
|
|
3808
|
+
}
|
|
3809
|
+
let renderPatch = explicitRenderPatch ?? {
|
|
3810
|
+
if case let .patch(patch)? = derivedRenderPatch {
|
|
3811
|
+
return patch
|
|
3812
|
+
}
|
|
3813
|
+
return nil
|
|
3814
|
+
}()
|
|
3815
|
+
let shouldSkipRender = if case .unchanged? = derivedRenderPatch {
|
|
3816
|
+
textStorage.string == lastAuthorizedText
|
|
3817
|
+
&& lastAppliedRenderAppearanceRevision == renderAppearanceRevision
|
|
3818
|
+
} else {
|
|
3819
|
+
false
|
|
3820
|
+
}
|
|
3821
|
+
|
|
3822
|
+
let patchTrace = renderPatch.map(applyRenderPatchIfPossible)
|
|
3823
|
+
let appliedPatch = patchTrace?.applied == true
|
|
3824
|
+
var usedSmallPatchTextMutation = patchTrace?.usedSmallPatchTextMutation ?? false
|
|
3825
|
+
var applyRenderReplaceUtf16Length = patchTrace?.applyRenderReplaceUtf16Length ?? 0
|
|
3826
|
+
var applyRenderReplacementUtf16Length =
|
|
3827
|
+
patchTrace?.applyRenderReplacementUtf16Length ?? 0
|
|
3828
|
+
var buildRenderNanos = patchTrace?.buildRenderNanos ?? 0
|
|
3829
|
+
var applyRenderNanos = patchTrace?.applyRenderNanos ?? 0
|
|
3830
|
+
var applyRenderTextMutationNanos = patchTrace?.applyRenderTextMutationNanos ?? 0
|
|
3831
|
+
var applyRenderBeginEditingNanos = patchTrace?.applyRenderBeginEditingNanos ?? 0
|
|
3832
|
+
var applyRenderEndEditingNanos = patchTrace?.applyRenderEndEditingNanos ?? 0
|
|
3833
|
+
var applyRenderStringMutationNanos = patchTrace?.applyRenderStringMutationNanos ?? 0
|
|
3834
|
+
var applyRenderAttributeMutationNanos =
|
|
3835
|
+
patchTrace?.applyRenderAttributeMutationNanos ?? 0
|
|
3836
|
+
var applyRenderAuthorizedTextNanos = patchTrace?.applyRenderAuthorizedTextNanos ?? 0
|
|
3837
|
+
var applyRenderCacheInvalidationNanos = patchTrace?.applyRenderCacheInvalidationNanos ?? 0
|
|
3838
|
+
if shouldSkipRender {
|
|
3839
|
+
lastRenderAppliedPatchForTesting = false
|
|
3840
|
+
if let resolvedRenderBlocks {
|
|
3841
|
+
currentRenderBlocks = resolvedRenderBlocks
|
|
3842
|
+
}
|
|
3843
|
+
} else if !appliedPatch {
|
|
3844
|
+
let buildStartedAt = DispatchTime.now().uptimeNanoseconds
|
|
3845
|
+
let attrStr: NSAttributedString
|
|
3846
|
+
if let resolvedRenderBlocks {
|
|
3847
|
+
attrStr = RenderBridge.renderBlocks(
|
|
3848
|
+
fromArray: resolvedRenderBlocks,
|
|
3849
|
+
baseFont: baseFont,
|
|
3850
|
+
textColor: baseTextColor,
|
|
3851
|
+
theme: theme
|
|
3852
|
+
)
|
|
3853
|
+
currentRenderBlocks = resolvedRenderBlocks
|
|
3854
|
+
} else if let renderElements {
|
|
3855
|
+
attrStr = RenderBridge.renderElements(
|
|
3856
|
+
fromArray: renderElements,
|
|
3857
|
+
baseFont: baseFont,
|
|
3858
|
+
textColor: baseTextColor,
|
|
3859
|
+
theme: theme
|
|
3860
|
+
)
|
|
3861
|
+
currentRenderBlocks = nil
|
|
3862
|
+
} else {
|
|
3863
|
+
return
|
|
3864
|
+
}
|
|
3865
|
+
buildRenderNanos = DispatchTime.now().uptimeNanoseconds - buildStartedAt
|
|
3866
|
+
let applyTrace = applyAttributedRender(
|
|
3867
|
+
attrStr,
|
|
3868
|
+
usedPatch: false,
|
|
3869
|
+
positionCacheUpdate: .invalidate
|
|
3870
|
+
)
|
|
3871
|
+
refreshTopLevelChildMetadata(from: attrStr)
|
|
3872
|
+
applyRenderReplaceUtf16Length = applyTrace.replaceUtf16Length
|
|
3873
|
+
applyRenderReplacementUtf16Length = applyTrace.replacementUtf16Length
|
|
3874
|
+
applyRenderNanos = applyTrace.totalNanos
|
|
3875
|
+
applyRenderTextMutationNanos = applyTrace.textMutationNanos
|
|
3876
|
+
applyRenderBeginEditingNanos = applyTrace.beginEditingNanos
|
|
3877
|
+
applyRenderEndEditingNanos = applyTrace.endEditingNanos
|
|
3878
|
+
applyRenderStringMutationNanos = applyTrace.stringMutationNanos
|
|
3879
|
+
applyRenderAttributeMutationNanos = applyTrace.attributeMutationNanos
|
|
3880
|
+
applyRenderAuthorizedTextNanos = applyTrace.authorizedTextNanos
|
|
3881
|
+
applyRenderCacheInvalidationNanos = applyTrace.cacheInvalidationNanos
|
|
3882
|
+
usedSmallPatchTextMutation = applyTrace.usedSmallPatchTextMutation
|
|
3883
|
+
lastAppliedRenderAppearanceRevision = renderAppearanceRevision
|
|
3884
|
+
} else if let resolvedRenderBlocks {
|
|
3885
|
+
currentRenderBlocks = resolvedRenderBlocks
|
|
3886
|
+
lastAppliedRenderAppearanceRevision = renderAppearanceRevision
|
|
3887
|
+
}
|
|
2454
3888
|
|
|
2455
3889
|
refreshPlaceholderVisibility()
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
3890
|
+
Self.updateLog.debug(
|
|
3891
|
+
"[applyUpdateJSON.rendered] mode=\(appliedPatch ? "patch" : "full", privacy: .public) after=\(self.textSnapshotSummary(), privacy: .public)"
|
|
3892
|
+
)
|
|
3893
|
+
|
|
3894
|
+
// Apply the selection from the update.
|
|
3895
|
+
let selectionTrace: SelectionApplyTrace
|
|
3896
|
+
if let selection = update["selection"] as? [String: Any] {
|
|
3897
|
+
selectionTrace = applySelectionFromJSON(selection)
|
|
3898
|
+
} else {
|
|
3899
|
+
selectionTrace = SelectionApplyTrace(
|
|
3900
|
+
totalNanos: 0,
|
|
3901
|
+
resolveNanos: 0,
|
|
3902
|
+
assignmentNanos: 0,
|
|
3903
|
+
chromeNanos: 0
|
|
3904
|
+
)
|
|
2459
3905
|
}
|
|
2460
|
-
|
|
3906
|
+
let postApplyTrace = performPostApplyMaintenance()
|
|
3907
|
+
let postApplyNanos = postApplyTrace.totalNanos
|
|
3908
|
+
|
|
3909
|
+
if captureApplyUpdateTraceForTesting {
|
|
3910
|
+
lastApplyUpdateTraceForTesting = ApplyUpdateTrace(
|
|
3911
|
+
attemptedPatch: renderPatch != nil,
|
|
3912
|
+
usedPatch: appliedPatch,
|
|
3913
|
+
usedSmallPatchTextMutation: usedSmallPatchTextMutation,
|
|
3914
|
+
applyRenderReplaceUtf16Length: applyRenderReplaceUtf16Length,
|
|
3915
|
+
applyRenderReplacementUtf16Length: applyRenderReplacementUtf16Length,
|
|
3916
|
+
parseNanos: parseNanos,
|
|
3917
|
+
resolveRenderBlocksNanos: resolveRenderBlocksNanos,
|
|
3918
|
+
patchEligibilityNanos: patchTrace?.eligibilityNanos ?? 0,
|
|
3919
|
+
patchTrimNanos: patchTrace?.trimNanos ?? 0,
|
|
3920
|
+
patchMetadataNanos: patchTrace?.metadataNanos ?? 0,
|
|
3921
|
+
buildRenderNanos: buildRenderNanos,
|
|
3922
|
+
applyRenderNanos: applyRenderNanos,
|
|
3923
|
+
selectionNanos: selectionTrace.totalNanos,
|
|
3924
|
+
postApplyNanos: postApplyNanos,
|
|
3925
|
+
totalNanos: DispatchTime.now().uptimeNanoseconds - totalStartedAt,
|
|
3926
|
+
applyRenderTextMutationNanos: applyRenderTextMutationNanos,
|
|
3927
|
+
applyRenderBeginEditingNanos: applyRenderBeginEditingNanos,
|
|
3928
|
+
applyRenderEndEditingNanos: applyRenderEndEditingNanos,
|
|
3929
|
+
applyRenderStringMutationNanos: applyRenderStringMutationNanos,
|
|
3930
|
+
applyRenderAttributeMutationNanos: applyRenderAttributeMutationNanos,
|
|
3931
|
+
applyRenderAuthorizedTextNanos: applyRenderAuthorizedTextNanos,
|
|
3932
|
+
applyRenderCacheInvalidationNanos: applyRenderCacheInvalidationNanos,
|
|
3933
|
+
selectionResolveNanos: selectionTrace.resolveNanos,
|
|
3934
|
+
selectionAssignmentNanos: selectionTrace.assignmentNanos,
|
|
3935
|
+
selectionChromeNanos: selectionTrace.chromeNanos,
|
|
3936
|
+
postApplyTypingAttributesNanos: postApplyTrace.typingAttributesNanos,
|
|
3937
|
+
postApplyHeightNotifyNanos: postApplyTrace.heightNotifyNanos,
|
|
3938
|
+
postApplyHeightNotifyMeasureNanos: postApplyTrace.heightNotifyMeasureNanos,
|
|
3939
|
+
postApplyHeightNotifyCallbackNanos: postApplyTrace.heightNotifyCallbackNanos,
|
|
3940
|
+
postApplyHeightNotifyEnsureLayoutNanos: postApplyTrace.heightNotifyEnsureLayoutNanos,
|
|
3941
|
+
postApplyHeightNotifyUsedRectNanos: postApplyTrace.heightNotifyUsedRectNanos,
|
|
3942
|
+
postApplyHeightNotifyContentSizeNanos: postApplyTrace.heightNotifyContentSizeNanos,
|
|
3943
|
+
postApplyHeightNotifySizeThatFitsNanos: postApplyTrace.heightNotifySizeThatFitsNanos,
|
|
3944
|
+
postApplySelectionOrContentCallbackNanos:
|
|
3945
|
+
postApplyTrace.selectionOrContentCallbackNanos
|
|
3946
|
+
)
|
|
3947
|
+
}
|
|
3948
|
+
Self.updateLog.debug(
|
|
3949
|
+
"[applyUpdateJSON.end] finalSelection=\(self.selectionSummary(), privacy: .public) textState=\(self.textSnapshotSummary(), privacy: .public)"
|
|
3950
|
+
)
|
|
3951
|
+
|
|
3952
|
+
// Notify the delegate.
|
|
3953
|
+
if notifyDelegate {
|
|
3954
|
+
editorDelegate?.editorTextView(self, didReceiveUpdate: updateJSON)
|
|
3955
|
+
}
|
|
3956
|
+
}
|
|
3957
|
+
|
|
3958
|
+
/// Apply a render JSON string (just render elements, no update wrapper).
|
|
3959
|
+
///
|
|
3960
|
+
/// Used for initial content loading (set_html / set_json return render
|
|
3961
|
+
/// elements directly, not wrapped in an EditorUpdate).
|
|
3962
|
+
func applyRenderJSON(_ renderJSON: String) {
|
|
3963
|
+
Self.updateLog.debug(
|
|
3964
|
+
"[applyRenderJSON.begin] before=\(self.textSnapshotSummary(), privacy: .public)"
|
|
3965
|
+
)
|
|
3966
|
+
let attrStr = RenderBridge.renderElements(
|
|
3967
|
+
fromJSON: renderJSON,
|
|
3968
|
+
baseFont: baseFont,
|
|
3969
|
+
textColor: baseTextColor,
|
|
3970
|
+
theme: theme
|
|
3971
|
+
)
|
|
3972
|
+
_ = applyAttributedRender(attrStr, usedPatch: false)
|
|
3973
|
+
currentRenderBlocks = nil
|
|
3974
|
+
lastAppliedRenderAppearanceRevision = renderAppearanceRevision
|
|
3975
|
+
|
|
3976
|
+
refreshPlaceholderVisibility()
|
|
3977
|
+
_ = performPostApplyMaintenance()
|
|
2461
3978
|
Self.updateLog.debug(
|
|
2462
3979
|
"[applyRenderJSON.end] after=\(self.textSnapshotSummary(), privacy: .public)"
|
|
2463
3980
|
)
|
|
@@ -2471,54 +3988,136 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
2471
3988
|
/// {"type": "node", "pos": 10}
|
|
2472
3989
|
/// {"type": "all"}
|
|
2473
3990
|
/// ```
|
|
2474
|
-
private func applySelectionFromJSON(_ selection: [String: Any]) {
|
|
2475
|
-
guard let type = selection["type"] as? String else {
|
|
3991
|
+
private func applySelectionFromJSON(_ selection: [String: Any]) -> SelectionApplyTrace {
|
|
3992
|
+
guard let type = selection["type"] as? String else {
|
|
3993
|
+
return SelectionApplyTrace(totalNanos: 0, resolveNanos: 0, assignmentNanos: 0, chromeNanos: 0)
|
|
3994
|
+
}
|
|
2476
3995
|
|
|
3996
|
+
let totalStartedAt = DispatchTime.now().uptimeNanoseconds
|
|
2477
3997
|
isApplyingRustState = true
|
|
2478
3998
|
defer { isApplyingRustState = false }
|
|
2479
3999
|
|
|
2480
4000
|
switch type {
|
|
2481
4001
|
case "text":
|
|
4002
|
+
let resolveStartedAt = DispatchTime.now().uptimeNanoseconds
|
|
2482
4003
|
guard let anchorNum = selection["anchor"] as? NSNumber,
|
|
2483
4004
|
let headNum = selection["head"] as? NSNumber
|
|
2484
|
-
else {
|
|
4005
|
+
else {
|
|
4006
|
+
return SelectionApplyTrace(totalNanos: 0, resolveNanos: 0, assignmentNanos: 0, chromeNanos: 0)
|
|
4007
|
+
}
|
|
2485
4008
|
// anchor/head from Rust are document positions; convert to scalar offsets first.
|
|
2486
|
-
let anchorScalar =
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
let
|
|
2491
|
-
|
|
2492
|
-
|
|
4009
|
+
let anchorScalar = (selection["anchorScalar"] as? NSNumber)?.uint32Value
|
|
4010
|
+
?? editorDocToScalar(id: editorId, docPos: anchorNum.uint32Value)
|
|
4011
|
+
let headScalar = (selection["headScalar"] as? NSNumber)?.uint32Value
|
|
4012
|
+
?? editorDocToScalar(id: editorId, docPos: headNum.uint32Value)
|
|
4013
|
+
let startUtf16 = PositionBridge.scalarToUtf16Offset(
|
|
4014
|
+
min(anchorScalar, headScalar),
|
|
4015
|
+
in: self
|
|
4016
|
+
)
|
|
4017
|
+
let endUtf16 = PositionBridge.scalarToUtf16Offset(
|
|
4018
|
+
max(anchorScalar, headScalar),
|
|
4019
|
+
in: self
|
|
4020
|
+
)
|
|
4021
|
+
let resolveNanos = DispatchTime.now().uptimeNanoseconds - resolveStartedAt
|
|
4022
|
+
|
|
4023
|
+
let assignmentStartedAt = DispatchTime.now().uptimeNanoseconds
|
|
4024
|
+
if anchorScalar == headScalar {
|
|
4025
|
+
let endPos = position(from: beginningOfDocument, offset: endUtf16) ?? endOfDocument
|
|
4026
|
+
if let adjustedPosition = autocapitalizationFriendlyEmptyBlockPosition(for: endPos) {
|
|
4027
|
+
let adjustedOffset = offset(from: beginningOfDocument, to: adjustedPosition)
|
|
4028
|
+
let adjustedRange = NSRange(location: adjustedOffset, length: 0)
|
|
4029
|
+
if selectedRange != adjustedRange {
|
|
4030
|
+
selectedRange = adjustedRange
|
|
4031
|
+
}
|
|
4032
|
+
} else {
|
|
4033
|
+
let targetRange = NSRange(location: endUtf16, length: 0)
|
|
4034
|
+
if selectedRange != targetRange {
|
|
4035
|
+
selectedRange = targetRange
|
|
4036
|
+
}
|
|
4037
|
+
}
|
|
4038
|
+
} else {
|
|
4039
|
+
let targetRange = NSRange(location: startUtf16, length: endUtf16 - startUtf16)
|
|
4040
|
+
if selectedRange != targetRange {
|
|
4041
|
+
selectedRange = targetRange
|
|
4042
|
+
}
|
|
4043
|
+
}
|
|
4044
|
+
let assignmentNanos = DispatchTime.now().uptimeNanoseconds - assignmentStartedAt
|
|
4045
|
+
let chromeStartedAt = DispatchTime.now().uptimeNanoseconds
|
|
4046
|
+
showNativeSelectionChromeIfNeeded()
|
|
4047
|
+
let chromeNanos = DispatchTime.now().uptimeNanoseconds - chromeStartedAt
|
|
2493
4048
|
Self.selectionLog.debug(
|
|
2494
4049
|
"[applySelectionFromJSON.text] doc=\(anchorNum.uint32Value)-\(headNum.uint32Value) scalar=\(anchorScalar)-\(headScalar) final=\(self.selectionSummary(), privacy: .public)"
|
|
2495
4050
|
)
|
|
4051
|
+
return SelectionApplyTrace(
|
|
4052
|
+
totalNanos: DispatchTime.now().uptimeNanoseconds - totalStartedAt,
|
|
4053
|
+
resolveNanos: resolveNanos,
|
|
4054
|
+
assignmentNanos: assignmentNanos,
|
|
4055
|
+
chromeNanos: chromeNanos
|
|
4056
|
+
)
|
|
2496
4057
|
|
|
2497
4058
|
case "node":
|
|
2498
4059
|
// Node selection: select the object replacement character at that position.
|
|
2499
|
-
|
|
4060
|
+
let resolveStartedAt = DispatchTime.now().uptimeNanoseconds
|
|
4061
|
+
guard let posNum = selection["pos"] as? NSNumber else {
|
|
4062
|
+
return SelectionApplyTrace(totalNanos: 0, resolveNanos: 0, assignmentNanos: 0, chromeNanos: 0)
|
|
4063
|
+
}
|
|
2500
4064
|
// pos from Rust is a document position; convert to scalar offset.
|
|
2501
|
-
let posScalar =
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
4065
|
+
let posScalar = (selection["posScalar"] as? NSNumber)?.uint32Value
|
|
4066
|
+
?? editorDocToScalar(id: editorId, docPos: posNum.uint32Value)
|
|
4067
|
+
let startUtf16 = PositionBridge.scalarToUtf16Offset(posScalar, in: self)
|
|
4068
|
+
let targetRange = NSRange(location: startUtf16, length: 1)
|
|
4069
|
+
let resolveNanos = DispatchTime.now().uptimeNanoseconds - resolveStartedAt
|
|
4070
|
+
let assignmentStartedAt = DispatchTime.now().uptimeNanoseconds
|
|
4071
|
+
if selectedRange != targetRange {
|
|
4072
|
+
selectedRange = targetRange
|
|
2506
4073
|
}
|
|
4074
|
+
let assignmentNanos = DispatchTime.now().uptimeNanoseconds - assignmentStartedAt
|
|
4075
|
+
let chromeStartedAt = DispatchTime.now().uptimeNanoseconds
|
|
2507
4076
|
refreshNativeSelectionChromeVisibility()
|
|
4077
|
+
let chromeNanos = DispatchTime.now().uptimeNanoseconds - chromeStartedAt
|
|
2508
4078
|
Self.selectionLog.debug(
|
|
2509
4079
|
"[applySelectionFromJSON.node] doc=\(posNum.uint32Value) scalar=\(posScalar) final=\(self.selectionSummary(), privacy: .public)"
|
|
2510
4080
|
)
|
|
4081
|
+
return SelectionApplyTrace(
|
|
4082
|
+
totalNanos: DispatchTime.now().uptimeNanoseconds - totalStartedAt,
|
|
4083
|
+
resolveNanos: resolveNanos,
|
|
4084
|
+
assignmentNanos: assignmentNanos,
|
|
4085
|
+
chromeNanos: chromeNanos
|
|
4086
|
+
)
|
|
2511
4087
|
|
|
2512
4088
|
case "all":
|
|
4089
|
+
let assignmentStartedAt = DispatchTime.now().uptimeNanoseconds
|
|
2513
4090
|
selectedTextRange = textRange(from: beginningOfDocument, to: endOfDocument)
|
|
2514
|
-
|
|
4091
|
+
let assignmentNanos = DispatchTime.now().uptimeNanoseconds - assignmentStartedAt
|
|
4092
|
+
let chromeStartedAt = DispatchTime.now().uptimeNanoseconds
|
|
4093
|
+
showNativeSelectionChromeIfNeeded()
|
|
4094
|
+
let chromeNanos = DispatchTime.now().uptimeNanoseconds - chromeStartedAt
|
|
2515
4095
|
Self.selectionLog.debug(
|
|
2516
4096
|
"[applySelectionFromJSON.all] final=\(self.selectionSummary(), privacy: .public)"
|
|
2517
4097
|
)
|
|
4098
|
+
return SelectionApplyTrace(
|
|
4099
|
+
totalNanos: DispatchTime.now().uptimeNanoseconds - totalStartedAt,
|
|
4100
|
+
resolveNanos: 0,
|
|
4101
|
+
assignmentNanos: assignmentNanos,
|
|
4102
|
+
chromeNanos: chromeNanos
|
|
4103
|
+
)
|
|
2518
4104
|
|
|
2519
4105
|
default:
|
|
2520
|
-
|
|
4106
|
+
return SelectionApplyTrace(totalNanos: 0, resolveNanos: 0, assignmentNanos: 0, chromeNanos: 0)
|
|
4107
|
+
}
|
|
4108
|
+
}
|
|
4109
|
+
|
|
4110
|
+
private func autocapitalizationFriendlyEmptyBlockPosition(
|
|
4111
|
+
for position: UITextPosition
|
|
4112
|
+
) -> UITextPosition? {
|
|
4113
|
+
guard textStorage.length == 1 else { return nil }
|
|
4114
|
+
guard textStorage.string.unicodeScalars.elementsEqual([Self.emptyBlockPlaceholderScalar]) else {
|
|
4115
|
+
return nil
|
|
2521
4116
|
}
|
|
4117
|
+
|
|
4118
|
+
let utf16Offset = offset(from: beginningOfDocument, to: position)
|
|
4119
|
+
guard utf16Offset == textStorage.length else { return nil }
|
|
4120
|
+
return beginningOfDocument
|
|
2522
4121
|
}
|
|
2523
4122
|
|
|
2524
4123
|
}
|
|
@@ -2549,6 +4148,7 @@ extension EditorTextView: NSTextStorageDelegate {
|
|
|
2549
4148
|
// Compare current text storage content against last authorized snapshot.
|
|
2550
4149
|
let currentText = textStorage.string
|
|
2551
4150
|
guard currentText != lastAuthorizedText else { return }
|
|
4151
|
+
currentTopLevelChildMetadata = nil
|
|
2552
4152
|
let authorizedPreview = preview(lastAuthorizedText)
|
|
2553
4153
|
let storagePreview = preview(currentText)
|
|
2554
4154
|
|
|
@@ -2606,6 +4206,22 @@ extension EditorTextView: NSTextStorageDelegate {
|
|
|
2606
4206
|
/// and serves as the integration point for the future Fabric component.
|
|
2607
4207
|
final class RichTextEditorView: UIView {
|
|
2608
4208
|
|
|
4209
|
+
struct HostedLayoutTrace {
|
|
4210
|
+
let intrinsicContentSizeNanos: UInt64
|
|
4211
|
+
let intrinsicContentSizeCount: Int
|
|
4212
|
+
let measuredEditorHeightNanos: UInt64
|
|
4213
|
+
let measuredEditorHeightCount: Int
|
|
4214
|
+
let layoutSubviewsNanos: UInt64
|
|
4215
|
+
let layoutSubviewsCount: Int
|
|
4216
|
+
let refreshOverlaysNanos: UInt64
|
|
4217
|
+
let refreshOverlaysCount: Int
|
|
4218
|
+
let overlayScheduleRequestCount: Int
|
|
4219
|
+
let overlayScheduleExecuteCount: Int
|
|
4220
|
+
let overlayScheduleSkipCount: Int
|
|
4221
|
+
let onHeightMayChangeNanos: UInt64
|
|
4222
|
+
let onHeightMayChangeCount: Int
|
|
4223
|
+
}
|
|
4224
|
+
|
|
2609
4225
|
// MARK: - Properties
|
|
2610
4226
|
|
|
2611
4227
|
/// The editor text view that handles input interception.
|
|
@@ -2613,9 +4229,29 @@ final class RichTextEditorView: UIView {
|
|
|
2613
4229
|
private let remoteSelectionOverlayView = RemoteSelectionOverlayView()
|
|
2614
4230
|
private let imageTapOverlayView = ImageTapOverlayView()
|
|
2615
4231
|
private let imageResizeOverlayView = ImageResizeOverlayView()
|
|
2616
|
-
var onHeightMayChange: (() -> Void)?
|
|
4232
|
+
var onHeightMayChange: ((CGFloat) -> Void)?
|
|
2617
4233
|
private var lastAutoGrowWidth: CGFloat = 0
|
|
4234
|
+
private var cachedAutoGrowMeasuredHeight: CGFloat = 0
|
|
2618
4235
|
private var remoteSelections: [RemoteSelectionDecoration] = []
|
|
4236
|
+
private var overlayRefreshScheduled = false
|
|
4237
|
+
var captureHostedLayoutTraceForTesting = false
|
|
4238
|
+
private var hostedLayoutTraceNanos = (
|
|
4239
|
+
intrinsicContentSize: UInt64(0),
|
|
4240
|
+
measuredEditorHeight: UInt64(0),
|
|
4241
|
+
layoutSubviews: UInt64(0),
|
|
4242
|
+
refreshOverlays: UInt64(0),
|
|
4243
|
+
onHeightMayChange: UInt64(0)
|
|
4244
|
+
)
|
|
4245
|
+
private var hostedLayoutTraceCounts = (
|
|
4246
|
+
intrinsicContentSize: 0,
|
|
4247
|
+
measuredEditorHeight: 0,
|
|
4248
|
+
layoutSubviews: 0,
|
|
4249
|
+
refreshOverlays: 0,
|
|
4250
|
+
overlayScheduleRequest: 0,
|
|
4251
|
+
overlayScheduleExecute: 0,
|
|
4252
|
+
overlayScheduleSkip: 0,
|
|
4253
|
+
onHeightMayChange: 0
|
|
4254
|
+
)
|
|
2619
4255
|
var allowImageResizing = true {
|
|
2620
4256
|
didSet {
|
|
2621
4257
|
guard oldValue != allowImageResizing else { return }
|
|
@@ -2630,9 +4266,23 @@ final class RichTextEditorView: UIView {
|
|
|
2630
4266
|
didSet {
|
|
2631
4267
|
guard oldValue != heightBehavior else { return }
|
|
2632
4268
|
textView.heightBehavior = heightBehavior
|
|
4269
|
+
textView.updateAutoGrowHostHeight(heightBehavior == .autoGrow ? bounds.height : 0)
|
|
4270
|
+
if heightBehavior != .autoGrow {
|
|
4271
|
+
cachedAutoGrowMeasuredHeight = 0
|
|
4272
|
+
}
|
|
2633
4273
|
invalidateIntrinsicContentSize()
|
|
2634
4274
|
setNeedsLayout()
|
|
2635
|
-
|
|
4275
|
+
if heightBehavior == .autoGrow {
|
|
4276
|
+
let measuredHeight = measuredEditorHeight()
|
|
4277
|
+
if measuredHeight > 0 {
|
|
4278
|
+
cachedAutoGrowMeasuredHeight = measuredHeight
|
|
4279
|
+
onHeightMayChange?(measuredHeight)
|
|
4280
|
+
} else {
|
|
4281
|
+
onHeightMayChange?(0)
|
|
4282
|
+
}
|
|
4283
|
+
} else {
|
|
4284
|
+
onHeightMayChange?(0)
|
|
4285
|
+
}
|
|
2636
4286
|
remoteSelectionOverlayView.refresh()
|
|
2637
4287
|
imageResizeOverlayView.refresh()
|
|
2638
4288
|
}
|
|
@@ -2670,54 +4320,45 @@ final class RichTextEditorView: UIView {
|
|
|
2670
4320
|
}
|
|
2671
4321
|
|
|
2672
4322
|
private func setupView() {
|
|
2673
|
-
// Add the text view as a subview.
|
|
2674
|
-
|
|
2675
|
-
remoteSelectionOverlayView.translatesAutoresizingMaskIntoConstraints = false
|
|
2676
|
-
imageTapOverlayView.translatesAutoresizingMaskIntoConstraints = false
|
|
2677
|
-
imageResizeOverlayView.translatesAutoresizingMaskIntoConstraints = false
|
|
4323
|
+
// Add the text view as a subview. These views always track the host bounds,
|
|
4324
|
+
// so manual layout is cheaper than driving them through Auto Layout.
|
|
2678
4325
|
remoteSelectionOverlayView.bind(textView: textView)
|
|
2679
4326
|
imageTapOverlayView.bind(editorView: self)
|
|
2680
4327
|
imageResizeOverlayView.bind(editorView: self)
|
|
2681
4328
|
textView.allowImageResizing = allowImageResizing
|
|
2682
4329
|
imageTapOverlayView.isHidden = editorId == 0 || !allowImageResizing
|
|
2683
|
-
textView.onHeightMayChange = { [weak self] in
|
|
4330
|
+
textView.onHeightMayChange = { [weak self] measuredHeight in
|
|
2684
4331
|
guard let self, self.heightBehavior == .autoGrow else { return }
|
|
4332
|
+
let startedAt = DispatchTime.now().uptimeNanoseconds
|
|
4333
|
+
self.cachedAutoGrowMeasuredHeight = measuredHeight
|
|
2685
4334
|
self.invalidateIntrinsicContentSize()
|
|
2686
|
-
self.
|
|
2687
|
-
self.
|
|
4335
|
+
self.onHeightMayChange?(measuredHeight)
|
|
4336
|
+
self.recordHostedLayoutTrace(
|
|
4337
|
+
durationNanos: DispatchTime.now().uptimeNanoseconds - startedAt,
|
|
4338
|
+
keyPath: .onHeightMayChange
|
|
4339
|
+
)
|
|
2688
4340
|
}
|
|
2689
4341
|
textView.onViewportMayChange = { [weak self] in
|
|
2690
|
-
self?.
|
|
4342
|
+
self?.refreshOverlaysIfNeeded()
|
|
2691
4343
|
}
|
|
2692
4344
|
textView.onSelectionOrContentMayChange = { [weak self] in
|
|
2693
|
-
self?.
|
|
4345
|
+
self?.scheduleRefreshOverlaysIfNeeded()
|
|
2694
4346
|
}
|
|
2695
4347
|
addSubview(textView)
|
|
2696
4348
|
addSubview(remoteSelectionOverlayView)
|
|
2697
4349
|
addSubview(imageTapOverlayView)
|
|
2698
4350
|
addSubview(imageResizeOverlayView)
|
|
2699
|
-
|
|
2700
|
-
NSLayoutConstraint.activate([
|
|
2701
|
-
textView.topAnchor.constraint(equalTo: topAnchor),
|
|
2702
|
-
textView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
|
2703
|
-
textView.trailingAnchor.constraint(equalTo: trailingAnchor),
|
|
2704
|
-
textView.bottomAnchor.constraint(equalTo: bottomAnchor),
|
|
2705
|
-
remoteSelectionOverlayView.topAnchor.constraint(equalTo: topAnchor),
|
|
2706
|
-
remoteSelectionOverlayView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
|
2707
|
-
remoteSelectionOverlayView.trailingAnchor.constraint(equalTo: trailingAnchor),
|
|
2708
|
-
remoteSelectionOverlayView.bottomAnchor.constraint(equalTo: bottomAnchor),
|
|
2709
|
-
imageTapOverlayView.topAnchor.constraint(equalTo: topAnchor),
|
|
2710
|
-
imageTapOverlayView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
|
2711
|
-
imageTapOverlayView.trailingAnchor.constraint(equalTo: trailingAnchor),
|
|
2712
|
-
imageTapOverlayView.bottomAnchor.constraint(equalTo: bottomAnchor),
|
|
2713
|
-
imageResizeOverlayView.topAnchor.constraint(equalTo: topAnchor),
|
|
2714
|
-
imageResizeOverlayView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
|
2715
|
-
imageResizeOverlayView.trailingAnchor.constraint(equalTo: trailingAnchor),
|
|
2716
|
-
imageResizeOverlayView.bottomAnchor.constraint(equalTo: bottomAnchor),
|
|
2717
|
-
])
|
|
4351
|
+
layoutManagedSubviews()
|
|
2718
4352
|
}
|
|
2719
4353
|
|
|
2720
4354
|
override var intrinsicContentSize: CGSize {
|
|
4355
|
+
let startedAt = DispatchTime.now().uptimeNanoseconds
|
|
4356
|
+
defer {
|
|
4357
|
+
recordHostedLayoutTrace(
|
|
4358
|
+
durationNanos: DispatchTime.now().uptimeNanoseconds - startedAt,
|
|
4359
|
+
keyPath: .intrinsicContentSize
|
|
4360
|
+
)
|
|
4361
|
+
}
|
|
2721
4362
|
guard heightBehavior == .autoGrow else {
|
|
2722
4363
|
return CGSize(width: UIView.noIntrinsicMetric, height: UIView.noIntrinsicMetric)
|
|
2723
4364
|
}
|
|
@@ -2730,12 +4371,22 @@ final class RichTextEditorView: UIView {
|
|
|
2730
4371
|
}
|
|
2731
4372
|
|
|
2732
4373
|
override func layoutSubviews() {
|
|
4374
|
+
let startedAt = DispatchTime.now().uptimeNanoseconds
|
|
4375
|
+
defer {
|
|
4376
|
+
recordHostedLayoutTrace(
|
|
4377
|
+
durationNanos: DispatchTime.now().uptimeNanoseconds - startedAt,
|
|
4378
|
+
keyPath: .layoutSubviews
|
|
4379
|
+
)
|
|
4380
|
+
}
|
|
2733
4381
|
super.layoutSubviews()
|
|
2734
|
-
|
|
4382
|
+
layoutManagedSubviews()
|
|
4383
|
+
refreshOverlaysIfNeeded()
|
|
2735
4384
|
guard heightBehavior == .autoGrow else { return }
|
|
4385
|
+
textView.updateAutoGrowHostHeight(bounds.height)
|
|
2736
4386
|
let currentWidth = bounds.width.rounded(.towardZero)
|
|
2737
4387
|
guard currentWidth != lastAutoGrowWidth else { return }
|
|
2738
4388
|
lastAutoGrowWidth = currentWidth
|
|
4389
|
+
cachedAutoGrowMeasuredHeight = 0
|
|
2739
4390
|
invalidateIntrinsicContentSize()
|
|
2740
4391
|
}
|
|
2741
4392
|
|
|
@@ -2777,11 +4428,50 @@ final class RichTextEditorView: UIView {
|
|
|
2777
4428
|
}
|
|
2778
4429
|
|
|
2779
4430
|
func refreshRemoteSelections() {
|
|
4431
|
+
guard remoteSelectionOverlayView.hasSelectionsOrVisibleDecorations else { return }
|
|
2780
4432
|
remoteSelectionOverlayView.refresh()
|
|
2781
4433
|
}
|
|
2782
4434
|
|
|
2783
4435
|
func remoteSelectionOverlaySubviewsForTesting() -> [UIView] {
|
|
2784
|
-
remoteSelectionOverlayView.subviews
|
|
4436
|
+
remoteSelectionOverlayView.subviews.filter { !$0.isHidden }
|
|
4437
|
+
}
|
|
4438
|
+
|
|
4439
|
+
func resetHostedLayoutTraceForTesting() {
|
|
4440
|
+
hostedLayoutTraceNanos = (
|
|
4441
|
+
intrinsicContentSize: 0,
|
|
4442
|
+
measuredEditorHeight: 0,
|
|
4443
|
+
layoutSubviews: 0,
|
|
4444
|
+
refreshOverlays: 0,
|
|
4445
|
+
onHeightMayChange: 0
|
|
4446
|
+
)
|
|
4447
|
+
hostedLayoutTraceCounts = (
|
|
4448
|
+
intrinsicContentSize: 0,
|
|
4449
|
+
measuredEditorHeight: 0,
|
|
4450
|
+
layoutSubviews: 0,
|
|
4451
|
+
refreshOverlays: 0,
|
|
4452
|
+
overlayScheduleRequest: 0,
|
|
4453
|
+
overlayScheduleExecute: 0,
|
|
4454
|
+
overlayScheduleSkip: 0,
|
|
4455
|
+
onHeightMayChange: 0
|
|
4456
|
+
)
|
|
4457
|
+
}
|
|
4458
|
+
|
|
4459
|
+
func lastHostedLayoutTraceForTesting() -> HostedLayoutTrace {
|
|
4460
|
+
HostedLayoutTrace(
|
|
4461
|
+
intrinsicContentSizeNanos: hostedLayoutTraceNanos.intrinsicContentSize,
|
|
4462
|
+
intrinsicContentSizeCount: hostedLayoutTraceCounts.intrinsicContentSize,
|
|
4463
|
+
measuredEditorHeightNanos: hostedLayoutTraceNanos.measuredEditorHeight,
|
|
4464
|
+
measuredEditorHeightCount: hostedLayoutTraceCounts.measuredEditorHeight,
|
|
4465
|
+
layoutSubviewsNanos: hostedLayoutTraceNanos.layoutSubviews,
|
|
4466
|
+
layoutSubviewsCount: hostedLayoutTraceCounts.layoutSubviews,
|
|
4467
|
+
refreshOverlaysNanos: hostedLayoutTraceNanos.refreshOverlays,
|
|
4468
|
+
refreshOverlaysCount: hostedLayoutTraceCounts.refreshOverlays,
|
|
4469
|
+
overlayScheduleRequestCount: hostedLayoutTraceCounts.overlayScheduleRequest,
|
|
4470
|
+
overlayScheduleExecuteCount: hostedLayoutTraceCounts.overlayScheduleExecute,
|
|
4471
|
+
overlayScheduleSkipCount: hostedLayoutTraceCounts.overlayScheduleSkip,
|
|
4472
|
+
onHeightMayChangeNanos: hostedLayoutTraceNanos.onHeightMayChange,
|
|
4473
|
+
onHeightMayChangeCount: hostedLayoutTraceCounts.onHeightMayChange
|
|
4474
|
+
)
|
|
2785
4475
|
}
|
|
2786
4476
|
|
|
2787
4477
|
func imageResizeOverlayRectForTesting() -> CGRect? {
|
|
@@ -2830,8 +4520,8 @@ final class RichTextEditorView: UIView {
|
|
|
2830
4520
|
/// - Parameter html: The HTML string to load.
|
|
2831
4521
|
func setContent(html: String) {
|
|
2832
4522
|
guard editorId != 0 else { return }
|
|
2833
|
-
|
|
2834
|
-
textView.
|
|
4523
|
+
_ = editorSetHtml(id: editorId, html: html)
|
|
4524
|
+
textView.applyUpdateJSON(editorGetCurrentState(id: editorId), notifyDelegate: false)
|
|
2835
4525
|
}
|
|
2836
4526
|
|
|
2837
4527
|
/// Set initial content from ProseMirror JSON.
|
|
@@ -2839,18 +4529,28 @@ final class RichTextEditorView: UIView {
|
|
|
2839
4529
|
/// - Parameter json: The JSON string to load.
|
|
2840
4530
|
func setContent(json: String) {
|
|
2841
4531
|
guard editorId != 0 else { return }
|
|
2842
|
-
|
|
2843
|
-
textView.
|
|
4532
|
+
_ = editorSetJson(id: editorId, json: json)
|
|
4533
|
+
textView.applyUpdateJSON(editorGetCurrentState(id: editorId), notifyDelegate: false)
|
|
2844
4534
|
}
|
|
2845
4535
|
|
|
2846
4536
|
private func measuredEditorHeight() -> CGFloat {
|
|
4537
|
+
let startedAt = DispatchTime.now().uptimeNanoseconds
|
|
4538
|
+
defer {
|
|
4539
|
+
recordHostedLayoutTrace(
|
|
4540
|
+
durationNanos: DispatchTime.now().uptimeNanoseconds - startedAt,
|
|
4541
|
+
keyPath: .measuredEditorHeight
|
|
4542
|
+
)
|
|
4543
|
+
}
|
|
4544
|
+
if cachedAutoGrowMeasuredHeight > 0 {
|
|
4545
|
+
return cachedAutoGrowMeasuredHeight
|
|
4546
|
+
}
|
|
2847
4547
|
let width = resolvedMeasurementWidth()
|
|
2848
4548
|
guard width > 0 else { return 0 }
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
|
|
4549
|
+
let measuredHeight = textView.measuredAutoGrowHeightForTesting(width: width)
|
|
4550
|
+
if measuredHeight > 0 {
|
|
4551
|
+
cachedAutoGrowMeasuredHeight = measuredHeight
|
|
4552
|
+
}
|
|
4553
|
+
return measuredHeight
|
|
2854
4554
|
}
|
|
2855
4555
|
|
|
2856
4556
|
private func resolvedMeasurementWidth() -> CGFloat {
|
|
@@ -2863,6 +4563,22 @@ final class RichTextEditorView: UIView {
|
|
|
2863
4563
|
return UIScreen.main.bounds.width
|
|
2864
4564
|
}
|
|
2865
4565
|
|
|
4566
|
+
private func layoutManagedSubviews() {
|
|
4567
|
+
let managedFrame = bounds
|
|
4568
|
+
if textView.frame != managedFrame {
|
|
4569
|
+
textView.frame = managedFrame
|
|
4570
|
+
}
|
|
4571
|
+
if remoteSelectionOverlayView.frame != managedFrame {
|
|
4572
|
+
remoteSelectionOverlayView.frame = managedFrame
|
|
4573
|
+
}
|
|
4574
|
+
if imageTapOverlayView.frame != managedFrame {
|
|
4575
|
+
imageTapOverlayView.frame = managedFrame
|
|
4576
|
+
}
|
|
4577
|
+
if imageResizeOverlayView.frame != managedFrame {
|
|
4578
|
+
imageResizeOverlayView.frame = managedFrame
|
|
4579
|
+
}
|
|
4580
|
+
}
|
|
4581
|
+
|
|
2866
4582
|
fileprivate func selectedImageGeometry() -> (docPos: UInt32, rect: CGRect)? {
|
|
2867
4583
|
guard let geometry = textView.selectedImageGeometry() else { return nil }
|
|
2868
4584
|
return (
|
|
@@ -2903,10 +4619,90 @@ final class RichTextEditorView: UIView {
|
|
|
2903
4619
|
}
|
|
2904
4620
|
|
|
2905
4621
|
private func refreshOverlays() {
|
|
4622
|
+
let startedAt = DispatchTime.now().uptimeNanoseconds
|
|
4623
|
+
defer {
|
|
4624
|
+
recordHostedLayoutTrace(
|
|
4625
|
+
durationNanos: DispatchTime.now().uptimeNanoseconds - startedAt,
|
|
4626
|
+
keyPath: .refreshOverlays
|
|
4627
|
+
)
|
|
4628
|
+
}
|
|
2906
4629
|
remoteSelectionOverlayView.refresh()
|
|
2907
4630
|
imageResizeOverlayView.refresh()
|
|
2908
4631
|
}
|
|
2909
4632
|
|
|
4633
|
+
private func refreshOverlaysIfNeeded() {
|
|
4634
|
+
guard shouldRefreshOverlays() else { return }
|
|
4635
|
+
refreshOverlays()
|
|
4636
|
+
}
|
|
4637
|
+
|
|
4638
|
+
private func scheduleRefreshOverlaysIfNeeded() {
|
|
4639
|
+
if !shouldRefreshOverlays() {
|
|
4640
|
+
if captureHostedLayoutTraceForTesting {
|
|
4641
|
+
hostedLayoutTraceCounts.overlayScheduleSkip += 1
|
|
4642
|
+
}
|
|
4643
|
+
return
|
|
4644
|
+
}
|
|
4645
|
+
scheduleRefreshOverlays()
|
|
4646
|
+
}
|
|
4647
|
+
|
|
4648
|
+
private func scheduleRefreshOverlays() {
|
|
4649
|
+
if captureHostedLayoutTraceForTesting {
|
|
4650
|
+
hostedLayoutTraceCounts.overlayScheduleRequest += 1
|
|
4651
|
+
}
|
|
4652
|
+
guard !overlayRefreshScheduled else { return }
|
|
4653
|
+
overlayRefreshScheduled = true
|
|
4654
|
+
DispatchQueue.main.async { [weak self] in
|
|
4655
|
+
guard let self else { return }
|
|
4656
|
+
self.overlayRefreshScheduled = false
|
|
4657
|
+
if self.captureHostedLayoutTraceForTesting {
|
|
4658
|
+
self.hostedLayoutTraceCounts.overlayScheduleExecute += 1
|
|
4659
|
+
}
|
|
4660
|
+
self.refreshOverlays()
|
|
4661
|
+
}
|
|
4662
|
+
}
|
|
4663
|
+
|
|
4664
|
+
private func shouldRefreshOverlays() -> Bool {
|
|
4665
|
+
if !remoteSelections.isEmpty || remoteSelectionOverlayView.hasVisibleDecorations {
|
|
4666
|
+
return true
|
|
4667
|
+
}
|
|
4668
|
+
if imageResizeOverlayView.isOverlayVisible {
|
|
4669
|
+
return true
|
|
4670
|
+
}
|
|
4671
|
+
if textView.selectedImageGeometry() != nil {
|
|
4672
|
+
return true
|
|
4673
|
+
}
|
|
4674
|
+
return false
|
|
4675
|
+
}
|
|
4676
|
+
|
|
4677
|
+
private enum HostedLayoutTraceKey {
|
|
4678
|
+
case intrinsicContentSize
|
|
4679
|
+
case measuredEditorHeight
|
|
4680
|
+
case layoutSubviews
|
|
4681
|
+
case refreshOverlays
|
|
4682
|
+
case onHeightMayChange
|
|
4683
|
+
}
|
|
4684
|
+
|
|
4685
|
+
private func recordHostedLayoutTrace(durationNanos: UInt64, keyPath: HostedLayoutTraceKey) {
|
|
4686
|
+
guard captureHostedLayoutTraceForTesting else { return }
|
|
4687
|
+
switch keyPath {
|
|
4688
|
+
case .intrinsicContentSize:
|
|
4689
|
+
hostedLayoutTraceNanos.intrinsicContentSize += durationNanos
|
|
4690
|
+
hostedLayoutTraceCounts.intrinsicContentSize += 1
|
|
4691
|
+
case .measuredEditorHeight:
|
|
4692
|
+
hostedLayoutTraceNanos.measuredEditorHeight += durationNanos
|
|
4693
|
+
hostedLayoutTraceCounts.measuredEditorHeight += 1
|
|
4694
|
+
case .layoutSubviews:
|
|
4695
|
+
hostedLayoutTraceNanos.layoutSubviews += durationNanos
|
|
4696
|
+
hostedLayoutTraceCounts.layoutSubviews += 1
|
|
4697
|
+
case .refreshOverlays:
|
|
4698
|
+
hostedLayoutTraceNanos.refreshOverlays += durationNanos
|
|
4699
|
+
hostedLayoutTraceCounts.refreshOverlays += 1
|
|
4700
|
+
case .onHeightMayChange:
|
|
4701
|
+
hostedLayoutTraceNanos.onHeightMayChange += durationNanos
|
|
4702
|
+
hostedLayoutTraceCounts.onHeightMayChange += 1
|
|
4703
|
+
}
|
|
4704
|
+
}
|
|
4705
|
+
|
|
2910
4706
|
// MARK: - Cleanup
|
|
2911
4707
|
|
|
2912
4708
|
deinit {
|