Merge commit '7621e2f8dec938cf48181c8b10afc9b01f444e68' into beta

This commit is contained in:
Ilya Laktyushin
2025-12-06 02:17:48 +04:00
commit 8344b97e03
28070 changed files with 7995182 additions and 0 deletions
@@ -0,0 +1,60 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "EntityKeyboard",
module_name = "EntityKeyboard",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
"//submodules/Display:Display",
"//submodules/ComponentFlow:ComponentFlow",
"//submodules/Components/PagerComponent:PagerComponent",
"//submodules/Components/BlurredBackgroundComponent:BlurredBackgroundComponent",
"//submodules/Components/ComponentDisplayAdapters:ComponentDisplayAdapters",
"//submodules/Components/BundleIconComponent:BundleIconComponent",
"//submodules/TelegramPresentationData:TelegramPresentationData",
"//submodules/TelegramCore:TelegramCore",
"//submodules/Postbox:Postbox",
"//submodules/AnimatedStickerNode:AnimatedStickerNode",
"//submodules/TelegramAnimatedStickerNode:TelegramAnimatedStickerNode",
"//submodules/YuvConversion:YuvConversion",
"//submodules/AccountContext:AccountContext",
"//submodules/TelegramUI/Components/AnimationCache:AnimationCache",
"//submodules/TelegramUI/Components/LottieAnimationCache:LottieAnimationCache",
"//submodules/TelegramUI/Components/VideoAnimationCache:VideoAnimationCache",
"//submodules/TelegramUI/Components/MultiAnimationRenderer:MultiAnimationRenderer",
"//submodules/TelegramUI/Components/EmojiTextAttachmentView:EmojiTextAttachmentView",
"//submodules/TelegramUI/Components/EmojiStatusComponent:EmojiStatusComponent",
"//submodules/TelegramUI/Components/LottieComponent",
"//submodules/TelegramUI/Components/LottieComponentEmojiContent",
"//submodules/SoftwareVideo:SoftwareVideo",
"//submodules/ShimmerEffect:ShimmerEffect",
"//submodules/PhotoResources:PhotoResources",
"//submodules/StickerResources:StickerResources",
"//submodules/AppBundle:AppBundle",
"//submodules/UndoUI:UndoUI",
"//submodules/Components/MultilineTextComponent:MultilineTextComponent",
"//submodules/Components/SolidRoundedButtonComponent:SolidRoundedButtonComponent",
"//submodules/Components/LottieAnimationComponent:LottieAnimationComponent",
"//submodules/LocalizedPeerData:LocalizedPeerData",
"//submodules/TelegramNotices:TelegramNotices",
"//submodules/GZip",
"//submodules/rlottie:RLottieBinding",
"//submodules/lottie-ios:Lottie",
"//submodules/TelegramUI/Components/Utils/GenerateStickerPlaceholderImage",
"//submodules/TelegramUIPreferences",
"//submodules/TelegramCore/FlatBuffers",
"//submodules/TelegramCore/FlatSerialization",
"//submodules/TelegramUI/Components/BatchVideoRendering",
"//submodules/TelegramUI/Components/GifVideoLayer",
"//submodules/TelegramUI/Components/GlassBackgroundComponent",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,543 @@
import Foundation
import UIKit
import Display
import ComponentFlow
import MultiAnimationRenderer
import AnimationCache
import SwiftSignalKit
import TelegramCore
import AccountContext
import TelegramPresentationData
import EmojiTextAttachmentView
import EmojiStatusComponent
import CoreVideo
final class EmojiKeyboardCloneItemLayer: SimpleLayer {
}
public final class EmojiKeyboardItemLayer: MultiAnimationRenderTarget {
public struct Key: Hashable {
var groupId: AnyHashable
var itemId: EmojiPagerContentComponent.ItemContent.Id
public init(
groupId: AnyHashable,
itemId: EmojiPagerContentComponent.ItemContent.Id
) {
self.groupId = groupId
self.itemId = itemId
}
}
enum Badge: Equatable {
case premium
case locked
case featured
case text(String)
case customFile(TelegramMediaFile.Accessor)
}
public let item: EmojiPagerContentComponent.Item
private let context: AccountContext
private var content: EmojiPagerContentComponent.ItemContent
private var theme: PresentationTheme?
private let placeholderColor: UIColor
let pixelSize: CGSize
let pointSize: CGSize
private let size: CGSize
private var disposable: Disposable?
private var fetchDisposable: Disposable?
private var premiumBadgeView: PremiumBadgeView?
private var iconLayer: SimpleLayer?
private var tintIconLayer: SimpleLayer?
private(set) var underlyingContentLayer: SimpleLayer?
private(set) var tintContentLayer: SimpleLayer?
private var badge: Badge?
private var validSize: CGSize?
private var isInHierarchyValue: Bool = false
public var isVisibleForAnimations: Bool = false {
didSet {
if self.isVisibleForAnimations != oldValue {
self.updatePlayback()
}
}
}
public private(set) var displayPlaceholder: Bool = false
public let onUpdateDisplayPlaceholder: (Bool, Double) -> Void
weak var cloneLayer: EmojiKeyboardCloneItemLayer? {
didSet {
if let cloneLayer = self.cloneLayer {
cloneLayer.contents = self.contents
}
}
}
override public var contents: Any? {
get {
return super.contents
} set(value) {
#if targetEnvironment(simulator)
if let value, CFGetTypeID(value as CFTypeRef) == CVPixelBufferGetTypeID() {
let pixelBuffer = value as! CVPixelBuffer
super.contents = CVPixelBufferGetIOSurface(pixelBuffer)
} else {
super.contents = value
}
#else
super.contents = value
#endif
self.onContentsUpdate()
if let cloneLayer = self.cloneLayer {
cloneLayer.contents = self.contents
}
}
}
override public var position: CGPoint {
get {
return super.position
} set(value) {
if let mirrorLayer = self.tintContentLayer {
mirrorLayer.position = value
}
if let mirrorLayer = self.underlyingContentLayer {
mirrorLayer.position = value
}
super.position = value
}
}
override public var bounds: CGRect {
get {
return super.bounds
} set(value) {
if let mirrorLayer = self.tintContentLayer {
mirrorLayer.bounds = value
}
if let mirrorLayer = self.underlyingContentLayer {
mirrorLayer.bounds = value
}
super.bounds = value
}
}
override public func add(_ animation: CAAnimation, forKey key: String?) {
if let mirrorLayer = self.tintContentLayer {
mirrorLayer.add(animation, forKey: key)
}
if let mirrorLayer = self.underlyingContentLayer {
mirrorLayer.add(animation, forKey: key)
}
super.add(animation, forKey: key)
}
override public func removeAllAnimations() {
if let mirrorLayer = self.tintContentLayer {
mirrorLayer.removeAllAnimations()
}
if let mirrorLayer = self.underlyingContentLayer {
mirrorLayer.removeAllAnimations()
}
super.removeAllAnimations()
}
override public func removeAnimation(forKey: String) {
if let mirrorLayer = self.tintContentLayer {
mirrorLayer.removeAnimation(forKey: forKey)
}
if let mirrorLayer = self.underlyingContentLayer {
mirrorLayer.removeAnimation(forKey: forKey)
}
super.removeAnimation(forKey: forKey)
}
public var onContentsUpdate: () -> Void = {}
public var onLoop: () -> Void = {}
public init(
item: EmojiPagerContentComponent.Item,
context: AccountContext,
attemptSynchronousLoad: Bool,
content: EmojiPagerContentComponent.ItemContent,
cache: AnimationCache,
renderer: MultiAnimationRenderer,
placeholderColor: UIColor,
blurredBadgeColor: UIColor,
accentIconColor: UIColor,
pointSize: CGSize,
onUpdateDisplayPlaceholder: @escaping (Bool, Double) -> Void
) {
self.item = item
self.context = context
self.content = content
self.placeholderColor = placeholderColor
self.onUpdateDisplayPlaceholder = onUpdateDisplayPlaceholder
let scale = min(2.0, UIScreenScale)
let pixelSize = CGSize(width: pointSize.width * scale, height: pointSize.height * scale)
self.pixelSize = pixelSize
self.pointSize = pointSize
self.size = CGSize(width: pixelSize.width / scale, height: pixelSize.height / scale)
super.init()
switch content {
case let .animation(animationData):
guard let animationDataResource = animationData.resource._parse() else {
return
}
let loadAnimation: () -> Void = { [weak self] in
guard let strongSelf = self else {
return
}
strongSelf.disposable = renderer.add(target: strongSelf, cache: cache, itemId: animationDataResource.resource.id.stringRepresentation, unique: false, size: pixelSize, fetch: animationCacheFetchFile(context: context, userLocation: .other, userContentType: .sticker, resource: animationDataResource, type: animationData.type.animationCacheAnimationType, keyframeOnly: pixelSize.width >= 120.0, customColor: animationData.isTemplate ? .white : nil))
}
if attemptSynchronousLoad {
if !renderer.loadFirstFrameSynchronously(target: self, cache: cache, itemId: animationDataResource.resource.id.stringRepresentation, size: pixelSize) {
self.updateDisplayPlaceholder(displayPlaceholder: true)
self.fetchDisposable = renderer.loadFirstFrame(target: self, cache: cache, itemId: animationDataResource.resource.id.stringRepresentation, size: pixelSize, fetch: animationCacheFetchFile(context: context, userLocation: .other, userContentType: .sticker, resource: animationDataResource, type: animationData.type.animationCacheAnimationType, keyframeOnly: true, customColor: animationData.isTemplate ? .white : nil), completion: { [weak self] success, isFinal in
if !isFinal {
if !success {
Queue.mainQueue().async {
guard let strongSelf = self else {
return
}
strongSelf.updateDisplayPlaceholder(displayPlaceholder: true)
}
}
return
}
Queue.mainQueue().async {
loadAnimation()
if !success {
guard let strongSelf = self else {
return
}
strongSelf.updateDisplayPlaceholder(displayPlaceholder: true)
}
}
})
} else {
loadAnimation()
}
} else {
self.fetchDisposable = renderer.loadFirstFrame(target: self, cache: cache, itemId: animationDataResource.resource.id.stringRepresentation, size: pixelSize, fetch: animationCacheFetchFile(context: context, userLocation: .other, userContentType: .sticker, resource: animationDataResource, type: animationData.type.animationCacheAnimationType, keyframeOnly: true, customColor: animationData.isTemplate ? .white : nil), completion: { [weak self] success, isFinal in
if !isFinal {
if !success {
Queue.mainQueue().async {
guard let strongSelf = self else {
return
}
strongSelf.updateDisplayPlaceholder(displayPlaceholder: true)
}
}
return
}
Queue.mainQueue().async {
loadAnimation()
if !success {
guard let strongSelf = self else {
return
}
strongSelf.updateDisplayPlaceholder(displayPlaceholder: true)
}
}
})
}
if let particleColor = animationData.particleColor {
let underlyingContentLayer = SimpleLayer()
self.underlyingContentLayer = underlyingContentLayer
let starsLayer = StarsEffectLayer()
starsLayer.frame = CGRect(origin: CGPoint(x: -3.0, y: -3.0), size: CGSize(width: 42.0, height: 42.0))
starsLayer.update(color: particleColor, size: CGSize(width: 42.0, height: 42.0))
underlyingContentLayer.addSublayer(starsLayer)
}
case let .staticEmoji(staticEmoji):
let image = generateImage(pointSize, opaque: false, scale: min(UIScreenScale, 3.0), rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
let preScaleFactor: CGFloat = 1.0
let scaledSize = CGSize(width: floor(size.width * preScaleFactor), height: floor(size.height * preScaleFactor))
let scaleFactor = scaledSize.width / size.width
context.scaleBy(x: 1.0 / scaleFactor, y: 1.0 / scaleFactor)
let string = NSAttributedString(string: staticEmoji, font: Font.regular(floor(32.0 * scaleFactor)), textColor: .black)
let boundingRect = string.boundingRect(with: scaledSize, options: .usesLineFragmentOrigin, context: nil)
UIGraphicsPushContext(context)
string.draw(at: CGPoint(x: floorToScreenPixels((scaledSize.width - boundingRect.width) / 2.0 + boundingRect.minX), y: floorToScreenPixels((scaledSize.height - boundingRect.height) / 2.0 + boundingRect.minY)))
UIGraphicsPopContext()
})
self.contents = image?.cgImage
case let .icon(icon):
let image = generateImage(pointSize, opaque: false, scale: min(UIScreenScale, 3.0), rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
UIGraphicsPushContext(context)
switch icon {
case .premiumStar:
if let image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Media/EntityInputPremiumIcon"), color: accentIconColor) {
let imageSize = image.size.aspectFitted(CGSize(width: size.width - 6.0, height: size.height - 6.0))
image.draw(in: CGRect(origin: CGPoint(x: floor((size.width - imageSize.width) / 2.0), y: floor((size.height - imageSize.height) / 2.0)), size: imageSize))
}
case let .topic(title, color):
let colors = topicIconColors(for: color)
if let image = generateTopicIcon(backgroundColors: colors.0.map { UIColor(rgb: $0) }, strokeColors: colors.1.map { UIColor(rgb: $0) }, title: title) {
let imageSize = image.size//.aspectFitted(CGSize(width: size.width - 6.0, height: size.height - 6.0))
image.draw(in: CGRect(origin: CGPoint(x: floor((size.width - imageSize.width) / 2.0), y: floor((size.height - imageSize.height) / 2.0)), size: imageSize))
}
case .stop:
if let image = generateTintedImage(image: UIImage(bundleImageName: "Premium/NoIcon"), color: .white) {
let imageSize = image.size.aspectFitted(CGSize(width: size.width - 6.0, height: size.height - 6.0))
image.draw(in: CGRect(origin: CGPoint(x: floor((size.width - imageSize.width) / 2.0), y: floor((size.height - imageSize.height) / 2.0)), size: imageSize))
}
case .add:
break
}
UIGraphicsPopContext()
})?.withRenderingMode(icon == .stop ? .alwaysTemplate : .alwaysOriginal)
self.contents = image?.cgImage
}
if case .icon(.add) = content {
let tintContentLayer = SimpleLayer()
self.tintContentLayer = tintContentLayer
let iconLayer = SimpleLayer()
self.iconLayer = iconLayer
self.addSublayer(iconLayer)
let tintIconLayer = SimpleLayer()
self.tintIconLayer = tintIconLayer
tintContentLayer.addSublayer(tintIconLayer)
}
}
override public init(layer: Any) {
guard let layer = layer as? EmojiKeyboardItemLayer else {
preconditionFailure()
}
self.context = layer.context
self.item = layer.item
self.content = layer.content
self.placeholderColor = layer.placeholderColor
self.size = layer.size
self.pixelSize = layer.pixelSize
self.pointSize = layer.pointSize
self.onUpdateDisplayPlaceholder = { _, _ in }
super.init(layer: layer)
}
required public init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
self.disposable?.dispose()
self.fetchDisposable?.dispose()
}
public override func action(forKey event: String) -> CAAction? {
if event == kCAOnOrderIn {
self.isInHierarchyValue = true
} else if event == kCAOnOrderOut {
self.isInHierarchyValue = false
}
self.updatePlayback()
return nullAction
}
func update(
content: EmojiPagerContentComponent.ItemContent,
theme: PresentationTheme,
strings: PresentationStrings
) {
var themeUpdated = false
if self.theme !== theme {
self.theme = theme
themeUpdated = true
}
var contentUpdated = false
if self.content != content {
self.content = content
contentUpdated = true
}
if themeUpdated || contentUpdated {
if case let .icon(icon) = content, case let .topic(title, color) = icon {
let image = generateImage(self.size, opaque: false, scale: min(UIScreenScale, 3.0), rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
UIGraphicsPushContext(context)
let colors = topicIconColors(for: color)
if let image = generateTopicIcon(backgroundColors: colors.0.map { UIColor(rgb: $0) }, strokeColors: colors.1.map { UIColor(rgb: $0) }, title: title) {
let imageSize = image.size
image.draw(in: CGRect(origin: CGPoint(x: floor((size.width - imageSize.width) / 2.0), y: floor((size.height - imageSize.height) / 2.0)), size: imageSize))
}
UIGraphicsPopContext()
})
self.contents = image?.cgImage
} else if case .icon(.add) = content {
guard let iconLayer = self.iconLayer, let tintIconLayer = self.tintIconLayer else {
return
}
func generateIcon(color: UIColor) -> UIImage? {
return generateImage(self.pointSize, opaque: false, scale: min(UIScreenScale, 3.0), rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
UIGraphicsPushContext(context)
context.setFillColor(color.withMultipliedAlpha(0.2).cgColor)
context.addPath(UIBezierPath(roundedRect: CGRect(origin: .zero, size: size), cornerRadius: 21.0).cgPath)
context.fillPath()
context.setFillColor(color.cgColor)
let plusSize = CGSize(width: 3.5, height: 28.0)
context.addPath(UIBezierPath(roundedRect: CGRect(x: floorToScreenPixels((size.width - plusSize.width) / 2.0), y: floorToScreenPixels((size.height - plusSize.height) / 2.0), width: plusSize.width, height: plusSize.height).offsetBy(dx: 0.0, dy: -17.0), cornerRadius: plusSize.width / 2.0).cgPath)
context.addPath(UIBezierPath(roundedRect: CGRect(x: floorToScreenPixels((size.width - plusSize.height) / 2.0), y: floorToScreenPixels((size.height - plusSize.width) / 2.0), width: plusSize.height, height: plusSize.width).offsetBy(dx: 0.0, dy: -17.0), cornerRadius: plusSize.width / 2.0).cgPath)
context.fillPath()
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 string = strings.Stickers_CreateSticker
var lineOriginY = size.height / 2.0 - 18.0
let components = string.components(separatedBy: "\n")
for component in components {
context.saveGState()
let attributedString = NSAttributedString(string: component, attributes: [NSAttributedString.Key.font: Font.medium(17.0), NSAttributedString.Key.foregroundColor: color])
let line = CTLineCreateWithAttributedString(attributedString)
let lineBounds = CTLineGetBoundsWithOptions(line, .useGlyphPathBounds)
let lineOrigin = CGPoint(x: floorToScreenPixels((size.width - lineBounds.size.width) / 2.0), y: lineOriginY)
context.textPosition = lineOrigin
CTLineDraw(line, context)
lineOriginY -= lineBounds.height + 6.0
context.restoreGState()
}
UIGraphicsPopContext()
})
}
let needsVibrancy = !theme.overallDarkAppearance
let color = theme.chat.inputMediaPanel.panelContentVibrantOverlayColor
iconLayer.contents = generateIcon(color: color)?.cgImage
tintIconLayer.contents = generateIcon(color: .black)?.cgImage
tintIconLayer.isHidden = !needsVibrancy
}
}
}
func update(
transition: ComponentTransition,
size: CGSize,
badge: Badge?,
blurredBadgeColor: UIColor,
blurredBadgeBackgroundColor: UIColor
) {
if self.badge != badge || self.validSize != size {
self.badge = badge
self.validSize = size
if let iconLayer = self.iconLayer, let tintIconLayer = self.tintIconLayer {
transition.setFrame(layer: iconLayer, frame: CGRect(origin: .zero, size: size))
transition.setFrame(layer: tintIconLayer, frame: CGRect(origin: .zero, size: size))
}
if let badge = badge {
var badgeTransition = transition
let premiumBadgeView: PremiumBadgeView
if let current = self.premiumBadgeView {
premiumBadgeView = current
} else {
badgeTransition = .immediate
premiumBadgeView = PremiumBadgeView(context: self.context)
self.premiumBadgeView = premiumBadgeView
self.addSublayer(premiumBadgeView.layer)
}
let badgeDiameter = min(16.0, floor(size.height * 0.5))
let badgeSize = CGSize(width: badgeDiameter, height: badgeDiameter)
badgeTransition.setFrame(view: premiumBadgeView, frame: CGRect(origin: CGPoint(x: size.width - badgeSize.width, y: size.height - badgeSize.height), size: badgeSize))
premiumBadgeView.update(transition: badgeTransition, badge: badge, backgroundColor: blurredBadgeColor, size: badgeSize)
self.blurredRepresentationBackgroundColor = blurredBadgeBackgroundColor
self.blurredRepresentationTarget = premiumBadgeView.contentLayer
} else {
if let premiumBadgeView = self.premiumBadgeView {
self.premiumBadgeView = nil
premiumBadgeView.removeFromSuperview()
self.blurredRepresentationBackgroundColor = nil
self.blurredRepresentationTarget = nil
}
}
}
}
private func updatePlayback() {
let shouldBePlaying = self.isInHierarchyValue && self.isVisibleForAnimations
self.shouldBeAnimating = shouldBePlaying
}
public override func updateDisplayPlaceholder(displayPlaceholder: Bool) {
if self.displayPlaceholder == displayPlaceholder {
return
}
self.displayPlaceholder = displayPlaceholder
self.onUpdateDisplayPlaceholder(displayPlaceholder, 0.0)
}
public override func transitionToContents(_ contents: AnyObject, didLoop: Bool) {
self.contents = contents
if self.displayPlaceholder {
self.displayPlaceholder = false
self.onUpdateDisplayPlaceholder(false, 0.2)
self.animateAlpha(from: 0.0, to: 1.0, duration: 0.18)
}
if didLoop {
self.onLoop()
}
}
}
@@ -0,0 +1,90 @@
import Foundation
import AppBundle
private func loadStaticEmojiMapping() -> [(EmojiPagerContentComponent.StaticEmojiSegment, [String])] {
guard let path = getAppBundle().path(forResource: "emoji1", ofType: "txt") else {
return []
}
guard let string = try? String(contentsOf: URL(fileURLWithPath: path)) else {
return []
}
// Convert \r\n to \n for consistent line ending handling
let normalizedString = string.replacingOccurrences(of: "\r\n", with: "\n")
// Split into 4 large sections divided by =========================================
let largeSections = normalizedString.components(separatedBy: "=========================================")
if largeSections.count < 4 {
return []
}
// Use the first large section
let firstLargeSection = largeSections[0].trimmingCharacters(in: .whitespacesAndNewlines)
// Split into 8 sections divided by triple-newline
let emojiSections = firstLargeSection.components(separatedBy: "\n\n\n")
if emojiSections.count < 8 {
return []
}
var result: [(EmojiPagerContentComponent.StaticEmojiSegment, [String])] = []
let orderedSegments = EmojiPagerContentComponent.StaticEmojiSegment.allCases
for i in 0..<min(emojiSections.count, orderedSegments.count) {
let sectionContent = emojiSections[i].trimmingCharacters(in: .whitespacesAndNewlines)
// Parse emoji from this section and filter out skin-colored variants
let emojiList = parseEmojiSection(sectionContent)
result.append((orderedSegments[i], emojiList))
}
return result
}
private func parseEmojiSection(_ sectionContent: String) -> [String] {
// Remove quotes first
let cleanedContent = sectionContent.replacingOccurrences(of: "\"", with: "")
let items = cleanedContent.components(separatedBy: ",")
var result: [String] = []
var i = 0
while i < items.count {
let item = items[i]
let cleanItem = item.trimmingCharacters(in: .whitespacesAndNewlines)
// Skip empty items
if cleanItem.isEmpty {
i += 1
continue
}
result.append(cleanItem)
i += 1
// If this item started with a newline, it's the beginning of a skin-colored emoji group
// Skip all following items until we find the next item that starts with a newline (or reach the end)
if item.hasPrefix("\n") {
while i < items.count {
let nextItem = items[i]
if nextItem.hasPrefix("\n") {
// Found the start of the next group, break to process it
break
} else {
// Skip this skin-colored variant
i += 1
}
}
}
}
return result
}
public extension EmojiPagerContentComponent {
static let staticEmojiMapping: [(EmojiPagerContentComponent.StaticEmojiSegment, [String])] = loadStaticEmojiMapping()
}
@@ -0,0 +1,550 @@
import Foundation
import UIKit
import Display
import ComponentFlow
import TelegramPresentationData
import TelegramCore
import Postbox
import AnimationCache
import MultiAnimationRenderer
import AccountContext
import AsyncDisplayKit
import ComponentDisplayAdapters
import PagerComponent
import SwiftSignalKit
public final class EmojiSearchContent: ASDisplayNode, EntitySearchContainerNode {
private struct Params: Equatable {
var size: CGSize
var leftInset: CGFloat
var rightInset: CGFloat
var bottomInset: CGFloat
var inputHeight: CGFloat
var deviceMetrics: DeviceMetrics
}
private struct EmojiSearchResult {
var groups: [EmojiPagerContentComponent.ItemGroup]
var id: AnyHashable
var version: Int
var isPreset: Bool
}
private struct EmojiSearchState {
var result: EmojiSearchResult?
var isSearching: Bool
init(result: EmojiSearchResult?, isSearching: Bool) {
self.result = result
self.isSearching = isSearching
}
}
private let context: AccountContext
private let forceTheme: PresentationTheme?
private var initialFocusId: ItemCollectionId?
private let hasPremiumForUse: Bool
private let hasPremiumForInstallation: Bool
private let parentInputInteraction: EmojiPagerContentComponent.InputInteraction
private var presentationData: PresentationData
private let keyboardView = ComponentView<Empty>()
private let panelHostView: PagerExternalTopPanelContainer
private let inputInteractionHolder: EmojiPagerContentComponent.InputInteractionHolder
private var params: Params?
private var itemGroups: [EmojiPagerContentComponent.ItemGroup] = []
public var onCancel: (() -> Void)?
private let emojiSearchDisposable = MetaDisposable()
private let emojiSearchState = Promise<EmojiSearchState>(EmojiSearchState(result: nil, isSearching: false))
private var emojiSearchStateValue = EmojiSearchState(result: nil, isSearching: false) {
didSet {
self.emojiSearchState.set(.single(self.emojiSearchStateValue))
}
}
private var immediateEmojiSearchState: EmojiSearchState = EmojiSearchState(result: nil, isSearching: false)
private var dataDisposable: Disposable?
public init(
context: AccountContext,
forceTheme: PresentationTheme?,
items: [FeaturedStickerPackItem],
initialFocusId: ItemCollectionId?,
hasPremiumForUse: Bool,
hasPremiumForInstallation: Bool,
parentInputInteraction: EmojiPagerContentComponent.InputInteraction
) {
self.context = context
self.forceTheme = forceTheme
self.initialFocusId = initialFocusId
self.hasPremiumForUse = hasPremiumForUse
self.hasPremiumForInstallation = hasPremiumForInstallation
self.parentInputInteraction = parentInputInteraction
var presentationData = context.sharedContext.currentPresentationData.with { $0 }
if let forceTheme {
presentationData = presentationData.withUpdated(theme: forceTheme)
}
self.presentationData = presentationData
self.panelHostView = PagerExternalTopPanelContainer()
self.inputInteractionHolder = EmojiPagerContentComponent.InputInteractionHolder()
super.init()
for groupItem in items {
var groupItems: [EmojiPagerContentComponent.Item] = []
for item in groupItem.topItems {
var tintMode: EmojiPagerContentComponent.Item.TintMode = .none
if item.file.isCustomTemplateEmoji {
tintMode = .primary
}
let animationData = EntityKeyboardAnimationData(file: item.file)
let resultItem = EmojiPagerContentComponent.Item(
animationData: animationData,
content: .animation(animationData),
itemFile: item.file,
subgroupId: nil,
icon: .none,
tintMode: tintMode
)
groupItems.append(resultItem)
}
self.itemGroups.append(EmojiPagerContentComponent.ItemGroup(
supergroupId: AnyHashable(groupItem.info.id),
groupId: AnyHashable(groupItem.info.id),
title: groupItem.info.title,
subtitle: nil,
badge: nil,
actionButtonTitle: self.presentationData.strings.EmojiInput_AddPack(groupItem.info.title).string,
isFeatured: true,
isPremiumLocked: !self.hasPremiumForInstallation,
isEmbedded: false,
hasClear: false,
hasEdit: false,
collapsedLineCount: 3,
displayPremiumBadges: false,
headerItem: nil,
fillWithLoadingPlaceholders: false,
items: groupItems
))
}
self.inputInteractionHolder.inputInteraction = EmojiPagerContentComponent.InputInteraction(
performItemAction: { [weak self] groupId, item, sourceView, sourceRect, sourceLayer, isPreview in
guard let self else {
return
}
self.parentInputInteraction.performItemAction(groupId, item, sourceView, sourceRect, sourceLayer, isPreview)
if self.hasPremiumForUse {
self.onCancel?()
}
},
deleteBackwards: {
},
openStickerSettings: {
},
openFeatured: {
},
openSearch: {
},
addGroupAction: { [weak self] groupId, isPremiumLocked, _ in
guard let self else {
return
}
self.parentInputInteraction.addGroupAction(groupId, isPremiumLocked, false)
if !isPremiumLocked {
if self.itemGroups.count == 1 {
self.onCancel?()
} else {
self.itemGroups.removeAll(where: { $0.groupId == groupId })
self.update(transition: ComponentTransition(animation: .curve(duration: 0.4, curve: .spring)).withUserData(EmojiPagerContentComponent.ContentAnimation(type: .groupRemoved(id: groupId))))
}
}
},
clearGroup: { _ in
},
editAction: { _ in },
pushController: { _ in
},
presentController: { _ in
},
presentGlobalOverlayController: { _ in
},
navigationController: {
return nil
},
requestUpdate: { _ in
},
updateSearchQuery: { [weak self] query in
guard let self else {
return
}
switch query {
case .none:
self.emojiSearchDisposable.set(nil)
self.emojiSearchState.set(.single(EmojiSearchState(result: nil, isSearching: false)))
case let .text(rawQuery, languageCode):
let query = rawQuery.trimmingCharacters(in: .whitespacesAndNewlines)
if query.isEmpty {
self.emojiSearchDisposable.set(nil)
self.emojiSearchState.set(.single(EmojiSearchState(result: nil, isSearching: false)))
} else {
let context = self.context
var signal = context.engine.stickers.searchEmojiKeywords(inputLanguageCode: languageCode, query: query, completeMatch: false)
if !languageCode.lowercased().hasPrefix("en") {
signal = signal
|> mapToSignal { keywords in
return .single(keywords)
|> then(
context.engine.stickers.searchEmojiKeywords(inputLanguageCode: "en-US", query: query, completeMatch: query.count < 3)
|> map { englishKeywords in
return keywords + englishKeywords
}
)
}
}
let hasPremium = context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId))
|> map { peer -> Bool in
guard case let .user(user) = peer else {
return false
}
return user.isPremium
}
|> distinctUntilChanged
let resultSignal = signal
|> mapToSignal { keywords -> Signal<[EmojiPagerContentComponent.ItemGroup], NoError> in
return combineLatest(
context.account.postbox.itemCollectionsView(orderedItemListCollectionIds: [], namespaces: [Namespaces.ItemCollection.CloudEmojiPacks], aroundIndex: nil, count: 10000000),
context.engine.stickers.availableReactions(),
hasPremium
)
|> take(1)
|> map { view, availableReactions, hasPremium -> [EmojiPagerContentComponent.ItemGroup] in
var result: [(String, TelegramMediaFile.Accessor?, String)] = []
var allEmoticons: [String: String] = [:]
for keyword in keywords {
for emoticon in keyword.emoticons {
allEmoticons[emoticon] = keyword.keyword
}
}
for entry in view.entries {
guard let item = entry.item as? StickerPackItem else {
continue
}
if item.file.isCustomEmoji {
if !item.file.isPremiumEmoji || hasPremium {
if let alt = item.file.customEmojiAlt {
if !alt.isEmpty, let keyword = allEmoticons[alt] {
result.append((alt, item.file, keyword))
} else if alt == query {
result.append((alt, item.file, alt))
}
}
}
}
}
var items: [EmojiPagerContentComponent.Item] = []
var existingIds = Set<MediaId>()
for item in result {
if let itemFile = item.1 {
if existingIds.contains(itemFile.fileId) {
continue
}
existingIds.insert(itemFile.fileId)
let animationData = EntityKeyboardAnimationData(file: itemFile)
let item = EmojiPagerContentComponent.Item(
animationData: animationData,
content: .animation(animationData),
itemFile: itemFile, subgroupId: nil,
icon: .none,
tintMode: animationData.isTemplate ? .primary : .none
)
items.append(item)
}
}
return [EmojiPagerContentComponent.ItemGroup(
supergroupId: "search",
groupId: "search",
title: nil,
subtitle: nil,
badge: nil,
actionButtonTitle: nil,
isFeatured: false,
isPremiumLocked: false,
isEmbedded: false,
hasClear: false,
hasEdit: false,
collapsedLineCount: nil,
displayPremiumBadges: false,
headerItem: nil,
fillWithLoadingPlaceholders: false,
items: items
)]
}
}
var version = 0
self.emojiSearchStateValue.isSearching = true
self.emojiSearchDisposable.set((resultSignal
|> delay(0.15, queue: .mainQueue())
|> deliverOnMainQueue).start(next: { [weak self] result in
guard let self else {
return
}
self.emojiSearchStateValue = EmojiSearchState(result: EmojiSearchResult(groups: result, id: AnyHashable(query), version: version, isPreset: false), isSearching: false)
version += 1
}))
}
case let .category(value):
let resultSignal = self.context.engine.stickers.searchEmoji(category: value)
|> mapToSignal { files, isFinalResult -> Signal<(items: [EmojiPagerContentComponent.ItemGroup], isFinalResult: Bool), NoError> in
var items: [EmojiPagerContentComponent.Item] = []
var existingIds = Set<MediaId>()
for itemFile in files {
if existingIds.contains(itemFile.fileId) {
continue
}
existingIds.insert(itemFile.fileId)
let animationData = EntityKeyboardAnimationData(file: TelegramMediaFile.Accessor(itemFile))
let item = EmojiPagerContentComponent.Item(
animationData: animationData,
content: .animation(animationData),
itemFile: TelegramMediaFile.Accessor(itemFile),
subgroupId: nil,
icon: .none,
tintMode: animationData.isTemplate ? .primary : .none
)
items.append(item)
}
return .single(([EmojiPagerContentComponent.ItemGroup(
supergroupId: "search",
groupId: "search",
title: nil,
subtitle: nil,
badge: nil,
actionButtonTitle: nil,
isFeatured: false,
isPremiumLocked: false,
isEmbedded: false,
hasClear: false,
hasEdit: false,
collapsedLineCount: nil,
displayPremiumBadges: false,
headerItem: nil,
fillWithLoadingPlaceholders: false,
items: items
)], isFinalResult))
}
let _ = resultSignal
var version = 0
self.emojiSearchDisposable.set((resultSignal
|> deliverOnMainQueue).start(next: { [weak self] result in
guard let self else {
return
}
guard let group = result.items.first else {
return
}
if group.items.isEmpty && !result.isFinalResult {
//self.emojiSearchStateValue.isSearching = true
self.emojiSearchStateValue = EmojiSearchState(result: EmojiSearchResult(groups: [
EmojiPagerContentComponent.ItemGroup(
supergroupId: "search",
groupId: "search",
title: nil,
subtitle: nil,
badge: nil,
actionButtonTitle: nil,
isFeatured: false,
isPremiumLocked: false,
isEmbedded: false,
hasClear: false,
hasEdit: false,
collapsedLineCount: nil,
displayPremiumBadges: false,
headerItem: nil,
fillWithLoadingPlaceholders: true,
items: []
)
], id: AnyHashable(value.id), version: version, isPreset: true), isSearching: false)
return
}
self.emojiSearchStateValue = EmojiSearchState(result: EmojiSearchResult(groups: result.items, id: AnyHashable(value.id), version: version, isPreset: false), isSearching: false)
version += 1
}))
}
},
updateScrollingToItemGroup: {
},
externalCancel: { [weak self] in
guard let self else {
return
}
self.onCancel?()
},
onScroll: {},
chatPeerId: nil,
peekBehavior: nil,
customLayout: nil,
externalBackground: nil,
externalExpansionView: nil,
customContentView: nil,
useOpaqueTheme: true,
hideBackground: false,
stateContext: nil,
addImage: nil
)
self.dataDisposable = (
self.emojiSearchState.get()
|> deliverOnMainQueue
).start(next: { [weak self] emojiSearchState in
guard let self else {
return
}
self.immediateEmojiSearchState = emojiSearchState
self.update(transition: .immediate)
})
}
deinit {
self.emojiSearchDisposable.dispose()
self.dataDisposable?.dispose()
}
private func update(transition: ComponentTransition) {
if let params = self.params {
self.update(size: params.size, leftInset: params.leftInset, rightInset: params.rightInset, bottomInset: params.bottomInset, inputHeight: params.inputHeight, deviceMetrics: params.deviceMetrics, transition: transition)
}
}
public func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, inputHeight: CGFloat, deviceMetrics: DeviceMetrics, transition: ContainedViewLayoutTransition) {
self.update(size: size, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, inputHeight: inputHeight, deviceMetrics: deviceMetrics, transition: ComponentTransition(transition))
}
private func update(size: CGSize, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, inputHeight: CGFloat, deviceMetrics: DeviceMetrics, transition: ComponentTransition) {
self.backgroundColor = self.presentationData.theme.list.plainBackgroundColor
let params = Params(size: size, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, inputHeight: inputHeight, deviceMetrics: deviceMetrics)
self.params = params
var emojiContent = EmojiPagerContentComponent(
id: "emoji",
context: self.context,
avatarPeer: nil,
animationCache: self.context.animationCache,
animationRenderer: self.context.animationRenderer,
inputInteractionHolder: self.inputInteractionHolder,
panelItemGroups: [],
contentItemGroups: self.itemGroups,
itemLayoutType: .compact,
itemContentUniqueId: EmojiPagerContentComponent.ContentId(id: "main", version: 0),
searchState: .empty(hasResults: false),
warpContentsOnEdges: false,
hideBackground: false,
maskEdge: .none,
displaySearchWithPlaceholder: self.presentationData.strings.EmojiSearch_SearchEmojiPlaceholder,
searchCategories: nil,
searchInitiallyHidden: false,
searchAlwaysActive: true,
searchIsPlaceholderOnly: false,
searchUnicodeEmojiOnly: false,
emptySearchResults: nil,
enableLongPress: false,
selectedItems: Set(),
customTintColor: nil
)
if let emojiSearchResult = self.immediateEmojiSearchState.result {
var emptySearchResults: EmojiPagerContentComponent.EmptySearchResults?
if !emojiSearchResult.groups.contains(where: { !$0.items.isEmpty || $0.fillWithLoadingPlaceholders }) {
emptySearchResults = EmojiPagerContentComponent.EmptySearchResults(
text: self.presentationData.strings.EmojiSearch_SearchEmojiEmptyResult,
iconFile: nil
)
}
emojiContent = emojiContent.withUpdatedItemGroups(panelItemGroups: emojiContent.panelItemGroups, contentItemGroups: emojiSearchResult.groups, itemContentUniqueId: EmojiPagerContentComponent.ContentId(id: emojiSearchResult.id, version: emojiSearchResult.version), emptySearchResults: emptySearchResults, searchState: self.immediateEmojiSearchState.isSearching ? .searching : .empty(hasResults: true))
}
let _ = self.keyboardView.update(
transition: transition.withUserData(EmojiPagerContentComponent.SynchronousLoadBehavior(isDisabled: true)),
component: AnyComponent(EntityKeyboardComponent(
theme: self.presentationData.theme,
strings: self.presentationData.strings,
isContentInFocus: true,
containerInsets: UIEdgeInsets(top: 0.0, left: leftInset, bottom: bottomInset, right: rightInset),
topPanelInsets: UIEdgeInsets(top: 0.0, left: 4.0, bottom: 0.0, right: 4.0),
emojiContent: emojiContent,
stickerContent: nil,
maskContent: nil,
gifContent: nil,
hasRecentGifs: false,
availableGifSearchEmojies: [],
defaultToEmojiTab: true,
externalTopPanelContainer: self.panelHostView,
externalBottomPanelContainer: nil,
externalTintMaskContainer: nil,
displayTopPanelBackground: .blur,
topPanelExtensionUpdated: { _, _ in },
topPanelScrollingOffset: { _, _ in },
hideInputUpdated: { _, _, _ in },
hideTopPanelUpdated: { _, _ in
},
switchToTextInput: {},
switchToGifSubject: { _ in },
reorderItems: { _, _ in },
makeSearchContainerNode: { _ in return nil },
contentIdUpdated: { _ in },
deviceMetrics: deviceMetrics,
hiddenInputHeight: 0.0,
inputHeight: 0.0,
displayBottomPanel: false,
isExpanded: false,
clipContentToTopPanel: false,
useExternalSearchContainer: false,
hidePanels: true
)),
environment: {},
containerSize: size
)
if let keyboardComponentView = self.keyboardView.view as? EntityKeyboardComponent.View {
if keyboardComponentView.superview == nil {
self.view.addSubview(keyboardComponentView)
}
transition.setFrame(view: keyboardComponentView, frame: CGRect(origin: CGPoint(), size: size))
if let initialFocusId = self.initialFocusId {
self.initialFocusId = nil
keyboardComponentView.scrollToItemGroup(contentId: "emoji", groupId: AnyHashable(initialFocusId), subgroupId: nil, animated: false)
}
}
}
}
@@ -0,0 +1,633 @@
import Foundation
import UIKit
import Display
import ComponentFlow
import TelegramCore
import TelegramPresentationData
import AccountContext
import SwiftSignalKit
public final class EmojiSearchHeaderView: UIView, UITextFieldDelegate {
private final class EmojiSearchTextField: UITextField {
override func textRect(forBounds bounds: CGRect) -> CGRect {
return bounds.integral
}
}
private struct Params: Equatable {
var context: AccountContext
var theme: PresentationTheme
var forceNeedsVibrancy: Bool
var strings: PresentationStrings
var text: String
var useOpaqueTheme: Bool
var isActive: Bool
var hasPresetSearch: Bool
var textInputState: EmojiSearchSearchBarComponent.TextInputState
var searchState: EmojiPagerContentComponent.SearchState
var size: CGSize
var canFocus: Bool
var searchCategories: EmojiSearchCategories?
static func ==(lhs: Params, rhs: Params) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.theme !== rhs.theme {
return false
}
if lhs.forceNeedsVibrancy != rhs.forceNeedsVibrancy {
return false
}
if lhs.strings !== rhs.strings {
return false
}
if lhs.text != rhs.text {
return false
}
if lhs.useOpaqueTheme != rhs.useOpaqueTheme {
return false
}
if lhs.isActive != rhs.isActive {
return false
}
if lhs.hasPresetSearch != rhs.hasPresetSearch {
return false
}
if lhs.textInputState != rhs.textInputState {
return false
}
if lhs.searchState != rhs.searchState {
return false
}
if lhs.size != rhs.size {
return false
}
if lhs.canFocus != rhs.canFocus {
return false
}
if lhs.searchCategories != rhs.searchCategories {
return false
}
return true
}
}
override public static var layerClass: AnyClass {
return PassthroughLayer.self
}
private let activated: (Bool) -> Void
private let deactivated: (Bool) -> Void
private let updateQuery: (EmojiPagerContentComponent.SearchQuery?) -> Void
let tintContainerView: UIView
private let backgroundLayer: SimpleLayer
private let tintBackgroundLayer: SimpleLayer
private let statusIcon = ComponentView<Empty>()
private let clearIconView: UIImageView
private let clearIconTintView: UIImageView
private let clearIconButton: HighlightTrackingButton
private let cancelButtonTintTitle: ComponentView<Empty>
private let cancelButtonTitle: ComponentView<Empty>
private let cancelButton: HighlightTrackingButton
private var placeholderContent = ComponentView<Empty>()
private var textFrame: CGRect?
private var textField: EmojiSearchTextField?
private var tapRecognizer: UITapGestureRecognizer?
private(set) var currentPresetSearchTerm: EmojiSearchCategories.Group?
private var params: Params?
public var wantsDisplayBelowKeyboard: Bool {
return self.textField != nil
}
init(activated: @escaping (Bool) -> Void, deactivated: @escaping (Bool) -> Void, updateQuery: @escaping (EmojiPagerContentComponent.SearchQuery?) -> Void) {
self.activated = activated
self.deactivated = deactivated
self.updateQuery = updateQuery
self.tintContainerView = UIView()
self.backgroundLayer = SimpleLayer()
self.tintBackgroundLayer = SimpleLayer()
self.clearIconView = UIImageView()
self.clearIconTintView = UIImageView()
self.clearIconButton = HighlightableButton()
self.clearIconView.isHidden = true
self.clearIconTintView.isHidden = true
self.clearIconButton.isHidden = true
self.cancelButtonTintTitle = ComponentView()
self.cancelButtonTitle = ComponentView()
self.cancelButton = HighlightTrackingButton()
super.init(frame: CGRect())
self.layer.addSublayer(self.backgroundLayer)
self.tintContainerView.layer.addSublayer(self.tintBackgroundLayer)
self.addSubview(self.clearIconView)
self.tintContainerView.addSubview(self.clearIconTintView)
self.addSubview(self.clearIconButton)
self.addSubview(self.cancelButton)
self.clipsToBounds = true
(self.layer as? PassthroughLayer)?.mirrorLayer = self.tintContainerView.layer
let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))
self.tapRecognizer = tapRecognizer
self.addGestureRecognizer(tapRecognizer)
self.cancelButton.highligthedChanged = { [weak self] highlighted in
if let strongSelf = self {
if highlighted {
if let cancelButtonTitleView = strongSelf.cancelButtonTitle.view {
cancelButtonTitleView.layer.removeAnimation(forKey: "opacity")
cancelButtonTitleView.alpha = 0.4
}
if let cancelButtonTintTitleView = strongSelf.cancelButtonTintTitle.view {
cancelButtonTintTitleView.layer.removeAnimation(forKey: "opacity")
cancelButtonTintTitleView.alpha = 0.4
}
} else {
if let cancelButtonTitleView = strongSelf.cancelButtonTitle.view {
cancelButtonTitleView.alpha = 1.0
cancelButtonTitleView.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
}
if let cancelButtonTintTitleView = strongSelf.cancelButtonTintTitle.view {
cancelButtonTintTitleView.alpha = 1.0
cancelButtonTintTitleView.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
}
}
}
}
self.cancelButton.addTarget(self, action: #selector(self.cancelPressed), for: .touchUpInside)
self.clearIconButton.highligthedChanged = { [weak self] highlighted in
if let strongSelf = self {
if highlighted {
strongSelf.clearIconView.layer.removeAnimation(forKey: "opacity")
strongSelf.clearIconView.alpha = 0.4
strongSelf.clearIconTintView.layer.removeAnimation(forKey: "opacity")
strongSelf.clearIconTintView.alpha = 0.4
} else {
strongSelf.clearIconView.alpha = 1.0
strongSelf.clearIconView.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
strongSelf.clearIconTintView.alpha = 1.0
strongSelf.clearIconTintView.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
}
}
}
self.clearIconButton.addTarget(self, action: #selector(self.clearPressed), for: .touchUpInside)
}
required public init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@objc private func tapGesture(_ recognizer: UITapGestureRecognizer) {
if case .ended = recognizer.state {
let location = recognizer.location(in: self)
if let view = self.statusIcon.view, view.frame.contains(location), self.currentPresetSearchTerm != nil {
self.clearCategorySearch()
} else {
self.activateTextInput()
}
}
}
func clearCategorySearch() {
if let placeholderContentView = self.placeholderContent.view as? EmojiSearchSearchBarComponent.View {
placeholderContentView.clearSelection(dispatchEvent : true)
}
}
override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if let textField = self.textField, let text = textField.text, text.isEmpty {
if self.bounds.contains(point), let placeholderContentView = self.placeholderContent.view as? EmojiSearchSearchBarComponent.View {
let leftTextPosition = placeholderContentView.leftTextPosition()
if point.x >= placeholderContentView.frame.minX + leftTextPosition {
if let result = placeholderContentView.hitTest(self.convert(point, to: placeholderContentView), with: event) {
return result
}
}
}
}
return super.hitTest(point, with: event)
}
private func activateTextInput() {
guard let params = self.params else {
return
}
if self.textField == nil, let textFrame = self.textFrame, params.canFocus == true {
let backgroundFrame = self.backgroundLayer.frame
let textFieldFrame = CGRect(origin: CGPoint(x: textFrame.minX, y: backgroundFrame.minY), size: CGSize(width: backgroundFrame.maxX - textFrame.minX, height: backgroundFrame.height))
let textField = EmojiSearchTextField(frame: textFieldFrame)
textField.keyboardAppearance = params.theme.rootController.keyboardColor.keyboardAppearance
textField.autocorrectionType = .no
textField.returnKeyType = .search
self.textField = textField
if let placeholderContentView = self.placeholderContent.view {
self.insertSubview(textField, belowSubview: placeholderContentView)
} else {
self.insertSubview(textField, belowSubview: self.clearIconView)
}
textField.delegate = self
textField.addTarget(self, action: #selector(self.textFieldChanged(_:)), for: .editingChanged)
}
if params.canFocus {
self.currentPresetSearchTerm = nil
if let placeholderContentView = self.placeholderContent.view as? EmojiSearchSearchBarComponent.View {
placeholderContentView.clearSelection(dispatchEvent: false)
}
}
self.activated(true)
self.textField?.becomeFirstResponder()
}
@objc private func cancelPressed() {
let textField = self.textField
self.textField = nil
self.currentPresetSearchTerm = nil
self.updateQuery(nil)
self.clearIconView.isHidden = true
self.clearIconTintView.isHidden = true
self.clearIconButton.isHidden = true
self.deactivated(textField?.isFirstResponder ?? false)
if let textField {
textField.resignFirstResponder()
textField.removeFromSuperview()
}
}
@objc private func clearPressed() {
self.currentPresetSearchTerm = nil
self.updateQuery(nil)
self.textField?.text = ""
self.clearIconView.isHidden = true
self.clearIconTintView.isHidden = true
self.clearIconButton.isHidden = true
/*self.tintTextView.view?.isHidden = false
self.textView.view?.isHidden = false*/
}
var isActive: Bool {
return self.textField?.isFirstResponder ?? false
}
func deactivate() {
if let text = self.textField?.text, !text.isEmpty {
self.textField?.endEditing(true)
} else {
self.cancelPressed()
}
}
public func textFieldDidBeginEditing(_ textField: UITextField) {
}
public func textFieldDidEndEditing(_ textField: UITextField) {
}
public func textFieldShouldReturn(_ textField: UITextField) -> Bool {
textField.endEditing(true)
return false
}
@objc private func textFieldChanged(_ textField: UITextField) {
self.update(transition: .immediate)
let text = textField.text ?? ""
var inputLanguage = textField.textInputMode?.primaryLanguage ?? "en"
if let range = inputLanguage.range(of: "-") {
inputLanguage = String(inputLanguage[inputLanguage.startIndex ..< range.lowerBound])
}
if let range = inputLanguage.range(of: "_") {
inputLanguage = String(inputLanguage[inputLanguage.startIndex ..< range.lowerBound])
}
self.clearIconView.isHidden = text.isEmpty
self.clearIconTintView.isHidden = text.isEmpty
self.clearIconButton.isHidden = text.isEmpty
self.currentPresetSearchTerm = nil
self.updateQuery(.text(value: text, language: inputLanguage))
}
private func update(transition: ComponentTransition) {
guard let params = self.params else {
return
}
self.params = nil
self.update(context: params.context, theme: params.theme, forceNeedsVibrancy: params.forceNeedsVibrancy, strings: params.strings, text: params.text, useOpaqueTheme: params.useOpaqueTheme, isActive: params.isActive, size: params.size, canFocus: params.canFocus, searchCategories: params.searchCategories, searchState: params.searchState, transition: transition)
}
public func update(context: AccountContext, theme: PresentationTheme, forceNeedsVibrancy: Bool, strings: PresentationStrings, text: String, useOpaqueTheme: Bool, isActive: Bool, size: CGSize, canFocus: Bool, searchCategories: EmojiSearchCategories?, searchState: EmojiPagerContentComponent.SearchState, transition: ComponentTransition) {
let textInputState: EmojiSearchSearchBarComponent.TextInputState
if let textField = self.textField {
textInputState = .active(hasText: !(textField.text ?? "").isEmpty)
} else {
textInputState = .inactive
}
let params = Params(
context: context,
theme: theme,
forceNeedsVibrancy: forceNeedsVibrancy,
strings: strings,
text: text,
useOpaqueTheme: useOpaqueTheme,
isActive: isActive,
hasPresetSearch: self.currentPresetSearchTerm == nil,
textInputState: textInputState,
searchState: searchState,
size: size,
canFocus: canFocus,
searchCategories: searchCategories
)
if self.params == params {
return
}
let isActiveWithText = isActive && self.currentPresetSearchTerm == nil
if self.params?.theme !== theme {
/*self.searchIconView.image = generateTintedImage(image: UIImage(bundleImageName: "Components/Search Bar/Loupe"), color: .white)?.withRenderingMode(.alwaysTemplate)
self.searchIconView.tintColor = useOpaqueTheme ? theme.chat.inputMediaPanel.panelContentOpaqueSearchOverlayColor : theme.chat.inputMediaPanel.panelContentVibrantSearchOverlayColor
self.searchIconTintView.image = generateTintedImage(image: UIImage(bundleImageName: "Components/Search Bar/Loupe"), color: .white)
self.backIconView.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Back"), color: .white)?.withRenderingMode(.alwaysTemplate)
self.backIconView.tintColor = useOpaqueTheme ? theme.chat.inputMediaPanel.panelContentOpaqueSearchOverlayColor : theme.chat.inputMediaPanel.panelContentVibrantSearchOverlayColor
self.backIconTintView.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Back"), color: .white)*/
self.clearIconView.image = generateTintedImage(image: UIImage(bundleImageName: "Components/Search Bar/Clear"), color: .white)?.withRenderingMode(.alwaysTemplate)
self.clearIconView.tintColor = useOpaqueTheme ? theme.chat.inputMediaPanel.panelContentOpaqueSearchOverlayColor : theme.chat.inputMediaPanel.panelContentVibrantSearchOverlayColor
self.clearIconTintView.image = generateTintedImage(image: UIImage(bundleImageName: "Components/Search Bar/Clear"), color: .black)
}
self.params = params
let sideInset: CGFloat = 12.0
let topInset: CGFloat = 8.0
let inputHeight: CGFloat = 36.0
let sideTextInset: CGFloat = sideInset + 4.0 + 24.0
if theme.overallDarkAppearance && forceNeedsVibrancy {
self.backgroundLayer.backgroundColor = theme.chat.inputMediaPanel.panelContentControlVibrantSelectionColor.withMultipliedAlpha(0.3).cgColor
self.tintBackgroundLayer.backgroundColor = UIColor(white: 0.0, alpha: 0.2).cgColor
} else if useOpaqueTheme {
self.backgroundLayer.backgroundColor = theme.chat.inputMediaPanel.panelContentControlOpaqueSelectionColor.cgColor
self.tintBackgroundLayer.backgroundColor = UIColor.black.cgColor
} else {
self.backgroundLayer.backgroundColor = theme.chat.inputMediaPanel.panelContentControlVibrantSelectionColor.cgColor
self.tintBackgroundLayer.backgroundColor = UIColor(white: 0.0, alpha: 0.2).cgColor
}
self.backgroundLayer.cornerRadius = inputHeight * 0.5
self.tintBackgroundLayer.cornerRadius = inputHeight * 0.5
let cancelColor: UIColor
if theme.overallDarkAppearance && forceNeedsVibrancy {
cancelColor = theme.chat.inputMediaPanel.panelContentVibrantSearchOverlayColor.withMultipliedAlpha(0.3)
} else {
cancelColor = useOpaqueTheme ? theme.list.itemAccentColor : theme.chat.inputMediaPanel.panelContentVibrantSearchOverlayColor
}
let cancelTextSize = self.cancelButtonTitle.update(
transition: .immediate,
component: AnyComponent(Text(
text: strings.Common_Cancel,
font: Font.regular(17.0),
color: cancelColor
)),
environment: {},
containerSize: CGSize(width: size.width - 32.0, height: 100.0)
)
let _ = self.cancelButtonTintTitle.update(
transition: .immediate,
component: AnyComponent(Text(
text: strings.Common_Cancel,
font: Font.regular(17.0),
color: .black
)),
environment: {},
containerSize: CGSize(width: size.width - 32.0, height: 100.0)
)
let cancelButtonSpacing: CGFloat = 8.0
var backgroundFrame = CGRect(origin: CGPoint(x: sideInset, y: topInset), size: CGSize(width: size.width - sideInset * 2.0, height: inputHeight))
if isActiveWithText {
backgroundFrame.size.width -= cancelTextSize.width + cancelButtonSpacing
}
transition.setFrame(layer: self.backgroundLayer, frame: backgroundFrame)
transition.setFrame(layer: self.tintBackgroundLayer, frame: backgroundFrame)
transition.setFrame(view: self.cancelButton, frame: CGRect(origin: CGPoint(x: backgroundFrame.maxX, y: 0.0), size: CGSize(width: cancelButtonSpacing + cancelTextSize.width, height: size.height)))
let textX: CGFloat = backgroundFrame.minX + sideTextInset
let textFrame = CGRect(origin: CGPoint(x: textX, y: backgroundFrame.minY), size: CGSize(width: backgroundFrame.maxX - textX, height: backgroundFrame.height))
self.textFrame = textFrame
let statusContent: EmojiSearchStatusComponent.Content
switch searchState {
case .empty:
statusContent = .search
case .searching:
statusContent = .progress
case .active:
statusContent = .results
}
let statusSize = CGSize(width: 24.0, height: 24.0)
let _ = self.statusIcon.update(
transition: transition,
component: AnyComponent(EmojiSearchStatusComponent(
theme: theme,
forceNeedsVibrancy: forceNeedsVibrancy,
strings: strings,
useOpaqueTheme: useOpaqueTheme,
content: statusContent
)),
environment: {},
containerSize: statusSize
)
let iconFrame = CGRect(origin: CGPoint(x: textFrame.minX - statusSize.width - 4.0, y: backgroundFrame.minY + floor((backgroundFrame.height - statusSize.height) / 2.0)), size: statusSize)
if let statusIconView = self.statusIcon.view as? EmojiSearchStatusComponent.View {
if statusIconView.superview == nil {
self.addSubview(statusIconView)
self.tintContainerView.addSubview(statusIconView.tintContainerView)
}
transition.setFrame(view: statusIconView, frame: iconFrame)
transition.setFrame(view: statusIconView.tintContainerView, frame: iconFrame)
}
let placeholderContentFrame = CGRect(origin: CGPoint(x: textFrame.minX - 6.0, y: backgroundFrame.minY), size: CGSize(width: backgroundFrame.maxX - (textFrame.minX - 6.0), height: backgroundFrame.height))
let _ = self.placeholderContent.update(
transition: transition,
component: AnyComponent(EmojiSearchSearchBarComponent(
context: context,
theme: theme,
forceNeedsVibrancy: forceNeedsVibrancy,
strings: strings,
useOpaqueTheme: useOpaqueTheme,
textInputState: textInputState,
categories: searchCategories,
searchTermUpdated: { [weak self] term in
guard let self else {
return
}
var shouldChangeActivation = false
if (self.currentPresetSearchTerm == nil) != (term == nil) {
shouldChangeActivation = true
}
self.currentPresetSearchTerm = term
if shouldChangeActivation {
if let term {
self.update(transition: ComponentTransition(animation: .curve(duration: 0.4, curve: .spring)))
let textField = self.textField
self.textField = nil
self.clearIconView.isHidden = true
self.clearIconTintView.isHidden = true
self.clearIconButton.isHidden = true
self.updateQuery(.category(value: term))
self.activated(false)
if let textField {
textField.resignFirstResponder()
textField.removeFromSuperview()
}
} else {
self.deactivated(self.textField?.isFirstResponder ?? false)
self.updateQuery(nil)
}
} else {
if let term {
self.updateQuery(.category(value: term))
} else {
self.updateQuery(nil)
}
}
},
activateTextInput: { [weak self] in
guard let self else {
return
}
self.activateTextInput()
}
)),
environment: {},
containerSize: placeholderContentFrame.size
)
if let placeholderContentView = self.placeholderContent.view as? EmojiSearchSearchBarComponent.View {
if placeholderContentView.superview == nil {
self.addSubview(placeholderContentView)
self.tintContainerView.addSubview(placeholderContentView.tintContainerView)
}
transition.setFrame(view: placeholderContentView, frame: placeholderContentFrame)
transition.setFrame(view: placeholderContentView.tintContainerView, frame: placeholderContentFrame)
}
/*if let searchCategories {
let suggestedItemsView: ComponentView<Empty>
var suggestedItemsTransition = transition
if let current = self.suggestedItemsView {
suggestedItemsView = current
} else {
suggestedItemsTransition = .immediate
suggestedItemsView = ComponentView()
self.suggestedItemsView = suggestedItemsView
}
let itemsX: CGFloat = textFrame.maxX + 8.0
let suggestedItemsFrame = CGRect(origin: CGPoint(x: itemsX, y: backgroundFrame.minY), size: CGSize(width: backgroundFrame.maxX - itemsX, height: backgroundFrame.height))
if let suggestedItemsComponentView = suggestedItemsView.view {
if suggestedItemsComponentView.superview == nil {
self.addSubview(suggestedItemsComponentView)
}
suggestedItemsTransition.setFrame(view: suggestedItemsComponentView, frame: suggestedItemsFrame)
suggestedItemsTransition.setAlpha(view: suggestedItemsComponentView, alpha: isActiveWithText ? 0.0 : 1.0)
}
} else {
if let suggestedItemsView = self.suggestedItemsView {
self.suggestedItemsView = nil
if let suggestedItemsComponentView = suggestedItemsView.view {
transition.setAlpha(view: suggestedItemsComponentView, alpha: 0.0, completion: { [weak suggestedItemsComponentView] _ in
suggestedItemsComponentView?.removeFromSuperview()
})
}
}
}*/
if let image = self.clearIconView.image {
let iconFrame = CGRect(origin: CGPoint(x: backgroundFrame.maxX - image.size.width - 4.0, y: backgroundFrame.minY + floor((backgroundFrame.height - image.size.height) / 2.0)), size: image.size)
transition.setFrame(view: self.clearIconView, frame: iconFrame)
transition.setFrame(view: self.clearIconTintView, frame: iconFrame)
transition.setFrame(view: self.clearIconButton, frame: iconFrame.insetBy(dx: -8.0, dy: -10.0))
}
if let cancelButtonTitleComponentView = self.cancelButtonTitle.view {
if cancelButtonTitleComponentView.superview == nil {
self.addSubview(cancelButtonTitleComponentView)
cancelButtonTitleComponentView.isUserInteractionEnabled = false
}
transition.setFrame(view: cancelButtonTitleComponentView, frame: CGRect(origin: CGPoint(x: backgroundFrame.maxX + cancelButtonSpacing, y: floor((size.height - cancelTextSize.height) / 2.0)), size: cancelTextSize))
transition.setAlpha(view: cancelButtonTitleComponentView, alpha: isActiveWithText ? 1.0 : 0.0)
}
if let cancelButtonTintTitleComponentView = self.cancelButtonTintTitle.view {
if cancelButtonTintTitleComponentView.superview == nil {
self.tintContainerView.addSubview(cancelButtonTintTitleComponentView)
cancelButtonTintTitleComponentView.isUserInteractionEnabled = false
}
transition.setFrame(view: cancelButtonTintTitleComponentView, frame: CGRect(origin: CGPoint(x: backgroundFrame.maxX + cancelButtonSpacing, y: floor((size.height - cancelTextSize.height) / 2.0)), size: cancelTextSize))
transition.setAlpha(view: cancelButtonTintTitleComponentView, alpha: isActiveWithText ? 1.0 : 0.0)
}
var hasText = false
if let textField = self.textField {
textField.textColor = theme.contextMenu.primaryColor
transition.setFrame(view: textField, frame: CGRect(origin: CGPoint(x: backgroundFrame.minX + sideTextInset, y: backgroundFrame.minY), size: CGSize(width: backgroundFrame.width - sideTextInset - 32.0, height: backgroundFrame.height)))
if let text = textField.text, !text.isEmpty {
hasText = true
}
}
let _ = hasText
}
}
@@ -0,0 +1,752 @@
import Foundation
import UIKit
import Display
import ComponentFlow
import PagerComponent
import TelegramPresentationData
import TelegramCore
import Postbox
import AnimationCache
import MultiAnimationRenderer
import AccountContext
import AsyncDisplayKit
import ComponentDisplayAdapters
import LottieAnimationComponent
import EmojiStatusComponent
import LottieComponent
import LottieComponentEmojiContent
import AudioToolbox
private final class RoundMaskView: UIImageView {
private var currentDiameter: CGFloat?
func update(diameter: CGFloat) {
if self.currentDiameter != diameter {
self.currentDiameter = diameter
let shadowWidth: CGFloat = 6.0
self.image = generateImage(CGSize(width: shadowWidth * 2.0 + diameter, height: diameter), rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
let shadowColor = UIColor.black
let stepCount = 10
var colors: [CGColor] = []
var locations: [CGFloat] = []
for i in 0 ... stepCount {
let t = CGFloat(i) / CGFloat(stepCount)
colors.append(shadowColor.withAlphaComponent(t * t).cgColor)
locations.append(t)
}
let gradient = CGGradient(colorsSpace: deviceColorSpace, colors: colors as CFArray, locations: &locations)!
let center = CGPoint(x: size.width / 2.0, y: size.height / 2.0)
let gradientWidth = shadowWidth
context.drawRadialGradient(gradient, startCenter: center, startRadius: size.width / 2.0, endCenter: center, endRadius: size.width / 2.0 - gradientWidth, options: [])
context.setFillColor(shadowColor.cgColor)
context.fillEllipse(in: CGRect(origin: CGPoint(x: shadowWidth, y: 0.0), size: CGSize(width: size.height, height: size.height)).insetBy(dx: -0.5, dy: -0.5))
})?.stretchableImage(withLeftCapWidth: Int(shadowWidth * 0.5 + diameter * 0.5), topCapHeight: Int(diameter * 0.5))
}
}
}
private final class HoldGestureRecognizer: UITapGestureRecognizer {
private var currentHighlightPoint: CGPoint?
var updateHighlight: ((CGPoint?) -> Void)?
override var state: UIGestureRecognizer.State {
didSet {
print("set state \(self.state)")
}
}
override func reset() {
super.reset()
if let _ = self.currentHighlightPoint {
self.currentHighlightPoint = nil
self.updateHighlight?(nil)
}
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesBegan(touches, with: event)
let point = touches.first?.location(in: self.view)
if self.currentHighlightPoint == nil {
self.currentHighlightPoint = point
self.updateHighlight?(point)
}
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesEnded(touches, with: event)
}
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesCancelled(touches, with: event)
}
}
final class EmojiSearchSearchBarComponent: Component {
enum TextInputState: Equatable {
case inactive
case active(hasText: Bool)
}
let context: AccountContext
let theme: PresentationTheme
let forceNeedsVibrancy: Bool
let strings: PresentationStrings
let useOpaqueTheme: Bool
let textInputState: TextInputState
let categories: EmojiSearchCategories?
let searchTermUpdated: (EmojiSearchCategories.Group?) -> Void
let activateTextInput: () -> Void
init(
context: AccountContext,
theme: PresentationTheme,
forceNeedsVibrancy: Bool,
strings: PresentationStrings,
useOpaqueTheme: Bool,
textInputState: TextInputState,
categories: EmojiSearchCategories?,
searchTermUpdated: @escaping (EmojiSearchCategories.Group?) -> Void,
activateTextInput: @escaping () -> Void
) {
self.context = context
self.theme = theme
self.forceNeedsVibrancy = forceNeedsVibrancy
self.strings = strings
self.useOpaqueTheme = useOpaqueTheme
self.textInputState = textInputState
self.categories = categories
self.searchTermUpdated = searchTermUpdated
self.activateTextInput = activateTextInput
}
static func ==(lhs: EmojiSearchSearchBarComponent, rhs: EmojiSearchSearchBarComponent) -> Bool {
if lhs.theme !== rhs.theme {
return false
}
if lhs.forceNeedsVibrancy != rhs.forceNeedsVibrancy {
return false
}
if lhs.strings !== rhs.strings {
return false
}
if lhs.useOpaqueTheme != rhs.useOpaqueTheme {
return false
}
if lhs.textInputState != rhs.textInputState {
return false
}
if lhs.categories != rhs.categories {
return false
}
return true
}
private struct ItemLayout {
let containerSize: CGSize
let itemCount: Int
let itemSize: CGSize
let itemSpacing: CGFloat
let contentSize: CGSize
let leftInset: CGFloat
let rightInset: CGFloat
let itemStartX: CGFloat
let textSpacing: CGFloat
let textFrame: CGRect
init(containerSize: CGSize, textSize: CGSize, itemCount: Int) {
self.containerSize = containerSize
self.itemCount = itemCount
self.itemSpacing = 11.0
self.leftInset = 8.0
self.rightInset = 8.0
self.itemSize = CGSize(width: 24.0, height: 24.0)
self.textSpacing = 11.0
self.textFrame = CGRect(origin: CGPoint(x: self.leftInset, y: floor((containerSize.height - textSize.height) * 0.5)), size: textSize)
let itemsWidth: CGFloat = self.itemSize.width * CGFloat(self.itemCount) + self.itemSpacing * CGFloat(max(0, self.itemCount - 1))
var itemStartX = self.textFrame.maxX + self.textSpacing
if itemStartX + itemsWidth + self.rightInset < containerSize.width {
itemStartX = containerSize.width - self.rightInset - itemsWidth
}
self.itemStartX = itemStartX
self.contentSize = CGSize(width: self.itemStartX + itemsWidth + self.rightInset, height: containerSize.height)
}
func visibleItems(for rect: CGRect) -> Range<Int>? {
let baseItemX: CGFloat = self.itemStartX
let offsetRect = rect.offsetBy(dx: -baseItemX, dy: 0.0)
var minVisibleIndex = Int(floor((offsetRect.minX - self.itemSpacing) / (self.itemSize.width + self.itemSpacing)))
minVisibleIndex = max(0, minVisibleIndex)
var maxVisibleIndex = Int(ceil((offsetRect.maxX - self.itemSpacing) / (self.itemSize.height + self.itemSpacing)))
maxVisibleIndex = min(maxVisibleIndex, self.itemCount - 1)
if minVisibleIndex <= maxVisibleIndex {
return minVisibleIndex ..< (maxVisibleIndex + 1)
} else {
return nil
}
}
func frame(at index: Int) -> CGRect {
return CGRect(origin: CGPoint(x: self.itemStartX + CGFloat(index) * (self.itemSize.width + self.itemSpacing), y: floor((self.containerSize.height - self.itemSize.height) * 0.5)), size: self.itemSize)
}
}
private final class ContentScrollView: UIScrollView, PagerExpandableScrollView {
override static var layerClass: AnyClass {
return EmojiPagerContentComponent.View.ContentScrollLayer.self
}
private let mirrorView: UIView
init(mirrorView: UIView) {
self.mirrorView = mirrorView
super.init(frame: CGRect())
(self.layer as? EmojiPagerContentComponent.View.ContentScrollLayer)?.mirrorLayer = mirrorView.layer
self.canCancelContentTouches = true
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func touchesShouldCancel(in view: UIView) -> Bool {
return true
}
}
private final class ItemView {
let view = ComponentView<Empty>()
let tintView = UIImageView()
init() {
}
}
final class View: UIView, UIScrollViewDelegate {
let tintContainerView: UIView
private let scrollView: ContentScrollView
private let tintScrollView: UIView
private let textView = ComponentView<Empty>()
private let textContainerView: UIView
private let tintTextView = ComponentView<Empty>()
private let tintTextContainerView: UIView
private var visibleItemViews: [AnyHashable: ItemView] = [:]
private let selectedItemBackground: SimpleLayer
private let selectedItemTintBackground: SimpleLayer
private var component: EmojiSearchSearchBarComponent?
private weak var componentState: EmptyComponentState?
private var itemLayout: ItemLayout?
private var ignoreScrolling: Bool = false
private let roundMaskView: RoundMaskView
private let tintRoundMaskView: RoundMaskView
private var highlightedItem: AnyHashable?
private var selectedItem: AnyHashable?
private var disableInteraction: Bool = false
private lazy var hapticFeedback: HapticFeedback = {
return HapticFeedback()
}()
override init(frame: CGRect) {
self.tintContainerView = UIView()
self.tintScrollView = UIView()
self.tintScrollView.clipsToBounds = true
self.scrollView = ContentScrollView(mirrorView: self.tintScrollView)
self.textContainerView = UIView()
self.textContainerView.isUserInteractionEnabled = false
self.tintTextContainerView = UIView()
self.tintTextContainerView.isUserInteractionEnabled = false
self.roundMaskView = RoundMaskView()
self.tintRoundMaskView = RoundMaskView()
self.selectedItemBackground = SimpleLayer()
self.selectedItemTintBackground = SimpleLayer()
super.init(frame: frame)
self.scrollView.delaysContentTouches = false
if #available(iOSApplicationExtension 11.0, iOS 11.0, *) {
self.scrollView.contentInsetAdjustmentBehavior = .never
}
if #available(iOS 13.0, *) {
self.scrollView.automaticallyAdjustsScrollIndicatorInsets = false
}
self.scrollView.showsVerticalScrollIndicator = true
self.scrollView.showsHorizontalScrollIndicator = false
self.scrollView.delegate = self
self.scrollView.clipsToBounds = true
self.scrollView.scrollsToTop = false
self.addSubview(self.scrollView)
self.addSubview(self.textContainerView)
self.tintContainerView.addSubview(self.tintScrollView)
self.tintContainerView.addSubview(self.tintTextContainerView)
self.mask = self.roundMaskView
self.tintContainerView.mask = self.tintRoundMaskView
self.scrollView.layer.addSublayer(self.selectedItemBackground)
self.tintScrollView.layer.addSublayer(self.selectedItemTintBackground)
let tapRecognizer = HoldGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))
tapRecognizer.updateHighlight = { [weak self] point in
guard let self else {
return
}
var highlightedItem: AnyHashable?
if let point = point {
let location = self.convert(point, to: self.scrollView)
for (id, itemView) in self.visibleItemViews {
if let itemComponentView = itemView.view.view, itemComponentView.frame.contains(location) {
highlightedItem = id
break
}
}
}
if self.highlightedItem != highlightedItem {
self.highlightedItem = highlightedItem
self.componentState?.updated(transition: .easeInOut(duration: 0.2))
}
}
self.addGestureRecognizer(tapRecognizer)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@objc private func tapGesture(_ recognizer: UITapGestureRecognizer) {
if case .ended = recognizer.state {
guard let component = self.component, let itemLayout = self.itemLayout else {
return
}
let location = recognizer.location(in: self.scrollView)
if (component.categories?.groups ?? []).isEmpty || location.x <= itemLayout.itemStartX - itemLayout.textSpacing {
component.activateTextInput()
} else {
for (id, itemView) in self.visibleItemViews {
if let itemComponentView = itemView.view.view, itemComponentView.frame.contains(location), let itemId = id.base as? Int64 {
if self.selectedItem == AnyHashable(id) {
self.selectedItem = nil
} else {
self.selectedItem = AnyHashable(id)
AudioServicesPlaySystemSound(0x450)
self.hapticFeedback.tap()
}
self.componentState?.updated(transition: .easeInOut(duration: 0.2))
if let _ = self.selectedItem, let categories = component.categories, let group = categories.groups.first(where: { $0.id == itemId }) {
component.searchTermUpdated(group)
if let itemComponentView = itemView.view.view {
var offset = self.scrollView.contentOffset.x
let maxDistance: CGFloat = 44.0
if itemComponentView.frame.maxX - offset > self.scrollView.bounds.width - maxDistance {
offset = itemComponentView.frame.maxX - (self.scrollView.bounds.width - maxDistance)
}
if itemComponentView.frame.minX - offset < maxDistance {
offset = itemComponentView.frame.minX - maxDistance
}
offset = max(0.0, min(offset, self.scrollView.contentSize.width - self.scrollView.bounds.width))
if offset != self.scrollView.contentOffset.x {
self.scrollView.setContentOffset(CGPoint(x: offset, y: 0.0), animated: true)
}
}
} else {
let transition = ComponentTransition(animation: .curve(duration: 0.4, curve: .spring))
transition.setBoundsOrigin(view: self.scrollView, origin: CGPoint())
self.updateScrolling(transition: transition, fromScrolling: false)
//self.scrollView.setContentOffset(CGPoint(), animated: true)
component.searchTermUpdated(nil)
}
break
}
}
}
}
}
func clearSelection(dispatchEvent: Bool) {
if self.selectedItem != nil {
self.selectedItem = nil
let transition = ComponentTransition(animation: .curve(duration: 0.4, curve: .spring))
transition.setBoundsOrigin(view: self.scrollView, origin: CGPoint())
self.updateScrolling(transition: transition, fromScrolling: false)
self.componentState?.updated(transition: transition)
if dispatchEvent {
self.component?.searchTermUpdated(nil)
}
}
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if !self.ignoreScrolling {
self.updateScrolling(transition: .immediate, fromScrolling: true)
}
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if self.disableInteraction {
for (_, itemView) in self.visibleItemViews {
if let itemComponentView = itemView.view.view {
if itemComponentView.bounds.contains(self.convert(point, to: itemComponentView)) {
return self
}
}
}
return nil
}
return super.hitTest(point, with: event)
}
func leftTextPosition() -> CGFloat {
guard let itemLayout = self.itemLayout else {
return 0.0
}
let visibleBounds = self.scrollView.bounds
return (itemLayout.itemStartX - itemLayout.textSpacing) + visibleBounds.minX
}
private func updateScrolling(transition: ComponentTransition, fromScrolling: Bool) {
guard let component = self.component, let itemLayout = self.itemLayout else {
return
}
let itemAlpha: CGFloat
switch component.textInputState {
case let .active(hasText):
if hasText {
itemAlpha = 0.0
} else {
itemAlpha = 1.0
}
case .inactive:
itemAlpha = 1.0
}
var validItemIds = Set<AnyHashable>()
let visibleBounds = self.scrollView.bounds
var animateAppearingItems = false
if fromScrolling {
animateAppearingItems = true
}
let items = component.categories?.groups ?? []
for i in 0 ..< items.count {
let itemFrame = itemLayout.frame(at: i)
if visibleBounds.intersects(itemFrame) {
let item = items[i]
validItemIds.insert(AnyHashable(item.id))
var animateItem = false
var itemTransition = transition
let itemView: ItemView
if let current = self.visibleItemViews[AnyHashable(item.id)] {
itemView = current
} else {
animateItem = animateAppearingItems
itemTransition = .immediate
itemView = ItemView()
self.visibleItemViews[AnyHashable(item.id)] = itemView
}
let color: UIColor
if component.theme.overallDarkAppearance && component.forceNeedsVibrancy {
let tempColor = self.selectedItem == AnyHashable(item.id) ? component.theme.chat.inputMediaPanel.panelContentVibrantSearchOverlaySelectedColor : component.theme.chat.inputMediaPanel.panelContentVibrantSearchOverlayColor
color = tempColor.withMultipliedAlpha(0.3)
} else if component.useOpaqueTheme {
color = self.selectedItem == AnyHashable(item.id) ? component.theme.chat.inputMediaPanel.panelContentOpaqueSearchOverlaySelectedColor : component.theme.chat.inputMediaPanel.panelContentOpaqueSearchOverlayColor
} else {
color = self.selectedItem == AnyHashable(item.id) ? component.theme.chat.inputMediaPanel.panelContentVibrantSearchOverlaySelectedColor : component.theme.chat.inputMediaPanel.panelContentVibrantSearchOverlayColor
}
let _ = itemView.view.update(
transition: .immediate,
component: AnyComponent(LottieComponent(
content: LottieComponent.EmojiContent(
context: component.context,
fileId: item.id
),
color: color
)),
environment: {},
containerSize: itemLayout.itemSize
)
itemView.tintView.tintColor = .black
if let view = itemView.view.view as? LottieComponent.View {
if view.superview == nil {
self.scrollView.addSubview(view)
view.output = itemView.tintView
self.tintScrollView.addSubview(itemView.tintView)
}
itemTransition.setPosition(view: view, position: CGPoint(x: itemFrame.midX, y: itemFrame.midY))
itemTransition.setBounds(view: view, bounds: CGRect(origin: CGPoint(), size: CGSize(width: itemLayout.itemSize.width, height: itemLayout.itemSize.height)))
var scaleFactor = itemFrame.width / itemLayout.itemSize.width
if self.highlightedItem == AnyHashable(item.id) {
scaleFactor *= 0.8
}
itemTransition.setScale(view: view, scale: scaleFactor)
itemTransition.setPosition(view: itemView.tintView, position: CGPoint(x: itemFrame.midX, y: itemFrame.midY))
itemTransition.setBounds(view: itemView.tintView, bounds: CGRect(origin: CGPoint(), size: CGSize(width: itemLayout.itemSize.width, height: itemLayout.itemSize.height)))
itemTransition.setScale(view: itemView.tintView, scale: scaleFactor)
itemTransition.setAlpha(view: view, alpha: itemAlpha)
itemTransition.setAlpha(view: itemView.tintView, alpha: itemAlpha)
let isHidden = !visibleBounds.intersects(itemFrame)
if isHidden != view.isHidden {
view.isHidden = isHidden
itemView.tintView.isHidden = true
if !isHidden {
view.playOnce()
}
} else if animateItem {
if fromScrolling {
view.playOnce(delay: 0.08)
}
}
}
}
}
var removedItemIds: [AnyHashable] = []
for (id, itemView) in self.visibleItemViews {
if !validItemIds.contains(id) {
removedItemIds.append(id)
if let itemComponentView = itemView.view.view {
transition.attachAnimation(view: itemComponentView, id: "remove", completion: { [weak itemComponentView] _ in
itemComponentView?.removeFromSuperview()
})
}
let tintView = itemView.tintView
transition.attachAnimation(view: tintView, id: "remove", completion: { [weak tintView] _ in
tintView?.removeFromSuperview()
})
//itemView.view.view?.removeFromSuperview()
//itemView.tintView.removeFromSuperview()
}
}
for id in removedItemIds {
self.visibleItemViews.removeValue(forKey: id)
}
let selectedColor: UIColor
if component.theme.overallDarkAppearance && component.forceNeedsVibrancy {
let tempColor = component.useOpaqueTheme ? component.theme.chat.inputMediaPanel.panelContentOpaqueSearchOverlayHighlightColor : component.theme.chat.inputMediaPanel.panelContentVibrantSearchOverlayHighlightColor
selectedColor = tempColor.withMultipliedAlpha(0.3)
} else {
selectedColor = component.useOpaqueTheme ? component.theme.chat.inputMediaPanel.panelContentOpaqueSearchOverlayHighlightColor : component.theme.chat.inputMediaPanel.panelContentVibrantSearchOverlayHighlightColor
}
if let selectedItem = self.selectedItem, let index = items.firstIndex(where: { AnyHashable($0.id) == selectedItem }) {
let selectedItemCenter = itemLayout.frame(at: index).center
let selectionSize = CGSize(width: 28.0, height: 28.0)
self.selectedItemBackground.backgroundColor = selectedColor.cgColor
self.selectedItemTintBackground.backgroundColor = UIColor(white: 0.0, alpha: 0.15).cgColor
self.selectedItemBackground.cornerRadius = selectionSize.height * 0.5
self.selectedItemTintBackground.cornerRadius = selectionSize.height * 0.5
let selectionFrame = CGRect(origin: CGPoint(x: floor(selectedItemCenter.x - selectionSize.width * 0.5), y: floor(selectedItemCenter.y - selectionSize.height * 0.5)), size: selectionSize)
self.selectedItemBackground.bounds = CGRect(origin: CGPoint(), size: selectionFrame.size)
self.selectedItemTintBackground.bounds = CGRect(origin: CGPoint(), size: selectionFrame.size)
if self.selectedItemBackground.opacity == 0.0 {
self.selectedItemBackground.position = selectionFrame.center
self.selectedItemTintBackground.position = selectionFrame.center
self.selectedItemBackground.opacity = 1.0
self.selectedItemTintBackground.opacity = 1.0
ComponentTransition.immediate.setScale(layer: self.selectedItemBackground, scale: 1.0)
ComponentTransition.immediate.setScale(layer: self.selectedItemTintBackground, scale: 1.0)
if !transition.animation.isImmediate {
self.selectedItemBackground.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
self.selectedItemTintBackground.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
self.selectedItemBackground.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.5, damping: 92.0)
self.selectedItemTintBackground.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.5, damping: 92.0)
}
} else {
if self.selectedItemBackground.position != selectionFrame.center {
transition.setPosition(layer: self.selectedItemBackground, position: selectionFrame.center)
transition.setPosition(layer: self.selectedItemTintBackground, position: selectionFrame.center)
if case let .curve(duration, _) = transition.animation {
ComponentTransition.immediate.setScale(layer: self.selectedItemBackground, scale: 1.0)
ComponentTransition.immediate.setScale(layer: self.selectedItemTintBackground, scale: 1.0)
self.selectedItemBackground.animateKeyframes(values: [1.0 as NSNumber, 0.75 as NSNumber, 1.0 as NSNumber], duration: duration, keyPath: "transform.scale")
self.selectedItemTintBackground.animateKeyframes(values: [1.0 as NSNumber, 0.75 as NSNumber, 1.0 as NSNumber], duration: duration, keyPath: "transform.scale")
} else {
transition.setScale(layer: self.selectedItemBackground, scale: 1.0)
transition.setScale(layer: self.selectedItemTintBackground, scale: 1.0)
}
}
}
} else {
transition.setAlpha(layer: self.selectedItemBackground, alpha: 0.0)
transition.setScale(layer: self.selectedItemBackground, scale: 0.8)
transition.setAlpha(layer: self.selectedItemTintBackground, alpha: 0.0)
transition.setScale(layer: self.selectedItemTintBackground, scale: 0.8)
}
let scrollBounds = self.scrollView.bounds
let textOffset = max(0.0, scrollBounds.minX - (itemLayout.itemStartX - itemLayout.textFrame.maxX - itemLayout.textSpacing))
transition.setPosition(view: self.textContainerView, position: self.scrollView.center)
transition.setBounds(view: self.textContainerView, bounds: CGRect(origin: CGPoint(x: textOffset, y: 0.0), size: scrollBounds.size))
transition.setPosition(view: self.tintTextContainerView, position: self.scrollView.center)
transition.setBounds(view: self.tintTextContainerView, bounds: CGRect(origin: CGPoint(x: textOffset, y: 0.0), size: scrollBounds.size))
}
func update(component: EmojiSearchSearchBarComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
self.component = component
self.componentState = state
let textColor: UIColor
if component.theme.overallDarkAppearance && component.forceNeedsVibrancy {
textColor = component.theme.chat.inputMediaPanel.panelContentVibrantSearchOverlayColor.withMultipliedAlpha(0.3)
} else {
textColor = component.useOpaqueTheme ? component.theme.chat.inputMediaPanel.panelContentOpaqueSearchOverlayColor : component.theme.chat.inputMediaPanel.panelContentVibrantSearchOverlayColor
}
let textSize = self.textView.update(
transition: .immediate,
component: AnyComponent(Text(
text: component.strings.Common_Search,
font: Font.regular(17.0),
color: textColor
)),
environment: {},
containerSize: CGSize(width: availableSize.width - 32.0, height: 100.0)
)
let _ = self.tintTextView.update(
transition: .immediate,
component: AnyComponent(Text(
text: component.strings.Common_Search,
font: Font.regular(17.0),
color: .black
)),
environment: {},
containerSize: CGSize(width: availableSize.width - 32.0, height: 100.0)
)
let itemLayout = ItemLayout(containerSize: availableSize, textSize: textSize, itemCount: component.categories?.groups.count ?? 0)
self.itemLayout = itemLayout
if let textComponentView = self.textView.view {
if textComponentView.superview == nil {
self.textContainerView.addSubview(textComponentView)
}
transition.setFrame(view: textComponentView, frame: itemLayout.textFrame)
}
if let tintTextComponentView = self.tintTextView.view {
if tintTextComponentView.superview == nil {
self.tintTextContainerView.addSubview(tintTextComponentView)
}
transition.setFrame(view: tintTextComponentView, frame: itemLayout.textFrame)
}
self.ignoreScrolling = true
if self.scrollView.bounds.size != availableSize {
transition.setFrame(view: self.scrollView, frame: CGRect(origin: CGPoint(), size: availableSize))
}
if case .active(true) = component.textInputState {
transition.setBoundsOrigin(view: self.scrollView, origin: CGPoint())
}
if self.scrollView.contentSize != itemLayout.contentSize {
self.scrollView.contentSize = itemLayout.contentSize
}
self.ignoreScrolling = false
let maskFrame = CGRect(origin: CGPoint(), size: availableSize)
transition.setFrame(view: self.roundMaskView, frame: maskFrame)
self.roundMaskView.update(diameter: maskFrame.height)
transition.setFrame(view: self.tintRoundMaskView, frame: maskFrame)
self.tintRoundMaskView.update(diameter: maskFrame.height)
self.updateScrolling(transition: transition, fromScrolling: false)
switch component.textInputState {
case let .active(hasText):
if hasText {
self.disableInteraction = false
self.isUserInteractionEnabled = false
} else {
self.disableInteraction = true
self.isUserInteractionEnabled = true
}
self.textView.view?.isHidden = hasText
self.tintTextView.view?.isHidden = hasText
case .inactive:
self.disableInteraction = false
self.isUserInteractionEnabled = true
self.textView.view?.isHidden = false
self.tintTextView.view?.isHidden = false
}
return availableSize
}
}
func makeView() -> View {
return View(frame: CGRect())
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
@@ -0,0 +1,797 @@
import Foundation
import UIKit
import Display
import ComponentFlow
import PagerComponent
import TelegramPresentationData
import TelegramCore
import Postbox
import AnimationCache
import MultiAnimationRenderer
import AccountContext
import AsyncDisplayKit
import ComponentDisplayAdapters
import LottieAnimationComponent
import EmojiStatusComponent
import LottieComponent
import AudioToolbox
import SwiftSignalKit
import GZip
import RLottieBinding
import AppBundle
import Lottie
private final class LottieDirectContent: LottieComponent.Content {
let path: String
init(path: String) {
self.path = path
}
override var frameRange: Range<Double> {
return 0.0 ..< 1.0
}
override func isEqual(to other: LottieComponent.Content) -> Bool {
guard let other = other as? LottieDirectContent else {
return false
}
if self.path != other.path {
return false
}
return true
}
override func load(_ f: @escaping (LottieComponent.ContentData) -> Void) -> Disposable {
if let data = try? Data(contentsOf: URL(fileURLWithPath: self.path)) {
let result = TGGUnzipData(data, 2 * 1024 * 1024) ?? data
f(.animation(data: result, cacheKey: nil))
}
return EmptyDisposable
}
}
private protocol EmojiSearchStatusAnimationState {
var content: EmojiSearchStatusComponent.ContentState { get }
var image: UIImage? { get }
var isCompleted: Bool { get }
func advanceIfNeeded()
func updateImage()
}
final class EmojiSearchStatusComponent: Component {
enum Content: Equatable {
case search
case progress
case results
}
let theme: PresentationTheme
let forceNeedsVibrancy: Bool
let strings: PresentationStrings
let useOpaqueTheme: Bool
let content: Content
init(
theme: PresentationTheme,
forceNeedsVibrancy: Bool,
strings: PresentationStrings,
useOpaqueTheme: Bool,
content: Content
) {
self.theme = theme
self.forceNeedsVibrancy = forceNeedsVibrancy
self.strings = strings
self.useOpaqueTheme = useOpaqueTheme
self.content = content
}
static func ==(lhs: EmojiSearchStatusComponent, rhs: EmojiSearchStatusComponent) -> Bool {
if lhs.theme !== rhs.theme {
return false
}
if lhs.forceNeedsVibrancy != rhs.forceNeedsVibrancy {
return false
}
if lhs.strings !== rhs.strings {
return false
}
if lhs.useOpaqueTheme != rhs.useOpaqueTheme {
return false
}
if lhs.content != rhs.content {
return false
}
return true
}
fileprivate enum ContentState {
case search
case searchToProgress
case progress
case results
init(content: Content) {
switch content {
case .search:
self = .search
case .progress:
self = .progress
case .results:
self = .results
}
}
var content: Content {
switch self {
case .search:
return .search
case .searchToProgress, .progress:
return .progress
case .results:
return .results
}
}
var automaticNextState: ContentState? {
switch self {
case .searchToProgress:
return .progress
default:
return nil
}
}
}
private final class LottieAnimationState: EmojiSearchStatusAnimationState {
let content: ContentState
private let animationInstance: LottieInstance
private var currentFrameStartTime: Double?
private var currentFrame: Int = 0
private let frameRange: ClosedRange<Int>?
private(set) var image: UIImage?
private(set) var previousAnimationState: EmojiSearchStatusAnimationState?
private(set) var isCompleted: Bool = false
var displaySize: CGSize {
didSet {
if self.displaySize != oldValue {
self.image = nil
}
}
}
init?(content: ContentState, data: Data, displaySize: CGSize, frameRange: ClosedRange<Int>?, previousAnimationState: EmojiSearchStatusAnimationState?) {
guard let animationInstance = LottieInstance(data: data, fitzModifier: .none, colorReplacements: nil, cacheKey: "") else {
return nil
}
self.content = content
self.animationInstance = animationInstance
self.displaySize = displaySize
self.frameRange = frameRange
self.previousAnimationState = previousAnimationState
if let frameRange {
self.currentFrame = frameRange.lowerBound
}
}
func advanceIfNeeded() {
if let previousAnimationState = self.previousAnimationState {
previousAnimationState.advanceIfNeeded()
if previousAnimationState.isCompleted {
self.previousAnimationState = nil
}
if previousAnimationState.image == nil {
self.image = nil
}
}
if self.isCompleted {
return
}
if let frameRange = self.frameRange {
if frameRange.lowerBound == frameRange.upperBound {
self.isCompleted = true
return
}
}
let timestamp = CACurrentMediaTime()
guard let currentFrameStartTime = self.currentFrameStartTime else {
currentFrameStartTime = timestamp
return
}
let secondsPerFrame: Double
if animationInstance.frameRate == 0 {
secondsPerFrame = 1.0 / 60.0
} else {
secondsPerFrame = 1.0 / Double(animationInstance.frameRate)
}
if currentFrameStartTime + secondsPerFrame * 0.9 <= timestamp {
self.currentFrame += 1
let maxFrame: Int
if let frameRange = self.frameRange {
maxFrame = frameRange.upperBound
} else {
maxFrame = Int(animationInstance.frameCount) - 1
}
if self.currentFrame >= maxFrame {
self.currentFrame = maxFrame
self.isCompleted = true
} else {
self.currentFrameStartTime = timestamp
self.image = nil
}
}
}
func updateImage() {
guard let frameContext = DrawingContext(size: self.displaySize, scale: 1.0, opaque: false, clear: true) else {
return
}
self.animationInstance.renderFrame(with: Int32(self.currentFrame % Int(self.animationInstance.frameCount)), into: frameContext.bytes.assumingMemoryBound(to: UInt8.self), width: Int32(self.displaySize.width), height: Int32(self.displaySize.height), bytesPerRow: Int32(frameContext.bytesPerRow))
if let previousAnimationState = self.previousAnimationState as? ProgressAnimationState {
guard let context = DrawingContext(size: self.displaySize, scale: 1.0, opaque: false, clear: true) else {
return
}
if previousAnimationState.image == nil {
previousAnimationState.updateImage()
}
if let frameImage = frameContext.generateImage()?.cgImage, let cgImage = previousAnimationState.image?.cgImage {
context.withFlippedContext { c in
c.draw(cgImage, in: CGRect(origin: CGPoint(), size: context.size))
c.translateBy(x: self.displaySize.width * 0.5, y: self.displaySize.height * 0.5)
c.rotate(by: previousAnimationState.currentRotationAngle.truncatingRemainder(dividingBy: CGFloat.pi * 2.0))
c.translateBy(x: -self.displaySize.width * 0.5, y: -self.displaySize.height * 0.5)
c.draw(frameImage, in: CGRect(origin: CGPoint(), size: context.size))
}
}
self.image = context.generateImage()?.withRenderingMode(.alwaysTemplate)
} else {
self.image = frameContext.generateImage()?.withRenderingMode(.alwaysTemplate)
}
}
}
private final class ProgressAnimationState: EmojiSearchStatusAnimationState {
let content: ContentState
private var currentFrameStartTime: Double?
private var currentOffset: CGFloat
private(set) var currentRotationAngle: CGFloat
private var lastStageStartOffset: CGFloat?
private var lastStageRotationAngle: CGFloat?
private(set) var image: UIImage?
var shouldComplete: Bool = false {
didSet {
if self.shouldComplete != oldValue && self.shouldComplete {
self.lastStageStartOffset = self.currentOffset
self.currentRotationAngle = self.currentRotationAngle.truncatingRemainder(dividingBy: CGFloat.pi * 2.0)
self.lastStageRotationAngle = self.currentRotationAngle
}
}
}
private(set) var isCompleted: Bool = false
var displaySize: CGSize {
didSet {
if self.displaySize != oldValue {
self.image = nil
}
}
}
init(content: ContentState, displaySize: CGSize) {
self.content = content
self.displaySize = displaySize
self.currentOffset = 0.0
self.currentRotationAngle = 0.0
}
func advanceIfNeeded() {
if self.isCompleted {
return
}
let timestamp = CACurrentMediaTime()
guard let currentFrameStartTime = self.currentFrameStartTime else {
currentFrameStartTime = timestamp
return
}
let secondsPerFrame: Double = 1.0 / 60.0
let offsetVelocity: CGFloat = CGFloat.pi * 3.0
let maxOffset: CGFloat = CGFloat.pi * 2.0 - CGFloat.pi * 1.0 / 1.4
let rotationVelocity: CGFloat = CGFloat.pi * 3.0 * 1.0
if currentFrameStartTime + secondsPerFrame * 0.9 <= timestamp {
if let lastStageStartOffset = self.lastStageStartOffset {
let lastStageRemainingOffset: CGFloat = CGFloat.pi * 2.0 - lastStageStartOffset
let lastStageRemainingVelocity: CGFloat = lastStageRemainingOffset / 9.0 * 60.0
self.currentOffset = min(CGFloat.pi * 2.0, self.currentOffset + lastStageRemainingVelocity * secondsPerFrame)
} else if self.shouldComplete {
self.currentOffset = min(CGFloat.pi * 2.0, self.currentOffset + offsetVelocity * secondsPerFrame)
if self.currentOffset == CGFloat.pi * 2.0 {
self.isCompleted = true
}
} else {
self.currentOffset = min(maxOffset, self.currentOffset + offsetVelocity * secondsPerFrame)
}
if let lastStageRotationAngle = self.lastStageRotationAngle {
let _ = lastStageRotationAngle
/*let lastStageRemainingAngle: CGFloat = CGFloat.pi * 2.0 + lastStageRotationAngle
let lastStageRemainingAngleVelocity: CGFloat = lastStageRemainingAngle / 12.0 * 60.0
self.currentRotationAngle = max(-CGFloat.pi * 2.0, self.currentRotationAngle - lastStageRemainingAngleVelocity * secondsPerFrame)*/
self.currentRotationAngle = max(-CGFloat.pi * 2.0, self.currentRotationAngle - rotationVelocity * secondsPerFrame)
} else {
self.currentRotationAngle -= rotationVelocity * secondsPerFrame
}
if self.lastStageStartOffset != nil && self.lastStageRotationAngle != nil {
if self.currentOffset == CGFloat.pi * 2.0 && self.currentRotationAngle == -CGFloat.pi * 2.0 {
self.isCompleted = true
}
}
self.currentFrameStartTime = timestamp
self.image = nil
}
}
func updateImage() {
guard let context = DrawingContext(size: self.displaySize, scale: 1.0, opaque: false, clear: true) else {
return
}
context.withFlippedContext { c in
c.setStrokeColor(UIColor.white.cgColor)
c.setLineCap(.round)
let lineWidth: CGFloat = 1.33 * UIScreenScale
let fullDiameter = 20.0 * UIScreenScale
c.setLineWidth(lineWidth)
let startAngle: CGFloat = 0.0
let endAngle: CGFloat = startAngle + (CGFloat.pi * 2.0 - self.currentOffset.truncatingRemainder(dividingBy: CGFloat.pi * 2.0))
c.translateBy(x: self.displaySize.width * 0.5, y: self.displaySize.height * 0.5)
c.rotate(by: self.currentRotationAngle.truncatingRemainder(dividingBy: CGFloat.pi * 2.0))
c.translateBy(x: -self.displaySize.width * 0.5, y: -self.displaySize.height * 0.5)
if self.currentOffset != CGFloat.pi * 2.0 {
c.addArc(center: CGPoint(x: self.displaySize.width * 0.5, y: self.displaySize.height * 0.5), radius: fullDiameter * 0.5 - lineWidth, startAngle: startAngle, endAngle: endAngle, clockwise: false)
c.strokePath()
}
}
self.image = context.generateImage()?.withRenderingMode(.alwaysTemplate)
}
}
final class View: UIView {
private var component: EmojiSearchStatusComponent?
private var disappearingAnimationStates: [(UIImageView, UIImageView, EmojiSearchStatusAnimationState)] = []
private var currentAnimationState: EmojiSearchStatusAnimationState?
private var pendingContent: Content?
private var displaySize: CGSize?
private var displayLink: SharedDisplayLinkDriver.Link?
public let contentView: UIImageView
public let tintContainerView: UIView
public let tintContentView: UIImageView
override init(frame: CGRect) {
self.contentView = UIImageView()
self.tintContainerView = UIView()
self.tintContentView = UIImageView()
super.init(frame: frame)
self.addSubview(self.contentView)
self.tintContainerView.isUserInteractionEnabled = false
self.tintContainerView.addSubview(self.tintContentView)
//self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))))
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@objc private func tapGesture(_ recognizer: UITapGestureRecognizer) {
if case .ended = recognizer.state {
}
}
func update(component: EmojiSearchStatusComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
self.component = component
let displaySize = CGSize(width: availableSize.width * UIScreenScale, height: availableSize.height * UIScreenScale)
self.displaySize = displaySize
let overlayColor: UIColor
if component.theme.overallDarkAppearance && component.forceNeedsVibrancy {
overlayColor = component.theme.chat.inputMediaPanel.panelContentVibrantSearchOverlayColor.withMultipliedAlpha(0.3)
} else {
overlayColor = component.useOpaqueTheme ? component.theme.chat.inputMediaPanel.panelContentOpaqueSearchOverlayColor : component.theme.chat.inputMediaPanel.panelContentVibrantSearchOverlayColor
}
let baseColor: UIColor = .black
if self.contentView.tintColor != overlayColor {
self.contentView.tintColor = overlayColor
}
if self.tintContentView.tintColor != baseColor {
self.tintContentView.tintColor = baseColor
}
let currentTargetContent = self.pendingContent ?? self.currentAnimationState?.content.content
if component.content != currentTargetContent {
var canSwitchNow = false
if let currentAnimationState = self.currentAnimationState {
if currentAnimationState.isCompleted {
canSwitchNow = true
} else if let _ = currentAnimationState as? ProgressAnimationState {
canSwitchNow = true
}
} else {
canSwitchNow = true
}
if canSwitchNow {
/*if let currentAnimationState = self.currentAnimationState, case .search = currentAnimationState.content, case .progress = component.content {
self.switchToContent(content: .searchToProgress)
} else {*/
self.switchToContent(content: ContentState(content: component.content))
//}
} else {
self.pendingContent = component.content
}
}
self.updateAnimation()
transition.setFrame(view: self.contentView, frame: CGRect(origin: CGPoint(), size: availableSize))
transition.setFrame(view: self.tintContentView, frame: CGRect(origin: CGPoint(), size: availableSize))
return availableSize
}
private func switchToContent(content: ContentState) {
guard let displaySize = self.displaySize else {
return
}
enum FrameRangeValue {
case index(Int)
case marker(String)
case end
}
var name: String?
var isJson = false
var frameRange: (FrameRangeValue, FrameRangeValue)?
var manualTransition = false
var previousAnimationState: EmojiSearchStatusAnimationState?
previousAnimationState = nil
let manualPreviousState = self.currentAnimationState
if let currentAnimationState = self.currentAnimationState {
switch currentAnimationState.content {
case .search:
switch content {
case .search:
name = "emoji_search_to_arrow"
frameRange = (.index(0), .index(0))
case .searchToProgress:
name = "emoji_search_to_progress"
isJson = true
//frameRange = (.index(0), .marker("{\r\"name\":\"Search to Progress\"\r}"))
frameRange = (.index(0), .index(7))
case .progress:
manualTransition = true
break
case .results:
name = "emoji_search_to_arrow"
}
case .searchToProgress:
switch content {
case .search:
manualTransition = true
name = "emoji_search_to_arrow"
frameRange = (.index(0), .index(0))
case .searchToProgress:
break
case .progress:
break
case .results:
manualTransition = true
name = "emoji_arrow_to_search"
frameRange = (.index(0), .index(0))
}
case .progress:
switch content {
case .search:
manualTransition = true
name = "emoji_search_to_arrow"
frameRange = (.index(0), .index(0))
case .searchToProgress:
break
case .progress:
break
case .results:
manualTransition = true
name = "emoji_arrow_to_search"
frameRange = (.index(0), .index(0))
}
/*switch content {
case .search:
manualTransition = true
name = "emoji_search_to_arrow"
frameRange = (.index(0), .index(0))
case .searchToProgress:
name = "emoji_search_to_progress"
isJson = true
case .progress:
break
case .results:
name = "emoji_search_to_progress"
isJson = true
//frameRange = (.marker("{\n\"name\":\"Progress to Arrow\"\n}"), .end)
frameRange = (.index(87), .end)
previousAnimationState = currentAnimationState
(currentAnimationState as? ProgressAnimationState)?.shouldComplete = true
/*name = "emoji_arrow_to_search"
frameRange = (.index(0), .index(0))*/
}*/
case .results:
switch content {
case .search:
name = "emoji_arrow_to_search"
case .searchToProgress:
name = "emoji_search_to_progress"
isJson = true
case .progress:
manualTransition = true
case .results:
name = "emoji_arrow_to_search"
frameRange = (.index(0), .index(0))
}
}
} else {
switch content {
case .search:
name = "emoji_search_to_arrow"
frameRange = (.index(0), .index(0))
case .searchToProgress:
name = "emoji_search_to_progress"
isJson = true
case .progress:
break
case .results:
name = "emoji_arrow_to_search"
frameRange = (.index(0), .index(0))
}
}
if manualTransition, let manualPreviousState {
let tempImageView = UIImageView()
tempImageView.image = self.contentView.image
tempImageView.frame = self.contentView.frame
tempImageView.tintColor = self.contentView.tintColor
self.contentView.superview?.insertSubview(tempImageView, aboveSubview: self.contentView)
let tempTintImageView = UIImageView()
tempTintImageView.image = self.tintContentView.image
tempTintImageView.frame = self.tintContentView.frame
tempTintImageView.tintColor = self.tintContentView.tintColor
self.tintContentView.superview?.insertSubview(tempTintImageView, aboveSubview: self.tintContentView)
self.disappearingAnimationStates.append((tempImageView, tempTintImageView, manualPreviousState))
let minScale: CGFloat = 0.6
tempImageView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.18, removeOnCompletion: false, completion: { [weak self, weak tempImageView] _ in
if let self, let tempImageView {
tempImageView.removeFromSuperview()
self.disappearingAnimationStates.removeAll(where: { $0.0 === tempImageView })
}
})
tempImageView.layer.animateScale(from: 1.0, to: minScale, duration: 0.18, removeOnCompletion: false)
tempTintImageView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.18, removeOnCompletion: false, completion: { [weak self, weak tempTintImageView] _ in
if let self, let tempTintImageView {
tempImageView.removeFromSuperview()
self.disappearingAnimationStates.removeAll(where: { $0.1 === tempTintImageView })
}
})
tempTintImageView.layer.animateScale(from: 1.0, to: minScale, duration: 0.18, removeOnCompletion: false)
self.contentView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.18)
self.contentView.layer.animateScale(from: minScale, to: 1.0, duration: 0.18)
self.tintContentView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.18)
self.tintContentView.layer.animateScale(from: minScale, to: 1.0, duration: 0.18)
}
if case .progress = content {
self.currentAnimationState = ProgressAnimationState(content: content, displaySize: displaySize)
} else if let name, let data = getAppBundle().path(forResource: name, ofType: isJson ? "json" : "tgs").flatMap({
return try? Data(contentsOf: URL(fileURLWithPath: $0))
}).flatMap({ data -> Data in
if isJson {
return data
}
return TGGUnzipData(data, 2 * 1024 * 1024) ?? data
}) {
var resolvedFrameRange: ClosedRange<Int>?
if let frameRange {
var hasMarkers = false
if case .marker = frameRange.0 {
hasMarkers = true
}
if case .marker = frameRange.1 {
hasMarkers = true
}
if case .end = frameRange.0 {
hasMarkers = true
}
if case .end = frameRange.1 {
hasMarkers = true
}
var resolvedLowerBound: Int = 0
var resolvedUpperBound: Int = 0
if case let .index(index) = frameRange.0 {
resolvedLowerBound = index
}
if case let .index(index) = frameRange.1 {
resolvedUpperBound = index
}
if hasMarkers, let json = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any], let animation = try? Animation(dictionary: json) {
let numFrames = animation.endFrame - animation.startFrame
if case let .marker(markerName) = frameRange.0 {
if let value = animation.progressTime(forMarker: markerName) {
resolvedLowerBound = Int(value * numFrames)
}
}
if case .end = frameRange.0 {
resolvedLowerBound = Int(numFrames) - 1
}
if case let .marker(markerName) = frameRange.1 {
if let value = animation.progressTime(forMarker: markerName) {
resolvedUpperBound = Int(round(value * numFrames))
}
}
if case .end = frameRange.1 {
resolvedUpperBound = Int(numFrames) - 1
}
}
resolvedFrameRange = resolvedLowerBound ... max(resolvedLowerBound, resolvedUpperBound)
}
self.currentAnimationState = LottieAnimationState(content: content, data: data, displaySize: displaySize, frameRange: resolvedFrameRange, previousAnimationState: previousAnimationState)
} else {
self.currentAnimationState = nil
}
}
private func updateAnimation() {
var needsAnimation = false
for (tempView, tempTintView, animationState) in self.disappearingAnimationStates {
animationState.advanceIfNeeded()
if animationState.image == nil {
animationState.updateImage()
}
tempView.image = animationState.image
tempTintView.image = animationState.image
needsAnimation = true
}
while true {
if let currentAnimationState = self.currentAnimationState {
if self.pendingContent != nil, let currentAnimationState = currentAnimationState as? ProgressAnimationState {
currentAnimationState.shouldComplete = true
}
currentAnimationState.advanceIfNeeded()
if currentAnimationState.image == nil {
currentAnimationState.updateImage()
}
if let previousAnimationState = (currentAnimationState as? LottieAnimationState)?.previousAnimationState, !previousAnimationState.isCompleted {
needsAnimation = true
}
if currentAnimationState.isCompleted {
if self.pendingContent == nil, let automaticNextState = currentAnimationState.content.automaticNextState {
self.switchToContent(content: automaticNextState)
} else if let pendingContent = self.pendingContent {
self.pendingContent = nil
self.switchToContent(content: ContentState(content: pendingContent))
} else {
break
}
} else {
needsAnimation = true
break
}
} else {
break
}
}
if let currentAnimationState = self.currentAnimationState {
if currentAnimationState.image == nil {
currentAnimationState.updateImage()
}
if let image = currentAnimationState.image {
self.contentView.image = image
self.tintContentView.image = image
}
}
if needsAnimation {
if self.displayLink == nil {
var counter = 0
self.displayLink = SharedDisplayLinkDriver.shared.add { [weak self] _ in
counter += 1
if counter % 1 == 0 {
self?.updateAnimation()
}
}
}
} else {
if let displayLink = self.displayLink {
self.displayLink = nil
displayLink.invalidate()
}
}
}
}
func makeView() -> View {
return View(frame: CGRect())
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
@@ -0,0 +1,101 @@
import Foundation
import UIKit
import Display
import ComponentFlow
import AccountContext
import TelegramCore
import TelegramPresentationData
import EmojiStatusComponent
final class EmptySearchResultsView: UIView {
override public static var layerClass: AnyClass {
return PassthroughLayer.self
}
let tintContainerView: UIView
let titleLabel: ComponentView<Empty>
let titleTintLabel: ComponentView<Empty>
let icon: ComponentView<Empty>
override init(frame: CGRect) {
self.tintContainerView = UIView()
self.titleLabel = ComponentView()
self.titleTintLabel = ComponentView()
self.icon = ComponentView()
super.init(frame: frame)
(self.layer as? PassthroughLayer)?.mirrorLayer = self.tintContainerView.layer
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(context: AccountContext, theme: PresentationTheme, useOpaqueTheme: Bool, text: String, file: TelegramMediaFile?, size: CGSize, searchInitiallyHidden: Bool, transition: ComponentTransition) {
let titleColor: UIColor
if useOpaqueTheme {
titleColor = theme.chat.inputMediaPanel.panelContentOpaqueSearchOverlayColor
} else {
titleColor = theme.chat.inputMediaPanel.panelContentVibrantSearchOverlayColor
}
let iconSize: CGSize
if let file = file {
iconSize = self.icon.update(
transition: .immediate,
component: AnyComponent(EmojiStatusComponent(
context: context,
animationCache: context.animationCache,
animationRenderer: context.animationRenderer,
content: .animation(content: .file(file: file), size: CGSize(width: 32.0, height: 32.0), placeholderColor: titleColor, themeColor: nil, loopMode: .forever),
isVisibleForAnimations: context.sharedContext.energyUsageSettings.loopEmoji,
action: nil
)),
environment: {},
containerSize: CGSize(width: 32.0, height: 32.0)
)
} else {
iconSize = CGSize()
}
let titleSize = self.titleLabel.update(
transition: .immediate,
component: AnyComponent(Text(text: text, font: Font.regular(15.0), color: titleColor)),
environment: {},
containerSize: CGSize(width: size.width, height: 100.0)
)
let _ = self.titleTintLabel.update(
transition: .immediate,
component: AnyComponent(Text(text: text, font: Font.regular(15.0), color: .black)),
environment: {},
containerSize: CGSize(width: size.width, height: 100.0)
)
let spacing: CGFloat = 4.0
let contentHeight = iconSize.height + spacing + titleSize.height
let contentOriginY = searchInitiallyHidden ? floor((size.height - contentHeight) / 2.0) : 10.0
let iconFrame = CGRect(origin: CGPoint(x: floor((size.width - iconSize.width) / 2.0), y: contentOriginY), size: iconSize)
let titleFrame = CGRect(origin: CGPoint(x: floor((size.width - titleSize.width) / 2.0), y: iconFrame.maxY + spacing), size: titleSize)
if let iconView = self.icon.view {
if iconView.superview == nil {
self.addSubview(iconView)
}
transition.setFrame(view: iconView, frame: iconFrame)
}
if let titleLabelView = self.titleLabel.view {
if titleLabelView.superview == nil {
self.addSubview(titleLabelView)
}
transition.setFrame(view: titleLabelView, frame: titleFrame)
}
if let titleTintLabelView = self.titleTintLabel.view {
if titleTintLabelView.superview == nil {
self.tintContainerView.addSubview(titleTintLabelView)
}
transition.setFrame(view: titleTintLabelView, frame: titleFrame)
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,176 @@
import Foundation
import UIKit
import Display
import ComponentFlow
import PagerComponent
import ComponentDisplayAdapters
import BundleIconComponent
import GlassBackgroundComponent
import AppBundle
final class EntityKeyboardBottomPanelButton: Component {
let icon: String
let color: UIColor
let action: () -> Void
let holdAction: (() -> Void)?
init(
icon: String,
color: UIColor,
action: @escaping () -> Void,
holdAction: (() -> Void)? = nil
) {
self.icon = icon
self.color = color
self.action = action
self.holdAction = holdAction
}
static func ==(lhs: EntityKeyboardBottomPanelButton, rhs: EntityKeyboardBottomPanelButton) -> Bool {
if lhs.icon != rhs.icon {
return false
}
if lhs.color != rhs.color {
return false
}
if (lhs.holdAction == nil) != (rhs.holdAction == nil) {
return false
}
return true
}
final class View: HighlightTrackingButton {
let iconView: GlassBackgroundView.ContentImageView
let tintMaskContainer: UIView
private var holdActionTriggerred: Bool = false
private var holdActionTimer: Timer?
var component: EntityKeyboardBottomPanelButton?
private var currentIsHighlighted: Bool = false {
didSet {
if self.currentIsHighlighted != oldValue {
self.updateAlpha(transition: .immediate)
}
}
}
override init(frame: CGRect) {
self.iconView = GlassBackgroundView.ContentImageView()
self.iconView.isUserInteractionEnabled = false
self.tintMaskContainer = UIView()
self.tintMaskContainer.addSubview(self.iconView.tintMask)
super.init(frame: frame)
self.addSubview(self.iconView)
self.addTarget(self, action: #selector(self.pressed), for: .touchUpInside)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
self.holdActionTimer?.invalidate()
}
@objc private func pressed() {
if self.holdActionTriggerred {
self.holdActionTriggerred = false
} else {
self.component?.action()
}
}
override public func beginTracking(_ touch: UITouch, with event: UIEvent?) -> Bool {
self.currentIsHighlighted = true
self.holdActionTriggerred = false
if self.component?.holdAction != nil {
self.holdActionTriggerred = true
self.component?.action()
self.holdActionTimer?.invalidate()
let holdActionTimer = Timer(timeInterval: 0.5, repeats: false, block: { [weak self] _ in
guard let strongSelf = self else {
return
}
strongSelf.holdActionTimer?.invalidate()
strongSelf.component?.holdAction?()
strongSelf.beginExecuteHoldActionTimer()
})
self.holdActionTimer = holdActionTimer
RunLoop.main.add(holdActionTimer, forMode: .common)
}
return super.beginTracking(touch, with: event)
}
private func beginExecuteHoldActionTimer() {
self.holdActionTimer?.invalidate()
let holdActionTimer = Timer(timeInterval: 0.1, repeats: true, block: { [weak self] _ in
guard let strongSelf = self else {
return
}
strongSelf.component?.holdAction?()
})
self.holdActionTimer = holdActionTimer
RunLoop.main.add(holdActionTimer, forMode: .common)
}
override public func endTracking(_ touch: UITouch?, with event: UIEvent?) {
self.currentIsHighlighted = false
self.holdActionTimer?.invalidate()
self.holdActionTimer = nil
super.endTracking(touch, with: event)
}
override public func cancelTracking(with event: UIEvent?) {
self.currentIsHighlighted = false
self.holdActionTimer?.invalidate()
self.holdActionTimer = nil
super.cancelTracking(with: event)
}
private func updateAlpha(transition: ComponentTransition) {
let alpha: CGFloat = self.currentIsHighlighted ? 0.6 : 1.0
transition.setAlpha(view: self.iconView, alpha: alpha)
}
func update(component: EntityKeyboardBottomPanelButton, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
if self.component?.icon != component.icon {
self.iconView.image = UIImage(bundleImageName: component.icon)?.withRenderingMode(.alwaysTemplate)
}
self.component = component
self.iconView.tintColor = component.color
let size = CGSize(width: 38.0, height: 38.0)
if let image = self.iconView.image {
let iconFrame = CGRect(origin: CGPoint(x: floor((size.width - image.size.width) * 0.5), y: floor((size.height - image.size.height) * 0.5)), size: image.size)
self.iconView.frame = iconFrame
}
return size
}
}
func makeView() -> View {
return View(frame: CGRect())
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
@@ -0,0 +1,490 @@
import Foundation
import UIKit
import Display
import ComponentFlow
import PagerComponent
import TelegramPresentationData
import TelegramCore
import ComponentDisplayAdapters
import BundleIconComponent
import GlassBackgroundComponent
private final class BottomPanelIconComponent: Component {
let title: String
let isHighlighted: Bool
let theme: PresentationTheme
let action: () -> Void
init(
title: String,
isHighlighted: Bool,
theme: PresentationTheme,
action: @escaping () -> Void
) {
self.title = title
self.isHighlighted = isHighlighted
self.theme = theme
self.action = action
}
static func ==(lhs: BottomPanelIconComponent, rhs: BottomPanelIconComponent) -> Bool {
if lhs.title != rhs.title {
return false
}
if lhs.isHighlighted != rhs.isHighlighted {
return false
}
if lhs.theme !== rhs.theme {
return false
}
return true
}
final class View: UIView {
let contentView: GlassBackgroundView.ContentImageView
let tintMaskContainer: UIView
var component: BottomPanelIconComponent?
override init(frame: CGRect) {
self.contentView = GlassBackgroundView.ContentImageView()
self.contentView.isUserInteractionEnabled = false
self.tintMaskContainer = UIView()
self.tintMaskContainer.addSubview(self.contentView.tintMask)
super.init(frame: frame)
self.addSubview(self.contentView)
self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))))
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@objc private func tapGesture(_ recognizer: UITapGestureRecognizer) {
if case .ended = recognizer.state {
self.component?.action()
}
}
func update(component: BottomPanelIconComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
if self.component?.title != component.title {
let text = NSAttributedString(string: component.title, font: Font.medium(15.0), textColor: .white)
let textBounds = text.boundingRect(with: CGSize(width: 120.0, height: 100.0), options: .usesLineFragmentOrigin, context: nil)
self.contentView.image = generateImage(CGSize(width: ceil(textBounds.width), height: ceil(textBounds.height)), rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
UIGraphicsPushContext(context)
text.draw(in: textBounds)
UIGraphicsPopContext()
})?.withRenderingMode(.alwaysTemplate)
}
self.component = component
let textInset: CGFloat = 12.0
let textSize = self.contentView.image?.size ?? CGSize()
let size = CGSize(width: textSize.width + textInset * 2.0, height: 28.0)
let color = component.theme.chat.inputPanel.inputControlColor
if self.contentView.tintColor != color {
if !transition.animation.isImmediate {
UIView.animate(withDuration: 0.15, delay: 0.0, options: [], animations: {
self.contentView.tintColor = color
}, completion: nil)
} else {
self.contentView.tintColor = color
}
}
transition.setFrame(view: self.contentView, frame: CGRect(origin: CGPoint(x: floor((size.width - textSize.width) / 2.0), y: (size.height - textSize.height) / 2.0 - 1.0), size: textSize))
return size
}
}
func makeView() -> View {
return View(frame: CGRect())
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
final class EntityKeyboardBottomPanelComponent: Component {
typealias EnvironmentType = PagerComponentPanelEnvironment<EntityKeyboardTopContainerPanelEnvironment>
let theme: PresentationTheme
let containerInsets: UIEdgeInsets
let deleteBackwards: () -> Void
init(
theme: PresentationTheme,
containerInsets: UIEdgeInsets,
deleteBackwards: @escaping () -> Void
) {
self.theme = theme
self.containerInsets = containerInsets
self.deleteBackwards = deleteBackwards
}
static func ==(lhs: EntityKeyboardBottomPanelComponent, rhs: EntityKeyboardBottomPanelComponent) -> Bool {
if lhs.theme !== rhs.theme {
return false
}
if lhs.containerInsets != rhs.containerInsets {
return false
}
return true
}
final class View: UIView, PagerTopPanelView {
private final class AccessoryButtonView {
let id: AnyHashable
var component: AnyComponent<Empty>
let view: ComponentHostView<Empty>
init(id: AnyHashable, component: AnyComponent<Empty>, view: ComponentHostView<Empty>) {
self.id = id
self.component = component
self.view = view
}
}
private let backgroundView: BlurredBackgroundView
private let separatorView: UIView
private let tintSeparatorView: UIView
private var leftAccessoryButton: AccessoryButtonView?
private var rightAccessoryButton: AccessoryButtonView?
private var iconViews: [AnyHashable: ComponentHostView<Empty>] = [:]
private var highlightedIconBackgroundView: UIView
private var highlightedTintIconBackgroundView: UIView
let tintContentMask: UIView
private var component: EntityKeyboardBottomPanelComponent?
override init(frame: CGRect) {
self.tintContentMask = UIView()
self.backgroundView = BlurredBackgroundView(color: .clear, enableBlur: true, customBlurRadius: 10.0)
self.separatorView = UIView()
self.separatorView.isUserInteractionEnabled = false
self.tintSeparatorView = UIView()
self.tintSeparatorView.isUserInteractionEnabled = false
self.tintSeparatorView.backgroundColor = UIColor(white: 0.0, alpha: 0.7)
self.tintContentMask.addSubview(self.tintSeparatorView)
self.highlightedIconBackgroundView = UIView()
self.highlightedIconBackgroundView.isUserInteractionEnabled = false
self.highlightedIconBackgroundView.layer.cornerRadius = 10.0
self.highlightedIconBackgroundView.clipsToBounds = true
self.highlightedTintIconBackgroundView = UIView()
self.highlightedTintIconBackgroundView.isUserInteractionEnabled = false
self.highlightedTintIconBackgroundView.layer.cornerRadius = 10.0
self.highlightedTintIconBackgroundView.clipsToBounds = true
self.highlightedTintIconBackgroundView.backgroundColor = UIColor(white: 0.0, alpha: 0.1)
self.tintContentMask.addSubview(self.highlightedTintIconBackgroundView)
super.init(frame: frame)
self.addSubview(self.backgroundView)
self.addSubview(self.highlightedIconBackgroundView)
self.addSubview(self.separatorView)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(component: EntityKeyboardBottomPanelComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: ComponentTransition) -> CGSize {
if self.component?.theme !== component.theme {
self.separatorView.backgroundColor = component.theme.list.itemPlainSeparatorColor.withMultipliedAlpha(0.5)
self.backgroundView.updateColor(color: component.theme.chat.inputPanel.panelBackgroundColor.withMultipliedAlpha(1.0), transition: .immediate)
self.highlightedIconBackgroundView.backgroundColor = component.theme.chat.inputMediaPanel.panelHighlightedIconBackgroundColor
}
let intrinsicHeight: CGFloat = 34.0
let height = intrinsicHeight + component.containerInsets.bottom
let accessoryButtonOffset: CGFloat
if component.containerInsets.bottom > 0.0 {
accessoryButtonOffset = 2.0
} else {
accessoryButtonOffset = -2.0
}
let panelEnvironment = environment[PagerComponentPanelEnvironment<EntityKeyboardTopContainerPanelEnvironment>.self].value
let activeContentId = panelEnvironment.activeContentId
var leftAccessoryButtonComponent: AnyComponentWithIdentity<Empty>?
for contentAccessoryLeftButton in panelEnvironment.contentAccessoryLeftButtons {
if contentAccessoryLeftButton.id == activeContentId {
leftAccessoryButtonComponent = contentAccessoryLeftButton
break
}
}
let previousLeftAccessoryButton = self.leftAccessoryButton
if let leftAccessoryButtonComponent = leftAccessoryButtonComponent {
var leftAccessoryButtonTransition = transition
let leftAccessoryButton: AccessoryButtonView
if let current = self.leftAccessoryButton, (current.id == leftAccessoryButtonComponent.id || current.component == leftAccessoryButtonComponent.component) {
leftAccessoryButton = current
leftAccessoryButton.component = leftAccessoryButtonComponent.component
} else {
leftAccessoryButtonTransition = .immediate
leftAccessoryButton = AccessoryButtonView(id: leftAccessoryButtonComponent.id, component: leftAccessoryButtonComponent.component, view: ComponentHostView<Empty>())
self.leftAccessoryButton = leftAccessoryButton
self.addSubview(leftAccessoryButton.view)
}
let leftAccessoryButtonSize = leftAccessoryButton.view.update(
transition: leftAccessoryButtonTransition,
component: leftAccessoryButtonComponent.component,
environment: {},
containerSize: CGSize(width: .greatestFiniteMagnitude, height: intrinsicHeight)
)
let leftAccessoryButtonFrame = CGRect(origin: CGPoint(x: component.containerInsets.left + 2.0, y: accessoryButtonOffset), size: leftAccessoryButtonSize)
leftAccessoryButtonTransition.setFrame(view: leftAccessoryButton.view, frame: leftAccessoryButtonFrame)
if let leftAccessoryButtonView = leftAccessoryButton.view.componentView as? PagerTopPanelView {
if leftAccessoryButtonView.tintContentMask.superview == nil {
self.tintContentMask.addSubview(leftAccessoryButtonView.tintContentMask)
}
leftAccessoryButtonTransition.setFrame(view: leftAccessoryButtonView.tintContentMask, frame: leftAccessoryButtonFrame)
}
} else {
self.leftAccessoryButton = nil
}
if previousLeftAccessoryButton?.view !== self.leftAccessoryButton?.view {
if case .none = transition.animation {
previousLeftAccessoryButton?.view.removeFromSuperview()
if let previousLeftAccessoryButton = previousLeftAccessoryButton?.view.componentView as? PagerTopPanelView {
previousLeftAccessoryButton.tintContentMask.removeFromSuperview()
}
} else {
if let previousLeftAccessoryButton = previousLeftAccessoryButton {
let previousLeftAccessoryButtonView = previousLeftAccessoryButton.view
previousLeftAccessoryButtonView.layer.animateScale(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false)
previousLeftAccessoryButtonView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak previousLeftAccessoryButtonView] _ in
previousLeftAccessoryButtonView?.removeFromSuperview()
if let previousLeftAccessoryButton = previousLeftAccessoryButtonView?.componentView as? PagerTopPanelView {
previousLeftAccessoryButton.tintContentMask.removeFromSuperview()
}
})
}
if let leftAccessoryButtonView = self.leftAccessoryButton?.view {
leftAccessoryButtonView.layer.animateScale(from: 0.01, to: 1.0, duration: 0.2)
leftAccessoryButtonView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
if let leftAccessoryButtonView = leftAccessoryButtonView.componentView as? PagerTopPanelView {
leftAccessoryButtonView.tintContentMask.layer.animateScale(from: 0.01, to: 1.0, duration: 0.2)
leftAccessoryButtonView.tintContentMask.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
}
}
}
var rightAccessoryButtonComponent: AnyComponentWithIdentity<Empty>?
for contentAccessoryRightButton in panelEnvironment.contentAccessoryRightButtons {
if contentAccessoryRightButton.id == activeContentId {
rightAccessoryButtonComponent = contentAccessoryRightButton
break
}
}
let previousRightAccessoryButton = self.rightAccessoryButton
if let rightAccessoryButtonComponent = rightAccessoryButtonComponent {
var rightAccessoryButtonTransition = transition
let rightAccessoryButton: AccessoryButtonView
if let current = self.rightAccessoryButton, (current.id == rightAccessoryButtonComponent.id || current.component == rightAccessoryButtonComponent.component) {
rightAccessoryButton = current
current.component = rightAccessoryButtonComponent.component
} else {
rightAccessoryButtonTransition = .immediate
rightAccessoryButton = AccessoryButtonView(id: rightAccessoryButtonComponent.id, component: rightAccessoryButtonComponent.component, view: ComponentHostView<Empty>())
self.rightAccessoryButton = rightAccessoryButton
self.addSubview(rightAccessoryButton.view)
}
let rightAccessoryButtonSize = rightAccessoryButton.view.update(
transition: rightAccessoryButtonTransition,
component: rightAccessoryButtonComponent.component,
environment: {},
containerSize: CGSize(width: .greatestFiniteMagnitude, height: intrinsicHeight)
)
let rightAccessoryButtonFrame = CGRect(origin: CGPoint(x: availableSize.width - component.containerInsets.right - 2.0 - rightAccessoryButtonSize.width, y: accessoryButtonOffset), size: rightAccessoryButtonSize)
rightAccessoryButtonTransition.setFrame(view: rightAccessoryButton.view, frame: rightAccessoryButtonFrame)
if let rightAccessoryButtonView = rightAccessoryButton.view.componentView as? PagerTopPanelView {
if rightAccessoryButtonView.tintContentMask.superview == nil {
self.tintContentMask.addSubview(rightAccessoryButtonView.tintContentMask)
}
rightAccessoryButtonTransition.setFrame(view: rightAccessoryButtonView.tintContentMask, frame: rightAccessoryButtonFrame)
}
} else {
self.rightAccessoryButton = nil
}
if previousRightAccessoryButton?.view !== self.rightAccessoryButton?.view {
if case .none = transition.animation {
previousRightAccessoryButton?.view.removeFromSuperview()
if let previousRightAccessoryButtonView = previousRightAccessoryButton?.view.componentView as? PagerTopPanelView {
previousRightAccessoryButtonView.tintContentMask.removeFromSuperview()
}
} else {
if let previousRightAccessoryButton = previousRightAccessoryButton {
let previousRightAccessoryButtonView = previousRightAccessoryButton.view
previousRightAccessoryButtonView.layer.animateScale(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false)
previousRightAccessoryButtonView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak previousRightAccessoryButtonView] _ in
previousRightAccessoryButtonView?.removeFromSuperview()
if let previousRightAccessoryButtonView = previousRightAccessoryButtonView?.componentView as? PagerTopPanelView {
previousRightAccessoryButtonView.tintContentMask.removeFromSuperview()
}
})
}
if let rightAccessoryButtonView = self.rightAccessoryButton?.view {
rightAccessoryButtonView.layer.animateScale(from: 0.01, to: 1.0, duration: 0.2)
rightAccessoryButtonView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
if let rightAccessoryButtonView = rightAccessoryButtonView.componentView as? PagerTopPanelView {
rightAccessoryButtonView.tintContentMask.layer.animateScale(from: 0.01, to: 1.0, duration: 0.2)
rightAccessoryButtonView.tintContentMask.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
}
}
}
var validIconIds: [AnyHashable] = []
var iconInfos: [AnyHashable: (size: CGSize, transition: ComponentTransition)] = [:]
var iconTotalSize = CGSize()
let iconSpacing: CGFloat = 4.0
let navigateToContentId = panelEnvironment.navigateToContentId
if panelEnvironment.contentIcons.count > 1 {
for icon in panelEnvironment.contentIcons {
validIconIds.append(icon.id)
var iconTransition = transition
let iconView: ComponentHostView<Empty>
if let current = self.iconViews[icon.id] {
iconView = current
} else {
iconTransition = .immediate
iconView = ComponentHostView<Empty>()
self.iconViews[icon.id] = iconView
self.addSubview(iconView)
}
let iconSize = iconView.update(
transition: iconTransition,
component: AnyComponent(BottomPanelIconComponent(
title: icon.title,
isHighlighted: icon.id == activeContentId,
theme: component.theme,
action: {
navigateToContentId(icon.id)
}
)),
environment: {},
containerSize: CGSize(width: 28.0, height: 28.0)
)
iconInfos[icon.id] = (size: iconSize, transition: iconTransition)
if !iconTotalSize.width.isZero {
iconTotalSize.width += iconSpacing
}
iconTotalSize.width += iconSize.width
iconTotalSize.height = max(iconTotalSize.height, iconSize.height)
}
}
var nextIconOrigin = CGPoint(x: floor((availableSize.width - iconTotalSize.width) / 2.0), y: floor((intrinsicHeight - iconTotalSize.height) / 2.0))
if component.containerInsets.bottom > 0.0 {
nextIconOrigin.y += 3.0
}
if panelEnvironment.contentIcons.count > 1 {
for icon in panelEnvironment.contentIcons {
guard let iconInfo = iconInfos[icon.id], let iconView = self.iconViews[icon.id] else {
continue
}
let iconFrame = CGRect(origin: nextIconOrigin, size: iconInfo.size)
iconInfo.transition.setFrame(view: iconView, frame: iconFrame, completion: nil)
if let iconView = iconView.componentView as? BottomPanelIconComponent.View {
if iconView.tintMaskContainer.superview == nil {
self.tintContentMask.addSubview(iconView.tintMaskContainer)
}
iconInfo.transition.setFrame(view: iconView.tintMaskContainer, frame: iconFrame, completion: nil)
}
if let activeContentId = activeContentId, activeContentId == icon.id {
self.highlightedIconBackgroundView.isHidden = false
self.highlightedTintIconBackgroundView.isHidden = false
transition.setFrame(view: self.highlightedIconBackgroundView, frame: iconFrame)
transition.setFrame(view: self.highlightedTintIconBackgroundView, frame: iconFrame)
let cornerRadius: CGFloat = min(iconFrame.width, iconFrame.height) / 2.0
transition.setCornerRadius(layer: self.highlightedIconBackgroundView.layer, cornerRadius: cornerRadius)
transition.setCornerRadius(layer: self.highlightedTintIconBackgroundView.layer, cornerRadius: cornerRadius)
}
nextIconOrigin.x += iconInfo.size.width + iconSpacing
}
}
if activeContentId == nil {
self.highlightedIconBackgroundView.isHidden = true
}
var removedIconViewIds: [AnyHashable] = []
for (id, iconView) in self.iconViews {
if !validIconIds.contains(id) {
removedIconViewIds.append(id)
iconView.removeFromSuperview()
}
}
for id in removedIconViewIds {
self.iconViews.removeValue(forKey: id)
}
transition.setFrame(view: self.separatorView, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: availableSize.width, height: UIScreenPixel)))
transition.setFrame(view: self.tintSeparatorView, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: availableSize.width, height: UIScreenPixel)))
transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: availableSize.width, height: height)))
//self.backgroundView.update(size: CGSize(width: availableSize.width, height: height), transition: transition.containedViewLayoutTransition)
self.component = component
return CGSize(width: availableSize.width, height: height)
}
}
func makeView() -> View {
return View(frame: CGRect())
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
@@ -0,0 +1,335 @@
import Foundation
import UIKit
import Display
import ComponentFlow
import PagerComponent
import TelegramPresentationData
import TelegramCore
import Postbox
public final class EntityKeyboardTopContainerPanelEnvironment: Equatable {
let isContentInFocus: Bool
let visibilityFractionUpdated: ActionSlot<(CGFloat, ComponentTransition)>
let isExpandedUpdated: (Bool, ComponentTransition) -> Void
init(
isContentInFocus: Bool,
visibilityFractionUpdated: ActionSlot<(CGFloat, ComponentTransition)>,
isExpandedUpdated: @escaping (Bool, ComponentTransition) -> Void
) {
self.isContentInFocus = isContentInFocus
self.visibilityFractionUpdated = visibilityFractionUpdated
self.isExpandedUpdated = isExpandedUpdated
}
public static func ==(lhs: EntityKeyboardTopContainerPanelEnvironment, rhs: EntityKeyboardTopContainerPanelEnvironment) -> Bool {
if lhs.isContentInFocus != rhs.isContentInFocus {
return false
}
if lhs.visibilityFractionUpdated !== rhs.visibilityFractionUpdated {
return false
}
return true
}
}
final class EntityKeyboardTopContainerPanelComponent: Component {
typealias EnvironmentType = PagerComponentPanelEnvironment<EntityKeyboardTopContainerPanelEnvironment>
let theme: PresentationTheme
let overflowHeight: CGFloat
let topInset: CGFloat
let displayBackground: EntityKeyboardComponent.DisplayTopPanelBackground
init(
theme: PresentationTheme,
overflowHeight: CGFloat,
topInset: CGFloat,
displayBackground: EntityKeyboardComponent.DisplayTopPanelBackground
) {
self.theme = theme
self.overflowHeight = overflowHeight
self.topInset = topInset
self.displayBackground = displayBackground
}
static func ==(lhs: EntityKeyboardTopContainerPanelComponent, rhs: EntityKeyboardTopContainerPanelComponent) -> Bool {
if lhs.theme !== rhs.theme {
return false
}
if lhs.overflowHeight != rhs.overflowHeight {
return false
}
if lhs.topInset != rhs.topInset {
return false
}
if lhs.displayBackground != rhs.displayBackground {
return false
}
return true
}
private final class PanelView {
let view = ComponentHostView<EntityKeyboardTopContainerPanelEnvironment>()
let tintContentView = UIView()
let visibilityFractionUpdated = ActionSlot<(CGFloat, ComponentTransition)>()
var isExpanded: Bool = false
}
final class View: UIView, PagerTopPanelView {
private var backgroundView: BlurredBackgroundView?
private var backgroundSeparatorView: UIView?
private var panelViews: [AnyHashable: PanelView] = [:]
private var component: EntityKeyboardTopContainerPanelComponent?
private var panelEnvironment: PagerComponentPanelEnvironment<EntityKeyboardTopContainerPanelEnvironment>?
private weak var state: EmptyComponentState?
private var visibilityFraction: CGFloat = 1.0
public let tintContentMask: UIView
override init(frame: CGRect) {
self.tintContentMask = UIView()
super.init(frame: frame)
self.disablesInteractiveKeyboardGestureRecognizer = true
self.disablesInteractiveTransitionGestureRecognizer = true
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(component: EntityKeyboardTopContainerPanelComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: ComponentTransition) -> CGSize {
let intrinsicHeight: CGFloat = 34.0
let height = intrinsicHeight + component.topInset
let panelEnvironment = environment[PagerComponentPanelEnvironment.self].value
var transitionOffsetFraction: CGFloat = 0.0
if case .none = transition.animation {
} else if let previousPanelEnvironment = self.panelEnvironment, let previousActiveContentId = previousPanelEnvironment.activeContentId, let activeContentId = panelEnvironment.activeContentId, previousActiveContentId != activeContentId {
if let previousIndex = panelEnvironment.contentTopPanels.firstIndex(where: { $0.id == previousActiveContentId }), let index = panelEnvironment.contentTopPanels.firstIndex(where: { $0.id == activeContentId }), previousIndex != index {
if index < previousIndex {
transitionOffsetFraction = -1.0
} else {
transitionOffsetFraction = 1.0
}
}
}
self.component = component
self.panelEnvironment = panelEnvironment
self.state = state
var validPanelIds = Set<AnyHashable>()
let visibleBounds = CGRect(origin: CGPoint(), size: CGSize(width: availableSize.width, height: intrinsicHeight))
if let centralId = panelEnvironment.activeContentId, let centralIndex = panelEnvironment.contentTopPanels.firstIndex(where: { $0.id == centralId }) {
for index in 0 ..< panelEnvironment.contentTopPanels.count {
let panel = panelEnvironment.contentTopPanels[index]
let indexOffset = index - centralIndex
let panelFrame = CGRect(origin: CGPoint(x: CGFloat(indexOffset) * availableSize.width, y: component.topInset - component.overflowHeight), size: CGSize(width: availableSize.width, height: intrinsicHeight + component.overflowHeight))
let isInBounds = visibleBounds.intersects(panelFrame)
let isPartOfTransition: Bool
if !transitionOffsetFraction.isZero && self.panelViews[panel.id] != nil {
isPartOfTransition = true
} else {
isPartOfTransition = false
}
if isInBounds || isPartOfTransition {
validPanelIds.insert(panel.id)
var panelTransition = transition
let panelView: PanelView
if let current = self.panelViews[panel.id] {
panelView = current
} else {
panelTransition = .immediate
panelView = PanelView()
self.panelViews[panel.id] = panelView
self.addSubview(panelView.view)
self.tintContentMask.addSubview(panelView.tintContentView)
}
let panelId = panel.id
let _ = panelView.view.update(
transition: panelTransition,
component: panel.component,
environment: {
EntityKeyboardTopContainerPanelEnvironment(
isContentInFocus: panelEnvironment.isContentInFocus,
visibilityFractionUpdated: panelView.visibilityFractionUpdated,
isExpandedUpdated: { [weak self] isExpanded, transition in
guard let strongSelf = self else {
return
}
strongSelf.panelIsExpandedUpdated(id: panelId, isExpanded: isExpanded, transition: transition)
}
)
},
containerSize: panelFrame.size
)
if isInBounds {
transition.animatePosition(view: panelView.view, from: CGPoint(x: transitionOffsetFraction * availableSize.width, y: 0.0), to: CGPoint(), additive: true, completion: nil)
transition.animatePosition(view: panelView.tintContentView, from: CGPoint(x: transitionOffsetFraction * availableSize.width, y: 0.0), to: CGPoint(), additive: true, completion: nil)
}
panelTransition.setFrame(view: panelView.view, frame: panelFrame, completion: { [weak self] completed in
if isPartOfTransition && completed {
self?.state?.updated(transition: .immediate)
}
})
panelTransition.setFrame(view: panelView.tintContentView, frame: panelFrame)
if let panelViewImpl = panelView.view.componentView as? PagerTopPanelView {
if panelViewImpl.tintContentMask.superview == nil {
panelView.tintContentView.addSubview(panelViewImpl.tintContentMask)
}
panelTransition.setFrame(view: panelViewImpl.tintContentMask, frame: CGRect(origin: CGPoint(), size: panelFrame.size))
}
}
}
}
var removedPanelIds: [AnyHashable] = []
for (id, panelView) in self.panelViews {
if !validPanelIds.contains(id) {
removedPanelIds.append(id)
panelView.view.removeFromSuperview()
panelView.tintContentView.removeFromSuperview()
}
}
for id in removedPanelIds {
self.panelViews.removeValue(forKey: id)
}
environment[PagerComponentPanelEnvironment.self].value.visibilityFractionUpdated.connect { [weak self] (fraction, transition) in
guard let strongSelf = self else {
return
}
strongSelf.updateVisibilityFraction(value: fraction, transition: transition)
}
if case .blur = component.displayBackground {
self.backgroundColor = nil
let backgroundView: BlurredBackgroundView
if let current = self.backgroundView {
backgroundView = current
} else {
backgroundView = BlurredBackgroundView(color: .clear, enableBlur: true, customBlurRadius: 5.0)
self.backgroundView = backgroundView
self.insertSubview(backgroundView, at: 0)
}
let backgroundSeparatorView: UIView
if let current = self.backgroundSeparatorView {
backgroundSeparatorView = current
} else {
backgroundSeparatorView = UIView()
self.backgroundSeparatorView = backgroundSeparatorView
self.insertSubview(backgroundSeparatorView, aboveSubview: backgroundView)
}
backgroundView.updateColor(color: component.theme.chat.inputPanel.panelBackgroundColor.withMultipliedAlpha(1.0), transition: .immediate)
backgroundView.update(size: CGSize(width: availableSize.width, height: height + component.overflowHeight), transition: transition.containedViewLayoutTransition)
transition.setFrame(view: backgroundView, frame: CGRect(origin: CGPoint(x: 0.0, y: -component.overflowHeight), size: CGSize(width: availableSize.width, height: height + component.overflowHeight)))
backgroundSeparatorView.backgroundColor = component.theme.chat.inputPanel.panelSeparatorColor
transition.setFrame(view: backgroundSeparatorView, frame: CGRect(origin: CGPoint(x: 0.0, y: height - UIScreenPixel), size: CGSize(width: availableSize.width, height: UIScreenPixel)))
} else if case .none = component.displayBackground {
self.backgroundColor = nil
if let backgroundView = self.backgroundView {
self.backgroundView = nil
backgroundView.removeFromSuperview()
}
if let backgroundSeparatorView = self.backgroundSeparatorView {
self.backgroundSeparatorView = nil
backgroundSeparatorView.removeFromSuperview()
}
} else if case .opaque = component.displayBackground {
if let backgroundView = self.backgroundView {
self.backgroundView = nil
backgroundView.removeFromSuperview()
}
if let backgroundSeparatorView = self.backgroundSeparatorView {
self.backgroundSeparatorView = nil
backgroundSeparatorView.removeFromSuperview()
}
self.backgroundColor = component.theme.chat.inputMediaPanel.backgroundColor
}
return CGSize(width: availableSize.width, height: height)
}
private func updateVisibilityFraction(value: CGFloat, transition: ComponentTransition) {
if self.visibilityFraction == value {
return
}
self.visibilityFraction = value
for (_, panelView) in self.panelViews {
panelView.visibilityFractionUpdated.invoke((value, transition))
transition.setSublayerTransform(view: panelView.view, transform: CATransform3DMakeTranslation(0.0, -panelView.view.bounds.height / 2.0 * (1.0 - value), 0.0))
}
}
private func panelIsExpandedUpdated(id: AnyHashable, isExpanded: Bool, transition: ComponentTransition) {
guard let panelView = self.panelViews[id] else {
return
}
if panelView.isExpanded == isExpanded {
return
}
panelView.isExpanded = isExpanded
var hasExpanded = false
for (_, panel) in self.panelViews {
if panel.isExpanded {
hasExpanded = true
break
}
}
self.panelEnvironment?.isExpandedUpdated(hasExpanded, transition)
}
public func internalUpdatePanelsAreCollapsed() {
for (_, panelView) in self.panelViews {
panelView.isExpanded = false
}
}
override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if self.alpha.isZero {
return nil
}
for view in self.subviews.reversed() {
if let result = view.hitTest(self.convert(point, to: view), with: event), result.isUserInteractionEnabled {
return result
}
}
let result = super.hitTest(point, with: event)
if result != self {
return result
} else {
return nil
}
}
}
func makeView() -> View {
return View(frame: CGRect())
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
@@ -0,0 +1,180 @@
import Foundation
import UIKit
import Display
import ComponentFlow
import PagerComponent
import TelegramPresentationData
import TelegramCore
import Postbox
import AnimationCache
import MultiAnimationRenderer
import AccountContext
import AsyncDisplayKit
import ComponentDisplayAdapters
public protocol EntitySearchContainerNode: ASDisplayNode {
var onCancel: (() -> Void)? { get set }
func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, inputHeight: CGFloat, deviceMetrics: DeviceMetrics, transition: ContainedViewLayoutTransition)
}
public final class EntitySearchContainerController: ViewController {
private var node: Node {
return self.displayNode as! Node
}
private let containerNode: EntitySearchContainerNode
public init(containerNode: EntitySearchContainerNode) {
self.containerNode = containerNode
super.init(navigationBarPresentationData: nil)
self.navigationPresentation = .modal
}
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override public func loadDisplayNode() {
self.displayNode = Node(containerNode: self.containerNode, controller: self)
self.displayNodeDidLoad()
}
public override func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition)
self.node.containerLayoutUpdated(layout, transition: transition)
}
private class Node: ViewControllerTracingNode, ASScrollViewDelegate {
private weak var controller: EntitySearchContainerController?
private let containerNode: EntitySearchContainerNode
init(containerNode: EntitySearchContainerNode, controller: EntitySearchContainerController) {
self.containerNode = containerNode
self.controller = controller
super.init()
self.addSubnode(containerNode)
containerNode.onCancel = { [weak self] in
self?.controller?.dismiss()
}
}
func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
self.containerNode.updateLayout(size: layout.size, leftInset: 0.0, rightInset: 0.0, bottomInset: layout.intrinsicInsets.bottom, inputHeight: layout.inputHeight ?? 0.0, deviceMetrics: layout.deviceMetrics, transition: transition)
transition.updateFrame(node: self.containerNode, frame: CGRect(origin: .zero, size: layout.size))
}
}
}
final class EntitySearchContentEnvironment: Equatable {
let context: AccountContext
let theme: PresentationTheme
let deviceMetrics: DeviceMetrics
let inputHeight: CGFloat
init(
context: AccountContext,
theme: PresentationTheme,
deviceMetrics: DeviceMetrics,
inputHeight: CGFloat
) {
self.context = context
self.theme = theme
self.deviceMetrics = deviceMetrics
self.inputHeight = inputHeight
}
static func ==(lhs: EntitySearchContentEnvironment, rhs: EntitySearchContentEnvironment) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.theme !== rhs.theme {
return false
}
if lhs.deviceMetrics != rhs.deviceMetrics {
return false
}
if lhs.inputHeight != rhs.inputHeight {
return false
}
return true
}
}
final class EntitySearchContentComponent: Component {
typealias EnvironmentType = EntitySearchContentEnvironment
let makeContainerNode: () -> EntitySearchContainerNode?
let dismissSearch: () -> Void
init(
makeContainerNode: @escaping () -> EntitySearchContainerNode?,
dismissSearch: @escaping () -> Void
) {
self.makeContainerNode = makeContainerNode
self.dismissSearch = dismissSearch
}
static func ==(lhs: EntitySearchContentComponent, rhs: EntitySearchContentComponent) -> Bool {
return true
}
final class View: UIView {
private var containerNode: EntitySearchContainerNode?
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(component: EntitySearchContentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: ComponentTransition) -> CGSize {
let containerNode: EntitySearchContainerNode?
if let current = self.containerNode {
containerNode = current
} else {
containerNode = component.makeContainerNode()
if let containerNode = containerNode {
self.containerNode = containerNode
self.addSubnode(containerNode)
}
}
if let containerNode = containerNode {
let environmentValue = environment[EntitySearchContentEnvironment.self].value
transition.setFrame(view: containerNode.view, frame: CGRect(origin: CGPoint(), size: availableSize))
containerNode.updateLayout(
size: availableSize,
leftInset: 0.0,
rightInset: 0.0,
bottomInset: 0.0,
inputHeight: environmentValue.inputHeight,
deviceMetrics: environmentValue.deviceMetrics,
transition: transition.containedViewLayoutTransition
)
containerNode.onCancel = {
component.dismissSearch()
}
}
return availableSize
}
}
func makeView() -> View {
return View(frame: CGRect())
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
@@ -0,0 +1,209 @@
import Foundation
import UIKit
import Display
import ComponentFlow
import AccountContext
import TelegramPresentationData
import AnimationCache
import MultiAnimationRenderer
import PagerComponent
final class GroupEmbeddedView: UIScrollView, UIScrollViewDelegate, PagerExpandableScrollView {
private struct ItemLayout {
var itemSize: CGFloat
var itemSpacing: CGFloat
var sideInset: CGFloat
var itemCount: Int
var contentSize: CGSize
init(height: CGFloat, sideInset: CGFloat, itemCount: Int) {
self.itemSize = 30.0
self.itemSpacing = 20.0
self.sideInset = sideInset
self.itemCount = itemCount
self.contentSize = CGSize(width: self.sideInset * 2.0 + CGFloat(self.itemCount) * self.itemSize + CGFloat(self.itemCount - 1) * self.itemSpacing, height: height)
}
func frame(at index: Int) -> CGRect {
return CGRect(origin: CGPoint(x: sideInset + CGFloat(index) * (self.itemSize + self.itemSpacing), y: floor((self.contentSize.height - self.itemSize) / 2.0)), size: CGSize(width: self.itemSize, height: self.itemSize))
}
func visibleItems(for rect: CGRect) -> Range<Int>? {
let offsetRect = rect.offsetBy(dx: -self.sideInset, dy: 0.0)
var minVisibleIndex = Int(floor((offsetRect.minX - self.itemSpacing) / (self.itemSize + self.itemSpacing)))
minVisibleIndex = max(0, minVisibleIndex)
var maxVisibleIndex = Int(ceil((offsetRect.maxX - self.itemSpacing) / (self.itemSize + self.itemSpacing)))
maxVisibleIndex = min(maxVisibleIndex, self.itemCount - 1)
if minVisibleIndex <= maxVisibleIndex {
return minVisibleIndex ..< (maxVisibleIndex + 1)
} else {
return nil
}
}
}
private let performItemAction: (EmojiPagerContentComponent.Item, UIView, CGRect, CALayer) -> Void
private var visibleItemLayers: [EmojiKeyboardItemLayer.Key: EmojiKeyboardItemLayer] = [:]
private var ignoreScrolling: Bool = false
private var context: AccountContext?
private var theme: PresentationTheme?
private var cache: AnimationCache?
private var renderer: MultiAnimationRenderer?
private var currentInsets: UIEdgeInsets?
private var currentSize: CGSize?
private var items: [EmojiPagerContentComponent.Item]?
private var isStickers: Bool = false
private var itemLayout: ItemLayout?
init(performItemAction: @escaping (EmojiPagerContentComponent.Item, UIView, CGRect, CALayer) -> Void) {
self.performItemAction = performItemAction
super.init(frame: CGRect())
self.delaysContentTouches = false
if #available(iOSApplicationExtension 11.0, iOS 11.0, *) {
self.contentInsetAdjustmentBehavior = .never
}
if #available(iOS 13.0, *) {
self.automaticallyAdjustsScrollIndicatorInsets = false
}
self.showsVerticalScrollIndicator = true
self.showsHorizontalScrollIndicator = false
self.delegate = self
self.clipsToBounds = true
self.scrollsToTop = false
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func tapGesture(point: CGPoint) -> Bool {
guard let itemLayout = self.itemLayout else {
return false
}
for (_, itemLayer) in self.visibleItemLayers {
if itemLayer.frame.inset(by: UIEdgeInsets(top: -6.0, left: -itemLayout.itemSpacing, bottom: -6.0, right: -itemLayout.itemSpacing)).contains(point) {
self.performItemAction(itemLayer.item, self, itemLayer.frame, itemLayer)
return true
}
}
return false
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if !self.ignoreScrolling {
self.updateVisibleItems(transition: .immediate, attemptSynchronousLoad: false)
}
}
private func updateVisibleItems(transition: ComponentTransition, attemptSynchronousLoad: Bool) {
guard let context = self.context, let theme = self.theme, let itemLayout = self.itemLayout, let items = self.items, let cache = self.cache, let renderer = self.renderer else {
return
}
var validIds = Set<EmojiKeyboardItemLayer.Key>()
if let itemRange = itemLayout.visibleItems(for: self.bounds) {
for index in itemRange.lowerBound ..< itemRange.upperBound {
let item = items[index]
let itemId = EmojiKeyboardItemLayer.Key(
groupId: AnyHashable(0),
itemId: item.content.id
)
validIds.insert(itemId)
let itemLayer: EmojiKeyboardItemLayer
if let current = self.visibleItemLayers[itemId] {
itemLayer = current
} else {
itemLayer = EmojiKeyboardItemLayer(
item: item,
context: context,
attemptSynchronousLoad: attemptSynchronousLoad,
content: item.content,
cache: cache,
renderer: renderer,
placeholderColor: .clear,
blurredBadgeColor: .clear,
accentIconColor: theme.list.itemAccentColor,
pointSize: CGSize(width: 32.0, height: 32.0),
onUpdateDisplayPlaceholder: { _, _ in
}
)
self.visibleItemLayers[itemId] = itemLayer
self.layer.addSublayer(itemLayer)
}
switch item.tintMode {
case let .custom(color):
itemLayer.layerTintColor = color.cgColor
case .accent:
itemLayer.layerTintColor = theme.list.itemAccentColor.cgColor
case .primary:
itemLayer.layerTintColor = theme.list.itemPrimaryTextColor.cgColor
case .none:
itemLayer.layerTintColor = nil
}
let itemFrame = itemLayout.frame(at: index)
itemLayer.frame = itemFrame
itemLayer.isVisibleForAnimations = self.isStickers ? context.sharedContext.energyUsageSettings.loopStickers : context.sharedContext.energyUsageSettings.loopEmoji
}
}
var removedIds: [EmojiKeyboardItemLayer.Key] = []
for (id, itemLayer) in self.visibleItemLayers {
if !validIds.contains(id) {
removedIds.append(id)
itemLayer.removeFromSuperlayer()
}
}
for id in removedIds {
self.visibleItemLayers.removeValue(forKey: id)
}
}
func update(
context: AccountContext,
theme: PresentationTheme,
insets: UIEdgeInsets,
size: CGSize,
items: [EmojiPagerContentComponent.Item],
isStickers: Bool,
cache: AnimationCache,
renderer: MultiAnimationRenderer,
attemptSynchronousLoad: Bool
) {
if self.theme === theme && self.currentInsets == insets && self.currentSize == size && self.items == items {
return
}
self.context = context
self.theme = theme
self.currentInsets = insets
self.currentSize = size
self.items = items
self.isStickers = isStickers
self.cache = cache
self.renderer = renderer
let itemLayout = ItemLayout(height: size.height, sideInset: insets.left, itemCount: items.count)
self.itemLayout = itemLayout
self.ignoreScrolling = true
if itemLayout.contentSize != self.contentSize {
self.contentSize = itemLayout.contentSize
}
self.ignoreScrolling = false
self.updateVisibleItems(transition: .immediate, attemptSynchronousLoad: attemptSynchronousLoad)
}
}
@@ -0,0 +1,128 @@
import Foundation
import UIKit
import Display
import ComponentFlow
import TelegramPresentationData
final class GroupExpandActionButton: UIButton {
override static var layerClass: AnyClass {
return PassthroughLayer.self
}
let tintContainerLayer: SimpleLayer
private var currentTextLayout: (string: String, color: UIColor, constrainedWidth: CGFloat, size: CGSize)?
private let backgroundLayer: SimpleLayer
private let tintBackgroundLayer: SimpleLayer
private let textLayer: SimpleLayer
private let pressed: () -> Void
init(pressed: @escaping () -> Void) {
self.pressed = pressed
self.tintContainerLayer = SimpleLayer()
self.backgroundLayer = SimpleLayer()
self.backgroundLayer.masksToBounds = true
self.tintBackgroundLayer = SimpleLayer()
self.tintBackgroundLayer.masksToBounds = true
self.textLayer = SimpleLayer()
super.init(frame: CGRect())
(self.layer as? PassthroughLayer)?.mirrorLayer = self.tintContainerLayer
self.layer.addSublayer(self.backgroundLayer)
self.layer.addSublayer(self.textLayer)
self.addTarget(self, action: #selector(self.onPressed), for: .touchUpInside)
}
required init(coder: NSCoder) {
preconditionFailure()
}
@objc private func onPressed() {
self.pressed()
}
override func beginTracking(_ touch: UITouch, with event: UIEvent?) -> Bool {
self.alpha = 0.6
return super.beginTracking(touch, with: event)
}
override func endTracking(_ touch: UITouch?, with event: UIEvent?) {
let alpha = self.alpha
self.alpha = 1.0
self.layer.animateAlpha(from: alpha, to: 1.0, duration: 0.25)
super.endTracking(touch, with: event)
}
override func cancelTracking(with event: UIEvent?) {
let alpha = self.alpha
self.alpha = 1.0
self.layer.animateAlpha(from: alpha, to: 1.0, duration: 0.25)
super.cancelTracking(with: event)
}
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
let alpha = self.alpha
self.alpha = 1.0
self.layer.animateAlpha(from: alpha, to: 1.0, duration: 0.25)
super.touchesCancelled(touches, with: event)
}
func update(theme: PresentationTheme, title: String, useOpaqueTheme: Bool) -> CGSize {
let textConstrainedWidth: CGFloat = 100.0
let color = theme.list.itemCheckColors.foregroundColor
if useOpaqueTheme {
self.backgroundLayer.backgroundColor = theme.chat.inputMediaPanel.panelContentControlOpaqueOverlayColor.cgColor
} else {
self.backgroundLayer.backgroundColor = theme.chat.inputMediaPanel.panelContentControlVibrantOverlayColor.cgColor
}
self.tintContainerLayer.backgroundColor = UIColor.black.cgColor
let textSize: CGSize
if let currentTextLayout = self.currentTextLayout, currentTextLayout.string == title, currentTextLayout.color == color, currentTextLayout.constrainedWidth == textConstrainedWidth {
textSize = currentTextLayout.size
} else {
let font: UIFont = Font.semibold(13.0)
let string = NSAttributedString(string: title.uppercased(), font: font, textColor: color)
let stringBounds = string.boundingRect(with: CGSize(width: textConstrainedWidth, height: 100.0), options: .usesLineFragmentOrigin, context: nil)
textSize = CGSize(width: ceil(stringBounds.width), height: ceil(stringBounds.height))
self.textLayer.contents = generateImage(textSize, opaque: false, scale: 0.0, rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
UIGraphicsPushContext(context)
string.draw(in: stringBounds)
UIGraphicsPopContext()
})?.cgImage
self.currentTextLayout = (title, color, textConstrainedWidth, textSize)
}
var sideInset: CGFloat = 10.0
if textSize.width > 24.0 {
sideInset = 6.0
}
let size = CGSize(width: textSize.width + sideInset * 2.0, height: 28.0)
let textFrame = CGRect(origin: CGPoint(x: floor((size.width - textSize.width) / 2.0), y: floor((size.height - textSize.height) / 2.0)), size: textSize)
self.textLayer.frame = textFrame
self.backgroundLayer.frame = CGRect(origin: CGPoint(), size: size)
self.tintBackgroundLayer.frame = CGRect(origin: CGPoint(), size: size)
self.backgroundLayer.cornerRadius = min(size.width, size.height) / 2.0
self.tintContainerLayer.cornerRadius = min(size.width, size.height) / 2.0
return size
}
}
@@ -0,0 +1,149 @@
import Foundation
import UIKit
import Display
import ComponentFlow
import TelegramPresentationData
final class GroupHeaderActionButton: UIButton {
override static var layerClass: AnyClass {
return PassthroughLayer.self
}
let tintContainerLayer: SimpleLayer
private var currentTextLayout: (string: String, color: UIColor, constrainedWidth: CGFloat, size: CGSize)?
private let backgroundLayer: SimpleLayer
private let tintBackgroundLayer: SimpleLayer
private let textLayer: SimpleLayer
private let tintTextLayer: SimpleLayer
private let pressed: () -> Void
init(pressed: @escaping () -> Void) {
self.pressed = pressed
self.tintContainerLayer = SimpleLayer()
self.backgroundLayer = SimpleLayer()
self.backgroundLayer.masksToBounds = true
self.tintBackgroundLayer = SimpleLayer()
self.tintBackgroundLayer.masksToBounds = true
self.textLayer = SimpleLayer()
self.tintTextLayer = SimpleLayer()
super.init(frame: CGRect())
(self.layer as? PassthroughLayer)?.mirrorLayer = self.tintContainerLayer
self.layer.addSublayer(self.backgroundLayer)
self.layer.addSublayer(self.textLayer)
self.addTarget(self, action: #selector(self.onPressed), for: .touchUpInside)
self.tintContainerLayer.addSublayer(self.tintBackgroundLayer)
self.tintContainerLayer.addSublayer(self.tintTextLayer)
}
required init(coder: NSCoder) {
preconditionFailure()
}
@objc private func onPressed() {
self.pressed()
}
override func beginTracking(_ touch: UITouch, with event: UIEvent?) -> Bool {
self.alpha = 0.6
return super.beginTracking(touch, with: event)
}
override func endTracking(_ touch: UITouch?, with event: UIEvent?) {
let alpha = self.alpha
self.alpha = 1.0
self.layer.animateAlpha(from: alpha, to: 1.0, duration: 0.25)
super.endTracking(touch, with: event)
}
override func cancelTracking(with event: UIEvent?) {
let alpha = self.alpha
self.alpha = 1.0
self.layer.animateAlpha(from: alpha, to: 1.0, duration: 0.25)
super.cancelTracking(with: event)
}
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
let alpha = self.alpha
self.alpha = 1.0
self.layer.animateAlpha(from: alpha, to: 1.0, duration: 0.25)
super.touchesCancelled(touches, with: event)
}
func update(theme: PresentationTheme, title: String, compact: Bool) -> CGSize {
let textConstrainedWidth: CGFloat = 100.0
let needsVibrancy = !theme.overallDarkAppearance && compact
let foregroundColor: UIColor
let backgroundColor: UIColor
if compact {
foregroundColor = theme.chat.inputMediaPanel.panelContentVibrantOverlayColor
backgroundColor = foregroundColor.withMultipliedAlpha(0.2)
} else {
foregroundColor = theme.list.itemCheckColors.foregroundColor
backgroundColor = theme.list.itemCheckColors.fillColor
}
self.backgroundLayer.backgroundColor = backgroundColor.cgColor
self.tintBackgroundLayer.backgroundColor = UIColor.black.withAlphaComponent(0.2).cgColor
self.tintContainerLayer.isHidden = !needsVibrancy
let textSize: CGSize
if let currentTextLayout = self.currentTextLayout, currentTextLayout.string == title, currentTextLayout.color == foregroundColor, currentTextLayout.constrainedWidth == textConstrainedWidth {
textSize = currentTextLayout.size
} else {
let font: UIFont = compact ? Font.medium(11.0) : Font.semibold(15.0)
let string = NSAttributedString(string: title.uppercased(), font: font, textColor: foregroundColor)
let tintString = NSAttributedString(string: title.uppercased(), font: font, textColor: .black)
let stringBounds = string.boundingRect(with: CGSize(width: textConstrainedWidth, height: 100.0), options: .usesLineFragmentOrigin, context: nil)
textSize = CGSize(width: ceil(stringBounds.width), height: ceil(stringBounds.height))
self.textLayer.contents = generateImage(textSize, opaque: false, scale: 0.0, rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
UIGraphicsPushContext(context)
string.draw(in: stringBounds)
UIGraphicsPopContext()
})?.cgImage
self.tintTextLayer.contents = generateImage(textSize, opaque: false, scale: 0.0, rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
UIGraphicsPushContext(context)
tintString.draw(in: stringBounds)
UIGraphicsPopContext()
})?.cgImage
self.currentTextLayout = (title, foregroundColor, textConstrainedWidth, textSize)
}
let size = CGSize(width: textSize.width + (compact ? 6.0 : 16.0) * 2.0, height: compact ? 16.0 : 28.0)
let textFrame = CGRect(origin: CGPoint(x: floor((size.width - textSize.width) / 2.0), y: floorToScreenPixels((size.height - textSize.height) / 2.0)), size: textSize)
self.textLayer.frame = textFrame
self.tintTextLayer.frame = textFrame
self.backgroundLayer.frame = CGRect(origin: CGPoint(), size: size)
self.backgroundLayer.cornerRadius = min(size.width, size.height) / 2.0
self.tintBackgroundLayer.frame = self.backgroundLayer.frame
self.tintBackgroundLayer.cornerRadius = self.backgroundLayer.cornerRadius
return size
}
}
@@ -0,0 +1,524 @@
import Foundation
import UIKit
import Display
import ComponentFlow
import AccountContext
import TelegramCore
import TelegramPresentationData
import AnimationCache
import MultiAnimationRenderer
final class GroupHeaderLayer: UIView {
override static var layerClass: AnyClass {
return PassthroughLayer.self
}
private let actionPressed: () -> Void
private let performItemAction: (EmojiPagerContentComponent.Item, UIView, CGRect, CALayer) -> Void
private let textLayer: SimpleLayer
private let tintTextLayer: SimpleLayer
private var subtitleLayer: SimpleLayer?
private var tintSubtitleLayer: SimpleLayer?
private var lockIconLayer: SimpleLayer?
private var tintLockIconLayer: SimpleLayer?
private var badgeLayer: SimpleLayer?
private var tintBadgeLayer: SimpleLayer?
private(set) var clearIconLayer: SimpleLayer?
private var tintClearIconLayer: SimpleLayer?
private var separatorLayer: SimpleLayer?
private var tintSeparatorLayer: SimpleLayer?
private var actionButton: GroupHeaderActionButton?
private var groupEmbeddedView: GroupEmbeddedView?
private var theme: PresentationTheme?
private var currentTextLayout: (string: String, color: UIColor, constrainedWidth: CGFloat, size: CGSize)?
private var currentSubtitleLayout: (string: String, color: UIColor, constrainedWidth: CGFloat, size: CGSize)?
let tintContentLayer: SimpleLayer
init(actionPressed: @escaping () -> Void, performItemAction: @escaping (EmojiPagerContentComponent.Item, UIView, CGRect, CALayer) -> Void) {
self.actionPressed = actionPressed
self.performItemAction = performItemAction
self.textLayer = SimpleLayer()
self.tintTextLayer = SimpleLayer()
self.tintContentLayer = SimpleLayer()
super.init(frame: CGRect())
self.layer.addSublayer(self.textLayer)
self.tintContentLayer.addSublayer(self.tintTextLayer)
(self.layer as? PassthroughLayer)?.mirrorLayer = self.tintContentLayer
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(
context: AccountContext,
theme: PresentationTheme,
forceNeedsVibrancy: Bool,
layoutType: EmojiPagerContentComponent.ItemLayoutType,
hasTopSeparator: Bool,
actionButtonTitle: String?,
actionButtonIsCompact: Bool,
title: String,
subtitle: String?,
badge: String?,
isPremiumLocked: Bool,
hasClear: Bool,
embeddedItems: [EmojiPagerContentComponent.Item]?,
isStickers: Bool,
constrainedSize: CGSize,
insets: UIEdgeInsets,
cache: AnimationCache,
renderer: MultiAnimationRenderer,
attemptSynchronousLoad: Bool
) -> (size: CGSize, centralContentWidth: CGFloat) {
var themeUpdated = false
if self.theme !== theme {
self.theme = theme
themeUpdated = true
}
let needsVibrancy = !theme.overallDarkAppearance || forceNeedsVibrancy
let textOffsetY: CGFloat
if hasTopSeparator {
textOffsetY = 9.0
} else {
textOffsetY = 0.0
}
let subtitleColor: UIColor
if theme.overallDarkAppearance && forceNeedsVibrancy {
subtitleColor = theme.chat.inputMediaPanel.panelContentVibrantOverlayColor.withMultipliedAlpha(0.2)
} else {
subtitleColor = theme.chat.inputMediaPanel.panelContentVibrantOverlayColor
}
let color: UIColor
let needsTintText: Bool
if subtitle != nil {
color = theme.chat.inputPanel.primaryTextColor
needsTintText = false
} else {
color = subtitleColor
needsTintText = true
}
let titleHorizontalOffset: CGFloat
if isPremiumLocked {
titleHorizontalOffset = 10.0 + 2.0
} else {
titleHorizontalOffset = 0.0
}
var actionButtonSize: CGSize?
if let actionButtonTitle = actionButtonTitle {
let actionButton: GroupHeaderActionButton
if let current = self.actionButton {
actionButton = current
} else {
actionButton = GroupHeaderActionButton(pressed: self.actionPressed)
self.actionButton = actionButton
self.addSubview(actionButton)
self.tintContentLayer.addSublayer(actionButton.tintContainerLayer)
}
actionButtonSize = actionButton.update(theme: theme, title: actionButtonTitle, compact: actionButtonIsCompact)
} else {
if let actionButton = self.actionButton {
self.actionButton = nil
actionButton.removeFromSuperview()
}
}
var clearSize: CGSize = .zero
var clearWidth: CGFloat = 0.0
if hasClear {
var updateImage = themeUpdated
let clearIconLayer: SimpleLayer
if let current = self.clearIconLayer {
clearIconLayer = current
} else {
updateImage = true
clearIconLayer = SimpleLayer()
self.clearIconLayer = clearIconLayer
self.layer.addSublayer(clearIconLayer)
}
let tintClearIconLayer: SimpleLayer
if let current = self.tintClearIconLayer {
tintClearIconLayer = current
} else {
updateImage = true
tintClearIconLayer = SimpleLayer()
self.tintClearIconLayer = tintClearIconLayer
self.tintContentLayer.addSublayer(tintClearIconLayer)
}
tintClearIconLayer.isHidden = !needsVibrancy
clearSize = clearIconLayer.bounds.size
if updateImage, let image = PresentationResourcesChat.chatInputMediaPanelGridDismissImage(theme, color: subtitleColor) {
clearSize = image.size
clearIconLayer.contents = image.cgImage
}
if updateImage, let image = PresentationResourcesChat.chatInputMediaPanelGridDismissImage(theme, color: .black) {
tintClearIconLayer.contents = image.cgImage
}
tintClearIconLayer.frame = clearIconLayer.frame
clearWidth = 4.0 + clearSize.width
} else {
if let clearIconLayer = self.clearIconLayer {
self.clearIconLayer = nil
clearIconLayer.removeFromSuperlayer()
}
if let tintClearIconLayer = self.tintClearIconLayer {
self.tintClearIconLayer = nil
tintClearIconLayer.removeFromSuperlayer()
}
}
var textConstrainedWidth = constrainedSize.width - titleHorizontalOffset - 10.0
if let actionButtonSize = actionButtonSize {
if actionButtonIsCompact {
textConstrainedWidth -= actionButtonSize.width * 2.0 + 10.0
} else {
textConstrainedWidth -= actionButtonSize.width + 10.0
}
}
if clearWidth > 0.0 {
textConstrainedWidth -= clearWidth + 8.0
}
let textSize: CGSize
if let currentTextLayout = self.currentTextLayout, currentTextLayout.string == title, currentTextLayout.color == color, currentTextLayout.constrainedWidth == textConstrainedWidth {
textSize = currentTextLayout.size
} else {
let font: UIFont
let stringValue: String
if subtitle == nil {
font = Font.medium(13.0)
stringValue = title.uppercased()
} else {
font = Font.semibold(16.0)
stringValue = title
}
let string = NSAttributedString(string: stringValue, font: font, textColor: color)
let whiteString = NSAttributedString(string: stringValue, font: font, textColor: .black)
let stringBounds = string.boundingRect(with: CGSize(width: textConstrainedWidth, height: 18.0), options: [.usesLineFragmentOrigin, .truncatesLastVisibleLine], context: nil)
textSize = CGSize(width: ceil(stringBounds.width), height: ceil(stringBounds.height))
self.textLayer.contents = generateImage(textSize, opaque: false, scale: 0.0, rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
UIGraphicsPushContext(context)
//string.draw(in: stringBounds)
string.draw(with: stringBounds, options: [.usesLineFragmentOrigin, .truncatesLastVisibleLine], context: nil)
UIGraphicsPopContext()
})?.cgImage
self.tintTextLayer.contents = generateImage(textSize, opaque: false, scale: 0.0, rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
UIGraphicsPushContext(context)
whiteString.draw(with: stringBounds, options: [.usesLineFragmentOrigin, .truncatesLastVisibleLine], context: nil)
UIGraphicsPopContext()
})?.cgImage
self.tintTextLayer.isHidden = !needsVibrancy
self.currentTextLayout = (title, color, textConstrainedWidth, textSize)
}
var badgeSize: CGSize = .zero
if let badge {
func generateBadgeImage(color: UIColor) -> UIImage? {
let string = NSAttributedString(string: badge, font: Font.semibold(11.0), textColor: .white)
let stringBounds = string.boundingRect(with: CGSize(width: 120, height: 18.0), options: [.usesLineFragmentOrigin, .truncatesLastVisibleLine], context: nil)
let badgeSize = CGSize(width: stringBounds.width + 8.0, height: 16.0)
return generateImage(badgeSize, opaque: false, scale: 0.0, rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setFillColor(color.cgColor)
context.addPath(UIBezierPath(roundedRect: CGRect(origin: .zero, size: badgeSize), cornerRadius: badgeSize.height / 2.0).cgPath)
context.fillPath()
context.setBlendMode(.clear)
UIGraphicsPushContext(context)
string.draw(with: CGRect(origin: CGPoint(x: floorToScreenPixels((badgeSize.width - stringBounds.size.width) / 2.0), y: floorToScreenPixels((badgeSize.height - stringBounds.size.height) / 2.0)), size: stringBounds.size), options: [.usesLineFragmentOrigin, .truncatesLastVisibleLine], context: nil)
UIGraphicsPopContext()
})
}
let badgeLayer: SimpleLayer
if let current = self.badgeLayer {
badgeLayer = current
} else {
badgeLayer = SimpleLayer()
self.badgeLayer = badgeLayer
self.layer.addSublayer(badgeLayer)
if let image = generateBadgeImage(color: color.withMultipliedAlpha(0.66)) {
badgeLayer.contents = image.cgImage
badgeLayer.bounds = CGRect(origin: .zero, size: image.size)
}
}
badgeSize = badgeLayer.bounds.size
let tintBadgeLayer: SimpleLayer
if let current = self.tintBadgeLayer {
tintBadgeLayer = current
} else {
tintBadgeLayer = SimpleLayer()
self.tintBadgeLayer = tintBadgeLayer
self.tintContentLayer.addSublayer(tintBadgeLayer)
if let image = generateBadgeImage(color: .black) {
tintBadgeLayer.contents = image.cgImage
}
}
} else {
if let badgeLayer = self.badgeLayer {
self.badgeLayer = nil
badgeLayer.removeFromSuperlayer()
}
if let tintBadgeLayer = self.tintBadgeLayer {
self.tintBadgeLayer = nil
tintBadgeLayer.removeFromSuperlayer()
}
}
let textFrame: CGRect
if subtitle == nil {
textFrame = CGRect(origin: CGPoint(x: titleHorizontalOffset + floor((constrainedSize.width - titleHorizontalOffset - (textSize.width + badgeSize.width)) / 2.0), y: textOffsetY), size: textSize)
} else {
textFrame = CGRect(origin: CGPoint(x: titleHorizontalOffset, y: textOffsetY), size: textSize)
}
self.textLayer.frame = textFrame
self.tintTextLayer.frame = textFrame
self.tintTextLayer.isHidden = !needsTintText
if let badgeLayer = self.badgeLayer, let tintBadgeLayer = self.tintBadgeLayer {
badgeLayer.frame = CGRect(origin: CGPoint(x: textFrame.maxX + 4.0, y: 0.0), size: badgeLayer.frame.size)
tintBadgeLayer.frame = badgeLayer.frame
}
if isPremiumLocked {
let lockIconLayer: SimpleLayer
if let current = self.lockIconLayer {
lockIconLayer = current
} else {
lockIconLayer = SimpleLayer()
self.lockIconLayer = lockIconLayer
self.layer.addSublayer(lockIconLayer)
}
if let image = PresentationResourcesChat.chatEntityKeyboardLock(theme, color: color) {
let imageSize = image.size
lockIconLayer.contents = image.cgImage
lockIconLayer.frame = CGRect(origin: CGPoint(x: textFrame.minX - imageSize.width - 3.0, y: 2.0 + UIScreenPixel), size: imageSize)
} else {
lockIconLayer.contents = nil
}
let tintLockIconLayer: SimpleLayer
if let current = self.tintLockIconLayer {
tintLockIconLayer = current
} else {
tintLockIconLayer = SimpleLayer()
self.tintLockIconLayer = tintLockIconLayer
self.tintContentLayer.addSublayer(tintLockIconLayer)
}
if let image = PresentationResourcesChat.chatEntityKeyboardLock(theme, color: .black) {
tintLockIconLayer.contents = image.cgImage
tintLockIconLayer.frame = lockIconLayer.frame
tintLockIconLayer.isHidden = !needsVibrancy
} else {
tintLockIconLayer.contents = nil
}
} else {
if let lockIconLayer = self.lockIconLayer {
self.lockIconLayer = nil
lockIconLayer.removeFromSuperlayer()
}
if let tintLockIconLayer = self.tintLockIconLayer {
self.tintLockIconLayer = nil
tintLockIconLayer.removeFromSuperlayer()
}
}
let subtitleSize: CGSize
if let subtitle = subtitle {
var updateSubtitleContents: UIImage?
var updateTintSubtitleContents: UIImage?
if let currentSubtitleLayout = self.currentSubtitleLayout, currentSubtitleLayout.string == subtitle, currentSubtitleLayout.color == subtitleColor, currentSubtitleLayout.constrainedWidth == textConstrainedWidth {
subtitleSize = currentSubtitleLayout.size
} else {
let string = NSAttributedString(string: subtitle, font: Font.regular(15.0), textColor: subtitleColor)
let whiteString = NSAttributedString(string: subtitle, font: Font.regular(15.0), textColor: .black)
let stringBounds = string.boundingRect(with: CGSize(width: textConstrainedWidth, height: 100.0), options: .usesLineFragmentOrigin, context: nil)
subtitleSize = CGSize(width: ceil(stringBounds.width), height: ceil(stringBounds.height))
updateSubtitleContents = generateImage(subtitleSize, opaque: false, scale: 0.0, rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
UIGraphicsPushContext(context)
string.draw(in: stringBounds)
UIGraphicsPopContext()
})
updateTintSubtitleContents = generateImage(subtitleSize, opaque: false, scale: 0.0, rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
UIGraphicsPushContext(context)
whiteString.draw(in: stringBounds)
UIGraphicsPopContext()
})
self.currentSubtitleLayout = (subtitle, subtitleColor, textConstrainedWidth, subtitleSize)
}
let subtitleLayer: SimpleLayer
if let current = self.subtitleLayer {
subtitleLayer = current
} else {
subtitleLayer = SimpleLayer()
self.subtitleLayer = subtitleLayer
self.layer.addSublayer(subtitleLayer)
}
if let updateSubtitleContents = updateSubtitleContents {
subtitleLayer.contents = updateSubtitleContents.cgImage
}
let tintSubtitleLayer: SimpleLayer
if let current = self.tintSubtitleLayer {
tintSubtitleLayer = current
} else {
tintSubtitleLayer = SimpleLayer()
self.tintSubtitleLayer = tintSubtitleLayer
self.tintContentLayer.addSublayer(tintSubtitleLayer)
}
tintSubtitleLayer.isHidden = !needsVibrancy
if let updateTintSubtitleContents = updateTintSubtitleContents {
tintSubtitleLayer.contents = updateTintSubtitleContents.cgImage
}
let subtitleFrame = CGRect(origin: CGPoint(x: 0.0, y: textFrame.maxY + 1.0), size: subtitleSize)
subtitleLayer.frame = subtitleFrame
tintSubtitleLayer.frame = subtitleFrame
} else {
subtitleSize = CGSize()
if let subtitleLayer = self.subtitleLayer {
self.subtitleLayer = nil
subtitleLayer.removeFromSuperlayer()
}
if let tintSubtitleLayer = self.tintSubtitleLayer {
self.tintSubtitleLayer = nil
tintSubtitleLayer.removeFromSuperlayer()
}
}
self.clearIconLayer?.frame = CGRect(origin: CGPoint(x: constrainedSize.width - clearSize.width, y: floorToScreenPixels((textSize.height - clearSize.height) / 2.0)), size: clearSize)
var size: CGSize
size = CGSize(width: constrainedSize.width, height: constrainedSize.height)
if let embeddedItems = embeddedItems {
let groupEmbeddedView: GroupEmbeddedView
if let current = self.groupEmbeddedView {
groupEmbeddedView = current
} else {
groupEmbeddedView = GroupEmbeddedView(performItemAction: self.performItemAction)
self.groupEmbeddedView = groupEmbeddedView
self.addSubview(groupEmbeddedView)
}
let groupEmbeddedViewSize = CGSize(width: constrainedSize.width + insets.left + insets.right, height: 36.0)
groupEmbeddedView.frame = CGRect(origin: CGPoint(x: -insets.left, y: size.height - groupEmbeddedViewSize.height), size: groupEmbeddedViewSize)
groupEmbeddedView.update(
context: context,
theme: theme,
insets: insets,
size: groupEmbeddedViewSize,
items: embeddedItems,
isStickers: isStickers,
cache: cache,
renderer: renderer,
attemptSynchronousLoad: attemptSynchronousLoad
)
} else {
if let groupEmbeddedView = self.groupEmbeddedView {
self.groupEmbeddedView = nil
groupEmbeddedView.removeFromSuperview()
}
}
if let actionButtonSize = actionButtonSize, let actionButton = self.actionButton {
let actionButtonFrame = CGRect(origin: CGPoint(x: size.width - actionButtonSize.width, y: textFrame.minY + (actionButtonIsCompact ? 0.0 : 3.0)), size: actionButtonSize)
actionButton.bounds = CGRect(origin: CGPoint(), size: actionButtonFrame.size)
actionButton.center = actionButtonFrame.center
}
if hasTopSeparator {
let separatorLayer: SimpleLayer
if let current = self.separatorLayer {
separatorLayer = current
} else {
separatorLayer = SimpleLayer()
self.separatorLayer = separatorLayer
self.layer.addSublayer(separatorLayer)
}
separatorLayer.backgroundColor = subtitleColor.cgColor
separatorLayer.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: UIScreenPixel))
let tintSeparatorLayer: SimpleLayer
if let current = self.tintSeparatorLayer {
tintSeparatorLayer = current
} else {
tintSeparatorLayer = SimpleLayer()
self.tintSeparatorLayer = tintSeparatorLayer
self.tintContentLayer.addSublayer(tintSeparatorLayer)
}
tintSeparatorLayer.backgroundColor = UIColor.black.cgColor
tintSeparatorLayer.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: UIScreenPixel))
tintSeparatorLayer.isHidden = !needsVibrancy
} else {
if let separatorLayer = self.separatorLayer {
self.separatorLayer = separatorLayer
separatorLayer.removeFromSuperlayer()
}
if let tintSeparatorLayer = self.tintSeparatorLayer {
self.tintSeparatorLayer = tintSeparatorLayer
tintSeparatorLayer.removeFromSuperlayer()
}
}
return (size, titleHorizontalOffset + textSize.width + clearWidth)
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
return super.hitTest(point, with: event)
}
func tapGesture(point: CGPoint) -> Bool {
if let groupEmbeddedView = self.groupEmbeddedView {
return groupEmbeddedView.tapGesture(point: self.convert(point, to: groupEmbeddedView))
} else {
return false
}
}
}
@@ -0,0 +1,375 @@
import Foundation
import UIKit
import Display
import ComponentFlow
import TelegramPresentationData
import TelegramCore
import Postbox
import SwiftSignalKit
import MultiAnimationRenderer
import AnimationCache
import AccountContext
import TelegramUIPreferences
import GenerateStickerPlaceholderImage
import EmojiTextAttachmentView
import LottieAnimationCache
public final class InlineFileIconLayer: MultiAnimationRenderTarget {
private final class Arguments {
let context: InlineFileIconLayer.Context
let userLocation: MediaResourceUserLocation
let file: TelegramMediaFile
let cache: AnimationCache
let renderer: MultiAnimationRenderer
let unique: Bool
let placeholderColor: UIColor
let pointSize: CGSize
let pixelSize: CGSize
init(context: InlineFileIconLayer.Context, userLocation: MediaResourceUserLocation, file: TelegramMediaFile, cache: AnimationCache, renderer: MultiAnimationRenderer, unique: Bool, placeholderColor: UIColor, pointSize: CGSize, pixelSize: CGSize) {
self.context = context
self.userLocation = userLocation
self.file = file
self.cache = cache
self.renderer = renderer
self.unique = unique
self.placeholderColor = placeholderColor
self.pointSize = pointSize
self.pixelSize = pixelSize
}
}
public enum Context: Equatable {
public final class Custom: Equatable {
public let postbox: Postbox
public let energyUsageSettings: () -> EnergyUsageSettings
public let resolveInlineStickers: ([Int64]) -> Signal<[Int64: TelegramMediaFile], NoError>
public init(postbox: Postbox, energyUsageSettings: @escaping () -> EnergyUsageSettings, resolveInlineStickers: @escaping ([Int64]) -> Signal<[Int64: TelegramMediaFile], NoError>) {
self.postbox = postbox
self.energyUsageSettings = energyUsageSettings
self.resolveInlineStickers = resolveInlineStickers
}
public static func ==(lhs: Custom, rhs: Custom) -> Bool {
if lhs.postbox !== rhs.postbox {
return false
}
return true
}
}
case account(AccountContext)
case custom(Custom)
var postbox: Postbox {
switch self {
case let .account(account):
return account.account.postbox
case let .custom(custom):
return custom.postbox
}
}
var energyUsageSettings: EnergyUsageSettings {
switch self {
case let .account(account):
return account.sharedContext.energyUsageSettings
case let .custom(custom):
return custom.energyUsageSettings()
}
}
func resolveInlineStickers(fileIds: [Int64]) -> Signal<[Int64: TelegramMediaFile], NoError> {
switch self {
case let .account(account):
return account.engine.stickers.resolveInlineStickers(fileIds: fileIds)
case let .custom(custom):
return custom.resolveInlineStickers(fileIds)
}
}
public static func ==(lhs: Context, rhs: Context) -> Bool {
switch lhs {
case let .account(lhsContext):
if case let .account(rhsContext) = rhs, lhsContext === rhsContext {
return true
} else {
return false
}
case let .custom(custom):
if case .custom(custom) = rhs {
return true
} else {
return false
}
}
}
}
public static let queue = Queue()
public struct Key: Hashable {
public var id: Int64
public var index: Int
public init(id: Int64, index: Int) {
self.id = id
self.index = index
}
}
private let arguments: Arguments?
private var isDisplayingPlaceholder: Bool = false
private var didProcessTintColor: Bool = false
public private(set) var file: TelegramMediaFile?
private var infoDisposable: Disposable?
private var disposable: Disposable?
private var fetchDisposable: Disposable?
private var loadDisposable: Disposable?
private var _contentTintColor: UIColor?
public var contentTintColor: UIColor? {
get {
return self._contentTintColor
}
set(value) {
if self._contentTintColor != value {
self._contentTintColor = value
}
}
}
private var _dynamicColor: UIColor?
public var dynamicColor: UIColor? {
get {
return self._dynamicColor
}
set(value) {
if self._dynamicColor != value {
self._dynamicColor = value
}
}
}
private var currentLoopCount: Int = 0
private var isInHierarchyValue: Bool = false
public convenience init(
context: AccountContext,
userLocation: MediaResourceUserLocation,
attemptSynchronousLoad: Bool,
file: TelegramMediaFile,
cache: AnimationCache,
renderer: MultiAnimationRenderer,
unique: Bool = false,
placeholderColor: UIColor,
pointSize: CGSize,
dynamicColor: UIColor? = nil
) {
self.init(
context: .account(context),
userLocation: userLocation,
attemptSynchronousLoad: attemptSynchronousLoad,
file: file,
cache: cache,
renderer: renderer,
unique: unique,
placeholderColor: placeholderColor,
pointSize: pointSize,
dynamicColor: dynamicColor
)
}
public init(
context: InlineFileIconLayer.Context,
userLocation: MediaResourceUserLocation,
attemptSynchronousLoad: Bool,
file: TelegramMediaFile,
cache: AnimationCache,
renderer: MultiAnimationRenderer,
unique: Bool = false,
placeholderColor: UIColor,
pointSize: CGSize,
dynamicColor: UIColor? = nil
) {
let scale = min(2.0, UIScreenScale)
self.arguments = Arguments(
context: context,
userLocation: userLocation,
file: file,
cache: cache,
renderer: renderer,
unique: unique,
placeholderColor: placeholderColor,
pointSize: pointSize,
pixelSize: CGSize(width: pointSize.width * scale, height: pointSize.height * scale)
)
self._dynamicColor = dynamicColor
super.init()
self.updateFile(file: file, attemptSynchronousLoad: attemptSynchronousLoad)
}
override public init(layer: Any) {
self.arguments = nil
super.init(layer: layer)
}
required public init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
self.loadDisposable?.dispose()
self.infoDisposable?.dispose()
self.disposable?.dispose()
self.fetchDisposable?.dispose()
}
override public func action(forKey event: String) -> CAAction? {
if event == kCAOnOrderIn {
self.isInHierarchyValue = true
} else if event == kCAOnOrderOut {
self.isInHierarchyValue = false
}
return nullAction
}
private func updateFile(file: TelegramMediaFile, attemptSynchronousLoad: Bool) {
guard let arguments = self.arguments else {
return
}
if self.file?.fileId == file.fileId {
return
}
self.file = file
if attemptSynchronousLoad {
if !arguments.renderer.loadFirstFrameSynchronously(target: self, cache: arguments.cache, itemId: file.resource.id.stringRepresentation, size: arguments.pixelSize) {
if let image = generateStickerPlaceholderImage(data: file.immediateThumbnailData, size: arguments.pointSize, imageSize: file.dimensions?.cgSize ?? CGSize(width: 512.0, height: 512.0), backgroundColor: nil, foregroundColor: arguments.placeholderColor) {
self.contents = image.cgImage
self.isDisplayingPlaceholder = true
}
}
self.loadAnimation()
} else {
let isTemplate = file.isCustomTemplateEmoji
let pointSize = arguments.pointSize
let placeholderColor = arguments.placeholderColor
let isThumbnailCancelled = Atomic<Bool>(value: false)
self.loadDisposable = arguments.renderer.loadFirstFrame(
target: self,
cache: arguments.cache,
itemId: file.resource.id.stringRepresentation,
size: arguments.pixelSize,
fetch: animationCacheFetchFile(postbox: arguments.context.postbox, userLocation: arguments.userLocation, userContentType: .sticker, resource: .media(media: .standalone(media: file), resource: file.resource), type: AnimationCacheAnimationType(file: file), keyframeOnly: true, customColor: isTemplate ? .white : nil), completion: { [weak self] result, isFinal in
if !result {
MultiAnimationRendererImpl.firstFrameQueue.async {
let image = generateStickerPlaceholderImage(data: file.immediateThumbnailData, size: pointSize, scale: min(2.0, UIScreenScale), imageSize: file.dimensions?.cgSize ?? CGSize(width: 512.0, height: 512.0), backgroundColor: nil, foregroundColor: placeholderColor)
DispatchQueue.main.async {
guard let strongSelf = self, !isThumbnailCancelled.with({ $0 }) else {
return
}
if let image = image {
strongSelf.contents = image.cgImage
strongSelf.isDisplayingPlaceholder = true
}
if isFinal {
strongSelf.loadAnimation()
}
}
}
} else {
guard let strongSelf = self else {
return
}
let _ = isThumbnailCancelled.swap(true)
strongSelf.loadAnimation()
}
})
}
}
private func loadAnimation() {
/*guard let arguments = self.arguments else {
return
}
guard let file = self.file else {
return
}
let isTemplate = file.isCustomTemplateEmoji
let context = arguments.context
if file.isAnimatedSticker || file.isVideoSticker || file.isVideoEmoji {
let keyframeOnly = arguments.pixelSize.width >= 120.0
self.disposable = arguments.renderer.add(target: self, cache: arguments.cache, itemId: file.resource.id.stringRepresentation, unique: arguments.unique, size: arguments.pixelSize, fetch: animationCacheFetchFile(postbox: arguments.context.postbox, userLocation: arguments.userLocation, userContentType: .sticker, resource: .media(media: .standalone(media: file), resource: file.resource), type: AnimationCacheAnimationType(file: file), keyframeOnly: keyframeOnly, customColor: isTemplate ? .white : nil))
} else {
self.disposable = arguments.renderer.add(target: self, cache: arguments.cache, itemId: file.resource.id.stringRepresentation, unique: arguments.unique, size: arguments.pixelSize, fetch: { options in
let dataDisposable = context.postbox.mediaBox.resourceData(file.resource).start(next: { result in
guard result.complete else {
return
}
cacheStillSticker(path: result.path, width: Int(options.size.width), height: Int(options.size.height), writer: options.writer, customColor: isTemplate ? .white : nil)
})
let fetchDisposable = freeMediaFileResourceInteractiveFetched(postbox: context.postbox, userLocation: arguments.userLocation, fileReference: .customEmoji(media: file), resource: file.resource).start()
return ActionDisposable {
dataDisposable.dispose()
fetchDisposable.dispose()
}
})
}*/
}
override public func updateDisplayPlaceholder(displayPlaceholder: Bool) {
if self.isDisplayingPlaceholder == displayPlaceholder {
return
}
self.isDisplayingPlaceholder = displayPlaceholder
}
override public func transitionToContents(_ contents: AnyObject, didLoop: Bool) {
if self.isDisplayingPlaceholder {
self.isDisplayingPlaceholder = false
if let current = self.contents {
let previousLayer = SimpleLayer()
previousLayer.contents = current
previousLayer.frame = self.frame
self.superlayer?.insertSublayer(previousLayer, below: self)
previousLayer.opacity = 0.0
previousLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, completion: { [weak previousLayer] _ in
previousLayer?.removeFromSuperlayer()
})
self.contents = contents
self.animateAlpha(from: 0.0, to: 1.0, duration: 0.18)
} else {
self.contents = contents
self.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
} else {
self.contents = contents
}
}
}
@@ -0,0 +1,346 @@
import Foundation
import UIKit
import Display
import ComponentFlow
public class PassthroughLayer: CALayer {
public var mirrorLayer: CALayer?
override init() {
super.init()
}
override init(layer: Any) {
super.init(layer: layer)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override public var position: CGPoint {
get {
return super.position
} set(value) {
if let mirrorLayer = self.mirrorLayer {
mirrorLayer.position = value
}
super.position = value
}
}
override public var bounds: CGRect {
get {
return super.bounds
} set(value) {
if let mirrorLayer = self.mirrorLayer {
mirrorLayer.bounds = value
}
super.bounds = value
}
}
override public var opacity: Float {
get {
return super.opacity
} set(value) {
if let mirrorLayer = self.mirrorLayer {
mirrorLayer.opacity = value
}
super.opacity = value
}
}
override public var sublayerTransform: CATransform3D {
get {
return super.sublayerTransform
} set(value) {
if let mirrorLayer = self.mirrorLayer {
mirrorLayer.sublayerTransform = value
}
super.sublayerTransform = value
}
}
override public var transform: CATransform3D {
get {
return super.transform
} set(value) {
if let mirrorLayer = self.mirrorLayer {
mirrorLayer.transform = value
}
super.transform = value
}
}
override public func add(_ animation: CAAnimation, forKey key: String?) {
if let mirrorLayer = self.mirrorLayer {
mirrorLayer.add(animation, forKey: key)
}
super.add(animation, forKey: key)
}
override public func removeAllAnimations() {
if let mirrorLayer = self.mirrorLayer {
mirrorLayer.removeAllAnimations()
}
super.removeAllAnimations()
}
override public func removeAnimation(forKey: String) {
if let mirrorLayer = self.mirrorLayer {
mirrorLayer.removeAnimation(forKey: forKey)
}
super.removeAnimation(forKey: forKey)
}
}
open class PassthroughView: UIView {
override public static var layerClass: AnyClass {
return PassthroughLayer.self
}
public let passthroughView: UIView
override public init(frame: CGRect) {
self.passthroughView = UIView()
super.init(frame: frame)
(self.layer as? PassthroughLayer)?.mirrorLayer = self.passthroughView.layer
}
required public init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
class PassthroughShapeLayer: CAShapeLayer {
var mirrorLayer: CAShapeLayer?
override init() {
super.init()
}
override init(layer: Any) {
super.init(layer: layer)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override var position: CGPoint {
get {
return super.position
} set(value) {
if let mirrorLayer = self.mirrorLayer {
mirrorLayer.position = value
}
super.position = value
}
}
override var bounds: CGRect {
get {
return super.bounds
} set(value) {
if let mirrorLayer = self.mirrorLayer {
mirrorLayer.bounds = value
}
super.bounds = value
}
}
override var opacity: Float {
get {
return super.opacity
} set(value) {
if let mirrorLayer = self.mirrorLayer {
mirrorLayer.opacity = value
}
super.opacity = value
}
}
override var sublayerTransform: CATransform3D {
get {
return super.sublayerTransform
} set(value) {
if let mirrorLayer = self.mirrorLayer {
mirrorLayer.sublayerTransform = value
}
super.sublayerTransform = value
}
}
override var transform: CATransform3D {
get {
return super.transform
} set(value) {
if let mirrorLayer = self.mirrorLayer {
mirrorLayer.transform = value
}
super.transform = value
}
}
override var path: CGPath? {
get {
return super.path
} set(value) {
if let mirrorLayer = self.mirrorLayer {
mirrorLayer.path = value
}
super.path = value
}
}
override var fillColor: CGColor? {
get {
return super.fillColor
} set(value) {
if let mirrorLayer = self.mirrorLayer {
mirrorLayer.fillColor = value
}
super.fillColor = value
}
}
override var fillRule: CAShapeLayerFillRule {
get {
return super.fillRule
} set(value) {
if let mirrorLayer = self.mirrorLayer {
mirrorLayer.fillRule = value
}
super.fillRule = value
}
}
override var strokeColor: CGColor? {
get {
return super.strokeColor
} set(value) {
/*if let mirrorLayer = self.mirrorLayer {
mirrorLayer.strokeColor = value
}*/
super.strokeColor = value
}
}
override var strokeStart: CGFloat {
get {
return super.strokeStart
} set(value) {
if let mirrorLayer = self.mirrorLayer {
mirrorLayer.strokeStart = value
}
super.strokeStart = value
}
}
override var strokeEnd: CGFloat {
get {
return super.strokeEnd
} set(value) {
if let mirrorLayer = self.mirrorLayer {
mirrorLayer.strokeEnd = value
}
super.strokeEnd = value
}
}
override var lineWidth: CGFloat {
get {
return super.lineWidth
} set(value) {
if let mirrorLayer = self.mirrorLayer {
mirrorLayer.lineWidth = value
}
super.lineWidth = value
}
}
override var miterLimit: CGFloat {
get {
return super.miterLimit
} set(value) {
if let mirrorLayer = self.mirrorLayer {
mirrorLayer.miterLimit = value
}
super.miterLimit = value
}
}
override var lineCap: CAShapeLayerLineCap {
get {
return super.lineCap
} set(value) {
if let mirrorLayer = self.mirrorLayer {
mirrorLayer.lineCap = value
}
super.lineCap = value
}
}
override var lineJoin: CAShapeLayerLineJoin {
get {
return super.lineJoin
} set(value) {
if let mirrorLayer = self.mirrorLayer {
mirrorLayer.lineJoin = value
}
super.lineJoin = value
}
}
override var lineDashPhase: CGFloat {
get {
return super.lineDashPhase
} set(value) {
if let mirrorLayer = self.mirrorLayer {
mirrorLayer.lineDashPhase = value
}
super.lineDashPhase = value
}
}
override var lineDashPattern: [NSNumber]? {
get {
return super.lineDashPattern
} set(value) {
if let mirrorLayer = self.mirrorLayer {
mirrorLayer.lineDashPattern = value
}
super.lineDashPattern = value
}
}
override func add(_ animation: CAAnimation, forKey key: String?) {
if let mirrorLayer = self.mirrorLayer {
mirrorLayer.add(animation, forKey: key)
}
super.add(animation, forKey: key)
}
override func removeAllAnimations() {
if let mirrorLayer = self.mirrorLayer {
mirrorLayer.removeAllAnimations()
}
super.removeAllAnimations()
}
override func removeAnimation(forKey: String) {
if let mirrorLayer = self.mirrorLayer {
mirrorLayer.removeAnimation(forKey: forKey)
}
super.removeAnimation(forKey: forKey)
}
}
@@ -0,0 +1,140 @@
import Foundation
import UIKit
import Display
import ComponentFlow
import AccountContext
import TelegramCore
private let premiumBadgeIcon: UIImage? = generateTintedImage(image: UIImage(bundleImageName: "Chat List/PeerPremiumIcon"), color: .white)
private let featuredBadgeIcon: UIImage? = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Media/PanelBadgeAdd"), color: .white)
private let lockedBadgeIcon: UIImage? = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Media/PanelBadgeLock"), color: .white)
private let itemBadgeTextFont: UIFont = {
return Font.regular(10.0)
}()
final class PremiumBadgeView: UIView {
private let context: AccountContext
private var badge: EmojiKeyboardItemLayer.Badge?
let contentLayer: SimpleLayer
private let overlayColorLayer: SimpleLayer
private let iconLayer: SimpleLayer
private var customFileLayer: InlineFileIconLayer?
init(context: AccountContext) {
self.context = context
self.contentLayer = SimpleLayer()
self.contentLayer.contentsGravity = .resize
self.contentLayer.masksToBounds = true
self.overlayColorLayer = SimpleLayer()
self.overlayColorLayer.masksToBounds = true
self.iconLayer = SimpleLayer()
super.init(frame: CGRect())
self.layer.addSublayer(self.contentLayer)
self.layer.addSublayer(self.overlayColorLayer)
self.layer.addSublayer(self.iconLayer)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(transition: ComponentTransition, badge: EmojiKeyboardItemLayer.Badge, backgroundColor: UIColor, size: CGSize) {
if self.badge != badge {
self.badge = badge
switch badge {
case .premium:
self.iconLayer.contents = premiumBadgeIcon?.cgImage
case .featured:
self.iconLayer.contents = featuredBadgeIcon?.cgImage
case .locked:
self.iconLayer.contents = lockedBadgeIcon?.cgImage
case let .text(text):
let string = NSAttributedString(string: text, font: itemBadgeTextFont)
let size = CGSize(width: 12.0, height: 12.0)
let stringBounds = string.boundingRect(with: CGSize(width: 100.0, height: 100.0), options: .usesLineFragmentOrigin, context: nil)
let image = generateImage(size, rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
UIGraphicsPushContext(context)
string.draw(at: CGPoint(x: floor((size.width - stringBounds.width) * 0.5), y: floor((size.height - stringBounds.height) * 0.5)))
UIGraphicsPopContext()
})
self.iconLayer.contents = image?.cgImage
case .customFile:
self.iconLayer.contents = nil
}
if case let .customFile(customFile) = badge {
let customFileLayer: InlineFileIconLayer
if let current = self.customFileLayer {
customFileLayer = current
} else {
customFileLayer = InlineFileIconLayer(
context: self.context,
userLocation: .other,
attemptSynchronousLoad: false,
file: customFile._parse(),
cache: self.context.animationCache,
renderer: self.context.animationRenderer,
unique: false,
placeholderColor: .clear,
pointSize: CGSize(width: 18.0, height: 18.0),
dynamicColor: nil
)
self.customFileLayer = customFileLayer
self.layer.addSublayer(customFileLayer)
}
let _ = customFileLayer
} else {
if let customFileLayer = self.customFileLayer {
self.customFileLayer = nil
customFileLayer.removeFromSuperlayer()
}
}
}
let iconInset: CGFloat
switch badge {
case .premium:
iconInset = 2.0
case .featured:
iconInset = 0.0
case .locked:
iconInset = 0.0
case .text, .customFile:
iconInset = 0.0
}
switch badge {
case .text, .customFile:
self.contentLayer.isHidden = true
self.overlayColorLayer.isHidden = true
default:
self.contentLayer.isHidden = false
self.overlayColorLayer.isHidden = false
}
self.overlayColorLayer.backgroundColor = backgroundColor.cgColor
transition.setFrame(layer: self.contentLayer, frame: CGRect(origin: CGPoint(), size: size))
transition.setCornerRadius(layer: self.contentLayer, cornerRadius: min(size.width / 2.0, size.height / 2.0))
transition.setFrame(layer: self.overlayColorLayer, frame: CGRect(origin: CGPoint(), size: size))
transition.setCornerRadius(layer: self.overlayColorLayer, cornerRadius: min(size.width / 2.0, size.height / 2.0))
transition.setFrame(layer: self.iconLayer, frame: CGRect(origin: CGPoint(), size: size).insetBy(dx: iconInset, dy: iconInset))
if let customFileLayer = self.customFileLayer {
let iconSize = CGSize(width: 18.0, height: 18.0)
transition.setFrame(layer: customFileLayer, frame: CGRect(origin: CGPoint(), size: iconSize))
}
}
}
@@ -0,0 +1,138 @@
import Foundation
import UIKit
import Display
import ComponentFlow
import TelegramPresentationData
final class WarpView: UIView {
private final class WarpPartView: UIView {
let cloneView: PortalView
init?(contentView: PortalSourceView) {
guard let cloneView = PortalView(matchPosition: false) else {
return nil
}
self.cloneView = cloneView
super.init(frame: CGRect())
self.layer.anchorPoint = CGPoint(x: 0.5, y: 0.0)
self.clipsToBounds = true
self.addSubview(cloneView.view)
contentView.addPortal(view: cloneView)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(containerSize: CGSize, rect: CGRect, transition: ComponentTransition) {
transition.setFrame(view: self.cloneView.view, frame: CGRect(origin: CGPoint(x: -rect.minX, y: -rect.minY), size: CGSize(width: containerSize.width, height: containerSize.height)))
}
}
let contentView: PortalSourceView
private let clippingView: UIView
private var warpViews: [WarpPartView] = []
private let warpMaskContainer: UIView
private let warpMaskGradientLayer: SimpleGradientLayer
override init(frame: CGRect) {
self.contentView = PortalSourceView()
self.clippingView = UIView()
self.warpMaskContainer = UIView()
self.warpMaskGradientLayer = SimpleGradientLayer()
self.warpMaskContainer.layer.mask = self.warpMaskGradientLayer
super.init(frame: frame)
self.clippingView.addSubview(self.contentView)
self.clippingView.clipsToBounds = true
self.addSubview(self.clippingView)
self.addSubview(self.warpMaskContainer)
for _ in 0 ..< 8 {
if let warpView = WarpPartView(contentView: self.contentView) {
self.warpViews.append(warpView)
self.warpMaskContainer.addSubview(warpView)
}
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(size: CGSize, topInset: CGFloat, warpHeight: CGFloat, theme: PresentationTheme, transition: ComponentTransition) {
transition.setFrame(view: self.contentView, frame: CGRect(origin: CGPoint(), size: size))
let allItemsHeight = warpHeight * 0.5
for i in 0 ..< self.warpViews.count {
let itemHeight = warpHeight / CGFloat(self.warpViews.count)
let itemFraction = CGFloat(i + 1) / CGFloat(self.warpViews.count)
let _ = itemHeight
let da = CGFloat.pi * 0.5 / CGFloat(self.warpViews.count)
let alpha = CGFloat.pi * 0.5 - itemFraction * CGFloat.pi * 0.5
let endPoint = CGPoint(x: cos(alpha), y: sin(alpha))
let prevAngle = alpha + da
let prevPt = CGPoint(x: cos(prevAngle), y: sin(prevAngle))
var angle: CGFloat
angle = -atan2(endPoint.y - prevPt.y, endPoint.x - prevPt.x)
let itemLengthVector = CGPoint(x: endPoint.x - prevPt.x, y: endPoint.y - prevPt.y)
let itemLength = sqrt(itemLengthVector.x * itemLengthVector.x + itemLengthVector.y * itemLengthVector.y) * warpHeight * 0.5
let _ = itemLength
var transform: CATransform3D
transform = CATransform3DIdentity
transform.m34 = 1.0 / 240.0
transform = CATransform3DTranslate(transform, 0.0, prevPt.x * allItemsHeight, (1.0 - prevPt.y) * allItemsHeight)
transform = CATransform3DRotate(transform, angle, 1.0, 0.0, 0.0)
let positionY = size.height - allItemsHeight + 4.0 + CGFloat(i) * itemLength
let rect = CGRect(origin: CGPoint(x: 0.0, y: positionY), size: CGSize(width: size.width, height: itemLength))
transition.setPosition(view: self.warpViews[i], position: CGPoint(x: rect.midX, y: 4.0))
transition.setBounds(view: self.warpViews[i], bounds: CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: itemLength)))
transition.setTransform(view: self.warpViews[i], transform: transform)
self.warpViews[i].update(containerSize: size, rect: rect, transition: transition)
}
let clippingTopInset: CGFloat = topInset
let frame = CGRect(origin: CGPoint(x: 0.0, y: clippingTopInset), size: CGSize(width: size.width, height: -clippingTopInset + size.height - 21.0))
transition.setPosition(view: self.clippingView, position: frame.center)
transition.setBounds(view: self.clippingView, bounds: CGRect(origin: CGPoint(x: 0.0, y: clippingTopInset), size: frame.size))
self.clippingView.clipsToBounds = true
transition.setFrame(view: self.warpMaskContainer, frame: CGRect(origin: CGPoint(x: 0.0, y: size.height - allItemsHeight), size: CGSize(width: size.width, height: allItemsHeight)))
var locations: [NSNumber] = []
var colors: [CGColor] = []
let numStops = 6
for i in 0 ..< numStops {
let step = CGFloat(i) / CGFloat(numStops - 1)
locations.append(step as NSNumber)
colors.append(UIColor.black.withAlphaComponent(1.0 - step * step).cgColor)
}
let gradientHeight: CGFloat = 6.0
self.warpMaskGradientLayer.startPoint = CGPoint(x: 0.0, y: (allItemsHeight - gradientHeight) / allItemsHeight)
self.warpMaskGradientLayer.endPoint = CGPoint(x: 0.0, y: 1.0)
self.warpMaskGradientLayer.locations = locations
self.warpMaskGradientLayer.colors = colors
self.warpMaskGradientLayer.type = .axial
transition.setFrame(layer: self.warpMaskGradientLayer, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: allItemsHeight)))
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
return self.contentView.hitTest(point, with: event)
}
}