Sources/TwitterTextEditor/LayoutManager.swift (265 lines of code) (raw):

// // LayoutManager.swift // TwitterTextEditor // // Copyright 2021 Twitter, Inc. // SPDX-License-Identifier: Apache-2.0 // import Foundation import UIKit final class LayoutManager: NSLayoutManager { private var glyphsCache: [CacheableGlyphs] = [] override init() { super.init() super.delegate = self } @available(*, unavailable) public required init?(coder: NSCoder) { fatalError() } // MARK: - NSLayoutManager /* UIKit behavior note - Confirmed on iOS 13.6 and prior. There are UIKit implementations that are replacing `delegate` to do its tasks which eventually set it back to original value, such as `sizeThatFits:`. Because of these behaviors, we should not refuse to modify `delegate` even if the setter is marked as unavailable and it can break a consistency of this layout manager implementation. - SeeAlso: - `-[UITextView _performLayoutCalculation:inSize:]` */ override var delegate: NSLayoutManagerDelegate? { get { super.delegate } @available(*, unavailable) set { if !(newValue is LayoutManager) { log(type: .error, "LayoutManager delegate should not be modified to delegate: %@", String(describing: newValue)) } super.delegate = newValue } } override func drawGlyphs(forGlyphRange glyphsToShow: NSRange, at origin: CGPoint) { log(type: .debug, "range: %@, at: %@", NSStringFromRange(glyphsToShow), NSCoder.string(for: origin)) super.drawGlyphs(forGlyphRange: glyphsToShow, at: origin) // See the inline comment for `delegate`. guard delegate is LayoutManager else { return } guard let textStorage = textStorage else { return } guard glyphsToShow.length > 0 else { return } let count = glyphsToShow.length var properties = [GlyphProperty](repeating: [], count: count) var characterIndexes = [Int](repeating: 0, count: count) properties.withUnsafeMutableBufferPointer { props -> Void in characterIndexes.withUnsafeMutableBufferPointer { charIndexes -> Void in getGlyphs(in: glyphsToShow, glyphs: nil, properties: props.baseAddress, characterIndexes: charIndexes.baseAddress, bidiLevels: nil) } } guard let context = UIGraphicsGetCurrentContext() else { return } context.saveGState() if Configuration.shared.isDebugLayoutManagerDrawGlyphsEnabled { context.setStrokeColor(UIColor.blue.withAlphaComponent(0.5).cgColor) context.setLineDash(phase: 0, lengths: [2, 2]) } // TODO: Measure performance and consider different approach. // This scans entire glyph range once. let signpostScanGlyphsToShow = signpost(name: "Scan glyphs to show", "length: %d", glyphsToShow.length) signpostScanGlyphsToShow.begin() for index in 0..<glyphsToShow.length where properties[index].contains(.controlCharacter) { let attributes = textStorage.attributes(at: characterIndexes[index], effectiveRange: nil) if let suffixedAttachment = attributes[.suffixedAttachment] as? TextAttributes.SuffixedAttachment, case .image(let image) = suffixedAttachment.attachment { let glyphIndex = glyphsToShow.location + index let lineFragmentOrigin = lineFragmentRect(forGlyphAt: glyphIndex, effectiveRange: nil, withoutAdditionalLayout: true).origin let locationInLineFragment = location(forGlyphAt: glyphIndex) let locationInContext = CGPoint(x: origin.x + lineFragmentOrigin.x + locationInLineFragment.x, y: origin.y + lineFragmentOrigin.y) let bounds = CGRect(origin: locationInContext, size: suffixedAttachment.size) if Configuration.shared.isDebugLayoutManagerDrawGlyphsEnabled { context.stroke(bounds, width: 2.0) } image.draw(in: bounds) } } signpostScanGlyphsToShow.end() context.restoreGState() } } // MARK: - NSLayoutManagerDelegate extension LayoutManager: NSLayoutManagerDelegate { private struct UnsafeBufferGlyphs { var count: Int var glyphs: UnsafeBufferPointer<CGGlyph> var properties: UnsafeBufferPointer<NSLayoutManager.GlyphProperty> var characterIndexes: UnsafeBufferPointer<Int> init(glyphs: UnsafePointer<CGGlyph>, properties: UnsafePointer<NSLayoutManager.GlyphProperty>, characterIndexes: UnsafePointer<Int>, count: Int) { self.count = count self.glyphs = UnsafeBufferPointer(start: glyphs, count: count) self.properties = UnsafeBufferPointer(start: properties, count: count) self.characterIndexes = UnsafeBufferPointer(start: characterIndexes, count: count) } } private struct MutableGlyphs { struct Insertion { var index: Int var glyph: CGGlyph var property: NSLayoutManager.GlyphProperty var characterIndex: Int } private(set) var glyphs: [CGGlyph] private(set) var properties: [NSLayoutManager.GlyphProperty] private(set) var characterIndexes: [Int] init(unsafeBufferGlyphs: UnsafeBufferGlyphs) { glyphs = Array(unsafeBufferGlyphs.glyphs) properties = Array(unsafeBufferGlyphs.properties) characterIndexes = Array(unsafeBufferGlyphs.characterIndexes) } mutating func insert(_ insertion: Insertion, offset: Int = 0) { let index = insertion.index + offset glyphs.insert(insertion.glyph, at: index) properties.insert(insertion.property, at: index) characterIndexes.insert(insertion.characterIndex, at: index) } } private class CacheableGlyphs { let glyphs: UnsafeMutableBufferPointer<CGGlyph> let properties: UnsafeMutableBufferPointer<NSLayoutManager.GlyphProperty> let characterIndexes: UnsafeMutableBufferPointer<Int> init(mutableGlyphs: MutableGlyphs) { let glyphs = UnsafeMutableBufferPointer<CGGlyph>.allocate(capacity: mutableGlyphs.glyphs.count) _ = glyphs.initialize(from: mutableGlyphs.glyphs) self.glyphs = glyphs let properties = UnsafeMutableBufferPointer<NSLayoutManager.GlyphProperty>.allocate(capacity: mutableGlyphs.properties.count) _ = properties.initialize(from: mutableGlyphs.properties) self.properties = properties let characterIndexes = UnsafeMutableBufferPointer<Int>.allocate(capacity: mutableGlyphs.characterIndexes.count) _ = characterIndexes.initialize(from: mutableGlyphs.characterIndexes) self.characterIndexes = characterIndexes } deinit { self.glyphs.deallocate() self.properties.deallocate() self.characterIndexes.deallocate() } } func layoutManager(_ layoutManager: NSLayoutManager, shouldGenerateGlyphs glyphs: UnsafePointer<CGGlyph>, properties props: UnsafePointer<NSLayoutManager.GlyphProperty>, characterIndexes charIndexes: UnsafePointer<Int>, font aFont: UIFont, forGlyphRange glyphRange: NSRange) -> Int { guard let textStorage = layoutManager.textStorage else { return 0 } let unsafeBufferGlyphs = UnsafeBufferGlyphs(glyphs: glyphs, properties: props, characterIndexes: charIndexes, count: glyphRange.length) var sortedInsertions = [MutableGlyphs.Insertion]() for index in 0..<unsafeBufferGlyphs.count { let characterIndex = unsafeBufferGlyphs.characterIndexes[index] let attributes = textStorage.attributes(at: characterIndex, effectiveRange: nil) // We can't derive two glyphs from a character that has `NSTextAttachment`. // For safely, check if there is no `.attachment` attribute. if attributes[.suffixedAttachment] is TextAttributes.SuffixedAttachment, attributes[.attachment] == nil { let insertion = MutableGlyphs.Insertion(index: index + 1, glyph: CGGlyph(0), property: .controlCharacter, characterIndex: characterIndex) sortedInsertions.append(insertion) } } if sortedInsertions.isEmpty { return 0 } var mutableGlyphs = MutableGlyphs(unsafeBufferGlyphs: unsafeBufferGlyphs) var offset = 0 for insertion in sortedInsertions { mutableGlyphs.insert(insertion, offset: offset) offset += 1 } let cacheableGlyphs = CacheableGlyphs(mutableGlyphs: mutableGlyphs) let mutatedLength = cacheableGlyphs.glyphs.count let mutatedGlyphRange = NSRange(location: glyphRange.location, length: mutatedLength) assert(glyphRange.length + offset == mutatedGlyphRange.length) glyphsCache.append(cacheableGlyphs) layoutManager.setGlyphs(cacheableGlyphs.glyphs.baseAddress!, properties: cacheableGlyphs.properties.baseAddress!, characterIndexes: cacheableGlyphs.characterIndexes.baseAddress!, font: aFont, forGlyphRange: mutatedGlyphRange) return mutatedGlyphRange.length } func layoutManager(_ layoutManager: NSLayoutManager, didCompleteLayoutFor textContainer: NSTextContainer?, atEnd layoutFinishedFlag: Bool) { log(type: .debug, "text container: %@, at end: %@", String(describing: textContainer), String(describing: layoutFinishedFlag)) // TODO: Verify if it's right timing to release cache. glyphsCache = [] // This is a timing to lay out views in attachments. guard let textStorage = layoutManager.textStorage, let textContainer = textContainer else { return } let glyphsToShow = layoutManager.glyphRange(for: textContainer) // Following logic is same as the one in LayoutManager for `drawGlyphs(forGlyphRange:at)`. let count = glyphsToShow.length var properties = [NSLayoutManager.GlyphProperty](repeating: [], count: count) var characterIndexes = [Int](repeating: 0, count: count) properties.withUnsafeMutableBufferPointer { props -> Void in characterIndexes.withUnsafeMutableBufferPointer { charIndexes -> Void in layoutManager.getGlyphs(in: glyphsToShow, glyphs: nil, properties: props.baseAddress, characterIndexes: charIndexes.baseAddress, bidiLevels: nil) } } // TODO: Measure performance and consider different approach. // This scans entire glyph range once. let signpostScanGlyphsToShow = signpost(name: "Scan glyphs to show", "length: %d", glyphsToShow.length) signpostScanGlyphsToShow.begin() for index in 0..<glyphsToShow.length where properties[index].contains(.controlCharacter) { let attributes = textStorage.attributes(at: characterIndexes[index], effectiveRange: nil) if let suffixedAttachment = attributes[.suffixedAttachment] as? TextAttributes.SuffixedAttachment, case .view(let view, let layoutInTextContainer) = suffixedAttachment.attachment { let glyphIndex = glyphsToShow.location + index let lineFragmentOrigin = layoutManager.lineFragmentRect(forGlyphAt: glyphIndex, effectiveRange: nil, withoutAdditionalLayout: true).origin let locationInLineFragment = layoutManager.location(forGlyphAt: glyphIndex) let locationInContext = CGPoint( x: lineFragmentOrigin.x + locationInLineFragment.x, y: lineFragmentOrigin.y ) let frame = CGRect(origin: locationInContext, size: suffixedAttachment.size) layoutInTextContainer(view, frame) } } signpostScanGlyphsToShow.end() } func layoutManager(_ layoutManager: NSLayoutManager, shouldUse action: NSLayoutManager.ControlCharacterAction, forControlCharacterAt charIndex: Int) -> NSLayoutManager.ControlCharacterAction { guard let textStorage = layoutManager.textStorage else { return action } let attributes = textStorage.attributes(at: charIndex, effectiveRange: nil) guard attributes[.suffixedAttachment] is TextAttributes.SuffixedAttachment else { return action } // `.whitespace` may not be set always by `NSTypesetter`. // This is only for control glyphs inserted by `layoutManager(_:shouldGenerateGlyphs:properties:characterIndexes:font:forGlyphRange:)`. return .whitespace } func layoutManager(_ layoutManager: NSLayoutManager, boundingBoxForControlGlyphAt glyphIndex: Int, for textContainer: NSTextContainer, proposedLineFragment proposedRect: CGRect, glyphPosition: CGPoint, characterIndex charIndex: Int) -> CGRect { guard let textStorage = layoutManager.textStorage else { return .zero } let attributes = textStorage.attributes(at: charIndex, effectiveRange: nil) guard let suffixedAttachment = attributes[.suffixedAttachment] as? TextAttributes.SuffixedAttachment else { // Should't reach here. // See `layoutManager(_:shouldUse:forControlCharacterAt:)`. assertionFailure("Glyphs that have .suffixedAttachment shouldn't be a control glyphs") return .zero } return CGRect(origin: glyphPosition, size: suffixedAttachment.size) } }