// // 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) } }