mirror of https://github.com/oxen-io/session-ios
				
				
				
			
			You cannot select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
	
	
		
			162 lines
		
	
	
		
			6.9 KiB
		
	
	
	
		
			Swift
		
	
		
		
			
		
	
	
			162 lines
		
	
	
		
			6.9 KiB
		
	
	
	
		
			Swift
		
	
| 
											3 years ago
										 | // 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..<lines.count { | ||
|  |             let line = lines[lineIndex] | ||
|  |             let runs: [CTRun] = line.ctruns | ||
|  |             var ascent: CGFloat = 0 | ||
|  |             var descent: CGFloat = 0 | ||
|  |             var leading: CGFloat = 0 | ||
|  |             let lineWidth = CGFloat(CTLineGetTypographicBounds(line, &ascent, &descent, &leading)) | ||
|  |              | ||
|  |             for run in runs { | ||
|  |                 let attributes: NSDictionary = CTRunGetAttributes(run) | ||
|  |                  | ||
|  |                 guard let mentionBackgroundColor: UIColor = attributes.value(forKey: NSAttributedString.Key.currentUserMentionBackgroundColor.rawValue) as? UIColor else { | ||
|  |                     continue | ||
|  |                 } | ||
|  |                  | ||
|  |                 let cornerRadius: CGFloat = (attributes | ||
|  |                     .value(forKey: NSAttributedString.Key.currentUserMentionBackgroundCornerRadius.rawValue) as? CGFloat) | ||
|  |                     .defaulting(to: 0) | ||
|  |                 let padding: CGFloat = (attributes | ||
|  |                     .value(forKey: NSAttributedString.Key.currentUserMentionBackgroundPadding.rawValue) as? CGFloat) | ||
|  |                     .defaulting(to: 0) | ||
|  |                  | ||
|  |                 let range = CTRunGetStringRange(run) | ||
|  |                 var runBounds: CGRect = .zero | ||
|  |                 var runAscent: CGFloat = 0 | ||
|  |                 var runDescent: CGFloat = 0 | ||
|  |                 runBounds.size.width = CGFloat(CTRunGetTypographicBounds(run, CFRangeMake(0, 0), &runAscent, &runDescent, nil) + (padding * 2)) | ||
|  |                 runBounds.size.height = (runAscent + runDescent + (padding * 2)) | ||
|  | 
 | ||
|  |                 let xOffset: CGFloat = { | ||
|  |                     switch CTRunGetStatus(run) { | ||
|  |                         case .rightToLeft: | ||
|  |                             return CTLineGetOffsetForStringIndex(line, range.location + range.length, nil) | ||
|  |                              | ||
|  |                         default: | ||
|  |                             return CTLineGetOffsetForStringIndex(line, range.location, nil) | ||
|  |                     } | ||
|  |                 }() | ||
|  |                  | ||
|  |                 // HACK: This `extraYOffset` value is a hack to resolve a weird issue where the positioning | ||
|  |                 // seems to be slightly off every additional line of text we add (it doesn't seem to be related | ||
|  |                 // to line spacing or anything, more related to the bold mention text being positioned slightly | ||
|  |                 // differently from the non-bold text) | ||
|  |                 let extraYOffset: CGFloat = (CGFloat(lineIndex) * (runDescent / 12)) | ||
|  |                  | ||
|  |                 // Note: Changes to `origin.y` need to be inverted since the context has been flipped | ||
|  |                 runBounds.origin.x = origins[lineIndex].x + rect.origin.x + self.maxPadding + xOffset - padding | ||
|  |                 runBounds.origin.y = ( | ||
|  |                     origins[lineIndex].y + rect.origin.y + | ||
|  |                     self.maxPadding - | ||
|  |                     padding - | ||
|  |                     runDescent - | ||
|  |                     extraYOffset | ||
|  |                 ) | ||
|  |                  | ||
|  |                 // We don't want to draw too far to the right | ||
|  |                 runBounds.size.width = (runBounds.width > 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]) ?? []) | ||
|  |     } | ||
|  | } |