forked from TelegramMessenger/Telegram-iOS
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathTextUtils.swift
194 lines (146 loc) · 7.53 KB
/
TextUtils.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
//
// TextUtils.swift
// GraphCore
//
// Created by Mikhail Filimonov on 26.02.2020.
// Copyright © 2020 Telegram. All rights reserved.
//
import Foundation
#if os(macOS)
import Cocoa
#else
import UIKit
#endif
#if os(iOS)
typealias NSFont = UIFont
#endif
private let defaultFont:NSFont = NSFont.systemFont(ofSize: 14)
extension NSAttributedString {
var size: CGSize {
return textSize(with: self.string, font: self.attribute(.font, at: 0, effectiveRange: nil) as? NSFont ?? defaultFont)
}
}
func textSize(with string: String, font: NSFont) -> CGSize {
let attributedString:NSAttributedString = NSAttributedString(string: string, attributes: [.font : font])
let layout = LabelNode.layoutText(attributedString, CGSize(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude))
var size:CGSize = layout.0.size
size.width = ceil(size.width)
size.height = ceil(size.height)
return size
}
private final class LabelNodeLine {
let line: CTLine
let frame: CGRect
init(line: CTLine, frame: CGRect) {
self.line = line
self.frame = frame
}
}
public final class LabelNodeLayout: NSObject {
fileprivate let attributedString: NSAttributedString?
fileprivate let truncationType: CTLineTruncationType
fileprivate let constrainedSize: CGSize
fileprivate let lines: [LabelNodeLine]
let size: CGSize
fileprivate init(attributedString: NSAttributedString?, truncationType: CTLineTruncationType, constrainedSize: CGSize, size: CGSize, lines: [LabelNodeLine]) {
self.attributedString = attributedString
self.truncationType = truncationType
self.constrainedSize = constrainedSize
self.size = size
self.lines = lines
}
var numberOfLines: Int {
return self.lines.count
}
var trailingLineWidth: CGFloat {
if let lastLine = self.lines.last {
return lastLine.frame.width
} else {
return 0.0
}
}
}
class LabelNode: NSObject {
private var currentLayout: LabelNodeLayout?
private class func getlayout(attributedString: NSAttributedString?, truncationType: CTLineTruncationType, constrainedSize: CGSize) -> LabelNodeLayout {
if let attributedString = attributedString {
let font: CTFont
if attributedString.length != 0 {
if let stringFont = attributedString.attribute(NSAttributedString.Key(kCTFontAttributeName as String), at: 0, effectiveRange: nil) {
font = stringFont as! CTFont
} else if let f = attributedString.attribute(.font, at: 0, effectiveRange: nil) as? NSFont {
font = f
} else {
font = defaultFont
}
} else {
font = defaultFont
}
let fontAscent = CTFontGetAscent(font)
let fontDescent = CTFontGetDescent(font)
let fontLineHeight = floor(fontAscent + fontDescent)
let fontLineSpacing = floor(fontLineHeight * 0.12)
var lines: [LabelNodeLine] = []
var maybeTypesetter: CTTypesetter?
maybeTypesetter = CTTypesetterCreateWithAttributedString(attributedString as CFAttributedString)
if maybeTypesetter == nil {
return LabelNodeLayout(attributedString: attributedString, truncationType: truncationType, constrainedSize: constrainedSize, size: CGSize(), lines: [])
}
let typesetter = maybeTypesetter!
var layoutSize = CGSize()
let lineOriginY = floor(layoutSize.height + fontLineHeight - fontLineSpacing * 2.0)
let lastLineCharacterIndex: CFIndex = 0
let coreTextLine: CTLine
let originalLine = CTTypesetterCreateLineWithOffset(typesetter, CFRange(location: lastLineCharacterIndex, length: attributedString.length - lastLineCharacterIndex), 0.0)
if CTLineGetTypographicBounds(originalLine, nil, nil, nil) - CTLineGetTrailingWhitespaceWidth(originalLine) < Double(constrainedSize.width) {
coreTextLine = originalLine
} else {
var truncationTokenAttributes: [NSAttributedString.Key : Any] = [:]
truncationTokenAttributes[NSAttributedString.Key(kCTFontAttributeName as String)] = font
truncationTokenAttributes[NSAttributedString.Key(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
}
let lineWidth = ceil(CGFloat(CTLineGetTypographicBounds(coreTextLine, nil, nil, nil) - CTLineGetTrailingWhitespaceWidth(coreTextLine)))
let lineFrame = CGRect(x: 0, y: lineOriginY, width: lineWidth, height: fontLineHeight)
layoutSize.height += fontLineHeight + fontLineSpacing
layoutSize.width = max(layoutSize.width, lineWidth)
lines.append(LabelNodeLine(line: coreTextLine, frame: lineFrame))
return LabelNodeLayout(attributedString: attributedString, truncationType: truncationType, constrainedSize: constrainedSize, size: CGSize(width: ceil(layoutSize.width), height: ceil(layoutSize.height)), lines: lines)
} else {
return LabelNodeLayout(attributedString: attributedString, truncationType: truncationType, constrainedSize: constrainedSize, size: CGSize(), lines: [])
}
}
func draw(_ dirtyRect: CGRect, in ctx: CGContext, backingScaleFactor: CGFloat) {
ctx.saveGState()
ctx.setAllowsFontSubpixelPositioning(true)
ctx.setShouldSubpixelPositionFonts(true)
ctx.setAllowsAntialiasing(true)
ctx.setShouldAntialias(true)
ctx.setAllowsFontSmoothing(backingScaleFactor == 1.0)
ctx.setShouldSmoothFonts(backingScaleFactor == 1.0)
let context:CGContext = ctx
if let layout = self.currentLayout {
let textMatrix = context.textMatrix
let textPosition = context.textPosition
context.textMatrix = CGAffineTransform(scaleX: 1.0, y: -1.0)
for i in 0 ..< layout.lines.count {
let line = layout.lines[i]
context.textPosition = CGPoint(x: dirtyRect.minX, y: line.frame.origin.y + dirtyRect.minY)
CTLineDraw(line.line, context)
}
context.textMatrix = textMatrix
context.textPosition = CGPoint(x: textPosition.x, y: textPosition.y)
}
ctx.restoreGState()
}
class func layoutText(_ attributedString: NSAttributedString?, _ constrainedSize: CGSize, _ truncationType: CTLineTruncationType = .end) -> (LabelNodeLayout, LabelNode) {
let layout: LabelNodeLayout
layout = LabelNode.getlayout(attributedString: attributedString, truncationType: truncationType, constrainedSize: constrainedSize)
let node = LabelNode()
node.currentLayout = layout
return (layout, node)
}
}