Swiftgram/Display/TextNode.swift

865 lines
39 KiB
Swift

import Foundation
import AsyncDisplayKit
import CoreText
private let defaultFont = UIFont.systemFont(ofSize: 15.0)
private final class TextNodeLine {
let line: CTLine
let frame: CGRect
let range: NSRange
let isRTL: Bool
init(line: CTLine, frame: CGRect, range: NSRange, isRTL: Bool) {
self.line = line
self.frame = frame
self.range = range
self.isRTL = isRTL
}
}
public enum TextNodeCutoutPosition {
case TopLeft
case TopRight
case BottomRight
}
public struct TextNodeCutout: Equatable {
public var topLeft: CGSize?
public var topRight: CGSize?
public var bottomRight: CGSize?
public init(topLeft: CGSize? = nil, topRight: CGSize? = nil, bottomRight: CGSize? = nil) {
self.topLeft = topLeft
self.topRight = topRight
self.bottomRight = bottomRight
}
}
private func displayLineFrame(frame: CGRect, isRTL: Bool, boundingRect: CGRect, cutout: TextNodeCutout?) -> CGRect {
if frame.width.isEqual(to: boundingRect.width) {
return frame
}
var lineFrame = frame
let intersectionFrame = lineFrame.offsetBy(dx: 0.0, dy: -lineFrame.height)
if isRTL {
lineFrame.origin.x = max(0.0, floor(boundingRect.width - lineFrame.size.width))
if let topRight = cutout?.topRight {
let topRightRect = CGRect(origin: CGPoint(x: boundingRect.width - topRight.width, y: 0.0), size: topRight)
if intersectionFrame.intersects(topRightRect) {
lineFrame.origin.x -= topRight.width
return lineFrame
}
}
if let bottomRight = cutout?.bottomRight {
let bottomRightRect = CGRect(origin: CGPoint(x: boundingRect.width - bottomRight.width, y: boundingRect.height - bottomRight.height), size: bottomRight)
if intersectionFrame.intersects(bottomRightRect) {
lineFrame.origin.x -= bottomRight.width
return lineFrame
}
}
}
return lineFrame
}
public final class TextNodeLayoutArguments {
public let attributedString: NSAttributedString?
public let backgroundColor: UIColor?
public let maximumNumberOfLines: Int
public let truncationType: CTLineTruncationType
public let constrainedSize: CGSize
public let alignment: NSTextAlignment
public let lineSpacing: CGFloat
public let cutout: TextNodeCutout?
public let insets: UIEdgeInsets
public init(attributedString: NSAttributedString?, backgroundColor: UIColor? = nil, maximumNumberOfLines: Int, truncationType: CTLineTruncationType, constrainedSize: CGSize, alignment: NSTextAlignment = .natural, lineSpacing: CGFloat = 0.12, cutout: TextNodeCutout? = nil, insets: UIEdgeInsets = UIEdgeInsets()) {
self.attributedString = attributedString
self.backgroundColor = backgroundColor
self.maximumNumberOfLines = maximumNumberOfLines
self.truncationType = truncationType
self.constrainedSize = constrainedSize
self.alignment = alignment
self.lineSpacing = lineSpacing
self.cutout = cutout
self.insets = insets
}
}
public final class TextNodeLayout: NSObject {
fileprivate let attributedString: NSAttributedString?
fileprivate let maximumNumberOfLines: Int
fileprivate let truncationType: CTLineTruncationType
fileprivate let backgroundColor: UIColor?
fileprivate let constrainedSize: CGSize
fileprivate let alignment: NSTextAlignment
fileprivate let lineSpacing: CGFloat
fileprivate let cutout: TextNodeCutout?
fileprivate let insets: UIEdgeInsets
public let size: CGSize
public let truncated: Bool
fileprivate let firstLineOffset: CGFloat
fileprivate let lines: [TextNodeLine]
public let hasRTL: Bool
fileprivate init(attributedString: NSAttributedString?, maximumNumberOfLines: Int, truncationType: CTLineTruncationType, constrainedSize: CGSize, alignment: NSTextAlignment, lineSpacing: CGFloat, cutout: TextNodeCutout?, insets: UIEdgeInsets, size: CGSize, truncated: Bool, firstLineOffset: CGFloat, lines: [TextNodeLine], backgroundColor: UIColor?) {
self.attributedString = attributedString
self.maximumNumberOfLines = maximumNumberOfLines
self.truncationType = truncationType
self.constrainedSize = constrainedSize
self.alignment = alignment
self.lineSpacing = lineSpacing
self.cutout = cutout
self.insets = insets
self.size = size
self.truncated = truncated
self.firstLineOffset = firstLineOffset
self.lines = lines
self.backgroundColor = backgroundColor
var hasRTL = false
for line in lines {
if line.isRTL {
hasRTL = true
}
}
self.hasRTL = hasRTL
}
public func areLinesEqual(to other: TextNodeLayout) -> Bool {
if self.lines.count != other.lines.count {
return false
}
for i in 0 ..< self.lines.count {
if !self.lines[i].frame.equalTo(other.lines[i].frame) {
return false
}
if self.lines[i].isRTL != other.lines[i].isRTL {
return false
}
if self.lines[i].range != other.lines[i].range {
return false
}
let lhsRuns = CTLineGetGlyphRuns(self.lines[i].line) as NSArray
let rhsRuns = CTLineGetGlyphRuns(other.lines[i].line) as NSArray
if lhsRuns.count != rhsRuns.count {
return false
}
for j in 0 ..< lhsRuns.count {
let lhsRun = lhsRuns[j] as! CTRun
let rhsRun = rhsRuns[j] as! CTRun
let lhsGlyphCount = CTRunGetGlyphCount(lhsRun)
let rhsGlyphCount = CTRunGetGlyphCount(rhsRun)
if lhsGlyphCount != rhsGlyphCount {
return false
}
for k in 0 ..< lhsGlyphCount {
var lhsGlyph = CGGlyph()
var rhsGlyph = CGGlyph()
CTRunGetGlyphs(lhsRun, CFRangeMake(k, 1), &lhsGlyph)
CTRunGetGlyphs(rhsRun, CFRangeMake(k, 1), &rhsGlyph)
if lhsGlyph != rhsGlyph {
return false
}
}
}
}
return true
}
public var numberOfLines: Int {
return self.lines.count
}
public var trailingLineWidth: CGFloat {
if let lastLine = self.lines.last {
return lastLine.frame.width
} else {
return 0.0
}
}
public func attributesAtPoint(_ point: CGPoint) -> (Int, [NSAttributedStringKey: Any])? {
if let attributedString = self.attributedString {
let transformedPoint = CGPoint(x: point.x - self.insets.left, y: point.y - self.insets.top)
var lineIndex = -1
for line in self.lines {
lineIndex += 1
var lineFrame = CGRect(origin: CGPoint(x: line.frame.origin.x, y: line.frame.origin.y - line.frame.size.height + self.firstLineOffset), size: line.frame.size)
switch self.alignment {
case .center:
lineFrame.origin.x = floor((self.size.width - lineFrame.size.width) / 2.0)
case .natural:
if line.isRTL {
lineFrame.origin.x = self.size.width - lineFrame.size.width
}
lineFrame = displayLineFrame(frame: lineFrame, isRTL: line.isRTL, boundingRect: CGRect(origin: CGPoint(), size: self.size), cutout: self.cutout)
default:
break
}
if lineFrame.contains(transformedPoint) {
var index = CTLineGetStringIndexForPosition(line.line, CGPoint(x: transformedPoint.x - lineFrame.minX, y: transformedPoint.y - lineFrame.minY))
if index == attributedString.length {
index -= 1
} else if index != 0 {
var glyphStart: CGFloat = 0.0
CTLineGetOffsetForStringIndex(line.line, index, &glyphStart)
if transformedPoint.x < glyphStart {
index -= 1
}
}
if index >= 0 && index < attributedString.length {
return (index, attributedString.attributes(at: index, effectiveRange: nil))
}
break
}
}
lineIndex = -1
for line in self.lines {
lineIndex += 1
var lineFrame = CGRect(origin: CGPoint(x: line.frame.origin.x, y: line.frame.origin.y - line.frame.size.height + self.firstLineOffset), size: line.frame.size)
switch self.alignment {
case .center:
lineFrame.origin.x = floor((self.size.width - lineFrame.size.width) / 2.0)
case .natural:
if line.isRTL {
lineFrame.origin.x = floor(self.size.width - lineFrame.size.width)
}
lineFrame = displayLineFrame(frame: lineFrame, isRTL: line.isRTL, boundingRect: CGRect(origin: CGPoint(), size: self.size), cutout: self.cutout)
default:
break
}
if lineFrame.offsetBy(dx: 0.0, dy: -lineFrame.size.height).insetBy(dx: -3.0, dy: -3.0).contains(transformedPoint) {
var index = CTLineGetStringIndexForPosition(line.line, CGPoint(x: transformedPoint.x - lineFrame.minX, y: transformedPoint.y - lineFrame.minY))
if index == attributedString.length {
index -= 1
} else if index != 0 {
var glyphStart: CGFloat = 0.0
CTLineGetOffsetForStringIndex(line.line, index, &glyphStart)
if transformedPoint.x < glyphStart {
index -= 1
}
}
if index >= 0 && index < attributedString.length {
return (index, attributedString.attributes(at: index, effectiveRange: nil))
}
break
}
}
}
return nil
}
public func linesRects() -> [CGRect] {
var rects: [CGRect] = []
for line in self.lines {
rects.append(line.frame)
}
return rects
}
public func textRangesRects(text: String) -> [[CGRect]] {
guard let attributedString = self.attributedString else {
return []
}
var ranges: [Range<String.Index>] = []
var searchRange = attributedString.string.startIndex ..< attributedString.string.endIndex
while searchRange.lowerBound != attributedString.string.endIndex {
if let range = attributedString.string.range(of: text, options: [.caseInsensitive, .diacriticInsensitive], range: searchRange, locale: nil) {
ranges.append(range)
searchRange = range.upperBound ..< attributedString.string.endIndex
} else {
break
}
}
var result: [[CGRect]] = []
for stringRange in ranges {
var rects: [CGRect] = []
let range = NSRange(stringRange, in: attributedString.string)
for line in self.lines {
let lineRange = NSIntersectionRange(range, line.range)
if lineRange.length != 0 {
var leftOffset: CGFloat = 0.0
if lineRange.location != line.range.location {
leftOffset = floor(CTLineGetOffsetForStringIndex(line.line, lineRange.location, nil))
}
var rightOffset: CGFloat = line.frame.width
if lineRange.location + lineRange.length != line.range.length {
var secondaryOffset: CGFloat = 0.0
let rawOffset = CTLineGetOffsetForStringIndex(line.line, lineRange.location + lineRange.length, &secondaryOffset)
rightOffset = ceil(rawOffset)
if !rawOffset.isEqual(to: secondaryOffset) {
rightOffset = ceil(secondaryOffset)
}
}
var lineFrame = CGRect(origin: CGPoint(x: line.frame.origin.x, y: line.frame.origin.y - line.frame.size.height + self.firstLineOffset), size: line.frame.size)
lineFrame = displayLineFrame(frame: lineFrame, isRTL: line.isRTL, boundingRect: CGRect(origin: CGPoint(), size: self.size), cutout: self.cutout)
rects.append(CGRect(origin: CGPoint(x: lineFrame.minX + leftOffset + self.insets.left, y: lineFrame.minY + self.insets.top), size: CGSize(width: rightOffset - leftOffset, height: lineFrame.size.height)))
}
}
if !rects.isEmpty {
result.append(rects)
}
}
return result
}
public func attributeSubstring(name: String, index: Int) -> String? {
if let attributedString = self.attributedString {
var range = NSRange()
let _ = attributedString.attribute(NSAttributedStringKey(rawValue: name), at: index, effectiveRange: &range)
if range.length != 0 {
return (attributedString.string as NSString).substring(with: range)
}
}
return nil
}
public func allAttributeRects(name: String) -> [(Any, CGRect)] {
guard let attributedString = self.attributedString else {
return []
}
var result: [(Any, CGRect)] = []
attributedString.enumerateAttribute(NSAttributedStringKey(rawValue: name), in: NSRange(location: 0, length: attributedString.length), options: []) { (value, range, _) in
if let value = value, range.length != 0 {
var coveringRect = CGRect()
for line in self.lines {
let lineRange = NSIntersectionRange(range, line.range)
if lineRange.length != 0 {
var leftOffset: CGFloat = 0.0
if lineRange.location != line.range.location {
leftOffset = floor(CTLineGetOffsetForStringIndex(line.line, lineRange.location, nil))
}
var rightOffset: CGFloat = line.frame.width
if lineRange.location + lineRange.length != line.range.length {
var secondaryOffset: CGFloat = 0.0
let rawOffset = CTLineGetOffsetForStringIndex(line.line, lineRange.location + lineRange.length, &secondaryOffset)
rightOffset = ceil(rawOffset)
if !rawOffset.isEqual(to: secondaryOffset) {
rightOffset = ceil(secondaryOffset)
}
}
var lineFrame = CGRect(origin: CGPoint(x: line.frame.origin.x, y: line.frame.origin.y - line.frame.size.height + self.firstLineOffset), size: line.frame.size)
lineFrame = displayLineFrame(frame: lineFrame, isRTL: line.isRTL, boundingRect: CGRect(origin: CGPoint(), size: self.size), cutout: self.cutout)
let rect = CGRect(origin: CGPoint(x: lineFrame.minX + leftOffset + self.insets.left, y: lineFrame.minY + self.insets.top), size: CGSize(width: rightOffset - leftOffset, height: lineFrame.size.height))
if coveringRect.isEmpty {
coveringRect = rect
} else {
coveringRect = coveringRect.union(rect)
}
}
}
if !coveringRect.isEmpty {
result.append((value, coveringRect))
}
}
}
return result
}
public func lineAndAttributeRects(name: String, at index: Int) -> [(CGRect, CGRect)]? {
if let attributedString = self.attributedString {
var range = NSRange()
let _ = attributedString.attribute(NSAttributedStringKey(rawValue: name), at: index, effectiveRange: &range)
if range.length != 0 {
var rects: [(CGRect, CGRect)] = []
for line in self.lines {
let lineRange = NSIntersectionRange(range, line.range)
if lineRange.length != 0 {
var leftOffset: CGFloat = 0.0
if lineRange.location != line.range.location {
leftOffset = floor(CTLineGetOffsetForStringIndex(line.line, lineRange.location, nil))
}
var rightOffset: CGFloat = line.frame.width
if lineRange.location + lineRange.length != line.range.length {
var secondaryOffset: CGFloat = 0.0
let rawOffset = CTLineGetOffsetForStringIndex(line.line, lineRange.location + lineRange.length, &secondaryOffset)
rightOffset = ceil(rawOffset)
if !rawOffset.isEqual(to: secondaryOffset) {
rightOffset = ceil(secondaryOffset)
}
}
var lineFrame = CGRect(origin: CGPoint(x: line.frame.origin.x, y: line.frame.origin.y - line.frame.size.height + self.firstLineOffset), size: line.frame.size)
lineFrame = displayLineFrame(frame: lineFrame, isRTL: line.isRTL, boundingRect: CGRect(origin: CGPoint(), size: self.size), cutout: self.cutout)
rects.append((lineFrame, CGRect(origin: CGPoint(x: lineFrame.minX + leftOffset + self.insets.left, y: lineFrame.minY + self.insets.top), size: CGSize(width: rightOffset - leftOffset, height: lineFrame.size.height))))
}
}
if !rects.isEmpty {
return rects
}
}
}
return nil
}
}
private final class TextAccessibilityOverlayElement: UIAccessibilityElement {
private let url: String
private let openUrl: (String) -> Void
init(accessibilityContainer: Any, url: String, openUrl: @escaping (String) -> Void) {
self.url = url
self.openUrl = openUrl
super.init(accessibilityContainer: accessibilityContainer)
}
override func accessibilityActivate() -> Bool {
self.openUrl(self.url)
return true
}
}
private final class TextAccessibilityOverlayNodeView: UIView {
fileprivate var cachedLayout: TextNodeLayout? {
didSet {
self.currentAccessibilityNodes?.forEach({ $0.removeFromSupernode() })
self.currentAccessibilityNodes = nil
}
}
fileprivate let openUrl: (String) -> Void
private var currentAccessibilityNodes: [AccessibilityAreaNode]?
override var accessibilityElements: [Any]? {
get {
if let _ = self.currentAccessibilityNodes {
return nil
}
guard let cachedLayout = self.cachedLayout else {
return nil
}
let urlAttributesAndRects = cachedLayout.allAttributeRects(name: "UrlAttributeT")
var urlElements: [AccessibilityAreaNode] = []
for (value, rect) in urlAttributesAndRects {
let element = AccessibilityAreaNode()
element.accessibilityLabel = value as? String ?? ""
element.frame = rect
element.accessibilityTraits = UIAccessibilityTraitLink
element.activate = { [weak self] in
self?.openUrl(value as? String ?? "")
return true
}
self.addSubnode(element)
urlElements.append(element)
}
self.currentAccessibilityNodes = urlElements
return nil
} set(value) {
}
}
init(openUrl: @escaping (String) -> Void) {
self.openUrl = openUrl
super.init(frame: CGRect())
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
public final class TextAccessibilityOverlayNode: ASDisplayNode {
public var cachedLayout: TextNodeLayout? {
didSet {
if self.isNodeLoaded {
(self.view as? TextAccessibilityOverlayNodeView)?.cachedLayout = self.cachedLayout
}
}
}
public var openUrl: ((String) -> Void)?
override public init() {
super.init()
self.isOpaque = false
self.backgroundColor = nil
let openUrl: (String) -> Void = { [weak self] url in
self?.openUrl?(url)
}
self.isAccessibilityElement = false
self.setViewBlock({
return TextAccessibilityOverlayNodeView(openUrl: openUrl)
})
}
override public func didLoad() {
super.didLoad()
(self.view as? TextAccessibilityOverlayNodeView)?.cachedLayout = self.cachedLayout
}
override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
return nil
}
}
public class TextNode: ASDisplayNode {
public private(set) var cachedLayout: TextNodeLayout?
override public init() {
super.init()
self.backgroundColor = UIColor.clear
self.isOpaque = false
self.clipsToBounds = false
}
public func attributesAtPoint(_ point: CGPoint) -> (Int, [NSAttributedStringKey: Any])? {
if let cachedLayout = self.cachedLayout {
return cachedLayout.attributesAtPoint(point)
} else {
return nil
}
}
public func textRangesRects(text: String) -> [[CGRect]] {
return self.cachedLayout?.textRangesRects(text: text) ?? []
}
public func attributeSubstring(name: String, index: Int) -> String? {
return self.cachedLayout?.attributeSubstring(name: name, index: index)
}
public func attributeRects(name: String, at index: Int) -> [CGRect]? {
if let cachedLayout = self.cachedLayout {
return cachedLayout.lineAndAttributeRects(name: name, at: index)?.map { $0.1 }
} else {
return nil
}
}
public func lineAndAttributeRects(name: String, at index: Int) -> [(CGRect, CGRect)]? {
if let cachedLayout = self.cachedLayout {
return cachedLayout.lineAndAttributeRects(name: name, at: index)
} else {
return nil
}
}
private class func calculateLayout(attributedString: NSAttributedString?, maximumNumberOfLines: Int, truncationType: CTLineTruncationType, backgroundColor: UIColor?, constrainedSize: CGSize, alignment: NSTextAlignment, lineSpacingFactor: CGFloat, cutout: TextNodeCutout?, insets: UIEdgeInsets) -> TextNodeLayout {
if let attributedString = attributedString {
let stringLength = attributedString.length
let font: CTFont
if stringLength != 0 {
if let stringFont = attributedString.attribute(NSAttributedStringKey.font, at: 0, effectiveRange: nil) {
font = stringFont as! CTFont
} else {
font = defaultFont
}
} else {
font = defaultFont
}
let fontAscent = CTFontGetAscent(font)
let fontDescent = CTFontGetDescent(font)
let fontLineHeight = floor(fontAscent + fontDescent)
let fontLineSpacing = floor(fontLineHeight * lineSpacingFactor)
var lines: [TextNodeLine] = []
var maybeTypesetter: CTTypesetter?
maybeTypesetter = CTTypesetterCreateWithAttributedString(attributedString as CFAttributedString)
if maybeTypesetter == nil {
return TextNodeLayout(attributedString: attributedString, maximumNumberOfLines: maximumNumberOfLines, truncationType: truncationType, constrainedSize: constrainedSize, alignment: alignment, lineSpacing: lineSpacingFactor, cutout: cutout, insets: insets, size: CGSize(), truncated: false, firstLineOffset: 0.0, lines: [], backgroundColor: backgroundColor)
}
let typesetter = maybeTypesetter!
var lastLineCharacterIndex: CFIndex = 0
var layoutSize = CGSize()
var cutoutEnabled = false
var cutoutMinY: CGFloat = 0.0
var cutoutMaxY: CGFloat = 0.0
var cutoutWidth: CGFloat = 0.0
var cutoutOffset: CGFloat = 0.0
var bottomCutoutEnabled = false
var bottomCutoutSize = CGSize()
if let topLeft = cutout?.topLeft {
cutoutMinY = -fontLineSpacing
cutoutMaxY = topLeft.height + fontLineSpacing
cutoutWidth = topLeft.width
cutoutOffset = cutoutWidth
cutoutEnabled = true
} else if let topRight = cutout?.topRight {
cutoutMinY = -fontLineSpacing
cutoutMaxY = topRight.height + fontLineSpacing
cutoutWidth = topRight.width
cutoutEnabled = true
}
if let bottomRight = cutout?.bottomRight {
bottomCutoutSize = bottomRight
bottomCutoutEnabled = true
}
let firstLineOffset = floorToScreenPixels(fontDescent)
var truncated = false
var first = true
while true {
var lineConstrainedWidth = constrainedSize.width
var lineOriginY = floorToScreenPixels(layoutSize.height + fontAscent)
if !first {
lineOriginY += fontLineSpacing
}
var lineCutoutOffset: CGFloat = 0.0
var lineAdditionalWidth: CGFloat = 0.0
if cutoutEnabled {
if lineOriginY - fontLineHeight < cutoutMaxY && lineOriginY + fontLineHeight > cutoutMinY {
lineConstrainedWidth = max(1.0, lineConstrainedWidth - cutoutWidth)
lineCutoutOffset = cutoutOffset
lineAdditionalWidth = cutoutWidth
}
}
let lineCharacterCount = CTTypesetterSuggestLineBreak(typesetter, lastLineCharacterIndex, Double(lineConstrainedWidth))
var isLastLine = false
if maximumNumberOfLines != 0 && lines.count == maximumNumberOfLines - 1 && lineCharacterCount > 0 {
isLastLine = true
} else if layoutSize.height + (fontLineSpacing + fontLineHeight) * 2.0 > constrainedSize.height {
isLastLine = true
}
if isLastLine {
if first {
first = false
} else {
layoutSize.height += fontLineSpacing
}
let lineRange = CFRange(location: lastLineCharacterIndex, length: stringLength - lastLineCharacterIndex)
if lineRange.length == 0 {
break
}
let coreTextLine: CTLine
let originalLine = CTTypesetterCreateLineWithOffset(typesetter, lineRange, 0.0)
if CTLineGetTypographicBounds(originalLine, nil, nil, nil) - CTLineGetTrailingWhitespaceWidth(originalLine) < Double(constrainedSize.width) {
coreTextLine = originalLine
} else {
var truncationTokenAttributes: [NSAttributedStringKey : AnyObject] = [:]
truncationTokenAttributes[NSAttributedStringKey.font] = font
truncationTokenAttributes[NSAttributedStringKey(rawValue: kCTForegroundColorFromContextAttributeName as String)] = true as NSNumber
let tokenString = "\u{2026}"
let truncatedTokenString = NSAttributedString(string: tokenString, attributes: truncationTokenAttributes)
let truncationToken = CTLineCreateWithAttributedString(truncatedTokenString)
coreTextLine = CTLineCreateTruncatedLine(originalLine, Double(constrainedSize.width), truncationType, truncationToken) ?? truncationToken
truncated = true
}
let lineWidth = min(constrainedSize.width, ceil(CGFloat(CTLineGetTypographicBounds(coreTextLine, nil, nil, nil) - CTLineGetTrailingWhitespaceWidth(coreTextLine))))
let lineFrame = CGRect(x: lineCutoutOffset, y: lineOriginY, width: lineWidth, height: fontLineHeight)
layoutSize.height += fontLineHeight + fontLineSpacing
layoutSize.width = max(layoutSize.width, lineWidth + lineAdditionalWidth)
var isRTL = false
let glyphRuns = CTLineGetGlyphRuns(coreTextLine) as NSArray
if glyphRuns.count != 0 {
let run = glyphRuns[0] as! CTRun
if CTRunGetStatus(run).contains(CTRunStatus.rightToLeft) {
isRTL = true
}
}
lines.append(TextNodeLine(line: coreTextLine, frame: lineFrame, range: NSMakeRange(lineRange.location, lineRange.length), isRTL: isRTL))
break
} else {
if lineCharacterCount > 0 {
if first {
first = false
} else {
layoutSize.height += fontLineSpacing
}
let lineRange = CFRangeMake(lastLineCharacterIndex, lineCharacterCount)
let coreTextLine = CTTypesetterCreateLineWithOffset(typesetter, lineRange, 100.0)
lastLineCharacterIndex += lineCharacterCount
let lineWidth = ceil(CGFloat(CTLineGetTypographicBounds(coreTextLine, nil, nil, nil) - CTLineGetTrailingWhitespaceWidth(coreTextLine)))
let lineFrame = CGRect(x: lineCutoutOffset, y: lineOriginY, width: lineWidth, height: fontLineHeight)
layoutSize.height += fontLineHeight
layoutSize.width = max(layoutSize.width, lineWidth + lineAdditionalWidth)
var isRTL = false
let glyphRuns = CTLineGetGlyphRuns(coreTextLine) as NSArray
if glyphRuns.count != 0 {
let run = glyphRuns[0] as! CTRun
if CTRunGetStatus(run).contains(CTRunStatus.rightToLeft) {
isRTL = true
}
}
lines.append(TextNodeLine(line: coreTextLine, frame: lineFrame, range: NSMakeRange(lineRange.location, lineRange.length), isRTL: isRTL))
} else {
if !lines.isEmpty {
layoutSize.height += fontLineSpacing
}
break
}
}
}
if !lines.isEmpty && bottomCutoutEnabled {
let proposedWidth = lines[lines.count - 1].frame.width + bottomCutoutSize.width
if proposedWidth > layoutSize.width {
if proposedWidth < constrainedSize.width {
layoutSize.width = proposedWidth
} else {
layoutSize.height += bottomCutoutSize.height
}
}
}
return TextNodeLayout(attributedString: attributedString, maximumNumberOfLines: maximumNumberOfLines, truncationType: truncationType, constrainedSize: constrainedSize, alignment: alignment, lineSpacing: lineSpacingFactor, cutout: cutout, insets: insets, size: CGSize(width: ceil(layoutSize.width) + insets.left + insets.right, height: ceil(layoutSize.height) + insets.top + insets.bottom), truncated: truncated, firstLineOffset: firstLineOffset, lines: lines, backgroundColor: backgroundColor)
} else {
return TextNodeLayout(attributedString: attributedString, maximumNumberOfLines: maximumNumberOfLines, truncationType: truncationType, constrainedSize: constrainedSize, alignment: alignment, lineSpacing: lineSpacingFactor, cutout: cutout, insets: insets, size: CGSize(), truncated: false, firstLineOffset: 0.0, lines: [], backgroundColor: backgroundColor)
}
}
override public func drawParameters(forAsyncLayer layer: _ASDisplayLayer) -> NSObjectProtocol? {
return self.cachedLayout
}
@objc override public class func draw(_ bounds: CGRect, withParameters parameters: Any?, isCancelled: () -> Bool, isRasterizing: Bool) {
if isCancelled() {
return
}
let context = UIGraphicsGetCurrentContext()!
context.setAllowsAntialiasing(true)
context.setAllowsFontSmoothing(false)
context.setShouldSmoothFonts(false)
context.setAllowsFontSubpixelPositioning(false)
context.setShouldSubpixelPositionFonts(false)
context.setAllowsFontSubpixelQuantization(true)
context.setShouldSubpixelQuantizeFonts(true)
if let layout = parameters as? TextNodeLayout {
if !isRasterizing || layout.backgroundColor != nil {
context.setBlendMode(.copy)
context.setFillColor((layout.backgroundColor ?? UIColor.clear).cgColor)
context.fill(bounds)
}
let textMatrix = context.textMatrix
let textPosition = context.textPosition
//CGContextSaveGState(context)
context.textMatrix = CGAffineTransform(scaleX: 1.0, y: -1.0)
//let clipRect = CGContextGetClipBoundingBox(context)
let alignment = layout.alignment
let offset = CGPoint(x: layout.insets.left, y: layout.insets.top)
for i in 0 ..< layout.lines.count {
let line = layout.lines[i]
var lineFrame = line.frame
lineFrame.origin.y += offset.y
if alignment == .center {
lineFrame.origin.x = offset.x + floor((bounds.size.width - lineFrame.width) / 2.0)
} else if alignment == .natural, line.isRTL {
lineFrame.origin.x = offset.x + floor(bounds.size.width - lineFrame.width)
lineFrame = displayLineFrame(frame: lineFrame, isRTL: line.isRTL, boundingRect: CGRect(origin: CGPoint(), size: bounds.size), cutout: layout.cutout)
}
context.textPosition = CGPoint(x: lineFrame.minX, y: lineFrame.minY)
CTLineDraw(line.line, context)
}
//CGContextRestoreGState(context)
context.textMatrix = textMatrix
context.textPosition = CGPoint(x: textPosition.x, y: textPosition.y)
}
context.setBlendMode(.normal)
}
public static func asyncLayout(_ maybeNode: TextNode?) -> (TextNodeLayoutArguments) -> (TextNodeLayout, () -> TextNode) {
let existingLayout: TextNodeLayout? = maybeNode?.cachedLayout
return { arguments in
let layout: TextNodeLayout
var updated = false
if let existingLayout = existingLayout, existingLayout.constrainedSize == arguments.constrainedSize && existingLayout.maximumNumberOfLines == arguments.maximumNumberOfLines && existingLayout.truncationType == arguments.truncationType && existingLayout.cutout == arguments.cutout && existingLayout.alignment == arguments.alignment && existingLayout.lineSpacing.isEqual(to: arguments.lineSpacing) {
let stringMatch: Bool
var colorMatch: Bool = true
if let backgroundColor = arguments.backgroundColor, let previousBackgroundColor = existingLayout.backgroundColor {
if !backgroundColor.isEqual(previousBackgroundColor) {
colorMatch = false
}
} else if (arguments.backgroundColor != nil) != (existingLayout.backgroundColor != nil) {
colorMatch = false
}
if !colorMatch {
stringMatch = false
} else if let existingString = existingLayout.attributedString, let string = arguments.attributedString {
stringMatch = existingString.isEqual(to: string)
} else if existingLayout.attributedString == nil && arguments.attributedString == nil {
stringMatch = true
} else {
stringMatch = false
}
if stringMatch {
layout = existingLayout
} else {
layout = TextNode.calculateLayout(attributedString: arguments.attributedString, maximumNumberOfLines: arguments.maximumNumberOfLines, truncationType: arguments.truncationType, backgroundColor: arguments.backgroundColor, constrainedSize: arguments.constrainedSize, alignment: arguments.alignment, lineSpacingFactor: arguments.lineSpacing, cutout: arguments.cutout, insets: arguments.insets)
updated = true
}
} else {
layout = TextNode.calculateLayout(attributedString: arguments.attributedString, maximumNumberOfLines: arguments.maximumNumberOfLines, truncationType: arguments.truncationType, backgroundColor: arguments.backgroundColor, constrainedSize: arguments.constrainedSize, alignment: arguments.alignment, lineSpacingFactor: arguments.lineSpacing, cutout: arguments.cutout, insets: arguments.insets)
updated = true
}
let node = maybeNode ?? TextNode()
return (layout, {
node.cachedLayout = layout
if updated {
if layout.size.width.isZero && layout.size.height.isZero {
node.contents = nil
}
node.setNeedsDisplay()
}
return node
})
}
}
}