// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. import UIKit public extension NSAttributedString.Key { static let currentUserMentionBackgroundColor: NSAttributedString.Key = NSAttributedString.Key(rawValue: "currentUserMentionBackgroundColor") static let currentUserMentionBackgroundCornerRadius: NSAttributedString.Key = NSAttributedString.Key(rawValue: "currentUserMentionBackgroundCornerRadius") static let currentUserMentionBackgroundPadding: NSAttributedString.Key = NSAttributedString.Key(rawValue: "currentUserMentionBackgroundPadding") } class HighlightMentionBackgroundView: UIView { var maxPadding: CGFloat = 0 init() { super.init(frame: .zero) self.isOpaque = false self.layer.zPosition = -1 } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } // MARK: - Functions public func calculateMaxPadding(for attributedText: NSAttributedString) -> CGFloat { var allMentionRadii: [CGFloat?] = [] let path: CGMutablePath = CGMutablePath() path.addRect(CGRect( x: 0, y: 0, width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude )) let framesetter = CTFramesetterCreateWithAttributedString(attributedText as CFAttributedString) let frame: CTFrame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, attributedText.length), path, nil) let lines: [CTLine] = frame.lines lines.forEach { line in let runs: [CTRun] = line.ctruns runs.forEach { run in let attributes: NSDictionary = CTRunGetAttributes(run) allMentionRadii.append( attributes .value(forKey: NSAttributedString.Key.currentUserMentionBackgroundPadding.rawValue) as? CGFloat ) } } return allMentionRadii .compactMap { $0 } .max() .defaulting(to: 0) } // MARK: - Drawing override func draw(_ rect: CGRect) { guard let superview: UITextView = (self.superview as? UITextView), let context = UIGraphicsGetCurrentContext() else { return } // Need to invery the Y axis because iOS likes to render from the bottom left instead of the top left context.textMatrix = .identity context.translateBy(x: 0, y: bounds.size.height) context.scaleBy(x: 1.0, y: -1.0) // Note: Calculations MUST happen based on the 'superview' size as this class has extra padding which // can result in calculations being off let path = CGMutablePath() let size = superview.sizeThatFits(CGSize(width: superview.bounds.width, height: .greatestFiniteMagnitude)) path.addRect(CGRect(x: 0, y: 0, width: size.width, height: size.height), transform: .identity) let framesetter = CTFramesetterCreateWithAttributedString(superview.attributedText as CFAttributedString) let frame: CTFrame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, superview.attributedText.length), path, nil) let lines: [CTLine] = frame.lines var origins = [CGPoint](repeating: .zero, count: lines.count) CTFrameGetLineOrigins(frame, CFRangeMake(0, 0), &origins) for lineIndex in 0.. lineWidth ? lineWidth : runBounds.width) let path = UIBezierPath(roundedRect: runBounds, cornerRadius: cornerRadius) mentionBackgroundColor.setFill() path.fill() } } } } extension CTFrame { var lines: [CTLine] { return ((CTFrameGetLines(self) as [AnyObject] as? [CTLine]) ?? []) } } extension CTLine { var ctruns: [CTRun] { return ((CTLineGetGlyphRuns(self) as [AnyObject] as? [CTRun]) ?? []) } }