mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
195 lines
7.5 KiB
Swift
195 lines
7.5 KiB
Swift
//
|
|
// 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)
|
|
}
|
|
}
|