Files
ghostgram/submodules/TelegramUI/Components/RichTextEditor/Sources/RichTextEngineEditor/ButtonRowTextAttachment.swift
T
ichmagmaus111andClaude Opus 5.5 534cabe35b bug fixes
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 16:55:08 +01:00

86 lines
4.2 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#if canImport(UIKit)
import UIKit
import RichTextEditorCore
import RichTextEditorUIKit
/// The attachment character a button row is on the engine's axis — the sibling of
/// `MediaTextAttachment`, built to the same rules.
///
/// It reserves the slot and draws nothing: the hosted `RichTextButtonRowView` is positioned over the
/// frame TextKit gives it (`+ButtonRows.swift`), exactly as a media atom's and an emoji's are. The
/// 1×1 spacer image is what makes TextKit call `attachmentBounds` at all — an image-less attachment
/// can lay out zero-width.
///
/// The bounds are DYNAMIC: packed from the proposed line fragment on every layout, so a width change
/// (rotation, the composer growing) re-packs with no document edit. There is no analogue of media's
/// `horizontalBleed` — legacy packs a row into the content column at every depth.
///
/// Value-equal on purpose, for ONE consumer: the document setter's differing-span diff, which
/// compares rendered blocks with `NSAttributedString.isEqual`. A pointer-unequal atom would read as
/// changed on every set.
@available(iOS 18.4, *)
public final class ButtonRowTextAttachment: NSTextAttachment {
public let shape: ButtonRowShape
/// The head indent of the atom's line inside a container. Probe P1 (measured 2026-09-05)
/// established that TextKit 2 proposes the WHOLE container width to `attachmentBounds` even for
/// a paragraph carrying a head indent, so the column the row may occupy is that much narrower
/// and the attachment must subtract it itself.
public let horizontalIndent: CGFloat
/// Whether the row reserves width for its "…" control. Part of the MEASUREMENT, not only the
/// drawing, so a composer row (no control, no reserve) matches the sent message exactly.
public let showsMenuAffordance: Bool
private let mapper: AttributedStringMapper
private static let spacerImage: UIImage =
UIGraphicsImageRenderer(size: CGSize(width: 1, height: 1)).image { _ in }
public init(shape: ButtonRowShape, horizontalIndent: CGFloat,
showsMenuAffordance: Bool, mapper: AttributedStringMapper) {
self.shape = shape
self.horizontalIndent = horizontalIndent
self.showsMenuAffordance = showsMenuAffordance
self.mapper = mapper
super.init(data: nil, ofType: nil)
self.image = Self.spacerImage
}
public required init?(coder: NSCoder) { fatalError("init(coder:) not used") }
/// The slot for a proposed line width. P1 answered "full fragment", so the indent is the
/// attachment's to remove.
public func slot(forLineWidth lineWidth: CGFloat) -> CGSize {
let line = max(lineWidth - horizontalIndent, 1)
return CGSize(width: line,
height: richTextButtonRowHeight(buttons: shape.buttons, alignment: shape.alignment,
width: line, mapper: mapper,
showsMenuAffordance: showsMenuAffordance))
}
private func box(proposedLineFragment: CGRect) -> CGRect {
let size = slot(forLineWidth: proposedLineFragment.width)
return CGRect(origin: .zero, size: size)
}
public override func attachmentBounds(for attributes: [NSAttributedString.Key: Any],
location: NSTextLocation, textContainer: NSTextContainer?,
proposedLineFragment: CGRect, position: CGPoint) -> CGRect {
box(proposedLineFragment: proposedLineFragment)
}
public override func attachmentBounds(for textContainer: NSTextContainer?,
proposedLineFragment lineFrag: CGRect,
glyphPosition position: CGPoint,
characterIndex charIndex: Int) -> CGRect {
box(proposedLineFragment: lineFrag)
}
public override func isEqual(_ object: Any?) -> Bool {
guard let other = object as? ButtonRowTextAttachment else { return false }
return other.shape == shape
&& other.horizontalIndent == horizontalIndent
&& other.showsMenuAffordance == showsMenuAffordance
}
public override var hash: Int { shape.id.rawValue.hashValue }
}
#endif