@apollohg/react-native-rich-text-editor 2.0.1 → 2.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/README.md +29 -123
  2. package/android/src/main/java/com/apollohg/editor/EditorEditText.kt +20 -6
  3. package/android/src/main/java/com/apollohg/editor/EditorEditTextClipboard.kt +181 -36
  4. package/android/src/main/java/com/apollohg/editor/EditorEditTextImages.kt +25 -0
  5. package/android/src/main/java/com/apollohg/editor/EditorEditTextSelection.kt +7 -0
  6. package/android/src/main/java/com/apollohg/editor/EditorTextSurfaceInteraction.kt +31 -11
  7. package/android/src/main/java/com/apollohg/editor/EditorV2Adapter.kt +36 -0
  8. package/android/src/main/java/com/apollohg/editor/EditorV2Backend.kt +1 -0
  9. package/android/src/main/java/com/apollohg/editor/EditorV2Driver.kt +10 -0
  10. package/android/src/main/java/com/apollohg/editor/EditorV2RenderValidation.kt +1 -0
  11. package/android/src/main/java/com/apollohg/editor/ImageResizeOverlayView.kt +101 -33
  12. package/android/src/main/java/com/apollohg/editor/NativeEditorExpoView.kt +2 -0
  13. package/android/src/main/java/com/apollohg/editor/NativeEditorExpoViewProperties.kt +4 -0
  14. package/android/src/main/java/com/apollohg/editor/NativeEditorModule.kt +3 -0
  15. package/android/src/main/java/com/apollohg/editor/RenderMediaSpans.kt +15 -3
  16. package/android/src/main/java/com/apollohg/editor/RichTextEditorView.kt +14 -0
  17. package/android/src/main/java/com/apollohg/editor/UniffiEditorV2Backend.kt +4 -0
  18. package/android/src/main/java/com/apollohg/editor/viewer/AndroidProseLayoutEngine.kt +1 -1
  19. package/dist/EditorStyleSheetNormalization.js +1 -1
  20. package/dist/EditorStyleSheetTypes.d.ts +1 -1
  21. package/dist/NativeRichTextEditor.d.ts +1 -1
  22. package/dist/RichTextEditorNativeTypes.d.ts +2 -1
  23. package/dist/RichTextEditorTypes.d.ts +4 -0
  24. package/dist/index.d.ts +1 -1
  25. package/dist/useRichTextEditorPresentation.d.ts +1 -1
  26. package/dist/useRichTextEditorPresentation.js +2 -2
  27. package/dist/useRichTextEditorState.d.ts +5 -3
  28. package/dist/useRichTextEditorState.js +4 -3
  29. package/ios/EditorCore.xcframework/ios-arm64/libeditor_core.a +0 -0
  30. package/ios/EditorCore.xcframework/ios-arm64_x86_64-simulator/libeditor_core.a +0 -0
  31. package/ios/EditorInteractionOverlays.swift +114 -122
  32. package/ios/EditorLayoutManager.swift +13 -3
  33. package/ios/EditorStyleSheet.swift +12 -1
  34. package/ios/EditorTextView+Images.swift +32 -10
  35. package/ios/EditorTextView+KeyboardInput.swift +299 -0
  36. package/ios/EditorTextView.swift +64 -44
  37. package/ios/EditorV2Adapter+Commands.swift +22 -0
  38. package/ios/EditorV2Adapter+RenderValidation.swift +2 -2
  39. package/ios/EditorV2Shadow.swift +12 -0
  40. package/ios/Generated_editor_core.swift +10 -0
  41. package/ios/NativeEditorExpoView+Properties.swift +4 -0
  42. package/ios/NativeEditorModule.swift +3 -0
  43. package/ios/NativeToolbar+ButtonStyle.swift +17 -5
  44. package/ios/NativeToolbar+Diagnostics.swift +15 -0
  45. package/ios/RenderBridge+Atoms.swift +0 -1
  46. package/ios/RenderBridge+Attachments.swift +0 -29
  47. package/ios/RichTextEditorView.swift +4 -12
  48. package/ios/Viewer/CoreTextProseLayoutEngine+AttributedText.swift +3 -2
  49. package/ios/editor_coreFFI/editor_coreFFI.h +11 -0
  50. package/package.json +1 -1
  51. package/rust/android/arm64-v8a/libeditor_core.so +0 -0
  52. package/rust/android/armeabi-v7a/libeditor_core.so +0 -0
  53. package/rust/android/x86/libeditor_core.so +0 -0
  54. package/rust/android/x86_64/libeditor_core.so +0 -0
  55. package/rust/bindings/kotlin/uniffi/editor_core/editor_core.kt +18 -0
  56. package/src/EditorStyleSheetNormalization.ts +1 -1
  57. package/src/EditorStyleSheetTypes.ts +1 -1
  58. package/src/NativeRichTextEditor.tsx +1 -0
  59. package/src/RichTextEditorNativeTypes.tsx +2 -0
  60. package/src/RichTextEditorTypes.tsx +5 -0
  61. package/src/index.ts +1 -0
  62. package/src/useRichTextEditorPresentation.tsx +3 -0
  63. package/src/useRichTextEditorState.tsx +5 -2
