From 7c0216b9077eb1c40299a43056020e396da2ba3a Mon Sep 17 00:00:00 2001 From: Isaac <> Date: Mon, 20 May 2024 18:51:35 +0400 Subject: [PATCH] Effect improvements --- .../Sources/AvatarVideoNode.swift | 4 +- .../ChatSendMessageContextScreen.swift | 41 +- .../Sources/StickerPackEmojisItem.swift | 16 +- .../Sources/ChatMessageBubbleItemNode.swift | 15 + .../Sources/EmojiKeyboardItemLayer.swift | 477 +++ .../Sources/EmojiPagerContentComponent.swift | 2838 +---------------- .../Sources/EmojiSearchHeaderView.swift | 607 ++++ .../EmojiSearchSearchBarComponent.swift | 19 +- .../Sources/EmptySearchResultsView.swift | 101 + .../EntityKeyboardTopPanelComponent.swift | 6 +- .../Sources/GroupEmbeddedView.swift | 209 ++ .../Sources/GroupExpandActionButton.swift | 128 + .../Sources/GroupHeaderActionButton.swift | 149 + .../Sources/GroupHeaderLayer.swift | 525 +++ .../Sources/PassthroughComponents.swift | 346 ++ .../Sources/PremiumBadgeView.swift | 140 + .../EntityKeyboard/Sources/WarpView.swift | 138 + .../Sources/EmojiListInputComponent.swift | 6 +- 18 files changed, 2939 insertions(+), 2826 deletions(-) create mode 100644 submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiKeyboardItemLayer.swift create mode 100644 submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiSearchHeaderView.swift create mode 100644 submodules/TelegramUI/Components/EntityKeyboard/Sources/EmptySearchResultsView.swift create mode 100644 submodules/TelegramUI/Components/EntityKeyboard/Sources/GroupEmbeddedView.swift create mode 100644 submodules/TelegramUI/Components/EntityKeyboard/Sources/GroupExpandActionButton.swift create mode 100644 submodules/TelegramUI/Components/EntityKeyboard/Sources/GroupHeaderActionButton.swift create mode 100644 submodules/TelegramUI/Components/EntityKeyboard/Sources/GroupHeaderLayer.swift create mode 100644 submodules/TelegramUI/Components/EntityKeyboard/Sources/PassthroughComponents.swift create mode 100644 submodules/TelegramUI/Components/EntityKeyboard/Sources/PremiumBadgeView.swift create mode 100644 submodules/TelegramUI/Components/EntityKeyboard/Sources/WarpView.swift diff --git a/submodules/AvatarVideoNode/Sources/AvatarVideoNode.swift b/submodules/AvatarVideoNode/Sources/AvatarVideoNode.swift index 6b17989a2e..634438cafa 100644 --- a/submodules/AvatarVideoNode/Sources/AvatarVideoNode.swift +++ b/submodules/AvatarVideoNode/Sources/AvatarVideoNode.swift @@ -27,7 +27,7 @@ public final class AvatarVideoNode: ASDisplayNode { private var fileDisposable = MetaDisposable() private var animationFile: TelegramMediaFile? - private var itemLayer: EmojiPagerContentComponent.View.ItemLayer? + private var itemLayer: EmojiKeyboardItemLayer? private var useAnimationNode = false private var animationNode: AnimatedStickerNode? private let stickerFetchedDisposable = MetaDisposable() @@ -101,7 +101,7 @@ public final class AvatarVideoNode: ASDisplayNode { let itemNativeFitSize = self.internalSize.width > 100.0 ? CGSize(width: 192.0, height: 192.0) : CGSize(width: 64.0, height: 64.0) let animationData = EntityKeyboardAnimationData(file: animationFile) - let itemLayer = EmojiPagerContentComponent.View.ItemLayer( + let itemLayer = EmojiKeyboardItemLayer( item: EmojiPagerContentComponent.Item( animationData: animationData, content: .animation(animationData), diff --git a/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageContextScreen.swift b/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageContextScreen.swift index e4cf552b8e..b4d1d47116 100644 --- a/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageContextScreen.swift +++ b/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageContextScreen.swift @@ -179,6 +179,9 @@ final class ChatSendMessageContextScreenComponent: Component { private var animateOutToEmpty: Bool = false private var initializationDisplayLink: SharedDisplayLinkDriver.Link? + private var updateSourcePositionsDisplayLink: SharedDisplayLinkDriver.Link? + + private var stableSourceSendButtonFrame: CGRect? override init(frame: CGRect) { self.backgroundView = BlurredBackgroundView(color: .clear, enableBlur: true) @@ -273,6 +276,21 @@ final class ChatSendMessageContextScreenComponent: Component { let environment = environment[EnvironmentType.self].value + if let previousEnvironment = self.environment, previousEnvironment.inputHeight != 0.0, environment.inputHeight == 0.0 { + DispatchQueue.main.async { [weak self] in + guard let self, let component = self.component else { + return + } + let stableSourceSendButtonFrame = convertFrame(component.sourceSendButton.bounds, from: component.sourceSendButton.view, to: self) + if self.stableSourceSendButtonFrame != stableSourceSendButtonFrame { + self.stableSourceSendButtonFrame = stableSourceSendButtonFrame + if !self.isUpdating { + self.state?.updated(transition: .spring(duration: 0.35)) + } + } + } + } + var transition = transition var transitionIsImmediate = transition.animation.isImmediate @@ -365,7 +383,19 @@ final class ChatSendMessageContextScreenComponent: Component { self.addSubview(sendButton) } - let sourceSendButtonFrame = convertFrame(component.sourceSendButton.bounds, from: component.sourceSendButton.view, to: self) + let sourceSendButtonFrame: CGRect + switch self.presentationAnimationState { + case .animatedOut: + sourceSendButtonFrame = convertFrame(component.sourceSendButton.bounds, from: component.sourceSendButton.view, to: self) + self.stableSourceSendButtonFrame = sourceSendButtonFrame + default: + if let stableSourceSendButtonFrame = self.stableSourceSendButtonFrame { + sourceSendButtonFrame = stableSourceSendButtonFrame + } else { + sourceSendButtonFrame = convertFrame(component.sourceSendButton.bounds, from: component.sourceSendButton.view, to: self) + self.stableSourceSendButtonFrame = sourceSendButtonFrame + } + } let sendButtonScale: CGFloat switch self.presentationAnimationState { @@ -843,6 +873,8 @@ final class ChatSendMessageContextScreenComponent: Component { if !self.isUpdating { self.state?.updated(transition: .easeInOut(duration: 0.2)) } + + self.endEditing(true) }) })) } @@ -1014,6 +1046,13 @@ final class ChatSendMessageContextScreenComponent: Component { } } + if let standaloneReactionAnimation, let targetView = messageItemView.effectIconView { + let effectSize = CGSize(width: 380.0, height: 380.0) + var effectFrame = effectSize.centered(around: targetView.convert(targetView.bounds.center, to: self)) + effectFrame.origin.x -= effectFrame.width * 0.3 + transition.setFrame(view: standaloneReactionAnimation.view, frame: effectFrame) + } + if let reactionContextNode = self.reactionContextNode { let reactionContextY = environment.statusBarHeight let size = availableSize diff --git a/submodules/StickerPackPreviewUI/Sources/StickerPackEmojisItem.swift b/submodules/StickerPackPreviewUI/Sources/StickerPackEmojisItem.swift index 3d629bea08..f47734863d 100644 --- a/submodules/StickerPackPreviewUI/Sources/StickerPackEmojisItem.swift +++ b/submodules/StickerPackPreviewUI/Sources/StickerPackEmojisItem.swift @@ -122,8 +122,8 @@ final class StickerPackEmojisItemNode: GridItemNode { private var boundsChangeTrackerLayer = SimpleLayer() - private var visibleItemLayers: [EmojiPagerContentComponent.View.ItemLayer.Key: EmojiPagerContentComponent.View.ItemLayer] = [:] - private var visibleItemPlaceholderViews: [EmojiPagerContentComponent.View.ItemLayer.Key: EmojiPagerContentComponent.View.ItemPlaceholderView] = [:] + private var visibleItemLayers: [EmojiKeyboardItemLayer.Key: EmojiKeyboardItemLayer] = [:] + private var visibleItemPlaceholderViews: [EmojiKeyboardItemLayer.Key: EmojiPagerContentComponent.View.ItemPlaceholderView] = [:] private let containerNode: ASDisplayNode private let titleNode: ImmediateTextNode @@ -195,7 +195,7 @@ final class StickerPackEmojisItemNode: GridItemNode { func targetItem(at point: CGPoint) -> (TelegramMediaFile, CALayer)? { if let (item, _) = self.item(atPoint: point), let file = item.itemFile { - let itemId = EmojiPagerContentComponent.View.ItemLayer.Key( + let itemId = EmojiKeyboardItemLayer.Key( groupId: 0, itemId: .animation(.file(file.fileId)) ) @@ -237,7 +237,7 @@ final class StickerPackEmojisItemNode: GridItemNode { private func item(atPoint point: CGPoint, extendedHitRange: Bool = false) -> (EmojiPagerContentComponent.Item, CGRect)? { let localPoint = point - var closestItem: (key: EmojiPagerContentComponent.View.ItemLayer.Key, distance: CGFloat)? + var closestItem: (key: EmojiKeyboardItemLayer.Key, distance: CGFloat)? for (key, itemLayer) in self.visibleItemLayers { if extendedHitRange { @@ -308,7 +308,7 @@ final class StickerPackEmojisItemNode: GridItemNode { let animationRenderer = item.animationRenderer let theme = item.theme let items = item.items - var validIds = Set() + var validIds = Set() let itemLayout: ItemLayout if let current = self.itemLayout, current.width == self.size.width && current.itemsCount == items.count { @@ -322,7 +322,7 @@ final class StickerPackEmojisItemNode: GridItemNode { for index in 0 ..< items.count { let item = items[index] - let itemId = EmojiPagerContentComponent.View.ItemLayer.Key( + let itemId = EmojiKeyboardItemLayer.Key( groupId: 0, itemId: .animation(.file(item.file.fileId)) ) @@ -334,7 +334,7 @@ final class StickerPackEmojisItemNode: GridItemNode { var updateItemLayerPlaceholder = false var itemTransition = transition - let itemLayer: EmojiPagerContentComponent.View.ItemLayer + let itemLayer: EmojiKeyboardItemLayer if let current = self.visibleItemLayers[itemId] { itemLayer = current } else { @@ -342,7 +342,7 @@ final class StickerPackEmojisItemNode: GridItemNode { itemTransition = .immediate let animationData = EntityKeyboardAnimationData(file: item.file) - itemLayer = EmojiPagerContentComponent.View.ItemLayer( + itemLayer = EmojiKeyboardItemLayer( item: EmojiPagerContentComponent.Item( animationData: animationData, content: .animation(animationData), diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift index 6d2a04c29c..3b078023ba 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift @@ -638,6 +638,8 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI private var replyRecognizer: ChatSwipeToReplyRecognizer? private var currentSwipeAction: ChatControllerInteractionSwipeAction? + private var fetchEffectDisposable: Disposable? + //private let debugNode: ASDisplayNode override public var visibility: ListViewItemNodeVisibility { @@ -838,6 +840,10 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI required public init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } + + deinit { + self.fetchEffectDisposable?.dispose() + } override public func cancelInsertionAnimations() { self.shadowNode.layer.removeAllAnimations() @@ -5877,6 +5883,9 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI private var additionalAnimationNodes: [ChatMessageTransitionNode.DecorationItemNode] = [] private func playPremiumStickerAnimation(effect: AvailableMessageEffects.MessageEffect, force: Bool) { + guard let item = self.item else { + return + } if self.playedPremiumStickerAnimation && !force { return } @@ -5884,10 +5893,16 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI if let effectAnimation = effect.effectAnimation { self.playEffectAnimation(resource: effectAnimation.resource, isStickerEffect: true) + if self.fetchEffectDisposable == nil { + self.fetchEffectDisposable = freeMediaFileResourceInteractiveFetched(account: item.context.account, userLocation: .other, fileReference: .standalone(media: effectAnimation), resource: effectAnimation.resource).startStrict() + } } else { let effectSticker = effect.effectSticker if let effectFile = effectSticker.videoThumbnails.first { self.playEffectAnimation(resource: effectFile.resource, isStickerEffect: true) + if self.fetchEffectDisposable == nil { + self.fetchEffectDisposable = freeMediaFileResourceInteractiveFetched(account: item.context.account, userLocation: .other, fileReference: .standalone(media: effectSticker), resource: effectFile.resource).startStrict() + } } } } diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiKeyboardItemLayer.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiKeyboardItemLayer.swift new file mode 100644 index 0000000000..19ee0562ae --- /dev/null +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiKeyboardItemLayer.swift @@ -0,0 +1,477 @@ +import Foundation +import UIKit +import Display +import ComponentFlow +import MultiAnimationRenderer +import AnimationCache +import SwiftSignalKit +import TelegramCore +import AccountContext +import TelegramPresentationData +import EmojiTextAttachmentView +import EmojiStatusComponent + +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) + } + + 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 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? { + didSet { + 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 + } + super.position = value + } + } + + override public var bounds: CGRect { + get { + return super.bounds + } set(value) { + if let mirrorLayer = self.tintContentLayer { + 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) + } + + super.add(animation, forKey: key) + } + + override public func removeAllAnimations() { + if let mirrorLayer = self.tintContentLayer { + mirrorLayer.removeAllAnimations() + } + + super.removeAllAnimations() + } + + override public func removeAnimation(forKey: String) { + if let mirrorLayer = self.tintContentLayer { + 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): + let loadAnimation: () -> Void = { [weak self] in + guard let strongSelf = self else { + return + } + + strongSelf.disposable = renderer.add(target: strongSelf, cache: cache, itemId: animationData.resource.resource.id.stringRepresentation, unique: false, size: pixelSize, fetch: animationCacheFetchFile(context: context, userLocation: .other, userContentType: .sticker, resource: animationData.resource, type: animationData.type.animationCacheAnimationType, keyframeOnly: pixelSize.width >= 120.0, customColor: animationData.isTemplate ? .white : nil)) + } + + if attemptSynchronousLoad { + if !renderer.loadFirstFrameSynchronously(target: self, cache: cache, itemId: animationData.resource.resource.id.stringRepresentation, size: pixelSize) { + self.updateDisplayPlaceholder(displayPlaceholder: true) + + self.fetchDisposable = renderer.loadFirstFrame(target: self, cache: cache, itemId: animationData.resource.resource.id.stringRepresentation, size: pixelSize, fetch: animationCacheFetchFile(context: context, userLocation: .other, userContentType: .sticker, resource: animationData.resource, 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: animationData.resource.resource.id.stringRepresentation, size: pixelSize, fetch: animationCacheFetchFile(context: context, userLocation: .other, userContentType: .sticker, resource: animationData.resource, 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) + } + } + }) + } + 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 + ) { + 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.fillEllipse(in: CGRect(origin: .zero, size: size).insetBy(dx: 8.0, dy: 8.0)) + context.setFillColor(color.cgColor) + + let plusSize = CGSize(width: 4.5, height: 31.5) + 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), 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), cornerRadius: plusSize.width / 2.0).cgPath) + context.fillPath() + + UIGraphicsPopContext() + }) + } + + let needsVibrancy = !theme.overallDarkAppearance + let color = theme.chat.inputMediaPanel.panelContentVibrantOverlayColor + + iconLayer.contents = generateIcon(color: color)?.cgImage + tintIconLayer.contents = generateIcon(color: .white)?.cgImage + + tintIconLayer.isHidden = !needsVibrancy + } + } + } + + func update( + transition: Transition, + 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() + } + } +} diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift index da9153aea9..157b94af2b 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift @@ -26,144 +26,6 @@ import EmojiStatusComponent import TelegramNotices import GenerateStickerPlaceholderImage -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 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: Transition) { - 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: Transition) { - 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) - } -} - public struct EmojiComponentReactionItem: Equatable { public var reaction: MessageReaction.Reaction public var file: TelegramMediaFile @@ -260,2177 +122,6 @@ public final class EntityKeyboardAnimationData: Equatable { } } -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") - } -} - -private 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) - } -} - -private let itemBadgeTextFont: UIFont = { - return Font.regular(10.0) -}() - -private final class PremiumBadgeView: UIView { - private let context: AccountContext - - private var badge: EmojiPagerContentComponent.View.ItemLayer.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: Transition, badge: EmojiPagerContentComponent.View.ItemLayer.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, - 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)) - } - } -} - -private 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, 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.white.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: .white) - 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 - } -} - -private 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: .white) { - 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: .white) - 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(in: stringBounds) - 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: .white) { - 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: .white) { - 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: .white) - 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.white.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 - } - } -} - -private 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? { - 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: [EmojiPagerContentComponent.View.ItemLayer.Key: EmojiPagerContentComponent.View.ItemLayer] = [:] - 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: Transition, 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() - if let itemRange = itemLayout.visibleItems(for: self.bounds) { - for index in itemRange.lowerBound ..< itemRange.upperBound { - let item = items[index] - let itemId = EmojiPagerContentComponent.View.ItemLayer.Key( - groupId: AnyHashable(0), - itemId: item.content.id - ) - validIds.insert(itemId) - - let itemLayer: EmojiPagerContentComponent.View.ItemLayer - if let current = self.visibleItemLayers[itemId] { - itemLayer = current - } else { - itemLayer = EmojiPagerContentComponent.View.ItemLayer( - 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: [EmojiPagerContentComponent.View.ItemLayer.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) - } -} - -private 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, 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.white.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 - } -} - -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() - - private let clearIconView: UIImageView - private let clearIconTintView: UIImageView - private let clearIconButton: HighlightTrackingButton - - private let cancelButtonTintTitle: ComponentView - private let cancelButtonTitle: ComponentView - private let cancelButton: HighlightTrackingButton - - private var placeholderContent = ComponentView() - - 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) - } - } - - 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 - 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() { - self.currentPresetSearchTerm = nil - self.updateQuery(nil) - - self.clearIconView.isHidden = true - self.clearIconTintView.isHidden = true - self.clearIconButton.isHidden = true - - let textField = self.textField - self.textField = nil - - self.deactivated(textField?.isFirstResponder ?? false) - - if let textField { - textField.resignFirstResponder() - textField.removeFromSuperview() - } - - /*self.tintTextView.view?.isHidden = false - self.textView.view?.isHidden = false*/ - } - - @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: Transition) { - 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: Transition) { - 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: .white) - } - - 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: 1.0, alpha: 0.2).cgColor - } else if useOpaqueTheme { - self.backgroundLayer.backgroundColor = theme.chat.inputMediaPanel.panelContentControlOpaqueSelectionColor.cgColor - self.tintBackgroundLayer.backgroundColor = UIColor.white.cgColor - } else { - self.backgroundLayer.backgroundColor = theme.chat.inputMediaPanel.panelContentControlVibrantSelectionColor.cgColor - self.tintBackgroundLayer.backgroundColor = UIColor(white: 1.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: .white - )), - 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) - } - - /*if let image = self.searchIconView.image { - let iconFrame = CGRect(origin: CGPoint(x: textFrame.minX - image.size.width - 4.0, y: backgroundFrame.minY + floor((backgroundFrame.height - image.size.height) / 2.0)), size: image.size) - transition.setBounds(view: self.searchIconView, bounds: CGRect(origin: CGPoint(), size: iconFrame.size)) - transition.setPosition(view: self.searchIconView, position: iconFrame.center) - transition.setBounds(view: self.searchIconTintView, bounds: CGRect(origin: CGPoint(), size: iconFrame.size)) - transition.setPosition(view: self.searchIconTintView, position: iconFrame.center) - transition.setScale(view: self.searchIconView, scale: self.currentPresetSearchTerm == nil ? 1.0 : 0.001) - transition.setAlpha(view: self.searchIconView, alpha: self.currentPresetSearchTerm == nil ? 1.0 : 0.0) - transition.setScale(view: self.searchIconTintView, scale: self.currentPresetSearchTerm == nil ? 1.0 : 0.001) - transition.setAlpha(view: self.searchIconTintView, alpha: self.currentPresetSearchTerm == nil ? 1.0 : 0.0) - } - - if let image = self.backIconView.image { - let iconFrame = CGRect(origin: CGPoint(x: textFrame.minX - image.size.width - 4.0, y: backgroundFrame.minY + floor((backgroundFrame.height - image.size.height) / 2.0)), size: image.size) - transition.setBounds(view: self.backIconView, bounds: CGRect(origin: CGPoint(), size: iconFrame.size)) - transition.setPosition(view: self.backIconView, position: iconFrame.center) - transition.setBounds(view: self.backIconTintView, bounds: CGRect(origin: CGPoint(), size: iconFrame.size)) - transition.setPosition(view: self.backIconTintView, position: iconFrame.center) - transition.setScale(view: self.backIconView, scale: self.currentPresetSearchTerm != nil ? 1.0 : 0.001) - transition.setAlpha(view: self.backIconView, alpha: self.currentPresetSearchTerm != nil ? 1.0 : 0.0) - transition.setScale(view: self.backIconTintView, scale: self.currentPresetSearchTerm != nil ? 1.0 : 0.001) - transition.setAlpha(view: self.backIconTintView, alpha: self.currentPresetSearchTerm != nil ? 1.0 : 0.0) - }*/ - - 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: Transition(animation: .curve(duration: 0.4, curve: .spring))) - - self.updateQuery(.category(value: term)) - self.activated(false) - } 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 - 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 - - /*self.tintTextView.view?.isHidden = hasText - self.textView.view?.isHidden = hasText*/ - } -} - -private final class EmptySearchResultsView: UIView { - override public static var layerClass: AnyClass { - return PassthroughLayer.self - } - - let tintContainerView: UIView - let titleLabel: ComponentView - let titleTintLabel: ComponentView - let icon: ComponentView - - 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: Transition) { - 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: .white)), - 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) - } - } -} - public protocol EmojiContentPeekBehavior: AnyObject { func setGestureRecognizerEnabled(view: UIView, isEnabled: Bool, itemAtPoint: @escaping (CGPoint) -> (AnyHashable, CALayer, TelegramMediaFile)?) } @@ -3499,471 +1190,6 @@ public final class EmojiPagerContentComponent: Component { } } - final class CloneItemLayer: SimpleLayer { - } - - public final class ItemLayer: MultiAnimationRenderTarget { - public struct Key: Hashable { - var groupId: AnyHashable - var itemId: ItemContent.Id - - public init( - groupId: AnyHashable, - itemId: ItemContent.Id - ) { - self.groupId = groupId - self.itemId = itemId - } - } - - enum Badge: Equatable { - case premium - case locked - case featured - case text(String) - case customFile(TelegramMediaFile) - } - - public let item: Item - private let context: AccountContext - - private var content: 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 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: CloneItemLayer? { - didSet { - if let cloneLayer = self.cloneLayer { - cloneLayer.contents = self.contents - } - } - } - - override public var contents: Any? { - didSet { - 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 - } - super.position = value - } - } - - override public var bounds: CGRect { - get { - return super.bounds - } set(value) { - if let mirrorLayer = self.tintContentLayer { - 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) - } - - super.add(animation, forKey: key) - } - - override public func removeAllAnimations() { - if let mirrorLayer = self.tintContentLayer { - mirrorLayer.removeAllAnimations() - } - - super.removeAllAnimations() - } - - override public func removeAnimation(forKey: String) { - if let mirrorLayer = self.tintContentLayer { - mirrorLayer.removeAnimation(forKey: forKey) - } - - super.removeAnimation(forKey: forKey) - } - - public var onContentsUpdate: () -> Void = {} - public var onLoop: () -> Void = {} - - public init( - item: Item, - context: AccountContext, - attemptSynchronousLoad: Bool, - content: 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): - let loadAnimation: () -> Void = { [weak self] in - guard let strongSelf = self else { - return - } - - strongSelf.disposable = renderer.add(target: strongSelf, cache: cache, itemId: animationData.resource.resource.id.stringRepresentation, unique: false, size: pixelSize, fetch: animationCacheFetchFile(context: context, userLocation: .other, userContentType: .sticker, resource: animationData.resource, type: animationData.type.animationCacheAnimationType, keyframeOnly: pixelSize.width >= 120.0, customColor: animationData.isTemplate ? .white : nil)) - } - - if attemptSynchronousLoad { - if !renderer.loadFirstFrameSynchronously(target: self, cache: cache, itemId: animationData.resource.resource.id.stringRepresentation, size: pixelSize) { - self.updateDisplayPlaceholder(displayPlaceholder: true) - - self.fetchDisposable = renderer.loadFirstFrame(target: self, cache: cache, itemId: animationData.resource.resource.id.stringRepresentation, size: pixelSize, fetch: animationCacheFetchFile(context: context, userLocation: .other, userContentType: .sticker, resource: animationData.resource, 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: animationData.resource.resource.id.stringRepresentation, size: pixelSize, fetch: animationCacheFetchFile(context: context, userLocation: .other, userContentType: .sticker, resource: animationData.resource, 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) - } - } - }) - } - 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? ItemLayer 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: ItemContent, - theme: PresentationTheme - ) { - 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.fillEllipse(in: CGRect(origin: .zero, size: size).insetBy(dx: 8.0, dy: 8.0)) - context.setFillColor(color.cgColor) - - let plusSize = CGSize(width: 4.5, height: 31.5) - 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), 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), cornerRadius: plusSize.width / 2.0).cgPath) - context.fillPath() - - UIGraphicsPopContext() - }) - } - - let needsVibrancy = !theme.overallDarkAppearance - let color = theme.chat.inputMediaPanel.panelContentVibrantOverlayColor - - iconLayer.contents = generateIcon(color: color)?.cgImage - tintIconLayer.contents = generateIcon(color: .white)?.cgImage - - tintIconLayer.isHidden = !needsVibrancy - } - } - } - - func update( - transition: Transition, - 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() - } - } - } - private final class GroupBorderLayer: PassthroughShapeLayer { let tintContainerLayer: CAShapeLayer @@ -4104,7 +1330,7 @@ public final class EmojiPagerContentComponent: Component { } private enum VisualItemKey: Hashable { - case item(id: ItemLayer.Key) + case item(id: EmojiKeyboardItemLayer.Key) case header(groupId: AnyHashable) case groupExpandButton(groupId: AnyHashable) case groupActionButton(groupId: AnyHashable) @@ -4134,10 +1360,10 @@ public final class EmojiPagerContentComponent: Component { private var visibleSearchHeader: EmojiSearchHeaderView? private var visibleEmptySearchResultsView: EmptySearchResultsView? private var visibleCustomContentView: EmojiCustomContentView? - private var visibleItemPlaceholderViews: [ItemLayer.Key: ItemPlaceholderView] = [:] + private var visibleItemPlaceholderViews: [EmojiKeyboardItemLayer.Key: ItemPlaceholderView] = [:] private var visibleFillPlaceholdersViews: [Int: ItemPlaceholderView] = [:] - private var visibleItemSelectionLayers: [ItemLayer.Key: ItemSelectionLayer] = [:] - private var visibleItemLayers: [ItemLayer.Key: ItemLayer] = [:] + private var visibleItemSelectionLayers: [EmojiKeyboardItemLayer.Key: ItemSelectionLayer] = [:] + private var visibleItemLayers: [EmojiKeyboardItemLayer.Key: EmojiKeyboardItemLayer] = [:] private var visibleGroupHeaders: [AnyHashable: GroupHeaderLayer] = [:] private var visibleGroupBorders: [AnyHashable: GroupBorderLayer] = [:] private var visibleGroupPremiumButtons: [AnyHashable: ComponentView] = [:] @@ -4163,7 +1389,7 @@ public final class EmojiPagerContentComponent: Component { private var activeItemUpdated: ActionSlot<(AnyHashable, AnyHashable?, Transition)>? private var itemLayout: ItemLayout? - private var contextFocusItemKey: EmojiPagerContentComponent.View.ItemLayer.Key? + private var contextFocusItemKey: EmojiKeyboardItemLayer.Key? private var contextGesture: ContextGesture? private var tapRecognizer: UITapGestureRecognizer? @@ -4503,7 +1729,7 @@ public final class EmojiPagerContentComponent: Component { } public func layerForItem(groupId: AnyHashable, item: EmojiPagerContentComponent.Item) -> CALayer? { - let itemKey = EmojiPagerContentComponent.View.ItemLayer.Key(groupId: groupId, itemId: item.content.id) + let itemKey = EmojiKeyboardItemLayer.Key(groupId: groupId, itemId: item.content.id) if let itemLayer = self.visibleItemLayers[itemKey] { return itemLayer } else { @@ -4530,15 +1756,15 @@ public final class EmojiPagerContentComponent: Component { let offsetDirectionSign: Double = scrollPosition < self.scrollView.bounds.minY ? -1.0 : 1.0 - var previousVisibleLayers: [ItemLayer.Key: (CALayer, CGRect)] = [:] + var previousVisibleLayers: [EmojiKeyboardItemLayer.Key: (CALayer, CGRect)] = [:] for (id, layer) in self.visibleItemLayers { previousVisibleLayers[id] = (layer, layer.frame.offsetBy(dx: 0.0, dy: -self.scrollView.bounds.minY)) } - var previousVisibleItemSelectionLayers: [ItemLayer.Key: (CALayer, CGRect)] = [:] + var previousVisibleItemSelectionLayers: [EmojiKeyboardItemLayer.Key: (CALayer, CGRect)] = [:] for (id, layer) in self.visibleItemSelectionLayers { previousVisibleItemSelectionLayers[id] = (layer, layer.frame.offsetBy(dx: 0.0, dy: -self.scrollView.bounds.minY)) } - var previousVisiblePlaceholderViews: [ItemLayer.Key: (UIView, CGRect)] = [:] + var previousVisiblePlaceholderViews: [EmojiKeyboardItemLayer.Key: (UIView, CGRect)] = [:] for (id, view) in self.visibleItemPlaceholderViews { previousVisiblePlaceholderViews[id] = (view, view.frame.offsetBy(dx: 0.0, dy: -self.scrollView.bounds.minY)) } @@ -5024,15 +2250,15 @@ public final class EmojiPagerContentComponent: Component { let offsetDirectionSign: Double = scrollPosition < self.scrollView.bounds.minY ? -1.0 : 1.0 - var previousVisibleLayers: [ItemLayer.Key: (CALayer, CGRect)] = [:] + var previousVisibleLayers: [EmojiKeyboardItemLayer.Key: (CALayer, CGRect)] = [:] for (id, layer) in self.visibleItemLayers { previousVisibleLayers[id] = (layer, layer.frame.offsetBy(dx: 0.0, dy: -self.scrollView.bounds.minY)) } - var previousVisibleItemSelectionLayers: [ItemLayer.Key: (ItemSelectionLayer, CGRect)] = [:] + var previousVisibleItemSelectionLayers: [EmojiKeyboardItemLayer.Key: (ItemSelectionLayer, CGRect)] = [:] for (id, layer) in self.visibleItemSelectionLayers { previousVisibleItemSelectionLayers[id] = (layer, layer.frame.offsetBy(dx: 0.0, dy: -self.scrollView.bounds.minY)) } - var previousVisiblePlaceholderViews: [ItemLayer.Key: (UIView, CGRect)] = [:] + var previousVisiblePlaceholderViews: [EmojiKeyboardItemLayer.Key: (UIView, CGRect)] = [:] for (id, view) in self.visibleItemPlaceholderViews { previousVisiblePlaceholderViews[id] = (view, view.frame.offsetBy(dx: 0.0, dy: -self.scrollView.bounds.minY)) } @@ -5503,8 +2729,8 @@ public final class EmojiPagerContentComponent: Component { } private let longPressDuration: Double = 0.5 - private var longPressItem: EmojiPagerContentComponent.View.ItemLayer.Key? - private var currentLongPressLayer: CloneItemLayer? + private var longPressItem: EmojiKeyboardItemLayer.Key? + private var currentLongPressLayer: EmojiKeyboardCloneItemLayer? private var hapticFeedback: HapticFeedback? private var continuousHaptic: AnyObject? private var longPressTimer: SwiftSignalKit.Timer? @@ -5543,7 +2769,7 @@ public final class EmojiPagerContentComponent: Component { self.currentLongPressLayer = nil currentLongPressLayer.removeFromSuperlayer() } - let currentLongPressLayer = CloneItemLayer() + let currentLongPressLayer = EmojiKeyboardCloneItemLayer() currentLongPressLayer.position = self.scrollView.layer.convert(itemLayer.position, to: externalExpansionView.layer) currentLongPressLayer.bounds = itemLayer.convert(itemLayer.bounds, to: externalExpansionView.layer) currentLongPressLayer.transform = itemLayer.transform @@ -5641,10 +2867,10 @@ public final class EmojiPagerContentComponent: Component { } } - private func item(atPoint point: CGPoint, extendedHitRange: Bool = false) -> (Item, ItemLayer.Key)? { + private func item(atPoint point: CGPoint, extendedHitRange: Bool = false) -> (Item, EmojiKeyboardItemLayer.Key)? { let localPoint = self.convert(point, to: self.scrollView) - var closestItem: (key: ItemLayer.Key, distance: CGFloat)? + var closestItem: (key: EmojiKeyboardItemLayer.Key, distance: CGFloat)? for (key, itemLayer) in self.visibleItemLayers { if extendedHitRange { @@ -5806,7 +3032,7 @@ public final class EmojiPagerContentComponent: Component { var topVisibleGroupId: AnyHashable? var topVisibleSubgroupId: AnyHashable? - var validIds = Set() + var validIds = Set() var validGroupHeaderIds = Set() var validGroupBorderIds = Set() var validGroupPremiumButtonIds = Set() @@ -6136,7 +3362,7 @@ public final class EmojiPagerContentComponent: Component { } } - let itemId = ItemLayer.Key( + let itemId = EmojiKeyboardItemLayer.Key( groupId: itemGroup.groupId, itemId: item.content.id ) @@ -6151,7 +3377,7 @@ public final class EmojiPagerContentComponent: Component { var animateItemIn = false var updateItemLayerPlaceholder = false var itemTransition = transition - let itemLayer: ItemLayer + let itemLayer: EmojiKeyboardItemLayer if let current = self.visibleItemLayers[itemId] { itemLayer = current } else { @@ -6167,7 +3393,7 @@ public final class EmojiPagerContentComponent: Component { } let placeholderColor = keyboardChildEnvironment.theme.chat.inputPanel.primaryTextColor.withMultipliedAlpha(0.1) - itemLayer = ItemLayer( + itemLayer = EmojiKeyboardItemLayer( item: item, context: component.context, attemptSynchronousLoad: attemptSynchronousLoads, @@ -6264,7 +3490,7 @@ public final class EmojiPagerContentComponent: Component { let itemPosition = CGPoint(x: itemFrame.midX, y: itemFrame.midY) itemTransition.setPosition(layer: itemLayer, position: itemPosition) - var badge: ItemLayer.Badge? + var badge: EmojiKeyboardItemLayer.Badge? if itemGroup.displayPremiumBadges, let file = item.itemFile, file.isPremiumSticker { badge = .premium } else { @@ -6445,7 +3671,7 @@ public final class EmojiPagerContentComponent: Component { } var removedPlaceholerViews = false - var removedIds: [ItemLayer.Key] = [] + var removedIds: [EmojiKeyboardItemLayer.Key] = [] for (id, itemLayer) in self.visibleItemLayers { if !validIds.contains(id) { removedIds.append(id) @@ -6535,7 +3761,7 @@ public final class EmojiPagerContentComponent: Component { removedPlaceholerViews = true } } - var removedItemSelectionLayerIds: [ItemLayer.Key] = [] + var removedItemSelectionLayerIds: [EmojiKeyboardItemLayer.Key] = [] for (id, itemSelectionLayer) in self.visibleItemSelectionLayers { var fileId: MediaId? switch id.itemId { @@ -6924,7 +4150,7 @@ public final class EmojiPagerContentComponent: Component { var hintDisappearingGroupFrame: (groupId: AnyHashable, frame: CGRect)? var previousAbsoluteItemPositions: [VisualItemKey: CGPoint] = [:] - var anchorItems: [ItemLayer.Key: CGRect] = [:] + var anchorItems: [EmojiKeyboardItemLayer.Key: CGRect] = [:] if let previousComponent = previousComponent, let previousItemLayout = self.itemLayout, previousComponent.contentItemGroups != component.contentItemGroups, previousComponent.itemContentUniqueId == component.itemContentUniqueId { if !transition.animation.isImmediate { var previousItemPositionsValue: [VisualItemKey: CGPoint] = [:] @@ -6932,8 +4158,8 @@ public final class EmojiPagerContentComponent: Component { let itemGroup = previousComponent.contentItemGroups[groupIndex] for itemIndex in 0 ..< itemGroup.items.count { let item = itemGroup.items[itemIndex] - let itemKey: ItemLayer.Key - itemKey = ItemLayer.Key( + let itemKey: EmojiKeyboardItemLayer.Key + itemKey = EmojiKeyboardItemLayer.Key( groupId: itemGroup.groupId, itemId: item.content.id ) @@ -7200,7 +4426,7 @@ public final class EmojiPagerContentComponent: Component { var animatedScrollOffset: CGFloat = 0.0 if !anchorItems.isEmpty && !keepOffset { - let sortedAnchorItems: [(ItemLayer.Key, CGRect)] = anchorItems.sorted(by: { lhs, rhs in + let sortedAnchorItems: [(EmojiKeyboardItemLayer.Key, CGRect)] = anchorItems.sorted(by: { lhs, rhs in if lhs.value.minY != rhs.value.minY { return lhs.value.minY < rhs.value.minY } else { @@ -7214,8 +4440,8 @@ public final class EmojiPagerContentComponent: Component { continue } for j in 0 ..< component.contentItemGroups[i].items.count { - let itemKey: ItemLayer.Key - itemKey = ItemLayer.Key( + let itemKey: EmojiKeyboardItemLayer.Key + itemKey = EmojiKeyboardItemLayer.Key( groupId: component.contentItemGroups[i].groupId, itemId: component.contentItemGroups[i].items[j].content.id ) @@ -7261,8 +4487,8 @@ public final class EmojiPagerContentComponent: Component { let itemGroupLayout = itemLayout.itemGroupLayouts[groupIndex] for itemIndex in 0 ..< itemGroup.items.count { let item = itemGroup.items[itemIndex] - let itemKey: ItemLayer.Key - itemKey = ItemLayer.Key( + let itemKey: EmojiKeyboardItemLayer.Key + itemKey = EmojiKeyboardItemLayer.Key( groupId: itemGroup.groupId, itemId: item.content.id ) diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiSearchHeaderView.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiSearchHeaderView.swift new file mode 100644 index 0000000000..d8369244f7 --- /dev/null +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiSearchHeaderView.swift @@ -0,0 +1,607 @@ +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() + + private let clearIconView: UIImageView + private let clearIconTintView: UIImageView + private let clearIconButton: HighlightTrackingButton + + private let cancelButtonTintTitle: ComponentView + private let cancelButtonTitle: ComponentView + private let cancelButton: HighlightTrackingButton + + private var placeholderContent = ComponentView() + + 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) + } + } + + 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 + 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() { + self.currentPresetSearchTerm = nil + self.updateQuery(nil) + + self.clearIconView.isHidden = true + self.clearIconTintView.isHidden = true + self.clearIconButton.isHidden = true + + let textField = self.textField + self.textField = nil + + self.deactivated(textField?.isFirstResponder ?? false) + + if let textField { + textField.resignFirstResponder() + textField.removeFromSuperview() + } + + /*self.tintTextView.view?.isHidden = false + self.textView.view?.isHidden = false*/ + } + + @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: Transition) { + 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: Transition) { + 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: .white) + } + + 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: 1.0, alpha: 0.2).cgColor + } else if useOpaqueTheme { + self.backgroundLayer.backgroundColor = theme.chat.inputMediaPanel.panelContentControlOpaqueSelectionColor.cgColor + self.tintBackgroundLayer.backgroundColor = UIColor.white.cgColor + } else { + self.backgroundLayer.backgroundColor = theme.chat.inputMediaPanel.panelContentControlVibrantSelectionColor.cgColor + self.tintBackgroundLayer.backgroundColor = UIColor(white: 1.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: .white + )), + 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: Transition(animation: .curve(duration: 0.4, curve: .spring))) + + self.updateQuery(.category(value: term)) + self.activated(false) + } 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 + 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 + + /*self.tintTextView.view?.isHidden = hasText + self.textView.view?.isHidden = hasText*/ + } +} diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiSearchSearchBarComponent.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiSearchSearchBarComponent.swift index 93c8f1e164..b8e7be9d63 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiSearchSearchBarComponent.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiSearchSearchBarComponent.swift @@ -420,6 +420,15 @@ final class EmojiSearchSearchBarComponent: Component { } } + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + guard let component = self.component else { + return nil + } + let _ = component + + return super.hitTest(point, with: event) + } + private func updateScrolling(transition: Transition, fromScrolling: Bool) { guard let component = self.component, let itemLayout = self.itemLayout else { return @@ -427,8 +436,12 @@ final class EmojiSearchSearchBarComponent: Component { let itemAlpha: CGFloat switch component.textInputState { - case .active: - itemAlpha = 0.0 + case let .active(hasText): + if hasText { + itemAlpha = 0.0 + } else { + itemAlpha = 1.0 + } case .inactive: itemAlpha = 1.0 } @@ -674,7 +687,7 @@ final class EmojiSearchSearchBarComponent: Component { if self.scrollView.bounds.size != availableSize { transition.setFrame(view: self.scrollView, frame: CGRect(origin: CGPoint(), size: availableSize)) } - if case .active = component.textInputState { + if case .active(true) = component.textInputState { transition.setBoundsOrigin(view: self.scrollView, origin: CGPoint()) } if self.scrollView.contentSize != itemLayout.contentSize { diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmptySearchResultsView.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmptySearchResultsView.swift new file mode 100644 index 0000000000..466c430e8d --- /dev/null +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmptySearchResultsView.swift @@ -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 + let titleTintLabel: ComponentView + let icon: ComponentView + + 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: Transition) { + 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: .white)), + 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) + } + } +} diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardTopPanelComponent.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardTopPanelComponent.swift index eea256000d..236588fff2 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardTopPanelComponent.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardTopPanelComponent.swift @@ -85,7 +85,7 @@ final class EntityKeyboardAnimationTopPanelComponent: Component { } final class View: UIView { - var itemLayer: EmojiPagerContentComponent.View.ItemLayer? + var itemLayer: EmojiKeyboardItemLayer? var placeholderView: EmojiPagerContentComponent.View.ItemPlaceholderView? var component: EntityKeyboardAnimationTopPanelComponent? var titleView: ComponentView? @@ -116,7 +116,7 @@ final class EntityKeyboardAnimationTopPanelComponent: Component { if self.itemLayer == nil { let tintColor: EmojiPagerContentComponent.Item.TintMode = component.customTintColor.flatMap { .custom($0) } ?? .primary - let itemLayer = EmojiPagerContentComponent.View.ItemLayer( + let itemLayer = EmojiKeyboardItemLayer( item: EmojiPagerContentComponent.Item( animationData: component.item, content: .animation(component.item), @@ -157,7 +157,7 @@ final class EntityKeyboardAnimationTopPanelComponent: Component { transition.setPosition(layer: itemLayer, position: CGPoint(x: iconFrame.midX, y: iconFrame.midY)) transition.setBounds(layer: itemLayer, bounds: CGRect(origin: CGPoint(), size: iconFrame.size)) - var badge: EmojiPagerContentComponent.View.ItemLayer.Badge? + var badge: EmojiKeyboardItemLayer.Badge? if component.isPremiumLocked { badge = .locked } else if component.isFeatured { diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/GroupEmbeddedView.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/GroupEmbeddedView.swift new file mode 100644 index 0000000000..4721e8897f --- /dev/null +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/GroupEmbeddedView.swift @@ -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? { + 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: Transition, 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() + 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) + } +} diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/GroupExpandActionButton.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/GroupExpandActionButton.swift new file mode 100644 index 0000000000..0b3be8405d --- /dev/null +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/GroupExpandActionButton.swift @@ -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, 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.white.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 + } +} diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/GroupHeaderActionButton.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/GroupHeaderActionButton.swift new file mode 100644 index 0000000000..cbc39084de --- /dev/null +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/GroupHeaderActionButton.swift @@ -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, 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.white.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: .white) + 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 + } +} diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/GroupHeaderLayer.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/GroupHeaderLayer.swift new file mode 100644 index 0000000000..857c0ca5d1 --- /dev/null +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/GroupHeaderLayer.swift @@ -0,0 +1,525 @@ +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: .white) { + 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: .white) + 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(in: stringBounds) + 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: .white) { + 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: .white) { + 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: .white) + 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.white.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 + } + } +} diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/PassthroughComponents.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/PassthroughComponents.swift new file mode 100644 index 0000000000..eb60512e4e --- /dev/null +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/PassthroughComponents.swift @@ -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) + } +} diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/PremiumBadgeView.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/PremiumBadgeView.swift new file mode 100644 index 0000000000..6e0a0497d2 --- /dev/null +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/PremiumBadgeView.swift @@ -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: Transition, 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, + 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)) + } + } +} diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/WarpView.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/WarpView.swift new file mode 100644 index 0000000000..7ccea1a793 --- /dev/null +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/WarpView.swift @@ -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: Transition) { + 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: Transition) { + 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) + } +} diff --git a/submodules/TelegramUI/Components/PeerAllowedReactionsScreen/Sources/EmojiListInputComponent.swift b/submodules/TelegramUI/Components/PeerAllowedReactionsScreen/Sources/EmojiListInputComponent.swift index 1d7a582d77..df49f1a264 100644 --- a/submodules/TelegramUI/Components/PeerAllowedReactionsScreen/Sources/EmojiListInputComponent.swift +++ b/submodules/TelegramUI/Components/PeerAllowedReactionsScreen/Sources/EmojiListInputComponent.swift @@ -105,7 +105,7 @@ final class EmojiListInputComponent: Component { private var component: EmojiListInputComponent? private weak var state: EmptyComponentState? - private var itemLayers: [Int64: EmojiPagerContentComponent.View.ItemLayer] = [:] + private var itemLayers: [Int64: EmojiKeyboardItemLayer] = [:] private let trailingPlaceholder = ComponentView() private let caretIndicator: CaretIndicatorView @@ -239,7 +239,7 @@ final class EmojiListInputComponent: Component { var itemTransition = transition var animateIn = false - let itemLayer: EmojiPagerContentComponent.View.ItemLayer + let itemLayer: EmojiKeyboardItemLayer if let current = self.itemLayers[itemKey] { itemLayer = current } else { @@ -249,7 +249,7 @@ final class EmojiListInputComponent: Component { let animationData = EntityKeyboardAnimationData( file: item.file ) - itemLayer = EmojiPagerContentComponent.View.ItemLayer( + itemLayer = EmojiKeyboardItemLayer( item: EmojiPagerContentComponent.Item( animationData: animationData, content: .animation(animationData),