Files
Mike Renoir b4b1b85d0a bug fixes
2023-10-26 10:24:46 +04:00

372 lines
16 KiB
Swift

//
// TextNode.swift
// TGUIKit
//
// Created by keepcoder on 10/09/16.
// Copyright © 2016 Telegram. All rights reserved.
//
import Cocoa
private let defaultFont:NSFont = .normal(.text)
private final class TextNodeLine {
let line: CTLine
let frame: CGRect
init(line: CTLine, frame: CGRect) {
self.line = line
self.frame = frame
}
}
public enum TextNodeCutoutPosition {
case TopLeft
case TopRight
}
public struct TextNodeCutout: Equatable {
public let position: TextNodeCutoutPosition
public let size: NSSize
public init(position: TextNodeCutoutPosition, size: NSSize) {
self.position = position
self.size = size
}
}
public func ==(lhs: TextNodeCutout, rhs: TextNodeCutout) -> Bool {
return lhs.position == rhs.position && lhs.size == rhs.size
}
public final class TextNodeLayout: NSObject {
fileprivate let attributedString: NSAttributedString?
fileprivate let maximumNumberOfLines: Int
fileprivate let truncationType: CTLineTruncationType
fileprivate let backgroundColor: NSColor?
fileprivate let constrainedSize: NSSize
fileprivate let cutout: TextNodeCutout?
fileprivate let alignment:NSTextAlignment
public let isPerfectSized: Bool
public let size: NSSize
fileprivate let lines: [TextNodeLine]
public var selected:Bool = false
fileprivate init(attributedString: NSAttributedString?, maximumNumberOfLines: Int, truncationType: CTLineTruncationType, constrainedSize: NSSize, cutout: TextNodeCutout?, size: NSSize, lines: [TextNodeLine], backgroundColor: NSColor?, alignment:NSTextAlignment = .left, isPerfectSized: Bool) {
self.attributedString = attributedString
self.maximumNumberOfLines = maximumNumberOfLines
self.truncationType = truncationType
self.constrainedSize = constrainedSize
self.cutout = cutout
self.size = size
self.lines = lines
self.backgroundColor = backgroundColor
self.alignment = alignment
self.isPerfectSized = isPerfectSized
}
public var numberOfLines: Int {
return self.lines.count
}
public var firstLineWidth: CGFloat {
if let first = self.lines.first {
return first.frame.width
} else {
return 0.0
}
}
var trailingLineWidth: CGFloat {
if let lastLine = self.lines.last {
return lastLine.frame.width
} else {
return 0.0
}
}
}
public class TextNode: NSObject {
private var currentLayout: TextNodeLayout?
public var backgroundColor:NSColor
public override init() {
self.backgroundColor = NSColor.red
super.init()
}
deinit {
var bp:Int = 0
bp += 1
}
private class func getlayout(attributedString: NSAttributedString?, maximumNumberOfLines: Int, truncationType: CTLineTruncationType, backgroundColor: NSColor?, constrainedSize: NSSize, cutout: TextNodeCutout?, selected:Bool, alignment:NSTextAlignment, lineSpacing: CGFloat? = nil) -> TextNodeLayout {
var attr = attributedString
let isPerfectSized = false
if let a = attr {
if (selected && a.length > 0) {
let c:NSMutableAttributedString = a.mutableCopy() as! NSMutableAttributedString
if let color = c.attribute(.selectedColor, at: 0, effectiveRange: nil) {
c.addAttribute(NSAttributedString.Key.foregroundColor, value: color, range: c.range)
}
attr = c
}
}
if let attributedString = attr {
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 {
font = defaultFont
}
} else {
font = defaultFont
}
let fontAscent = CTFontGetAscent(font)
let fontDescent = CTFontGetDescent(font)
let fontLineHeight = floor(fontAscent + fontDescent) + (lineSpacing ?? 0)
let fontLineSpacing = floor(fontLineHeight * 0.12)
var lines: [TextNodeLine] = []
var maybeTypesetter: CTTypesetter?
maybeTypesetter = CTTypesetterCreateWithAttributedString(attributedString as CFAttributedString)
if maybeTypesetter == nil {
return TextNodeLayout(attributedString: attributedString, maximumNumberOfLines: maximumNumberOfLines, truncationType: truncationType, constrainedSize: constrainedSize, cutout: cutout, size: NSSize(), lines: [], backgroundColor: backgroundColor, alignment:alignment, isPerfectSized: true)
}
let typesetter = maybeTypesetter!
var lastLineCharacterIndex: CFIndex = 0
var layoutSize = NSSize()
var cutoutEnabled = false
var cutoutMinY: CGFloat = 0.0
var cutoutMaxY: CGFloat = 0.0
var cutoutWidth: CGFloat = 0.0
var cutoutOffset: CGFloat = 0.0
if let cutout = cutout {
cutoutMinY = -fontLineSpacing
cutoutMaxY = cutout.size.height + fontLineSpacing
cutoutWidth = cutout.size.width
if case .TopLeft = cutout.position {
cutoutOffset = cutoutWidth
}
cutoutEnabled = true
}
var first = true
while true {
var lineConstrainedWidth = constrainedSize.width
var lineOriginY = floor(layoutSize.height + fontLineHeight - fontLineSpacing * 2.0)
if !first {
lineOriginY += fontLineSpacing
}
var lineCutoutOffset: CGFloat = 0.0
var lineAdditionalWidth: CGFloat = 0.0
if cutoutEnabled {
if lineOriginY < cutoutMaxY && lineOriginY + fontLineHeight > cutoutMinY {
lineConstrainedWidth = max(1.0, lineConstrainedWidth - cutoutWidth)
lineCutoutOffset = cutoutOffset
lineAdditionalWidth = cutoutWidth
}
}
let lineCharacterCount = CTTypesetterSuggestLineBreak(typesetter, lastLineCharacterIndex, Double(lineConstrainedWidth))
if maximumNumberOfLines != 0 && lines.count == maximumNumberOfLines - 1 && lineCharacterCount > 0 {
if first {
first = false
} else {
layoutSize.height += fontLineSpacing
}
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: lineCutoutOffset, y: lineOriginY, width: lineWidth, height: fontLineHeight)
layoutSize.height += fontLineHeight + fontLineSpacing
layoutSize.width = max(layoutSize.width, lineWidth + lineAdditionalWidth)
lines.append(TextNodeLine(line: coreTextLine, frame: lineFrame))
break
} else {
if lineCharacterCount > 0 {
if first {
first = false
} else {
layoutSize.height += fontLineSpacing
}
let coreTextLine = CTTypesetterCreateLineWithOffset(typesetter, CFRangeMake(lastLineCharacterIndex, lineCharacterCount), 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)
lines.append(TextNodeLine(line: coreTextLine, frame: lineFrame))
} else {
if !lines.isEmpty {
layoutSize.height += fontLineSpacing
}
break
}
}
}
return TextNodeLayout(attributedString: attributedString, maximumNumberOfLines: maximumNumberOfLines, truncationType: truncationType, constrainedSize: constrainedSize, cutout: cutout, size: NSSize(width: ceil(layoutSize.width), height: ceil(layoutSize.height)), lines: lines, backgroundColor: backgroundColor, alignment:alignment, isPerfectSized: isPerfectSized)
} else {
return TextNodeLayout(attributedString: attributedString, maximumNumberOfLines: maximumNumberOfLines, truncationType: truncationType, constrainedSize: constrainedSize, cutout: cutout, size: NSSize(), lines: [], backgroundColor: backgroundColor, alignment:alignment, isPerfectSized: isPerfectSized)
}
}
open func draw(_ dirtyRect: NSRect, in ctx: CGContext, backingScaleFactor: CGFloat, backgroundColor: NSColor) {
if !System.supportsTransparentFontDrawing {
if backingScaleFactor == 1.0 {
ctx.setFillColor(backgroundColor.cgColor)
ctx.fill(dirtyRect)
}
ctx.setAllowsAntialiasing(true)
ctx.setAllowsFontSmoothing(backingScaleFactor == 1.0)
ctx.setShouldSmoothFonts(backingScaleFactor == 1.0)
ctx.setAllowsFontSubpixelPositioning(true)
ctx.setShouldSubpixelPositionFonts(true)
} else {
ctx.setAllowsFontSubpixelPositioning(true)
ctx.setShouldSubpixelPositionFonts(true)
ctx.setAllowsAntialiasing(true)
ctx.setShouldAntialias(true)
ctx.setAllowsFontSmoothing(backingScaleFactor == 1.0)
ctx.setShouldSmoothFonts(backingScaleFactor == 1.0)
}
//let contextPtr = NSGraphicsContext.current()?.graphicsPort
let context:CGContext = ctx //unsafeBitCast(contextPtr, to: CGContext.self)
// ctx.setAllowsFontSmoothing(true)
// ctx.setAllowsAntialiasing(true)
// context.setAllowsAntialiasing(true)
// context.setShouldSmoothFonts(!System.isRetina)
// context.setAllowsFontSmoothing(!System.isRetina)
if #available(OSX 10.11, *) {
} else {
// context.setBlendMode(.hardLight)
}
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]
var penFlush:CGFloat = 0.0
if layout.alignment == .center {
penFlush = 0.5
}
let penOffset = CGFloat( CTLineGetPenOffsetForFlush(line.line, penFlush, Double(dirtyRect.width)))
context.textPosition = CGPoint(x: penOffset + dirtyRect.minX + line.frame.minX, y: line.frame.minY + dirtyRect.minY)
let glyphRuns = CTLineGetGlyphRuns(line.line) as NSArray
if glyphRuns.count != 0 {
for run in glyphRuns {
let run = run as! CTRun
let glyphCount = CTRunGetGlyphCount(run)
CTRunDraw(run, context, CFRangeMake(0, glyphCount))
}
}
}
context.textMatrix = textMatrix
context.textPosition = CGPoint(x: textPosition.x, y: textPosition.y)
}
// context.setBlendMode(.normal)
}
open class func layoutText(maybeNode:TextNode? = nil, _ attributedString: NSAttributedString?, _ backgroundColor: NSColor?, _ maximumNumberOfLines: Int, _ truncationType: CTLineTruncationType, _ constrainedSize: NSSize, _ cutout: TextNodeCutout?,_ selected:Bool, _ alignment:NSTextAlignment, _ lineSpacing: CGFloat? = nil) -> (TextNodeLayout, TextNode) {
let existingLayout: TextNodeLayout? = maybeNode?.currentLayout
let layout: TextNodeLayout
if let existingLayout = existingLayout, existingLayout.constrainedSize == constrainedSize && existingLayout.maximumNumberOfLines == maximumNumberOfLines && existingLayout.truncationType == truncationType && existingLayout.cutout == cutout && existingLayout.selected == selected && existingLayout.alignment == alignment {
let stringMatch: Bool
if let existingString = existingLayout.attributedString, let string = attributedString {
stringMatch = existingString.isEqual(to: string)
} else if existingLayout.attributedString == nil && attributedString == nil {
stringMatch = true
} else {
stringMatch = false
}
if stringMatch {
layout = existingLayout
} else {
layout = TextNode.getlayout(attributedString: attributedString, maximumNumberOfLines: maximumNumberOfLines, truncationType: truncationType, backgroundColor: backgroundColor, constrainedSize: constrainedSize, cutout: cutout,selected:selected, alignment: alignment, lineSpacing: lineSpacing)
}
} else {
layout = TextNode.getlayout(attributedString: attributedString, maximumNumberOfLines: maximumNumberOfLines, truncationType: truncationType, backgroundColor: backgroundColor, constrainedSize: constrainedSize, cutout: cutout,selected:selected, alignment: alignment, lineSpacing: lineSpacing)
}
let node = maybeNode ?? TextNode()
node.currentLayout = layout
return (layout, node)
}
}