@@ -408,38 +408,29 @@ final class ImageResizeOverlayView: UIView {
408
408
 
409
409
  private struct DragState {
410
410
  let corner: Corner
411
- let originalRect: CGRect
411
+ let originalSize: CGSize
412
412
  let docPos: UInt32
413
413
  let maximumWidth: CGFloat
414
+ let editorId: UInt64
415
+ let attachment: BlockImageAttachment
416
+ let originalWidth: CGFloat?
417
+ let originalHeight: CGFloat?
418
+ var previewSize: CGSize
414
419
  }
415
420
 
416
421
  private weak var editorView: RichTextEditorView?
417
422
  private let selectionLayer = CAShapeLayer()
418
- private let previewBackdropView = UIView()
419
- private let previewImageView = UIImageView()
420
423
  private var handleViews: [Corner: ImageResizeHandleView] = [:]
421
424
  private var currentRect: CGRect?
422
425
  private var currentDocPos: UInt32?
423
426
  private var dragState: DragState?
424
427
  private let handleSize: CGFloat = 20
425
- private let minimumImageSize: CGFloat = 48
426
428
 
427
429
  override init(frame: CGRect) {
428
430
  super.init(frame: frame)
429
431
  backgroundColor = .clear
430
432
  clipsToBounds = true
431
433
 
432
- previewBackdropView.isUserInteractionEnabled = false
433
- previewBackdropView.isHidden = true
434
- previewBackdropView.layer.zPosition = 1
435
- addSubview(previewBackdropView)
436
-
437
- previewImageView.isUserInteractionEnabled = false
438
- previewImageView.isHidden = true
439
- previewImageView.contentMode = .scaleToFill
440
- previewImageView.layer.zPosition = 2
441
- addSubview(previewImageView)
442
-
443
434
  selectionLayer.strokeColor = UIColor.systemBlue.cgColor
444
435
  selectionLayer.fillColor = UIColor.clear.cgColor
445
436
  selectionLayer.lineWidth = 2
@@ -467,8 +458,8 @@ final class ImageResizeOverlayView: UIView {
467
458
  }
468
459
 
469
460
  func refresh() {
470
- if dragState != nil {
471
- return
461
+ if let dragState, !isCurrentDrag(dragState) {
462
+ finishPreviewResize(commit: false)
472
463
  }
473
464
 
474
465
  guard let editorView,
@@ -478,7 +469,6 @@ final class ImageResizeOverlayView: UIView {
478
469
  return
479
470
  }
480
471
 
481
- hidePreviewLayers()
482
472
  applyGeometry(rect: geometry.rect, docPos: geometry.docPos)
483
473
  }
484
474
 
@@ -488,15 +478,16 @@ final class ImageResizeOverlayView: UIView {
488
478
  }
489
479
 
490
480
  func simulatePreviewResizeForTesting(width: CGFloat, height: CGFloat) {
491
- guard beginPreviewResize(from: .bottomRight) else { return }
492
- let nextRect = CGRect(
493
- origin: dragState?.originalRect.origin ?? .zero,
494
- size: editorView?.clampedImageSize(
495
- CGSize(width: width, height: height),
496
- maximumWidth: dragState?.maximumWidth
497
- ) ?? CGSize(width: width, height: height)
498
- )
499
- updatePreviewRect(nextRect)
481
+ if dragState == nil, !beginPreviewResize(from: .bottomRight) { return }
482
+ let size = editorView?.clampedImageSize(
483
+ CGSize(width: width, height: height), maximumWidth: dragState?.maximumWidth
484
+ ) ?? CGSize(width: width, height: height)
485
+ updatePreviewSize(size)
486
+ }
487
+
488
+ func simulatePreviewDragForTesting(corner: Corner, translation: CGPoint) {
489
+ if dragState == nil, !beginPreviewResize(from: corner) { return }
490
+ updatePreview(translation: translation)
500
491
  }
501
492
 
502
493
  func commitPreviewResizeForTesting() {
@@ -511,10 +502,6 @@ final class ImageResizeOverlayView: UIView {
511
502
  !isHidden
512
503
  }
513
504
 
514
- var previewHasImageForTesting: Bool {
515
- !previewImageView.isHidden && previewImageView.image != nil
516
- }
517
-
518
505
  func interceptsPointForTesting(_ location: CGPoint) -> Bool {
519
506
  self.point(inside: location, with: nil)
520
507
  }
@@ -530,8 +517,7 @@ final class ImageResizeOverlayView: UIView {
530
517
  }
531
518
 
532
519
  private func hideOverlay() {
533
- hidePreviewLayers()
534
- dragState = nil
520
+ if dragState != nil { finishPreviewResize(commit: false) }
535
521
  currentRect = nil
536
522
  currentDocPos = nil
537
523
  selectionLayer.path = nil
@@ -539,64 +525,94 @@ final class ImageResizeOverlayView: UIView {
539
525
  }
540
526
 
541
527
  private func applyGeometry(rect: CGRect, docPos: UInt32) {
542
- let integralRect = rect.integral
543
- currentRect = integralRect
528
+ guard currentRect != rect || currentDocPos != docPos || isHidden else { return }
529
+ currentRect = rect
544
530
  currentDocPos = docPos
545
- selectionLayer.path = UIBezierPath(roundedRect: integralRect, cornerRadius: 8).cgPath
531
+ CATransaction.begin()
532
+ CATransaction.setDisableActions(true)
533
+ selectionLayer.path = UIBezierPath(roundedRect: rect, cornerRadius: 8).cgPath
546
534
  isHidden = false
547
- layoutHandleViews(for: integralRect)
535
+ layoutHandleViews(for: rect)
536
+ CATransaction.commit()
548
537
  }
549
538
 
550
- private func hidePreviewLayers() {
551
- previewBackdropView.isHidden = true
552
- previewImageView.isHidden = true
553
- previewImageView.image = nil
554
- }
555
-
556
- private func showPreview(docPos: UInt32, originalRect: CGRect) {
557
- previewBackdropView.backgroundColor = editorView?.imageResizePreviewBackgroundColor() ?? .systemBackground
558
- previewBackdropView.frame = originalRect
559
- previewBackdropView.isHidden = false
560
-
561
- previewImageView.image = editorView?.imagePreviewForResize(docPos: docPos)
562
- previewImageView.frame = originalRect
563
- previewImageView.isHidden = previewImageView.image == nil
539
+ private func isCurrentDrag(_ state: DragState) -> Bool {
540
+ guard let editorView, editorView.editorId == state.editorId,
541
+ editorView.textView.selectedImageSelectionState()?.docPos == state.docPos,
542
+ editorView.textView.blockImageAttachment(docPos: state.docPos)?.attachment === state.attachment
543
+ else { return false }
544
+ return true
564
545
  }
565
546
 
566
547
  @discardableResult
567
548
  private func beginPreviewResize(from corner: Corner) -> Bool {
568
- guard let currentRect, let currentDocPos else { return false }
569
- editorView?.setImageResizePreviewActive(true)
570
- let maximumWidth = editorView?.maximumImageWidthForResizeGesture() ?? currentRect.width
549
+ guard let editorView, let currentRect, let currentDocPos,
550
+ let image = editorView.textView.blockImageAttachment(docPos: currentDocPos)
551
+ else { return false }
552
+ editorView.setImageResizePreviewActive(true)
553
+ let inset = image.attachment.styleBox?.inset ?? .zero
554
+ let contentSize = CGSize(
555
+ width: max(1, currentRect.width - inset.left - inset.right),
556
+ height: max(1, currentRect.height - inset.top - inset.bottom)
557
+ )
571
558
  dragState = DragState(
572
559
  corner: corner,
573
- originalRect: currentRect,
560
+ originalSize: contentSize,
574
561
  docPos: currentDocPos,
575
- maximumWidth: maximumWidth
562
+ maximumWidth: max(48, editorView.maximumImageWidthForResizeGesture() - inset.left - inset.right),
563
+ editorId: editorView.editorId,
564
+ attachment: image.attachment,
565
+ originalWidth: image.attachment.preferredWidth,
566
+ originalHeight: image.attachment.preferredHeight,
567
+ previewSize: contentSize
576
568
  )
577
- showPreview(docPos: currentDocPos, originalRect: currentRect)
578
569
  return true
579
570
  }
580
571
 
581
- private func updatePreviewRect(_ rect: CGRect) {
582
- guard let currentDocPos else { return }
583
- applyGeometry(rect: rect, docPos: currentDocPos)
584
- previewImageView.frame = currentRect ?? rect.integral
572
+ private func updatePreviewSize(_ proposedSize: CGSize) {
573
+ guard var state = dragState, let editorView else { return }
574
+ guard isCurrentDrag(state) else {
575
+ finishPreviewResize(commit: false)
576
+ return
577
+ }
578
+ let scale = window?.screen.scale ?? 1
579
+ let size = proposedSize == state.originalSize ? proposedSize : CGSize(
580
+ width: (proposedSize.width * scale).rounded() / scale,
581
+ height: (proposedSize.height * scale).rounded() / scale
582
+ )
583
+ guard size != state.previewSize else { return }
584
+ state.previewSize = size
585
+ dragState = state
586
+ UIView.performWithoutAnimation {
587
+ editorView.textView.previewResizeImageAtDocPos(
588
+ state.docPos,
589
+ width: size.width,
590
+ height: size.height
591
+ )
592
+ if let geometry = editorView.selectedImageGeometry() {
593
+ applyGeometry(rect: geometry.rect, docPos: geometry.docPos)
594
+ }
595
+ }
585
596
  }
586
597
 
587
598
  private func finishPreviewResize(commit: Bool) {
588
- guard let dragState else { return }
589
- let finalSize = currentRect?.size ?? dragState.originalRect.size
590
- self.dragState = nil
591
- editorView?.setImageResizePreviewActive(false)
592
- if commit {
593
- editorView?.resizeImage(docPos: dragState.docPos, size: finalSize)
599
+ guard let state = dragState, let editorView else { return }
600
+ let shouldCommit = commit && isCurrentDrag(state)
601
+ dragState = nil
602
+ if shouldCommit, state.previewSize != state.originalSize {
603
+ editorView.resizeImage(docPos: state.docPos, size: state.previewSize)
594
604
  } else {
595
- hidePreviewLayers()
596
- }
597
- DispatchQueue.main.async { [weak self] in
598
- self?.refresh()
605
+ editorView.textView.restoreImageResizePreview(
606
+ state.attachment, width: state.originalWidth, height: state.originalHeight
607
+ )
599
608
  }
609
+ editorView.setImageResizePreviewActive(false)
610
+ refresh()
611
+ }
612
+
613
+ override func didMoveToWindow() {
614
+ super.didMoveToWindow()
615
+ if window == nil { finishPreviewResize(commit: false) }
600
616
  }
601
617
 
602
618
  private func layoutHandleViews(for rect: CGRect) {
@@ -624,50 +640,30 @@ final class ImageResizeOverlayView: UIView {
624
640
  }
625
641
  }
626
642
 
627
- private func anchorPoint(for corner: Corner, in rect: CGRect) -> CGPoint {
628
- switch corner {
629
- case .topLeft:
630
- return CGPoint(x: rect.maxX, y: rect.maxY)
631
- case .topRight:
632
- return CGPoint(x: rect.minX, y: rect.maxY)
633
- case .bottomLeft:
634
- return CGPoint(x: rect.maxX, y: rect.minY)
635
- case .bottomRight:
636
- return CGPoint(x: rect.minX, y: rect.minY)
637
- }
643
+ static func resizedSize(from size: CGSize, corner: Corner, translation: CGPoint) -> CGSize {
644
+ let dx = (corner == .topRight || corner == .bottomRight) ? translation.x : -translation.x
645
+ let dy = (corner == .bottomLeft || corner == .bottomRight) ? translation.y : -translation.y
646
+ let lengthSquared = max(1, size.width * size.width + size.height * size.height)
647
+ let minimumScale = 48 / max(1, min(size.width, size.height))
648
+ let scale = max(minimumScale, 1 + (dx * size.width + dy * size.height) / lengthSquared)
649
+ return CGSize(width: size.width * scale, height: size.height * scale)
638
650
  }
639
651
 
640
- private func resizedRect(
641
- from originalRect: CGRect,
642
- corner: Corner,
643
- translation: CGPoint,
644
- maximumWidth: CGFloat?
645
- ) -> CGRect {
646
- let aspectRatio = max(originalRect.width / max(originalRect.height, 1), 0.1)
647
- let signedDx = (corner == .topRight || corner == .bottomRight) ? translation.x : -translation.x
648
- let signedDy = (corner == .bottomLeft || corner == .bottomRight) ? translation.y : -translation.y
649
- let widthScale = (originalRect.width + signedDx) / max(originalRect.width, 1)
650
- let heightScale = (originalRect.height + signedDy) / max(originalRect.height, 1)
651
- let scale = max(minimumImageSize / max(originalRect.width, 1), widthScale, heightScale)
652
- let unclampedSize = CGSize(
653
- width: max(minimumImageSize, originalRect.width * scale),
654
- height: max(minimumImageSize / aspectRatio, (max(minimumImageSize, originalRect.width * scale) / aspectRatio))
655
- )
656
- let clampedSize = editorView?.clampedImageSize(unclampedSize, maximumWidth: maximumWidth) ?? unclampedSize
657
- let width = clampedSize.width
658
- let height = clampedSize.height
659
- let anchor = anchorPoint(for: corner, in: originalRect)
660
-
661
- switch corner {
662
- case .topLeft:
663
- return CGRect(x: anchor.x - width, y: anchor.y - height, width: width, height: height)
664
- case .topRight:
665
- return CGRect(x: anchor.x, y: anchor.y - height, width: width, height: height)
666
- case .bottomLeft:
667
- return CGRect(x: anchor.x - width, y: anchor.y, width: width, height: height)
668
- case .bottomRight:
669
- return CGRect(x: anchor.x, y: anchor.y, width: width, height: height)
652
+ private func updatePreview(translation: CGPoint) {
653
+ guard let dragState else { return }
654
+ if translation == .zero {
655
+ updatePreviewSize(dragState.originalSize)
656
+ return
670
657
  }
658
+ let size = Self.resizedSize(
659
+ from: dragState.originalSize,
660
+ corner: dragState.corner,
661
+ translation: translation
662
+ )
663
+ let clampedSize = editorView?.clampedImageSize(
664
+ size, maximumWidth: dragState.maximumWidth
665
+ ) ?? size
666
+ updatePreviewSize(clampedSize)
671
667
  }
672
668
 
673
669
  @objc
@@ -676,22 +672,18 @@ final class ImageResizeOverlayView: UIView {
676
672
 
677
673
  switch gesture.state {
678
674
  case .began:
679
- _ = beginPreviewResize(from: handleView.corner)
675
+ if beginPreviewResize(from: handleView.corner) {
676
+ updatePreview(translation: gesture.translation(in: window))
677
+ }
680
678
  case .changed:
681
- guard let dragState else { return }
682
- let nextRect = resizedRect(
683
- from: dragState.originalRect,
684
- corner: dragState.corner,
685
- translation: gesture.translation(in: self),
686
- maximumWidth: dragState.maximumWidth
687
- )
688
- updatePreviewRect(nextRect)
679
+ updatePreview(translation: gesture.translation(in: window))
689
680
  case .ended:
681
+ updatePreview(translation: gesture.translation(in: window))
690
682
  finishPreviewResize(commit: true)
691
683
  case .cancelled, .failed:
692
684
  finishPreviewResize(commit: false)
693
685
  default:
694
- finishPreviewResize(commit: false)
686
+ break
695
687
  }
696
688
  }
697
689
  }
@@ -54,7 +54,16 @@ final class EditorLayoutManager: NSLayoutManager, NSLayoutManagerDelegate {
54
54
  storage.enumerateAttribute(editorInlineLineHeightAttribute, in: characters) { value, _, _ in
55
55
  height = max(height, EditorTheme.cgFloat(value) ?? 0)
56
56
  }
57
+ var ascent = baselineOffset.pointee + (height - lineFragmentUsedRect.pointee.height) / 2
58
+ var descent = height - ascent
59
+ storage.enumerateAttribute(editorMentionBoxAttribute, in: characters) { value, _, _ in
60
+ guard let chip = value as? EditorMentionRenderedBox else { return }
61
+ ascent = max(ascent, chip.baselineOffset)
62
+ descent = max(descent, chip.size.height - chip.baselineOffset)
63
+ }
64
+ height = ascent + descent
57
65
  let extra = height - lineFragmentUsedRect.pointee.height
66
+ let baselineAdjustment = ascent - baselineOffset.pointee
58
67
  var leading: CGFloat = 0
59
68
  var overlap: CGFloat = 0
60
69
  if characters.location < storage.length,
@@ -76,7 +85,7 @@ final class EditorLayoutManager: NSLayoutManager, NSLayoutManagerDelegate {
76
85
  lineFragmentRect.pointee.size.height += extra + leading
77
86
  lineFragmentUsedRect.pointee.size.height = height
78
87
  lineFragmentUsedRect.pointee.origin.y += leading + overlap
79
- baselineOffset.pointee += extra / 2 + leading
88
+ baselineOffset.pointee += baselineAdjustment + leading
80
89
  return true
81
90
  }
82
91
 
@@ -226,10 +235,11 @@ final class EditorLayoutManager: NSLayoutManager, NSLayoutManagerDelegate {
226
235
  storage.enumerateAttribute(editorMentionBoxAttribute, in: visible) { value, range, _ in
227
236
  guard let box = value as? EditorMentionRenderedBox else { return }
228
237
  let glyph = self.glyphIndexForCharacter(at: range.location)
229
- let line = self.lineFragmentUsedRect(forGlyphAt: glyph, effectiveRange: nil)
230
238
  let location = self.location(forGlyphAt: glyph)
231
239
  let fragment = self.lineFragmentRect(forGlyphAt: glyph, effectiveRange: nil)
232
- let rect = CGRect(x: fragment.minX + location.x + origin.x, y: line.midY - box.size.height / 2 + origin.y, width: box.size.width, height: box.size.height)
240
+ let rect = CGRect(x: fragment.minX + location.x + origin.x,
241
+ y: fragment.minY + location.y + origin.y - box.baselineOffset,
242
+ width: box.size.width, height: box.size.height)
233
243
  box.box.draw(in: rect, context: context)
234
244
  let labelHeight = box.label?.size().height ?? 0
235
245
  box.label?.draw(at: CGPoint(x: rect.minX + box.padding.left,
@@ -150,6 +150,14 @@ struct EditorStyleBox {
150
150
  }
151
151
  var inset: UIEdgeInsets { padding.adding(borders) }
152
152
  var outerInsets: UIEdgeInsets { inset.adding(margin) }
153
+ var mentionInsets: UIEdgeInsets {
154
+ UIEdgeInsets(
155
+ top: number("paddingTop", fallback: number("padding", fallback: 4)),
156
+ left: number("paddingLeft", fallback: number("padding", fallback: 6)),
157
+ bottom: number("paddingBottom", fallback: number("padding", fallback: 4)),
158
+ right: number("paddingRight", fallback: number("padding", fallback: 6))
159
+ ).adding(borders)
160
+ }
153
161
  private func insets(_ key: String) -> UIEdgeInsets {
154
162
  UIEdgeInsets(top: number(key + "Top", fallback: number(key)), left: number(key + "Left", fallback: number(key)), bottom: number(key + "Bottom", fallback: number(key)), right: number(key + "Right", fallback: number(key)))
155
163
  }
@@ -410,15 +418,18 @@ final class EditorMentionRenderedBox: NSObject {
410
418
  let label: NSAttributedString?
411
419
  let size: CGSize
412
420
  let padding: UIEdgeInsets
421
+ let baselineOffset: CGFloat
413
422
 
414
423
  init(box: EditorStyleBox, label: NSAttributedString? = nil) {
415
424
  self.box = box
416
425
  self.label = label
417
- padding = UIEdgeInsets(top: 4, left: 6, bottom: 4, right: 6).adding(box.inset)
426
+ padding = box.mentionInsets
418
427
  let measured = label?.size() ?? .zero
419
428
  let lineHeight = box.number("lineHeight", fallback: measured.height)
420
429
  size = CGSize(width: ceil(measured.width) + padding.left + padding.right,
421
430
  height: ceil(max(measured.height, lineHeight)) + padding.top + padding.bottom)
431
+ let font = label.flatMap { $0.length > 0 ? $0.attribute(.font, at: 0, effectiveRange: nil) as? UIFont : nil }
432
+ baselineOffset = padding.top + (size.height - padding.top - padding.bottom - measured.height) / 2 + (font?.ascender ?? 0)
422
433
  }
423
434
  }
424
435
 
@@ -289,7 +289,7 @@ extension EditorTextView {
289
289
  return (selectionState.docPos, rect)
290
290
  }
291
291
 
292
- private func blockImageAttachment(docPos: UInt32) -> (range: NSRange, attachment: BlockImageAttachment)? {
292
+ func blockImageAttachment(docPos: UInt32) -> (range: NSRange, attachment: BlockImageAttachment)? {
293
293
  let fullRange = NSRange(location: 0, length: textStorage.length)
294
294
  var resolved: (range: NSRange, attachment: BlockImageAttachment)?
295
295
  textStorage.enumerateAttribute(
@@ -309,10 +309,6 @@ extension EditorTextView {
309
309
  return resolved
310
310
  }
311
311
 
312
- func imagePreviewForDocPos(_ docPos: UInt32) -> UIImage? {
313
- blockImageAttachment(docPos: docPos)?.attachment.previewImage()
314
- }
315
-
316
312
  func maximumRenderableImageWidth() -> CGFloat {
317
313
  let containerWidth: CGFloat
318
314
  if bounds.width > 0 {
@@ -337,14 +333,40 @@ extension EditorTextView {
337
333
  }
338
334
  }
339
335
 
340
- func previewResizeImageAtDocPos(_ docPos: UInt32, width: CGFloat, height: CGFloat) {
336
+ func previewResizeImageAtDocPos(_ docPos: UInt32, width: CGFloat?, height: CGFloat?) {
341
337
  guard let attachmentState = blockImageAttachment(docPos: docPos) else { return }
342
- attachmentState.attachment.setPreferredSize(width: width, height: height)
343
- layoutManager.invalidateLayout(forCharacterRange: attachmentState.range, actualCharacterRange: nil)
344
- layoutManager.invalidateDisplay(forCharacterRange: attachmentState.range)
338
+ updateImageResizePreview(attachmentState.attachment, range: attachmentState.range, width: width, height: height)
339
+ }
340
+
341
+ func restoreImageResizePreview(_ attachment: BlockImageAttachment, width: CGFloat?, height: CGFloat?) {
342
+ var attachmentRange: NSRange?
343
+ textStorage.enumerateAttribute(.attachment, in: NSRange(location: 0, length: textStorage.length)) { value, range, stop in
344
+ if value as? BlockImageAttachment === attachment {
345
+ attachmentRange = range
346
+ stop.pointee = true
347
+ }
348
+ }
349
+ guard let attachmentRange else { return }
350
+ updateImageResizePreview(attachment, range: attachmentRange, width: width, height: height)
351
+ }
352
+
353
+ private func updateImageResizePreview(_ attachment: BlockImageAttachment, range: NSRange, width: CGFloat?, height: CGFloat?) {
354
+ guard attachment.preferredWidth != width || attachment.preferredHeight != height else { return }
355
+ let previousOffset = contentOffset
356
+ attachment.preferredWidth = width
357
+ attachment.preferredHeight = height
358
+ layoutManager.invalidateLayout(forCharacterRange: range, actualCharacterRange: nil)
359
+ layoutManager.invalidateDisplay(forCharacterRange: range)
345
360
  textStorage.beginEditing()
346
- textStorage.edited(.editedAttributes, range: attachmentState.range, changeInLength: 0)
361
+ textStorage.edited(.editedAttributes, range: range, changeInLength: 0)
347
362
  textStorage.endEditing()
363
+ invalidateAutoGrowHeightMeasurement()
364
+ layoutManager.ensureLayout(forCharacterRange: range)
365
+ setNeedsLayout()
366
+ layoutIfNeeded()
367
+ setContentOffset(previousOffset, animated: false)
368
+ notifyHeightChangeIfNeeded()
369
+ onViewportMayChange?()
348
370
  }
349
371
 
350
372
  func setImageResizePreviewActive(_ active: Bool) {