2023-01-08 01:37:02 +04:00

1072 lines
47 KiB
Swift

import Foundation
import UIKit
import AsyncDisplayKit
import Display
import TelegramCore
import SwiftSignalKit
import TelegramPresentationData
import AnimationUI
import AppBundle
import AccountContext
import Emoji
import Accelerate
private let deletedIcon = UIImage(bundleImageName: "Avatar/DeletedIcon")?.precomposed()
private let phoneIcon = generateTintedImage(image: UIImage(bundleImageName: "Avatar/PhoneIcon"), color: .white)
public let savedMessagesIcon = generateTintedImage(image: UIImage(bundleImageName: "Avatar/SavedMessagesIcon"), color: .white)
private let archivedChatsIcon = UIImage(bundleImageName: "Avatar/ArchiveAvatarIcon")?.precomposed()
private let repliesIcon = generateTintedImage(image: UIImage(bundleImageName: "Avatar/RepliesMessagesIcon"), color: .white)
public func avatarPlaceholderFont(size: CGFloat) -> UIFont {
return Font.with(size: size, design: .round, weight: .bold)
}
public enum AvatarNodeClipStyle {
case none
case round
case roundedRect
}
private class AvatarNodeParameters: NSObject {
let theme: PresentationTheme?
let accountPeerId: EnginePeer.Id?
let peerId: EnginePeer.Id?
let colors: [UIColor]
let letters: [String]
let font: UIFont
let icon: AvatarNodeIcon
let explicitColorIndex: Int?
let hasImage: Bool
let clipStyle: AvatarNodeClipStyle
init(theme: PresentationTheme?, accountPeerId: EnginePeer.Id?, peerId: EnginePeer.Id?, colors: [UIColor], letters: [String], font: UIFont, icon: AvatarNodeIcon, explicitColorIndex: Int?, hasImage: Bool, clipStyle: AvatarNodeClipStyle) {
self.theme = theme
self.accountPeerId = accountPeerId
self.peerId = peerId
self.colors = colors
self.letters = letters
self.font = font
self.icon = icon
self.explicitColorIndex = explicitColorIndex
self.hasImage = hasImage
self.clipStyle = clipStyle
super.init()
}
func withUpdatedHasImage(_ hasImage: Bool) -> AvatarNodeParameters {
return AvatarNodeParameters(theme: self.theme, accountPeerId: self.accountPeerId, peerId: self.peerId, colors: self.colors, letters: self.letters, font: self.font, icon: self.icon, explicitColorIndex: self.explicitColorIndex, hasImage: hasImage, clipStyle: self.clipStyle)
}
}
private let grayscaleColors: [UIColor] = [
UIColor(rgb: 0xb1b1b1), UIColor(rgb: 0xcdcdcd)
]
private let savedMessagesColors: [UIColor] = [
UIColor(rgb: 0x2a9ef1), UIColor(rgb: 0x72d5fd)
]
private func calculateColors(explicitColorIndex: Int?, peerId: EnginePeer.Id?, icon: AvatarNodeIcon, theme: PresentationTheme?) -> [UIColor] {
let colorIndex: Int
if let explicitColorIndex = explicitColorIndex {
colorIndex = explicitColorIndex
} else {
if let peerId {
if peerId.namespace == .max {
colorIndex = -1
} else {
colorIndex = abs(Int(clamping: peerId.id._internalGetInt64Value()))
}
} else {
colorIndex = -1
}
}
let colors: [UIColor]
if icon != .none {
if case .deletedIcon = icon {
colors = grayscaleColors
} else if case .phoneIcon = icon {
colors = grayscaleColors
} else if case .savedMessagesIcon = icon {
colors = savedMessagesColors
} else if case .repliesIcon = icon {
colors = savedMessagesColors
} else if case .editAvatarIcon = icon, let theme {
colors = [theme.list.itemAccentColor.withAlphaComponent(0.1), theme.list.itemAccentColor.withAlphaComponent(0.1)]
} else if case let .archivedChatsIcon(hiddenByDefault) = icon, let theme = theme {
let backgroundColors: (UIColor, UIColor)
if hiddenByDefault {
backgroundColors = theme.chatList.unpinnedArchiveAvatarColor.backgroundColors.colors
} else {
backgroundColors = theme.chatList.pinnedArchiveAvatarColor.backgroundColors.colors
}
colors = [backgroundColors.1, backgroundColors.0]
} else {
colors = grayscaleColors
}
} else if colorIndex == -1 {
if let theme {
let backgroundColors = theme.chatList.unpinnedArchiveAvatarColor.backgroundColors.colors
colors = [backgroundColors.1, backgroundColors.0]
} else {
colors = grayscaleColors
}
} else {
colors = AvatarNode.gradientColors[colorIndex % AvatarNode.gradientColors.count]
}
return colors
}
public enum AvatarNodeExplicitIcon {
case phone
}
private enum AvatarNodeState: Equatable {
case empty
case peerAvatar(EnginePeer.Id, [String], TelegramMediaImageRepresentation?, AvatarNodeClipStyle)
case custom(letter: [String], explicitColorIndex: Int?, explicitIcon: AvatarNodeExplicitIcon?)
}
private func ==(lhs: AvatarNodeState, rhs: AvatarNodeState) -> Bool {
switch (lhs, rhs) {
case (.empty, .empty):
return true
case let (.peerAvatar(lhsPeerId, lhsLetters, lhsPhotoRepresentations, lhsClipStyle), .peerAvatar(rhsPeerId, rhsLetters, rhsPhotoRepresentations, rhsClipStyle)):
return lhsPeerId == rhsPeerId && lhsLetters == rhsLetters && lhsPhotoRepresentations == rhsPhotoRepresentations && lhsClipStyle == rhsClipStyle
case let (.custom(lhsLetters, lhsIndex, lhsIcon), .custom(rhsLetters, rhsIndex, rhsIcon)):
return lhsLetters == rhsLetters && lhsIndex == rhsIndex && lhsIcon == rhsIcon
default:
return false
}
}
private enum AvatarNodeIcon: Equatable {
case none
case savedMessagesIcon
case repliesIcon
case archivedChatsIcon(hiddenByDefault: Bool)
case editAvatarIcon
case deletedIcon
case phoneIcon
}
public enum AvatarNodeImageOverride: Equatable {
case none
case image(TelegramMediaImageRepresentation)
case savedMessagesIcon
case repliesIcon
case archivedChatsIcon(hiddenByDefault: Bool)
case editAvatarIcon(forceNone: Bool)
case deletedIcon
case phoneIcon
}
public enum AvatarNodeColorOverride {
case blue
}
public final class AvatarEditOverlayNode: ASDisplayNode {
override public init() {
super.init()
self.isOpaque = false
self.displaysAsynchronously = true
}
@objc override public class func draw(_ bounds: CGRect, withParameters parameters: Any?, isCancelled: () -> Bool, isRasterizing: Bool) {
assertNotOnMainThread()
let context = UIGraphicsGetCurrentContext()!
if !isRasterizing {
context.setBlendMode(.copy)
context.setFillColor(UIColor.clear.cgColor)
context.fill(bounds)
}
context.beginPath()
context.addEllipse(in: CGRect(x: 0.0, y: 0.0, width: bounds.size.width, height:
bounds.size.height))
context.clip()
context.setFillColor(UIColor(rgb: 0x000000, alpha: 0.4).cgColor)
context.fill(bounds)
context.translateBy(x: bounds.size.width / 2.0, y: bounds.size.height / 2.0)
context.scaleBy(x: 1.0, y: -1.0)
context.translateBy(x: -bounds.size.width / 2.0, y: -bounds.size.height / 2.0)
context.setBlendMode(.normal)
if bounds.width > 90.0 {
if let editAvatarIcon = generateTintedImage(image: UIImage(bundleImageName: "Avatar/EditAvatarIconLarge"), color: .white) {
context.draw(editAvatarIcon.cgImage!, in: CGRect(origin: CGPoint(x: floor((bounds.size.width - editAvatarIcon.size.width) / 2.0) + 0.5, y: floor((bounds.size.height - editAvatarIcon.size.height) / 2.0) + 1.0), size: editAvatarIcon.size))
}
} else {
if let editAvatarIcon = generateTintedImage(image: UIImage(bundleImageName: "Avatar/EditAvatarIcon"), color: .white) {
context.draw(editAvatarIcon.cgImage!, in: CGRect(origin: CGPoint(x: floor((bounds.size.width - editAvatarIcon.size.width) / 2.0) + 0.5, y: floor((bounds.size.height - editAvatarIcon.size.height) / 2.0) + 1.0), size: editAvatarIcon.size))
}
}
}
}
public final class AvatarNode: ASDisplayNode {
public static let gradientColors: [[UIColor]] = [
[UIColor(rgb: 0xff516a), UIColor(rgb: 0xff885e)],
[UIColor(rgb: 0xffa85c), UIColor(rgb: 0xffcd6a)],
[UIColor(rgb: 0x665fff), UIColor(rgb: 0x82b1ff)],
[UIColor(rgb: 0x54cb68), UIColor(rgb: 0xa0de7e)],
[UIColor(rgb: 0x4acccd), UIColor(rgb: 0x00fcfd)],
[UIColor(rgb: 0x2a9ef1), UIColor(rgb: 0x72d5fd)],
[UIColor(rgb: 0xd669ed), UIColor(rgb: 0xe0a2f3)],
]
public var font: UIFont {
didSet {
if oldValue.pointSize != font.pointSize {
if let parameters = self.parameters {
self.parameters = AvatarNodeParameters(theme: parameters.theme, accountPeerId: parameters.accountPeerId, peerId: parameters.peerId, colors: parameters.colors, letters: parameters.letters, font: self.font, icon: parameters.icon, explicitColorIndex: parameters.explicitColorIndex, hasImage: parameters.hasImage, clipStyle: parameters.clipStyle)
}
if !self.displaySuspended {
self.setNeedsDisplay()
}
}
}
}
private var parameters: AvatarNodeParameters?
private var theme: PresentationTheme?
private var overrideImage: AvatarNodeImageOverride?
public let imageNode: ImageNode
private var animationBackgroundNode: ImageNode?
private var animationNode: AnimationNode?
public var editOverlayNode: AvatarEditOverlayNode?
private let imageReadyDisposable = MetaDisposable()
private var state: AvatarNodeState = .empty
public var unroundedImage: UIImage?
private var currentImage: UIImage?
public var badgeView: AvatarBadgeView? {
didSet {
if self.badgeView !== oldValue {
if let badgeView = self.badgeView, let parameters = self.parameters {
if parameters.hasImage {
if let currentImage = self.currentImage {
badgeView.update(content: .image(currentImage))
}
} else {
let badgeColor: UIColor
if parameters.colors.isEmpty {
badgeColor = .white
} else {
badgeColor = parameters.colors[parameters.colors.count - 1]
}
badgeView.update(content: .color(badgeColor))
}
}
}
}
}
private let imageReady = Promise<Bool>(false)
public var ready: Signal<Void, NoError> {
let imageReady = self.imageReady
return Signal { subscriber in
return imageReady.get().start(next: { next in
if next {
subscriber.putCompletion()
}
})
}
}
public init(font: UIFont) {
self.font = font
self.imageNode = ImageNode(enableHasImage: true, enableAnimatedTransition: true)
super.init()
self.isOpaque = false
self.displaysAsynchronously = true
self.imageNode.isLayerBacked = true
self.addSubnode(self.imageNode)
self.imageNode.contentUpdated = { [weak self] image in
guard let self else {
return
}
self.currentImage = image
guard let badgeView = self.badgeView, let parameters = self.parameters else {
return
}
if parameters.hasImage, let image {
badgeView.update(content: .image(image))
}
}
}
override public func didLoad() {
super.didLoad()
if #available(iOSApplicationExtension 11.0, iOS 11.0, *), !self.isLayerBacked {
self.view.accessibilityIgnoresInvertColors = true
}
}
override public var frame: CGRect {
get {
return super.frame
} set(value) {
let updateImage = !value.size.equalTo(super.frame.size)
super.frame = value
if updateImage {
self.updateSize(size: value.size)
}
}
}
public func updateSize(size: CGSize) {
self.imageNode.frame = CGRect(origin: CGPoint(), size: size)
self.editOverlayNode?.frame = self.imageNode.frame
if !self.displaySuspended {
self.setNeedsDisplay()
self.editOverlayNode?.setNeedsDisplay()
}
}
public func playArchiveAnimation() {
guard let theme = self.theme else {
return
}
var iconColor = theme.chatList.unpinnedArchiveAvatarColor.foregroundColor
var backgroundColor = theme.chatList.unpinnedArchiveAvatarColor.backgroundColors.topColor
let animationBackgroundNode = ASImageNode()
animationBackgroundNode.isUserInteractionEnabled = false
animationBackgroundNode.frame = self.imageNode.frame
if let overrideImage = self.overrideImage, case let .archivedChatsIcon(hiddenByDefault) = overrideImage {
let backgroundColors: (UIColor, UIColor)
if hiddenByDefault {
backgroundColors = theme.chatList.unpinnedArchiveAvatarColor.backgroundColors.colors
iconColor = theme.chatList.unpinnedArchiveAvatarColor.foregroundColor
} else {
backgroundColors = theme.chatList.pinnedArchiveAvatarColor.backgroundColors.colors
iconColor = theme.chatList.pinnedArchiveAvatarColor.foregroundColor
}
let colors: NSArray = [backgroundColors.1.cgColor, backgroundColors.0.cgColor]
backgroundColor = backgroundColors.1.mixedWith(backgroundColors.0, alpha: 0.5)
animationBackgroundNode.image = generateGradientFilledCircleImage(diameter: self.imageNode.frame.width, colors: colors)
}
self.addSubnode(animationBackgroundNode)
let animationNode = AnimationNode(animation: "anim_archiveAvatar", colors: ["box1.box1.Fill 1": iconColor, "box3.box3.Fill 1": iconColor, "box2.box2.Fill 1": backgroundColor], scale: 0.1653828)
animationNode.isUserInteractionEnabled = false
animationNode.completion = { [weak animationBackgroundNode, weak self] in
self?.imageNode.isHidden = false
animationBackgroundNode?.removeFromSupernode()
}
animationBackgroundNode.addSubnode(animationNode)
animationBackgroundNode.layer.animateScale(from: 1.0, to: 1.07, duration: 0.12, removeOnCompletion: false, completion: { [weak animationBackgroundNode] finished in
animationBackgroundNode?.layer.animateScale(from: 1.07, to: 1.0, duration: 0.12, removeOnCompletion: false)
})
if var size = animationNode.preferredSize() {
size = CGSize(width: ceil(size.width), height: ceil(size.height))
animationNode.frame = CGRect(x: floor((self.bounds.width - size.width) / 2.0), y: floor((self.bounds.height - size.height) / 2.0) + 1.0, width: size.width, height: size.height)
animationNode.play()
}
self.imageNode.isHidden = true
}
public func setPeer(context genericContext: AccountContext, account: Account? = nil, theme: PresentationTheme, peer: EnginePeer?, authorOfMessage: MessageReference? = nil, overrideImage: AvatarNodeImageOverride? = nil, emptyColor: UIColor? = nil, clipStyle: AvatarNodeClipStyle = .round, synchronousLoad: Bool = false, displayDimensions: CGSize = CGSize(width: 60.0, height: 60.0), storeUnrounded: Bool = false) {
var synchronousLoad = synchronousLoad
var representation: TelegramMediaImageRepresentation?
var icon = AvatarNodeIcon.none
if let overrideImage = overrideImage {
switch overrideImage {
case .none:
representation = nil
case let .image(image):
representation = image
synchronousLoad = false
case .savedMessagesIcon:
representation = nil
icon = .savedMessagesIcon
case .repliesIcon:
representation = nil
icon = .repliesIcon
case let .archivedChatsIcon(hiddenByDefault):
representation = nil
icon = .archivedChatsIcon(hiddenByDefault: hiddenByDefault)
case let .editAvatarIcon(forceNone):
representation = forceNone ? nil : peer?.smallProfileImage
icon = .editAvatarIcon
case .deletedIcon:
representation = nil
icon = .deletedIcon
case .phoneIcon:
representation = nil
icon = .phoneIcon
}
} else if peer?.restrictionText(platform: "ios", contentSettings: genericContext.currentContentSettings.with { $0 }) == nil {
representation = peer?.smallProfileImage
}
let updatedState: AvatarNodeState = .peerAvatar(peer?.id ?? EnginePeer.Id(0), peer?.displayLetters ?? [], representation, clipStyle)
if updatedState != self.state || overrideImage != self.overrideImage || theme !== self.theme {
self.state = updatedState
self.overrideImage = overrideImage
self.theme = theme
let parameters: AvatarNodeParameters
let account = account ?? genericContext.account
if let peer = peer, let signal = peerAvatarImage(account: account, peerReference: PeerReference(peer._asPeer()), authorOfMessage: authorOfMessage, representation: representation, displayDimensions: displayDimensions, clipStyle: clipStyle, emptyColor: emptyColor, synchronousLoad: synchronousLoad, provideUnrounded: storeUnrounded) {
self.contents = nil
self.displaySuspended = true
self.imageReady.set(self.imageNode.contentReady)
self.imageNode.setSignal(signal |> beforeNext { [weak self] next in
Queue.mainQueue().async {
self?.unroundedImage = next?.1
}
}
|> map { next -> UIImage? in
return next?.0
})
if case .editAvatarIcon = icon {
if self.editOverlayNode == nil {
let editOverlayNode = AvatarEditOverlayNode()
editOverlayNode.frame = self.imageNode.frame
editOverlayNode.isUserInteractionEnabled = false
self.addSubnode(editOverlayNode)
self.editOverlayNode = editOverlayNode
}
self.editOverlayNode?.isHidden = false
} else {
self.editOverlayNode?.isHidden = true
}
parameters = AvatarNodeParameters(theme: theme, accountPeerId: account.peerId, peerId: peer.id, colors: calculateColors(explicitColorIndex: nil, peerId: peer.id, icon: icon, theme: theme), letters: peer.displayLetters, font: self.font, icon: icon, explicitColorIndex: nil, hasImage: true, clipStyle: clipStyle)
} else {
self.imageReady.set(.single(true))
self.displaySuspended = false
if self.isNodeLoaded {
self.imageNode.contents = nil
}
self.editOverlayNode?.isHidden = true
let colors = calculateColors(explicitColorIndex: nil, peerId: peer?.id ?? EnginePeer.Id(0), icon: icon, theme: theme)
parameters = AvatarNodeParameters(theme: theme, accountPeerId: account.peerId, peerId: peer?.id ?? EnginePeer.Id(0), colors: colors, letters: peer?.displayLetters ?? [], font: self.font, icon: icon, explicitColorIndex: nil, hasImage: false, clipStyle: clipStyle)
if let badgeView = self.badgeView {
let badgeColor: UIColor
if colors.isEmpty {
badgeColor = .white
} else {
badgeColor = colors[colors.count - 1]
}
badgeView.update(content: .color(badgeColor))
}
}
if self.parameters == nil || self.parameters != parameters {
self.parameters = parameters
self.setNeedsDisplay()
if synchronousLoad {
self.recursivelyEnsureDisplaySynchronously(true)
}
}
}
}
public func setCustomLetters(_ letters: [String], explicitColor: AvatarNodeColorOverride? = nil, icon: AvatarNodeExplicitIcon? = nil) {
var explicitIndex: Int?
if let explicitColor = explicitColor {
switch explicitColor {
case .blue:
explicitIndex = 5
}
}
let updatedState: AvatarNodeState = .custom(letter: letters, explicitColorIndex: explicitIndex, explicitIcon: icon)
if updatedState != self.state {
self.state = updatedState
let parameters: AvatarNodeParameters
if let icon = icon, case .phone = icon {
parameters = AvatarNodeParameters(theme: nil, accountPeerId: nil, peerId: nil, colors: calculateColors(explicitColorIndex: explicitIndex, peerId: nil, icon: .phoneIcon, theme: nil), letters: [], font: self.font, icon: .phoneIcon, explicitColorIndex: explicitIndex, hasImage: false, clipStyle: .round)
} else {
parameters = AvatarNodeParameters(theme: nil, accountPeerId: nil, peerId: nil, colors: calculateColors(explicitColorIndex: explicitIndex, peerId: nil, icon: .none, theme: nil), letters: letters, font: self.font, icon: .none, explicitColorIndex: explicitIndex, hasImage: false, clipStyle: .round)
}
self.displaySuspended = true
self.contents = nil
self.imageReady.set(.single(true))
self.displaySuspended = false
if self.parameters == nil || self.parameters != parameters {
self.parameters = parameters
self.setNeedsDisplay()
}
}
}
override public func drawParameters(forAsyncLayer layer: _ASDisplayLayer) -> NSObjectProtocol {
return parameters ?? NSObject()
}
@objc override public class func draw(_ bounds: CGRect, withParameters parameters: Any?, isCancelled: () -> Bool, isRasterizing: Bool) {
assertNotOnMainThread()
let context = UIGraphicsGetCurrentContext()!
if !isRasterizing {
context.setBlendMode(.copy)
context.setFillColor(UIColor.clear.cgColor)
context.fill(bounds)
}
let colors: [UIColor]
if let parameters = parameters as? AvatarNodeParameters {
colors = parameters.colors
if case .round = parameters.clipStyle {
context.beginPath()
context.addEllipse(in: CGRect(x: 0.0, y: 0.0, width: bounds.size.width, height:
bounds.size.height))
context.clip()
} else if case .roundedRect = parameters.clipStyle {
context.beginPath()
context.addPath(UIBezierPath(roundedRect: CGRect(x: 0.0, y: 0.0, width: bounds.size.width, height: bounds.size.height), cornerRadius: floor(bounds.size.width * 0.25)).cgPath)
context.clip()
}
} else {
colors = grayscaleColors
}
let colorsArray: NSArray = colors.map(\.cgColor) as NSArray
var iconColor = UIColor.white
if let parameters = parameters as? AvatarNodeParameters, parameters.icon != .none {
if case let .archivedChatsIcon(hiddenByDefault) = parameters.icon, let theme = parameters.theme {
if hiddenByDefault {
iconColor = theme.chatList.unpinnedArchiveAvatarColor.foregroundColor
} else {
iconColor = theme.chatList.pinnedArchiveAvatarColor.foregroundColor
}
}
}
var locations: [CGFloat] = [1.0, 0.0]
let colorSpace = CGColorSpaceCreateDeviceRGB()
let gradient = CGGradient(colorsSpace: colorSpace, colors: colorsArray, locations: &locations)!
context.drawLinearGradient(gradient, start: CGPoint(), end: CGPoint(x: 0.0, y: bounds.size.height), options: CGGradientDrawingOptions())
context.setBlendMode(.normal)
if let parameters = parameters as? AvatarNodeParameters {
if case .deletedIcon = parameters.icon {
let factor = bounds.size.width / 60.0
context.translateBy(x: bounds.size.width / 2.0, y: bounds.size.height / 2.0)
context.scaleBy(x: factor, y: -factor)
context.translateBy(x: -bounds.size.width / 2.0, y: -bounds.size.height / 2.0)
if let deletedIcon = deletedIcon {
context.draw(deletedIcon.cgImage!, in: CGRect(origin: CGPoint(x: floor((bounds.size.width - deletedIcon.size.width) / 2.0), y: floor((bounds.size.height - deletedIcon.size.height) / 2.0)), size: deletedIcon.size))
}
} else if case .phoneIcon = parameters.icon {
let factor: CGFloat = 1.0
context.translateBy(x: bounds.size.width / 2.0, y: bounds.size.height / 2.0)
context.scaleBy(x: factor, y: -factor)
context.translateBy(x: -bounds.size.width / 2.0, y: -bounds.size.height / 2.0)
if let phoneIcon = phoneIcon {
context.draw(phoneIcon.cgImage!, in: CGRect(origin: CGPoint(x: floor((bounds.size.width - phoneIcon.size.width) / 2.0), y: floor((bounds.size.height - phoneIcon.size.height) / 2.0)), size: phoneIcon.size))
}
} else if case .savedMessagesIcon = parameters.icon {
let factor = bounds.size.width / 60.0
context.translateBy(x: bounds.size.width / 2.0, y: bounds.size.height / 2.0)
context.scaleBy(x: factor, y: -factor)
context.translateBy(x: -bounds.size.width / 2.0, y: -bounds.size.height / 2.0)
if let savedMessagesIcon = savedMessagesIcon {
context.draw(savedMessagesIcon.cgImage!, in: CGRect(origin: CGPoint(x: floor((bounds.size.width - savedMessagesIcon.size.width) / 2.0), y: floor((bounds.size.height - savedMessagesIcon.size.height) / 2.0)), size: savedMessagesIcon.size))
}
} else if case .repliesIcon = parameters.icon {
let factor = bounds.size.width / 60.0
context.translateBy(x: bounds.size.width / 2.0, y: bounds.size.height / 2.0)
context.scaleBy(x: factor, y: -factor)
context.translateBy(x: -bounds.size.width / 2.0, y: -bounds.size.height / 2.0)
if let repliesIcon = repliesIcon {
context.draw(repliesIcon.cgImage!, in: CGRect(origin: CGPoint(x: floor((bounds.size.width - repliesIcon.size.width) / 2.0), y: floor((bounds.size.height - repliesIcon.size.height) / 2.0)), size: repliesIcon.size))
}
} else if case .editAvatarIcon = parameters.icon, let theme = parameters.theme, !parameters.hasImage {
context.translateBy(x: bounds.size.width / 2.0, y: bounds.size.height / 2.0)
context.scaleBy(x: 1.0, y: -1.0)
context.translateBy(x: -bounds.size.width / 2.0, y: -bounds.size.height / 2.0)
if bounds.width > 90.0, let editAvatarIcon = generateTintedImage(image: UIImage(bundleImageName: "Avatar/EditAvatarIconLarge"), color: theme.list.itemAccentColor) {
context.draw(editAvatarIcon.cgImage!, in: CGRect(origin: CGPoint(x: floor((bounds.size.width - editAvatarIcon.size.width) / 2.0) + 0.5, y: floor((bounds.size.height - editAvatarIcon.size.height) / 2.0) + 1.0), size: editAvatarIcon.size))
} else if let editAvatarIcon = generateTintedImage(image: UIImage(bundleImageName: "Avatar/EditAvatarIcon"), color: theme.list.itemAccentColor) {
context.draw(editAvatarIcon.cgImage!, in: CGRect(origin: CGPoint(x: floor((bounds.size.width - editAvatarIcon.size.width) / 2.0) + 0.5, y: floor((bounds.size.height - editAvatarIcon.size.height) / 2.0) + 1.0), size: editAvatarIcon.size))
}
} else if case .archivedChatsIcon = parameters.icon {
let factor = bounds.size.width / 60.0
context.translateBy(x: bounds.size.width / 2.0, y: bounds.size.height / 2.0)
context.scaleBy(x: factor, y: -factor)
context.translateBy(x: -bounds.size.width / 2.0, y: -bounds.size.height / 2.0)
if let archivedChatsIcon = generateTintedImage(image: archivedChatsIcon, color: iconColor) {
context.draw(archivedChatsIcon.cgImage!, in: CGRect(origin: CGPoint(x: floor((bounds.size.width - archivedChatsIcon.size.width) / 2.0), y: floor((bounds.size.height - archivedChatsIcon.size.height) / 2.0)), size: archivedChatsIcon.size))
}
} else {
var letters = parameters.letters
if letters.count == 2 && letters[0].isSingleEmoji && letters[1].isSingleEmoji {
letters = [letters[0]]
}
let string = letters.count == 0 ? "" : (letters[0] + (letters.count == 1 ? "" : letters[1]))
let attributedString = NSAttributedString(string: string, attributes: [NSAttributedString.Key.font: parameters.font, NSAttributedString.Key.foregroundColor: UIColor.white])
let line = CTLineCreateWithAttributedString(attributedString)
let lineBounds = CTLineGetBoundsWithOptions(line, .useGlyphPathBounds)
let lineOffset = CGPoint(x: string == "B" ? 1.0 : 0.0, y: 0.0)
let lineOrigin = CGPoint(x: floorToScreenPixels(-lineBounds.origin.x + (bounds.size.width - lineBounds.size.width) / 2.0) + lineOffset.x, y: floorToScreenPixels(-lineBounds.origin.y + (bounds.size.height - lineBounds.size.height) / 2.0))
context.translateBy(x: bounds.size.width / 2.0, y: bounds.size.height / 2.0)
context.scaleBy(x: 1.0, y: -1.0)
context.translateBy(x: -bounds.size.width / 2.0, y: -bounds.size.height / 2.0)
context.translateBy(x: lineOrigin.x, y: lineOrigin.y)
CTLineDraw(line, context)
context.translateBy(x: -lineOrigin.x, y: -lineOrigin.y)
}
}
}
static func asyncLayout(_ node: AvatarNode?) -> (_ context: AccountContext, _ peer: EnginePeer, _ font: UIFont) -> () -> AvatarNode? {
let currentState = node?.state
let createNode = node == nil
return { [weak node] context, peer, font in
let state: AvatarNodeState = .peerAvatar(peer.id, peer.displayLetters, peer.smallProfileImage, .round)
if currentState != state {
}
var createdNode: AvatarNode?
if createNode {
createdNode = AvatarNode(font: font)
}
return {
let updatedNode: AvatarNode?
if let createdNode = createdNode {
updatedNode = createdNode
} else {
updatedNode = node
}
if let updatedNode = updatedNode {
return updatedNode
} else {
return nil
}
}
}
}
}
public func drawPeerAvatarLetters(context: CGContext, size: CGSize, round: Bool = true, font: UIFont, letters: [String], peerId: EnginePeer.Id) {
if round {
context.beginPath()
context.addEllipse(in: CGRect(x: 0.0, y: 0.0, width: size.width, height:
size.height))
context.clip()
}
let colorIndex: Int
if peerId.namespace == .max {
colorIndex = -1
} else {
colorIndex = Int(clamping: abs(peerId.id._internalGetInt64Value()))
}
let colorsArray: NSArray
if colorIndex == -1 {
colorsArray = grayscaleColors.map(\.cgColor) as NSArray
} else {
colorsArray = AvatarNode.gradientColors[colorIndex % AvatarNode.gradientColors.count].map(\.cgColor) as NSArray
}
var locations: [CGFloat] = [1.0, 0.0]
let colorSpace = CGColorSpaceCreateDeviceRGB()
let gradient = CGGradient(colorsSpace: colorSpace, colors: colorsArray, locations: &locations)!
context.drawLinearGradient(gradient, start: CGPoint(), end: CGPoint(x: 0.0, y: size.height), options: CGGradientDrawingOptions())
context.resetClip()
context.setBlendMode(.normal)
let string = letters.count == 0 ? "" : (letters[0] + (letters.count == 1 ? "" : letters[1]))
let attributedString = NSAttributedString(string: string, attributes: [NSAttributedString.Key.font: font, NSAttributedString.Key.foregroundColor: UIColor.white])
let line = CTLineCreateWithAttributedString(attributedString)
let lineBounds = CTLineGetBoundsWithOptions(line, .useGlyphPathBounds)
let lineOffset = CGPoint(x: string == "B" ? 1.0 : 0.0, y: 0.0)
let lineOrigin = CGPoint(x: floorToScreenPixels(-lineBounds.origin.x + (size.width - lineBounds.size.width) / 2.0) + lineOffset.x, y: floorToScreenPixels(-lineBounds.origin.y + (size.height - lineBounds.size.height) / 2.0))
context.translateBy(x: size.width / 2.0, y: size.height / 2.0)
context.scaleBy(x: 1.0, y: -1.0)
context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0)
let textPosition = context.textPosition
context.translateBy(x: lineOrigin.x, y: lineOrigin.y)
CTLineDraw(line, context)
context.translateBy(x: -lineOrigin.x, y: -lineOrigin.y)
context.textPosition = textPosition
}
public enum AvatarBackgroundColor {
case blue
case yellow
case green
case purple
case red
case violet
}
public func generateAvatarImage(size: CGSize, icon: UIImage?, iconScale: CGFloat = 1.0, cornerRadius: CGFloat? = nil, circleCorners: Bool = false, color: AvatarBackgroundColor) -> UIImage? {
return generateImage(size, rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
if let cornerRadius {
if circleCorners {
let roundedRect = CGPath(roundedRect: CGRect(origin: .zero, size: size), cornerWidth: cornerRadius, cornerHeight: cornerRadius, transform: nil)
context.addPath(roundedRect)
} else {
let roundedRect = UIBezierPath(roundedRect: CGRect(origin: .zero, size: size), cornerRadius: cornerRadius)
context.addPath(roundedRect.cgPath)
}
} else {
context.addEllipse(in: CGRect(x: 0.0, y: 0.0, width: size.width, height: size.height))
}
context.clip()
let colorIndex: Int
switch color {
case .blue:
colorIndex = 5
case .yellow:
colorIndex = 1
case .green:
colorIndex = 3
case .purple:
colorIndex = 2
case .red:
colorIndex = 0
case .violet:
colorIndex = 6
}
let colorsArray: NSArray
if colorIndex == -1 {
colorsArray = grayscaleColors.map(\.cgColor) as NSArray
} else {
colorsArray = AvatarNode.gradientColors[colorIndex % AvatarNode.gradientColors.count].map(\.cgColor) as NSArray
}
var locations: [CGFloat] = [1.0, 0.0]
let colorSpace = CGColorSpaceCreateDeviceRGB()
let gradient = CGGradient(colorsSpace: colorSpace, colors: colorsArray, locations: &locations)!
context.drawLinearGradient(gradient, start: CGPoint(), end: CGPoint(x: 0.0, y: size.height), options: CGGradientDrawingOptions())
context.resetClip()
context.setBlendMode(.normal)
context.translateBy(x: size.width / 2.0, y: size.height / 2.0)
context.scaleBy(x: 1.0, y: -1.0)
context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0)
if let icon = icon {
let iconSize = CGSize(width: icon.size.width * iconScale, height: icon.size.height * iconScale)
let iconFrame = CGRect(origin: CGPoint(x: floor((size.width - iconSize.width) / 2.0), y: floor((size.height - iconSize.height) / 2.0)), size: iconSize)
context.draw(icon.cgImage!, in: iconFrame)
}
})
}
public final class AvatarBadgeView: UIImageView {
enum OriginalContent: Equatable {
case color(UIColor)
case image(UIImage)
static func ==(lhs: OriginalContent, rhs: OriginalContent) -> Bool {
switch lhs {
case let .color(color):
if case .color(color) = rhs {
return true
} else {
return false
}
case let .image(lhsImage):
if case let .image(rhsImage) = rhs {
return lhsImage === rhsImage
} else {
return false
}
}
}
}
private struct Parameters: Equatable {
var size: CGSize
var text: String
}
private var originalContent: OriginalContent?
private var parameters: Parameters?
private var hasContent: Bool = false
override public init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(content: OriginalContent) {
if self.originalContent != content || !self.hasContent {
self.originalContent = content
self.update()
}
}
public func update(size: CGSize, text: String) {
let parameters = Parameters(size: size, text: text)
if self.parameters != parameters || !self.hasContent {
self.parameters = parameters
self.update()
}
}
private func update() {
guard let originalContent = self.originalContent, let parameters = self.parameters else {
return
}
self.hasContent = true
let blurredWidth = 16
let blurredHeight = 16
guard let blurredContext = DrawingContext(size: CGSize(width: CGFloat(blurredWidth), height: CGFloat(blurredHeight)), scale: 1.0, opaque: true) else {
return
}
let blurredSize = CGSize(width: CGFloat(blurredWidth), height: CGFloat(blurredHeight))
blurredContext.withContext { c in
switch originalContent {
case let .color(color):
c.setFillColor(color.cgColor)
c.fill(CGRect(origin: CGPoint(), size: blurredSize))
case let .image(image):
c.setFillColor(UIColor.black.cgColor)
c.fill(CGRect(origin: CGPoint(), size: blurredSize))
c.scaleBy(x: blurredSize.width / parameters.size.width, y: blurredSize.height / parameters.size.height)
let offsetFactor: CGFloat = 1.0 - 0.6
let imageFrame = CGRect(origin: CGPoint(x: parameters.size.width - image.size.width + offsetFactor * parameters.size.width, y: parameters.size.height - image.size.height + offsetFactor * parameters.size.height), size: image.size)
UIGraphicsPushContext(c)
image.draw(in: imageFrame)
UIGraphicsPopContext()
}
}
var rSum: Int64 = 0
var gSum: Int64 = 0
var bSum: Int64 = 0
for y in 0 ..< blurredHeight {
let row = blurredContext.bytes.assumingMemoryBound(to: UInt8.self).advanced(by: y * blurredContext.bytesPerRow)
for x in 0 ..< blurredWidth {
let pixel = row.advanced(by: x * 4)
bSum += Int64(pixel.advanced(by: 0).pointee)
gSum += Int64(pixel.advanced(by: 1).pointee)
rSum += Int64(pixel.advanced(by: 2).pointee)
}
}
let colorNorm = CGFloat(blurredWidth * blurredHeight)
let invColorNorm: CGFloat = 1.0 / (255.0 * colorNorm)
let aR = CGFloat(rSum) * invColorNorm
let aG = CGFloat(gSum) * invColorNorm
let aB = CGFloat(bSum) * invColorNorm
let luminance: CGFloat = 0.299 * aR + 0.587 * aG + 0.114 * aB
let isLightImage = luminance > 0.9
var brightness: CGFloat = 1.0
if isLightImage {
brightness = 0.99
} else {
brightness = 0.94
}
var destinationBuffer = vImage_Buffer()
destinationBuffer.width = UInt(blurredWidth)
destinationBuffer.height = UInt(blurredHeight)
destinationBuffer.data = blurredContext.bytes
destinationBuffer.rowBytes = blurredContext.bytesPerRow
vImageBoxConvolve_ARGB8888(
&destinationBuffer,
&destinationBuffer,
nil,
0, 0,
UInt32(15),
UInt32(15),
nil,
vImage_Flags(kvImageTruncateKernel | kvImageDoNotTile)
)
let divisor: Int32 = 0x1000
let rwgt: CGFloat = 0.3086
let gwgt: CGFloat = 0.6094
let bwgt: CGFloat = 0.0820
let adjustSaturation: CGFloat = 1.7
let a = (1.0 - adjustSaturation) * rwgt + adjustSaturation
let b = (1.0 - adjustSaturation) * rwgt
let c = (1.0 - adjustSaturation) * rwgt
let d = (1.0 - adjustSaturation) * gwgt
let e = (1.0 - adjustSaturation) * gwgt + adjustSaturation
let f = (1.0 - adjustSaturation) * gwgt
let g = (1.0 - adjustSaturation) * bwgt
let h = (1.0 - adjustSaturation) * bwgt
let i = (1.0 - adjustSaturation) * bwgt + adjustSaturation
let satMatrix: [CGFloat] = [
a, b, c, 0,
d, e, f, 0,
g, h, i, 0,
0, 0, 0, 1
]
let brighnessMatrix: [CGFloat] = [
brightness, 0, 0, 0,
0, brightness, 0, 0,
0, 0, brightness, 0,
0, 0, 0, 1
]
func matrixMul(a: [CGFloat], b: [CGFloat], result: inout [CGFloat]) {
for i in 0 ..< 4 {
for j in 0 ..< 4 {
var sum: CGFloat = 0.0
for k in 0 ..< 4 {
sum += a[i + k * 4] * b[k + j * 4]
}
result[i + j * 4] = sum
}
}
}
var resultMatrix = Array<CGFloat>(repeating: 0.0, count: 4 * 4)
matrixMul(a: satMatrix, b: brighnessMatrix, result: &resultMatrix)
var matrix: [Int16] = resultMatrix.map { value in
return Int16(value * CGFloat(divisor))
}
vImageMatrixMultiply_ARGB8888(&destinationBuffer, &destinationBuffer, &matrix, divisor, nil, nil, vImage_Flags(kvImageDoNotTile))
guard let blurredImage = blurredContext.generateImage() else {
return
}
self.image = generateImage(parameters.size, rotatedContext: { size, context in
UIGraphicsPushContext(context)
context.clear(CGRect(origin: CGPoint(), size: size))
context.setBlendMode(.copy)
context.setFillColor(UIColor.black.cgColor)
context.fillEllipse(in: CGRect(origin: CGPoint(), size: size))
blurredImage.draw(in: CGRect(origin: CGPoint(), size: size), blendMode: .sourceIn, alpha: 1.0)
context.setBlendMode(.normal)
let textColor: UIColor
if isLightImage {
textColor = UIColor(white: 0.7, alpha: 1.0)
} else {
textColor = .white
}
var fontSize: CGFloat = floor(parameters.size.height * 0.48)
while true {
let string = NSAttributedString(string: parameters.text, font: Font.bold(fontSize), textColor: textColor)
let stringBounds = string.boundingRect(with: CGSize(width: 100.0, height: 100.0), options: .usesLineFragmentOrigin, context: nil)
if stringBounds.width <= size.width - 5.0 * 2.0 || fontSize <= 2.0 {
string.draw(at: CGPoint(x: stringBounds.minX + floorToScreenPixels((size.width - stringBounds.width) / 2.0), y: stringBounds.minY + floorToScreenPixels((size.height - stringBounds.height) / 2.0)))
break
} else {
fontSize -= 1.0
}
}
let lineWidth: CGFloat = 1.5
let lineInset: CGFloat = 2.0
let lineRadius: CGFloat = size.width * 0.5 - lineInset - lineWidth * 0.5
context.setLineWidth(lineWidth)
context.setStrokeColor(textColor.cgColor)
context.setLineCap(.round)
context.addArc(center: CGPoint(x: size.width * 0.5, y: size.height * 0.5), radius: lineRadius, startAngle: CGFloat.pi * 0.5, endAngle: -CGFloat.pi * 0.5, clockwise: false)
context.strokePath()
let sectionAngle: CGFloat = CGFloat.pi / 11.0
for i in 0 ..< 10 {
if i % 2 == 0 {
continue
}
let startAngle = CGFloat.pi * 0.5 - CGFloat(i) * sectionAngle - sectionAngle * 0.15
let endAngle = startAngle - sectionAngle * 0.75
context.addArc(center: CGPoint(x: size.width * 0.5, y: size.height * 0.5), radius: lineRadius, startAngle: startAngle, endAngle: endAngle, clockwise: true)
context.strokePath()
}
/*if isLightImage {
context.setLineWidth(UIScreenPixel)
context.setStrokeColor(textColor.withMultipliedAlpha(1.0).cgColor)
context.strokeEllipse(in: CGRect(origin: CGPoint(), size: size).insetBy(dx: UIScreenPixel * 0.5, dy: UIScreenPixel * 0.5))
}*/
UIGraphicsPopContext()
})
}
}