Swiftgram/submodules/Display/Source/LinkHighlightingNode.swift

272 lines
13 KiB
Swift

import Foundation
import UIKit
import AsyncDisplayKit
private enum CornerType {
case topLeft
case topRight
case bottomLeft
case bottomRight
}
private func drawFullCorner(context: CGContext, color: UIColor, at point: CGPoint, type: CornerType, radius: CGFloat) {
if radius.isZero {
return
}
context.setFillColor(color.cgColor)
switch type {
case .topLeft:
context.clear(CGRect(origin: point, size: CGSize(width: radius, height: radius)))
context.fillEllipse(in: CGRect(origin: point, size: CGSize(width: radius * 2.0, height: radius * 2.0)))
case .topRight:
context.clear(CGRect(origin: CGPoint(x: point.x - radius, y: point.y), size: CGSize(width: radius, height: radius)))
context.fillEllipse(in: CGRect(origin: CGPoint(x: point.x - radius * 2.0, y: point.y), size: CGSize(width: radius * 2.0, height: radius * 2.0)))
case .bottomLeft:
context.clear(CGRect(origin: CGPoint(x: point.x, y: point.y - radius), size: CGSize(width: radius, height: radius)))
context.fillEllipse(in: CGRect(origin: CGPoint(x: point.x, y: point.y - radius * 2.0), size: CGSize(width: radius * 2.0, height: radius * 2.0)))
case .bottomRight:
context.clear(CGRect(origin: CGPoint(x: point.x - radius, y: point.y - radius), size: CGSize(width: radius, height: radius)))
context.fillEllipse(in: CGRect(origin: CGPoint(x: point.x - radius * 2.0, y: point.y - radius * 2.0), size: CGSize(width: radius * 2.0, height: radius * 2.0)))
}
}
private func drawConnectingCorner(context: CGContext, color: UIColor, at point: CGPoint, type: CornerType, radius: CGFloat) {
context.setFillColor(color.cgColor)
switch type {
case .topLeft:
context.fill(CGRect(origin: CGPoint(x: point.x - radius, y: point.y), size: CGSize(width: radius, height: radius)))
context.setFillColor(UIColor.clear.cgColor)
context.fillEllipse(in: CGRect(origin: CGPoint(x: point.x - radius * 2.0, y: point.y), size: CGSize(width: radius * 2.0, height: radius * 2.0)))
case .topRight:
context.fill(CGRect(origin: CGPoint(x: point.x, y: point.y), size: CGSize(width: radius, height: radius)))
context.setFillColor(UIColor.clear.cgColor)
context.fillEllipse(in: CGRect(origin: CGPoint(x: point.x, y: point.y), size: CGSize(width: radius * 2.0, height: radius * 2.0)))
case .bottomLeft:
context.fill(CGRect(origin: CGPoint(x: point.x - radius, y: point.y - radius), size: CGSize(width: radius, height: radius)))
context.setFillColor(UIColor.clear.cgColor)
context.fillEllipse(in: CGRect(origin: CGPoint(x: point.x - radius * 2.0, y: point.y - radius * 2.0), size: CGSize(width: radius * 2.0, height: radius * 2.0)))
case .bottomRight:
context.fill(CGRect(origin: CGPoint(x: point.x, y: point.y - radius), size: CGSize(width: radius, height: radius)))
context.setFillColor(UIColor.clear.cgColor)
context.fillEllipse(in: CGRect(origin: CGPoint(x: point.x, y: point.y - radius * 2.0), size: CGSize(width: radius * 2.0, height: radius * 2.0)))
}
}
private func generateRectsImage(color: UIColor, rects: [CGRect], inset: CGFloat, outerRadius: CGFloat, innerRadius: CGFloat) -> (CGPoint, UIImage?) {
if rects.isEmpty {
return (CGPoint(), nil)
}
var topLeft = rects[0].origin
var bottomRight = CGPoint(x: rects[0].maxX, y: rects[0].maxY)
for i in 1 ..< rects.count {
topLeft.x = min(topLeft.x, rects[i].origin.x)
topLeft.y = min(topLeft.y, rects[i].origin.y)
bottomRight.x = max(bottomRight.x, rects[i].maxX)
bottomRight.y = max(bottomRight.y, rects[i].maxY)
}
topLeft.x -= inset
topLeft.y -= inset
bottomRight.x += inset * 2.0
bottomRight.y += inset * 2.0
return (topLeft, generateImage(CGSize(width: bottomRight.x - topLeft.x, height: bottomRight.y - topLeft.y), rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setFillColor(color.cgColor)
context.setBlendMode(.copy)
/*context.move(to: CGPoint(x: rects[0].midX, y: rects[0].minY))
for i in 0 ..< rects.count {
let rect = rects[i].insetBy(dx: -inset, dy: -inset).offsetBy(dx: -topLeft.x, dy: -topLeft.y)
var next: CGRect?
if i + 1 < rects.count {
next = rects[i + 1].insetBy(dx: -inset, dy: -inset).offsetBy(dx: -topLeft.x, dy: -topLeft.y)
}
if let next = next {
} else {
}
}*/
for i in 0 ..< rects.count {
let rect = rects[i].insetBy(dx: -inset, dy: -inset)
context.fill(rect.offsetBy(dx: -topLeft.x, dy: -topLeft.y))
}
for i in 0 ..< rects.count {
let rect = rects[i].insetBy(dx: -inset, dy: -inset).offsetBy(dx: -topLeft.x, dy: -topLeft.y)
var previous: CGRect?
if i != 0 {
previous = rects[i - 1].insetBy(dx: -inset, dy: -inset).offsetBy(dx: -topLeft.x, dy: -topLeft.y)
}
var next: CGRect?
if i != rects.count - 1 {
next = rects[i + 1].insetBy(dx: -inset, dy: -inset).offsetBy(dx: -topLeft.x, dy: -topLeft.y)
}
if let previous = previous {
if previous.contains(rect.topLeft) {
if abs(rect.topLeft.x - previous.minX) >= innerRadius {
var radius = innerRadius
if let next = next {
radius = min(radius, floor((next.minY - previous.maxY) / 2.0))
}
drawConnectingCorner(context: context, color: color, at: CGPoint(x: rect.topLeft.x, y: previous.maxY), type: .topLeft, radius: radius)
}
} else {
drawFullCorner(context: context, color: color, at: rect.topLeft, type: .topLeft, radius: outerRadius)
}
if previous.maxY >= rect.minY && (rect.topRight.x >= previous.minX && rect.topRight.x <= previous.maxX) {
let radius = min(innerRadius, max(0.0, floor((previous.maxX - rect.topRight.x) / 2.0)))
drawConnectingCorner(context: context, color: color, at: CGPoint(x: rect.topRight.x, y: previous.maxY), type: .topRight, radius: radius)
} else if previous.maxY >= rect.minY && rect.topRight.x - previous.maxX < outerRadius * 2.0 {
let radius = max(0.0, floor((rect.bottomRight.x - previous.maxX) * 0.5))
drawFullCorner(context: context, color: color, at: rect.topRight, type: .topRight, radius: radius)
} else {
drawFullCorner(context: context, color: color, at: rect.topRight, type: .topRight, radius: outerRadius)
}
} else {
drawFullCorner(context: context, color: color, at: rect.topLeft, type: .topLeft, radius: outerRadius)
drawFullCorner(context: context, color: color, at: rect.topRight, type: .topRight, radius: outerRadius)
}
if let next = next {
if next.contains(rect.bottomLeft) {
if abs(rect.bottomRight.x - next.maxX) >= innerRadius {
var radius = innerRadius
if let previous = previous {
radius = min(radius, floor((next.minY - previous.maxY) / 2.0))
}
drawConnectingCorner(context: context, color: color, at: CGPoint(x: rect.bottomLeft.x, y: next.minY), type: .bottomLeft, radius: radius)
}
} else {
drawFullCorner(context: context, color: color, at: rect.bottomLeft, type: .bottomLeft, radius: outerRadius)
}
if next.minY <= rect.maxY && (rect.bottomRight.x >= next.minX && rect.bottomRight.x <= next.maxX) {
if abs(rect.bottomRight.x - next.maxX) >= innerRadius {
var radius = innerRadius
if let previous = previous {
radius = min(radius, floor((next.minY - previous.maxY) / 2.0))
}
drawFullCorner(context: context, color: color, at: rect.bottomRight, type: .bottomRight, radius: radius)
} else {
drawFullCorner(context: context, color: color, at: rect.bottomRight, type: .bottomRight, radius: outerRadius)
}
} else if next.minY <= rect.maxY && rect.bottomRight.x - next.maxX < outerRadius * 2.0 {
let radius = max(0.0, floor((rect.bottomRight.x - next.maxX) * 0.5))
drawFullCorner(context: context, color: color, at: rect.bottomRight, type: .bottomRight, radius: radius)
} else {
drawFullCorner(context: context, color: color, at: rect.bottomRight, type: .bottomRight, radius: outerRadius)
}
} else {
drawFullCorner(context: context, color: color, at: rect.bottomLeft, type: .bottomLeft, radius: outerRadius)
drawFullCorner(context: context, color: color, at: rect.bottomRight, type: .bottomRight, radius: outerRadius)
}
}
}))
}
public final class LinkHighlightingNode: ASDisplayNode {
public private(set) var rects: [CGRect] = []
public let imageNode: ASImageNode
public var innerRadius: CGFloat = 4.0
public var outerRadius: CGFloat = 4.0
public var inset: CGFloat = 2.0
private var _color: UIColor
public var color: UIColor {
get {
return _color
} set(value) {
self._color = value
if !self.rects.isEmpty {
self.updateImage()
}
}
}
public init(color: UIColor) {
self._color = color
self.imageNode = ASImageNode()
self.imageNode.isUserInteractionEnabled = false
self.imageNode.displaysAsynchronously = false
super.init()
self.addSubnode(self.imageNode)
}
public func updateRects(_ rects: [CGRect]) {
if self.rects != rects {
self.rects = rects
self.updateImage()
}
}
private func updateImage() {
if self.rects.isEmpty {
self.imageNode.image = nil
}
let (offset, image) = generateRectsImage(color: self.color, rects: self.rects, inset: self.inset, outerRadius: self.outerRadius, innerRadius: self.innerRadius)
if let image = image {
self.imageNode.image = image
self.imageNode.frame = CGRect(origin: offset, size: image.size)
}
}
public static func generateImage(color: UIColor, inset: CGFloat, innerRadius: CGFloat, outerRadius: CGFloat, rects: [CGRect]) -> (CGPoint, UIImage)? {
if rects.isEmpty {
return nil
}
let (offset, image) = generateRectsImage(color: color, rects: rects, inset: inset, outerRadius: outerRadius, innerRadius: innerRadius)
if let image = image {
return (offset, image)
} else {
return nil
}
}
public func asyncLayout() -> (UIColor, [CGRect], CGFloat, CGFloat, CGFloat) -> () -> Void {
let currentRects = self.rects
let currentColor = self._color
let currentInnerRadius = self.innerRadius
let currentOuterRadius = self.outerRadius
let currentInset = self.inset
return { [weak self] color, rects, innerRadius, outerRadius, inset in
var updatedImage: (CGPoint, UIImage?)?
if currentRects != rects || !currentColor.isEqual(color) || currentInnerRadius != innerRadius || currentOuterRadius != outerRadius || currentInset != inset {
updatedImage = generateRectsImage(color: color, rects: rects, inset: inset, outerRadius: outerRadius, innerRadius: innerRadius)
}
return {
if let strongSelf = self {
strongSelf._color = color
strongSelf.rects = rects
strongSelf.innerRadius = innerRadius
strongSelf.outerRadius = outerRadius
strongSelf.inset = inset
if let (offset, maybeImage) = updatedImage, let image = maybeImage {
strongSelf.imageNode.image = image
strongSelf.imageNode.frame = CGRect(origin: offset, size: image.size)
}
}
}
}
}
}