@apollohg/react-native-prose-editor 0.3.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 +515 -39
- package/android/src/main/java/com/apollohg/editor/NativeEditorExpoView.kt +58 -28
- package/android/src/main/java/com/apollohg/editor/NativeEditorModule.kt +25 -0
- package/android/src/main/java/com/apollohg/editor/NativeToolbar.kt +232 -62
- 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/EditorToolbar.d.ts +26 -6
- package/dist/EditorToolbar.js +299 -65
- package/dist/NativeEditorBridge.d.ts +40 -1
- package/dist/NativeEditorBridge.js +184 -90
- package/dist/NativeRichTextEditor.d.ts +5 -1
- package/dist/NativeRichTextEditor.js +201 -78
- package/dist/YjsCollaboration.d.ts +2 -0
- package/dist/YjsCollaboration.js +142 -20
- package/dist/index.d.ts +1 -1
- package/dist/schemas.js +12 -0
- package/dist/useNativeEditor.d.ts +2 -0
- package/dist/useNativeEditor.js +7 -0
- 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 +87 -0
- package/ios/NativeEditorExpoView.swift +488 -178
- package/ios/NativeEditorModule.swift +25 -0
- package/ios/PositionBridge.swift +310 -75
- package/ios/RenderBridge.swift +362 -27
- package/ios/RichTextEditorView.swift +2001 -189
- package/ios/editor_coreFFI/editor_coreFFI.h +55 -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 +128 -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(
|
|
@@ -2094,6 +2629,22 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
2094
2629
|
}
|
|
2095
2630
|
}
|
|
2096
2631
|
|
|
2632
|
+
func performToolbarToggleHeading(_ level: Int) {
|
|
2633
|
+
guard editorId != 0 else { return }
|
|
2634
|
+
guard isEditable else { return }
|
|
2635
|
+
guard let selection = currentScalarSelection() else { return }
|
|
2636
|
+
guard let level = UInt8(exactly: level), (1...6).contains(level) else { return }
|
|
2637
|
+
performInterceptedInput {
|
|
2638
|
+
let updateJSON = editorToggleHeadingAtSelectionScalar(
|
|
2639
|
+
id: editorId,
|
|
2640
|
+
scalarAnchor: selection.anchor,
|
|
2641
|
+
scalarHead: selection.head,
|
|
2642
|
+
level: level
|
|
2643
|
+
)
|
|
2644
|
+
applyUpdateJSON(updateJSON)
|
|
2645
|
+
}
|
|
2646
|
+
}
|
|
2647
|
+
|
|
2097
2648
|
func performToolbarIndentListItem() {
|
|
2098
2649
|
guard editorId != 0 else { return }
|
|
2099
2650
|
guard isEditable else { return }
|
|
@@ -2181,6 +2732,18 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
2181
2732
|
applyUpdateJSON(updateJSON)
|
|
2182
2733
|
}
|
|
2183
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
|
+
|
|
2184
2747
|
/// Delete a document-position range via the Rust editor.
|
|
2185
2748
|
private func deleteRangeInRust(from: UInt32, to: UInt32) {
|
|
2186
2749
|
guard from < to else { return }
|
|
@@ -2197,35 +2760,35 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
2197
2760
|
return (anchor: scalarRange.from, head: scalarRange.to)
|
|
2198
2761
|
}
|
|
2199
2762
|
|
|
2200
|
-
func
|
|
2763
|
+
private func selectedImageSelectionState() -> (docPos: UInt32, utf16Offset: Int)? {
|
|
2201
2764
|
guard allowImageResizing else { return nil }
|
|
2202
2765
|
guard isFirstResponder else { return nil }
|
|
2203
|
-
guard let selectedRange =
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2766
|
+
guard let selectedRange = selectedUtf16Range(),
|
|
2767
|
+
selectedRange.length == 1,
|
|
2768
|
+
selectedRange.location >= 0,
|
|
2769
|
+
selectedRange.location < textStorage.length
|
|
2770
|
+
else {
|
|
2208
2771
|
return nil
|
|
2209
2772
|
}
|
|
2210
2773
|
|
|
2211
|
-
let attrs = textStorage.attributes(at:
|
|
2774
|
+
let attrs = textStorage.attributes(at: selectedRange.location, effectiveRange: nil)
|
|
2212
2775
|
guard (attrs[RenderBridgeAttributes.voidNodeType] as? String) == "image",
|
|
2213
2776
|
attrs[.attachment] is NSTextAttachment
|
|
2214
2777
|
else {
|
|
2215
2778
|
return nil
|
|
2216
2779
|
}
|
|
2217
2780
|
|
|
2218
|
-
let docPos
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
}
|
|
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 }
|
|
2226
2789
|
|
|
2227
2790
|
let glyphRange = layoutManager.glyphRange(
|
|
2228
|
-
forCharacterRange: NSRange(location:
|
|
2791
|
+
forCharacterRange: NSRange(location: selectionState.utf16Offset, length: 1),
|
|
2229
2792
|
actualCharacterRange: nil
|
|
2230
2793
|
)
|
|
2231
2794
|
guard glyphRange.length > 0 else { return nil }
|
|
@@ -2234,13 +2797,17 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
2234
2797
|
rect.origin.x += textContainerInset.left
|
|
2235
2798
|
rect.origin.y += textContainerInset.top
|
|
2236
2799
|
guard rect.width > 0, rect.height > 0 else { return nil }
|
|
2237
|
-
return (docPos, rect)
|
|
2800
|
+
return (selectionState.docPos, rect)
|
|
2238
2801
|
}
|
|
2239
2802
|
|
|
2240
2803
|
private func blockImageAttachment(docPos: UInt32) -> (range: NSRange, attachment: BlockImageAttachment)? {
|
|
2241
2804
|
let fullRange = NSRange(location: 0, length: textStorage.length)
|
|
2242
2805
|
var resolved: (range: NSRange, attachment: BlockImageAttachment)?
|
|
2243
|
-
textStorage.enumerateAttribute(
|
|
2806
|
+
textStorage.enumerateAttribute(
|
|
2807
|
+
.attachment,
|
|
2808
|
+
in: fullRange,
|
|
2809
|
+
options: [.longestEffectiveRangeNotRequired]
|
|
2810
|
+
) { value, range, stop in
|
|
2244
2811
|
guard let attachment = value as? BlockImageAttachment, range.length > 0 else { return }
|
|
2245
2812
|
let attrs = textStorage.attributes(at: range.location, effectiveRange: nil)
|
|
2246
2813
|
guard (attrs[RenderBridgeAttributes.voidNodeType] as? String) == "image" else { return }
|
|
@@ -2355,93 +2922,1059 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
2355
2922
|
|
|
2356
2923
|
// MARK: - Applying Rust State
|
|
2357
2924
|
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
/// - Parameter updateJSON: The JSON string from editor_insert_text, etc.
|
|
2364
|
-
func applyUpdateJSON(_ updateJSON: String, notifyDelegate: Bool = true) {
|
|
2365
|
-
guard let data = updateJSON.data(using: .utf8),
|
|
2366
|
-
let update = try? JSONSerialization.jsonObject(with: data) as? [String: Any]
|
|
2367
|
-
else { return }
|
|
2925
|
+
private struct ParsedRenderPatch {
|
|
2926
|
+
let startIndex: Int
|
|
2927
|
+
let deleteCount: Int
|
|
2928
|
+
let renderBlocks: [[[String: Any]]]
|
|
2929
|
+
}
|
|
2368
2930
|
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
Self.updateLog.debug(
|
|
2374
|
-
"[applyUpdateJSON.begin] renderCount=\(renderElements.count) updateSelection=\(selectionFromUpdate, privacy: .public) before=\(self.textSnapshotSummary(), privacy: .public)"
|
|
2375
|
-
)
|
|
2931
|
+
private enum DerivedRenderPatch {
|
|
2932
|
+
case unchanged
|
|
2933
|
+
case patch(ParsedRenderPatch)
|
|
2934
|
+
}
|
|
2376
2935
|
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
textColor: baseTextColor,
|
|
2381
|
-
theme: theme
|
|
2382
|
-
)
|
|
2936
|
+
private func parseRenderBlocks(_ value: Any?) -> [[[String: Any]]]? {
|
|
2937
|
+
value as? [[[String: Any]]]
|
|
2938
|
+
}
|
|
2383
2939
|
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
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
|
+
}
|
|
2391
2948
|
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2949
|
+
return ParsedRenderPatch(
|
|
2950
|
+
startIndex: startIndex,
|
|
2951
|
+
deleteCount: deleteCount,
|
|
2952
|
+
renderBlocks: renderBlocks
|
|
2395
2953
|
)
|
|
2954
|
+
}
|
|
2396
2955
|
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
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
|
|
2404
2966
|
}
|
|
2405
|
-
onSelectionOrContentMayChange?()
|
|
2406
2967
|
|
|
2407
|
-
|
|
2408
|
-
|
|
2968
|
+
var merged = current
|
|
2969
|
+
merged.replaceSubrange(
|
|
2970
|
+
patch.startIndex..<(patch.startIndex + patch.deleteCount),
|
|
2971
|
+
with: patch.renderBlocks
|
|
2409
2972
|
)
|
|
2973
|
+
return merged
|
|
2974
|
+
}
|
|
2410
2975
|
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2976
|
+
private func renderBlockEquals(
|
|
2977
|
+
_ lhs: [[String: Any]],
|
|
2978
|
+
_ rhs: [[String: Any]]
|
|
2979
|
+
) -> Bool {
|
|
2980
|
+
(lhs as NSArray).isEqual(rhs)
|
|
2415
2981
|
}
|
|
2416
2982
|
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
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
|
|
2992
|
+
}
|
|
2993
|
+
|
|
2994
|
+
if prefix == current.count, prefix == updated.count {
|
|
2995
|
+
return .unchanged
|
|
2996
|
+
}
|
|
2997
|
+
|
|
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
|
+
)
|
|
2424
3019
|
)
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
3020
|
+
}
|
|
3021
|
+
|
|
3022
|
+
private func topLevelChildIndex(from value: Any?) -> Int? {
|
|
3023
|
+
if let number = value as? NSNumber {
|
|
3024
|
+
return number.intValue
|
|
3025
|
+
}
|
|
3026
|
+
return value as? Int
|
|
3027
|
+
}
|
|
3028
|
+
|
|
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
|
|
2430
3137
|
)
|
|
2431
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)
|
|
2432
3195
|
isApplyingRustState = true
|
|
3196
|
+
let textMutationStartedAt = DispatchTime.now().uptimeNanoseconds
|
|
3197
|
+
let beginEditingStartedAt = DispatchTime.now().uptimeNanoseconds
|
|
2433
3198
|
textStorage.beginEditing()
|
|
2434
|
-
|
|
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
|
|
2435
3234
|
textStorage.endEditing()
|
|
2436
|
-
|
|
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
|
|
2437
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
|
+
}
|
|
2438
3888
|
|
|
2439
3889
|
refreshPlaceholderVisibility()
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
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
|
+
)
|
|
3905
|
+
}
|
|
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
|
+
)
|
|
2443
3947
|
}
|
|
2444
|
-
|
|
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()
|
|
2445
3978
|
Self.updateLog.debug(
|
|
2446
3979
|
"[applyRenderJSON.end] after=\(self.textSnapshotSummary(), privacy: .public)"
|
|
2447
3980
|
)
|
|
@@ -2455,54 +3988,136 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
2455
3988
|
/// {"type": "node", "pos": 10}
|
|
2456
3989
|
/// {"type": "all"}
|
|
2457
3990
|
/// ```
|
|
2458
|
-
private func applySelectionFromJSON(_ selection: [String: Any]) {
|
|
2459
|
-
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
|
+
}
|
|
2460
3995
|
|
|
3996
|
+
let totalStartedAt = DispatchTime.now().uptimeNanoseconds
|
|
2461
3997
|
isApplyingRustState = true
|
|
2462
3998
|
defer { isApplyingRustState = false }
|
|
2463
3999
|
|
|
2464
4000
|
switch type {
|
|
2465
4001
|
case "text":
|
|
4002
|
+
let resolveStartedAt = DispatchTime.now().uptimeNanoseconds
|
|
2466
4003
|
guard let anchorNum = selection["anchor"] as? NSNumber,
|
|
2467
4004
|
let headNum = selection["head"] as? NSNumber
|
|
2468
|
-
else {
|
|
4005
|
+
else {
|
|
4006
|
+
return SelectionApplyTrace(totalNanos: 0, resolveNanos: 0, assignmentNanos: 0, chromeNanos: 0)
|
|
4007
|
+
}
|
|
2469
4008
|
// anchor/head from Rust are document positions; convert to scalar offsets first.
|
|
2470
|
-
let anchorScalar =
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
let
|
|
2475
|
-
|
|
2476
|
-
|
|
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
|
|
2477
4048
|
Self.selectionLog.debug(
|
|
2478
4049
|
"[applySelectionFromJSON.text] doc=\(anchorNum.uint32Value)-\(headNum.uint32Value) scalar=\(anchorScalar)-\(headScalar) final=\(self.selectionSummary(), privacy: .public)"
|
|
2479
4050
|
)
|
|
4051
|
+
return SelectionApplyTrace(
|
|
4052
|
+
totalNanos: DispatchTime.now().uptimeNanoseconds - totalStartedAt,
|
|
4053
|
+
resolveNanos: resolveNanos,
|
|
4054
|
+
assignmentNanos: assignmentNanos,
|
|
4055
|
+
chromeNanos: chromeNanos
|
|
4056
|
+
)
|
|
2480
4057
|
|
|
2481
4058
|
case "node":
|
|
2482
4059
|
// Node selection: select the object replacement character at that position.
|
|
2483
|
-
|
|
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
|
+
}
|
|
2484
4064
|
// pos from Rust is a document position; convert to scalar offset.
|
|
2485
|
-
let posScalar =
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
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
|
|
2490
4073
|
}
|
|
4074
|
+
let assignmentNanos = DispatchTime.now().uptimeNanoseconds - assignmentStartedAt
|
|
4075
|
+
let chromeStartedAt = DispatchTime.now().uptimeNanoseconds
|
|
2491
4076
|
refreshNativeSelectionChromeVisibility()
|
|
4077
|
+
let chromeNanos = DispatchTime.now().uptimeNanoseconds - chromeStartedAt
|
|
2492
4078
|
Self.selectionLog.debug(
|
|
2493
4079
|
"[applySelectionFromJSON.node] doc=\(posNum.uint32Value) scalar=\(posScalar) final=\(self.selectionSummary(), privacy: .public)"
|
|
2494
4080
|
)
|
|
4081
|
+
return SelectionApplyTrace(
|
|
4082
|
+
totalNanos: DispatchTime.now().uptimeNanoseconds - totalStartedAt,
|
|
4083
|
+
resolveNanos: resolveNanos,
|
|
4084
|
+
assignmentNanos: assignmentNanos,
|
|
4085
|
+
chromeNanos: chromeNanos
|
|
4086
|
+
)
|
|
2495
4087
|
|
|
2496
4088
|
case "all":
|
|
4089
|
+
let assignmentStartedAt = DispatchTime.now().uptimeNanoseconds
|
|
2497
4090
|
selectedTextRange = textRange(from: beginningOfDocument, to: endOfDocument)
|
|
2498
|
-
|
|
4091
|
+
let assignmentNanos = DispatchTime.now().uptimeNanoseconds - assignmentStartedAt
|
|
4092
|
+
let chromeStartedAt = DispatchTime.now().uptimeNanoseconds
|
|
4093
|
+
showNativeSelectionChromeIfNeeded()
|
|
4094
|
+
let chromeNanos = DispatchTime.now().uptimeNanoseconds - chromeStartedAt
|
|
2499
4095
|
Self.selectionLog.debug(
|
|
2500
4096
|
"[applySelectionFromJSON.all] final=\(self.selectionSummary(), privacy: .public)"
|
|
2501
4097
|
)
|
|
4098
|
+
return SelectionApplyTrace(
|
|
4099
|
+
totalNanos: DispatchTime.now().uptimeNanoseconds - totalStartedAt,
|
|
4100
|
+
resolveNanos: 0,
|
|
4101
|
+
assignmentNanos: assignmentNanos,
|
|
4102
|
+
chromeNanos: chromeNanos
|
|
4103
|
+
)
|
|
2502
4104
|
|
|
2503
4105
|
default:
|
|
2504
|
-
|
|
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
|
|
2505
4116
|
}
|
|
4117
|
+
|
|
4118
|
+
let utf16Offset = offset(from: beginningOfDocument, to: position)
|
|
4119
|
+
guard utf16Offset == textStorage.length else { return nil }
|
|
4120
|
+
return beginningOfDocument
|
|
2506
4121
|
}
|
|
2507
4122
|
|
|
2508
4123
|
}
|
|
@@ -2533,6 +4148,7 @@ extension EditorTextView: NSTextStorageDelegate {
|
|
|
2533
4148
|
// Compare current text storage content against last authorized snapshot.
|
|
2534
4149
|
let currentText = textStorage.string
|
|
2535
4150
|
guard currentText != lastAuthorizedText else { return }
|
|
4151
|
+
currentTopLevelChildMetadata = nil
|
|
2536
4152
|
let authorizedPreview = preview(lastAuthorizedText)
|
|
2537
4153
|
let storagePreview = preview(currentText)
|
|
2538
4154
|
|
|
@@ -2590,6 +4206,22 @@ extension EditorTextView: NSTextStorageDelegate {
|
|
|
2590
4206
|
/// and serves as the integration point for the future Fabric component.
|
|
2591
4207
|
final class RichTextEditorView: UIView {
|
|
2592
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
|
+
|
|
2593
4225
|
// MARK: - Properties
|
|
2594
4226
|
|
|
2595
4227
|
/// The editor text view that handles input interception.
|
|
@@ -2597,9 +4229,29 @@ final class RichTextEditorView: UIView {
|
|
|
2597
4229
|
private let remoteSelectionOverlayView = RemoteSelectionOverlayView()
|
|
2598
4230
|
private let imageTapOverlayView = ImageTapOverlayView()
|
|
2599
4231
|
private let imageResizeOverlayView = ImageResizeOverlayView()
|
|
2600
|
-
var onHeightMayChange: (() -> Void)?
|
|
4232
|
+
var onHeightMayChange: ((CGFloat) -> Void)?
|
|
2601
4233
|
private var lastAutoGrowWidth: CGFloat = 0
|
|
4234
|
+
private var cachedAutoGrowMeasuredHeight: CGFloat = 0
|
|
2602
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
|
+
)
|
|
2603
4255
|
var allowImageResizing = true {
|
|
2604
4256
|
didSet {
|
|
2605
4257
|
guard oldValue != allowImageResizing else { return }
|
|
@@ -2614,9 +4266,23 @@ final class RichTextEditorView: UIView {
|
|
|
2614
4266
|
didSet {
|
|
2615
4267
|
guard oldValue != heightBehavior else { return }
|
|
2616
4268
|
textView.heightBehavior = heightBehavior
|
|
4269
|
+
textView.updateAutoGrowHostHeight(heightBehavior == .autoGrow ? bounds.height : 0)
|
|
4270
|
+
if heightBehavior != .autoGrow {
|
|
4271
|
+
cachedAutoGrowMeasuredHeight = 0
|
|
4272
|
+
}
|
|
2617
4273
|
invalidateIntrinsicContentSize()
|
|
2618
4274
|
setNeedsLayout()
|
|
2619
|
-
|
|
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
|
+
}
|
|
2620
4286
|
remoteSelectionOverlayView.refresh()
|
|
2621
4287
|
imageResizeOverlayView.refresh()
|
|
2622
4288
|
}
|
|
@@ -2654,54 +4320,45 @@ final class RichTextEditorView: UIView {
|
|
|
2654
4320
|
}
|
|
2655
4321
|
|
|
2656
4322
|
private func setupView() {
|
|
2657
|
-
// Add the text view as a subview.
|
|
2658
|
-
|
|
2659
|
-
remoteSelectionOverlayView.translatesAutoresizingMaskIntoConstraints = false
|
|
2660
|
-
imageTapOverlayView.translatesAutoresizingMaskIntoConstraints = false
|
|
2661
|
-
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.
|
|
2662
4325
|
remoteSelectionOverlayView.bind(textView: textView)
|
|
2663
4326
|
imageTapOverlayView.bind(editorView: self)
|
|
2664
4327
|
imageResizeOverlayView.bind(editorView: self)
|
|
2665
4328
|
textView.allowImageResizing = allowImageResizing
|
|
2666
4329
|
imageTapOverlayView.isHidden = editorId == 0 || !allowImageResizing
|
|
2667
|
-
textView.onHeightMayChange = { [weak self] in
|
|
4330
|
+
textView.onHeightMayChange = { [weak self] measuredHeight in
|
|
2668
4331
|
guard let self, self.heightBehavior == .autoGrow else { return }
|
|
4332
|
+
let startedAt = DispatchTime.now().uptimeNanoseconds
|
|
4333
|
+
self.cachedAutoGrowMeasuredHeight = measuredHeight
|
|
2669
4334
|
self.invalidateIntrinsicContentSize()
|
|
2670
|
-
self.
|
|
2671
|
-
self.
|
|
4335
|
+
self.onHeightMayChange?(measuredHeight)
|
|
4336
|
+
self.recordHostedLayoutTrace(
|
|
4337
|
+
durationNanos: DispatchTime.now().uptimeNanoseconds - startedAt,
|
|
4338
|
+
keyPath: .onHeightMayChange
|
|
4339
|
+
)
|
|
2672
4340
|
}
|
|
2673
4341
|
textView.onViewportMayChange = { [weak self] in
|
|
2674
|
-
self?.
|
|
4342
|
+
self?.refreshOverlaysIfNeeded()
|
|
2675
4343
|
}
|
|
2676
4344
|
textView.onSelectionOrContentMayChange = { [weak self] in
|
|
2677
|
-
self?.
|
|
4345
|
+
self?.scheduleRefreshOverlaysIfNeeded()
|
|
2678
4346
|
}
|
|
2679
4347
|
addSubview(textView)
|
|
2680
4348
|
addSubview(remoteSelectionOverlayView)
|
|
2681
4349
|
addSubview(imageTapOverlayView)
|
|
2682
4350
|
addSubview(imageResizeOverlayView)
|
|
2683
|
-
|
|
2684
|
-
NSLayoutConstraint.activate([
|
|
2685
|
-
textView.topAnchor.constraint(equalTo: topAnchor),
|
|
2686
|
-
textView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
|
2687
|
-
textView.trailingAnchor.constraint(equalTo: trailingAnchor),
|
|
2688
|
-
textView.bottomAnchor.constraint(equalTo: bottomAnchor),
|
|
2689
|
-
remoteSelectionOverlayView.topAnchor.constraint(equalTo: topAnchor),
|
|
2690
|
-
remoteSelectionOverlayView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
|
2691
|
-
remoteSelectionOverlayView.trailingAnchor.constraint(equalTo: trailingAnchor),
|
|
2692
|
-
remoteSelectionOverlayView.bottomAnchor.constraint(equalTo: bottomAnchor),
|
|
2693
|
-
imageTapOverlayView.topAnchor.constraint(equalTo: topAnchor),
|
|
2694
|
-
imageTapOverlayView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
|
2695
|
-
imageTapOverlayView.trailingAnchor.constraint(equalTo: trailingAnchor),
|
|
2696
|
-
imageTapOverlayView.bottomAnchor.constraint(equalTo: bottomAnchor),
|
|
2697
|
-
imageResizeOverlayView.topAnchor.constraint(equalTo: topAnchor),
|
|
2698
|
-
imageResizeOverlayView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
|
2699
|
-
imageResizeOverlayView.trailingAnchor.constraint(equalTo: trailingAnchor),
|
|
2700
|
-
imageResizeOverlayView.bottomAnchor.constraint(equalTo: bottomAnchor),
|
|
2701
|
-
])
|
|
4351
|
+
layoutManagedSubviews()
|
|
2702
4352
|
}
|
|
2703
4353
|
|
|
2704
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
|
+
}
|
|
2705
4362
|
guard heightBehavior == .autoGrow else {
|
|
2706
4363
|
return CGSize(width: UIView.noIntrinsicMetric, height: UIView.noIntrinsicMetric)
|
|
2707
4364
|
}
|
|
@@ -2714,12 +4371,22 @@ final class RichTextEditorView: UIView {
|
|
|
2714
4371
|
}
|
|
2715
4372
|
|
|
2716
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
|
+
}
|
|
2717
4381
|
super.layoutSubviews()
|
|
2718
|
-
|
|
4382
|
+
layoutManagedSubviews()
|
|
4383
|
+
refreshOverlaysIfNeeded()
|
|
2719
4384
|
guard heightBehavior == .autoGrow else { return }
|
|
4385
|
+
textView.updateAutoGrowHostHeight(bounds.height)
|
|
2720
4386
|
let currentWidth = bounds.width.rounded(.towardZero)
|
|
2721
4387
|
guard currentWidth != lastAutoGrowWidth else { return }
|
|
2722
4388
|
lastAutoGrowWidth = currentWidth
|
|
4389
|
+
cachedAutoGrowMeasuredHeight = 0
|
|
2723
4390
|
invalidateIntrinsicContentSize()
|
|
2724
4391
|
}
|
|
2725
4392
|
|
|
@@ -2761,11 +4428,50 @@ final class RichTextEditorView: UIView {
|
|
|
2761
4428
|
}
|
|
2762
4429
|
|
|
2763
4430
|
func refreshRemoteSelections() {
|
|
4431
|
+
guard remoteSelectionOverlayView.hasSelectionsOrVisibleDecorations else { return }
|
|
2764
4432
|
remoteSelectionOverlayView.refresh()
|
|
2765
4433
|
}
|
|
2766
4434
|
|
|
2767
4435
|
func remoteSelectionOverlaySubviewsForTesting() -> [UIView] {
|
|
2768
|
-
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
|
+
)
|
|
2769
4475
|
}
|
|
2770
4476
|
|
|
2771
4477
|
func imageResizeOverlayRectForTesting() -> CGRect? {
|
|
@@ -2814,8 +4520,8 @@ final class RichTextEditorView: UIView {
|
|
|
2814
4520
|
/// - Parameter html: The HTML string to load.
|
|
2815
4521
|
func setContent(html: String) {
|
|
2816
4522
|
guard editorId != 0 else { return }
|
|
2817
|
-
|
|
2818
|
-
textView.
|
|
4523
|
+
_ = editorSetHtml(id: editorId, html: html)
|
|
4524
|
+
textView.applyUpdateJSON(editorGetCurrentState(id: editorId), notifyDelegate: false)
|
|
2819
4525
|
}
|
|
2820
4526
|
|
|
2821
4527
|
/// Set initial content from ProseMirror JSON.
|
|
@@ -2823,18 +4529,28 @@ final class RichTextEditorView: UIView {
|
|
|
2823
4529
|
/// - Parameter json: The JSON string to load.
|
|
2824
4530
|
func setContent(json: String) {
|
|
2825
4531
|
guard editorId != 0 else { return }
|
|
2826
|
-
|
|
2827
|
-
textView.
|
|
4532
|
+
_ = editorSetJson(id: editorId, json: json)
|
|
4533
|
+
textView.applyUpdateJSON(editorGetCurrentState(id: editorId), notifyDelegate: false)
|
|
2828
4534
|
}
|
|
2829
4535
|
|
|
2830
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
|
+
}
|
|
2831
4547
|
let width = resolvedMeasurementWidth()
|
|
2832
4548
|
guard width > 0 else { return 0 }
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
4549
|
+
let measuredHeight = textView.measuredAutoGrowHeightForTesting(width: width)
|
|
4550
|
+
if measuredHeight > 0 {
|
|
4551
|
+
cachedAutoGrowMeasuredHeight = measuredHeight
|
|
4552
|
+
}
|
|
4553
|
+
return measuredHeight
|
|
2838
4554
|
}
|
|
2839
4555
|
|
|
2840
4556
|
private func resolvedMeasurementWidth() -> CGFloat {
|
|
@@ -2847,6 +4563,22 @@ final class RichTextEditorView: UIView {
|
|
|
2847
4563
|
return UIScreen.main.bounds.width
|
|
2848
4564
|
}
|
|
2849
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
|
+
|
|
2850
4582
|
fileprivate func selectedImageGeometry() -> (docPos: UInt32, rect: CGRect)? {
|
|
2851
4583
|
guard let geometry = textView.selectedImageGeometry() else { return nil }
|
|
2852
4584
|
return (
|
|
@@ -2887,10 +4619,90 @@ final class RichTextEditorView: UIView {
|
|
|
2887
4619
|
}
|
|
2888
4620
|
|
|
2889
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
|
+
}
|
|
2890
4629
|
remoteSelectionOverlayView.refresh()
|
|
2891
4630
|
imageResizeOverlayView.refresh()
|
|
2892
4631
|
}
|
|
2893
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
|
+
|
|
2894
4706
|
// MARK: - Cleanup
|
|
2895
4707
|
|
|
2896
4708
|
deinit {
|