GLEGram 12.5 — Initial public release

Based on Swiftgram 12.5 (Telegram iOS 12.5).
All GLEGram features ported and organized in GLEGram/ folder.

Features: Ghost Mode, Saved Deleted Messages, Content Protection Bypass,
Font Replacement, Fake Profile, Chat Export, Plugin System, and more.

See CHANGELOG_12.5.md for full details.
This commit is contained in:
Leeksov
2026-04-06 09:48:12 +03:00
commit 4647310322
39685 changed files with 11052678 additions and 0 deletions
@@ -0,0 +1,23 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "ActionPanelComponent",
module_name = "ActionPanelComponent",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/Display:Display",
"//submodules/TelegramPresentationData:TelegramPresentationData",
"//submodules/ComponentFlow",
"//submodules/AnimatedCountLabelNode",
"//submodules/Components/ComponentDisplayAdapters",
"//submodules/AppBundle",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,191 @@
import Foundation
import UIKit
import Display
import TelegramPresentationData
import ComponentFlow
import ComponentDisplayAdapters
import AppBundle
public final class ActionPanelComponent: Component {
public enum Color {
case accent
case destructive
}
public let theme: PresentationTheme
public let title: String
public let color: Color
public let action: () -> Void
public let dismissAction: () -> Void
public init(
theme: PresentationTheme,
title: String,
color: Color,
action: @escaping () -> Void,
dismissAction: @escaping () -> Void
) {
self.theme = theme
self.title = title
self.color = color
self.action = action
self.dismissAction = dismissAction
}
public static func ==(lhs: ActionPanelComponent, rhs: ActionPanelComponent) -> Bool {
if lhs.theme !== rhs.theme {
return false
}
if lhs.title != rhs.title {
return false
}
if lhs.color != rhs.color {
return false
}
return true
}
public final class View: HighlightTrackingButton {
private let backgroundView: BlurredBackgroundView
private let separatorLayer: SimpleLayer
private let contentView: UIView
private let title = ComponentView<Empty>()
private let dismissButton: HighlightTrackingButton
private let dismissIconView: UIImageView
private var component: ActionPanelComponent?
public override init(frame: CGRect) {
self.backgroundView = BlurredBackgroundView(color: nil, enableBlur: true)
self.backgroundView.isUserInteractionEnabled = false
self.separatorLayer = SimpleLayer()
self.contentView = UIView()
self.contentView.isUserInteractionEnabled = false
self.dismissButton = HighlightTrackingButton()
self.dismissIconView = UIImageView()
super.init(frame: frame)
self.addSubview(self.backgroundView)
self.layer.addSublayer(self.separatorLayer)
self.addSubview(self.contentView)
self.dismissButton.addSubview(self.dismissIconView)
self.addSubview(self.dismissButton)
self.highligthedChanged = { [weak self] highlighted in
if let self {
if highlighted {
self.contentView.layer.removeAnimation(forKey: "opacity")
self.contentView.alpha = 0.65
} else {
self.contentView.alpha = 1.0
self.contentView.layer.animateAlpha(from: 0.65, to: 1.0, duration: 0.2)
}
}
}
self.addTarget(self, action: #selector(self.pressed), for: .touchUpInside)
self.dismissButton.highligthedChanged = { [weak self] highlighted in
if let self {
if highlighted {
self.dismissButton.layer.removeAnimation(forKey: "opacity")
self.dismissButton.alpha = 0.65
} else {
self.dismissButton.alpha = 1.0
self.dismissButton.layer.animateAlpha(from: 0.65, to: 1.0, duration: 0.2)
}
}
}
self.dismissButton.addTarget(self, action: #selector(self.dismissPressed), for: .touchUpInside)
}
required public init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@objc private func pressed() {
guard let component = self.component else {
return
}
component.action()
}
@objc private func dismissPressed() {
guard let component = self.component else {
return
}
component.dismissAction()
}
override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
return super.hitTest(point, with: event)
}
func update(component: ActionPanelComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
let themeUpdated = self.component?.theme !== component.theme
self.component = component
if themeUpdated {
self.backgroundView.updateColor(color: component.theme.rootController.navigationBar.blurredBackgroundColor, transition: .immediate)
self.separatorLayer.backgroundColor = component.theme.rootController.navigationBar.separatorColor.cgColor
self.dismissIconView.image = UIImage(bundleImageName: "Chat/Input/Accessory Panels/EncircledCloseButton")?.withRenderingMode(.alwaysTemplate)
self.dismissIconView.tintColor = component.theme.rootController.navigationBar.accentTextColor
}
transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(), size: availableSize))
self.backgroundView.update(size: availableSize, transition: transition.containedViewLayoutTransition)
transition.setFrame(layer: self.separatorLayer, frame: CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - UIScreenPixel), size: CGSize(width: availableSize.width, height: UIScreenPixel)))
transition.setFrame(view: self.contentView, frame: CGRect(origin: CGPoint(), size: availableSize))
let rightInset: CGFloat = 44.0
let resolvedColor: UIColor
switch component.color {
case .accent:
resolvedColor = component.theme.rootController.navigationBar.accentTextColor
case .destructive:
resolvedColor = component.theme.list.itemDestructiveColor
}
let titleSize = self.title.update(
transition: .immediate,
component: AnyComponent(Text(text: component.title, font: Font.regular(17.0), color: resolvedColor)),
environment: {},
containerSize: CGSize(width: availableSize.width - rightInset, height: availableSize.height)
)
if let titleView = self.title.view {
if titleView.superview == nil {
titleView.layer.anchorPoint = CGPoint()
self.contentView.addSubview(titleView)
}
let titleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - titleSize.width) * 0.5), y: floor((availableSize.height - titleSize.height) * 0.5)), size: titleSize)
transition.setPosition(view: titleView, position: titleFrame.origin)
titleView.bounds = CGRect(origin: CGPoint(), size: titleFrame.size)
}
let dismissButtonFrame = CGRect(origin: CGPoint(x: availableSize.width - rightInset, y: 0.0), size: CGSize(width: rightInset, height: availableSize.height))
transition.setFrame(view: self.dismissButton, frame: dismissButtonFrame)
if let iconImage = self.dismissIconView.image {
transition.setFrame(view: self.dismissIconView, frame: CGRect(origin: CGPoint(x: floor((dismissButtonFrame.width - iconImage.size.width) * 0.5), y: floor((dismissButtonFrame.height - iconImage.size.height) * 0.5)), size: iconImage.size))
}
return availableSize
}
}
public func makeView() -> View {
return View(frame: CGRect())
}
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
@@ -0,0 +1,36 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "AdPanelHeaderPanelComponent",
module_name = "AdPanelHeaderPanelComponent",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/Display",
"//submodules/AsyncDisplayKit",
"//submodules/TelegramPresentationData",
"//submodules/AccountContext",
"//submodules/SSignalKit/SwiftSignalKit",
"//submodules/Postbox",
"//submodules/TelegramCore",
"//submodules/TelegramUIPreferences",
"//submodules/StickerResources",
"//submodules/PhotoResources",
"//submodules/TelegramStringFormatting",
"//submodules/AnimatedCountLabelNode",
"//submodules/AnimatedNavigationStripeNode",
"//submodules/ContextUI",
"//submodules/RadialStatusNode",
"//submodules/TextFormat",
"//submodules/TelegramUI/Components/TextNodeWithEntities",
"//submodules/TranslateUI",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,127 @@
import Foundation
import UIKit
import Display
import TelegramPresentationData
import ComponentFlow
import ComponentDisplayAdapters
import AccountContext
import TelegramCore
import SwiftSignalKit
import Postbox
import PresentationDataUtils
import ContextUI
import AsyncDisplayKit
public final class AdPanelHeaderPanelComponent: Component {
public struct Info: Equatable {
public let message: EngineMessage
public init(message: EngineMessage) {
self.message = message
}
}
public let context: AccountContext
public let theme: PresentationTheme
public let strings: PresentationStrings
public let info: Info
public let action: (EngineMessage) -> Void
public let contextAction: (EngineMessage, ASDisplayNode, ContextGesture?) -> Void
public let close: () -> Void
public init(
context: AccountContext,
theme: PresentationTheme,
strings: PresentationStrings,
info: Info,
action: @escaping (EngineMessage) -> Void,
contextAction: @escaping (EngineMessage, ASDisplayNode, ContextGesture?) -> Void,
close: @escaping () -> Void
) {
self.context = context
self.theme = theme
self.strings = strings
self.info = info
self.action = action
self.contextAction = contextAction
self.close = close
}
public static func ==(lhs: AdPanelHeaderPanelComponent, rhs: AdPanelHeaderPanelComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.theme !== rhs.theme {
return false
}
if lhs.strings !== rhs.strings {
return false
}
if lhs.info != rhs.info {
return false
}
return true
}
public final class View: UIView {
private var panel: ChatAdPanelNode?
private var component: AdPanelHeaderPanelComponent?
private weak var state: EmptyComponentState?
public var message: EngineMessage? {
return self.component?.info.message
}
public override init(frame: CGRect) {
super.init(frame: frame)
}
required public init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
}
func update(component: AdPanelHeaderPanelComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
self.component = component
self.state = state
let panel: ChatAdPanelNode
if let current = self.panel {
panel = current
} else {
panel = ChatAdPanelNode(
context: component.context,
action: component.action,
contextAction: component.contextAction,
close: component.close
)
self.panel = panel
self.addSubview(panel.view)
}
let height = panel.updateLayout(
width: availableSize.width,
theme: component.theme,
strings: component.strings,
info: component.info,
transition: transition.containedViewLayoutTransition
)
let size = CGSize(width: availableSize.width, height: height)
let panelFrame = CGRect(origin: CGPoint(), size: size)
transition.setFrame(view: panel.view, frame: panelFrame)
return size
}
}
public func makeView() -> View {
return View(frame: CGRect())
}
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
@@ -0,0 +1,523 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import Postbox
import TelegramCore
import SwiftSignalKit
import TelegramPresentationData
import TelegramUIPreferences
import AccountContext
import StickerResources
import PhotoResources
import TelegramStringFormatting
import AnimatedCountLabelNode
import AnimatedNavigationStripeNode
import ContextUI
import RadialStatusNode
import TextFormat
import TextNodeWithEntities
import TranslateUI
private enum PinnedMessageAnimation {
case slideToTop
case slideToBottom
}
final class ChatAdPanelNode: ASDisplayNode {
private let context: AccountContext
private let action: (EngineMessage) -> Void
private let contextAction: (EngineMessage, ASDisplayNode, ContextGesture?) -> Void
private let close: () -> Void
private(set) var message: EngineMessage?
private let tapButton: HighlightTrackingButtonNode
private let contextContainer: ContextControllerSourceNode
private let clippingContainer: ASDisplayNode
private let contentContainer: ASDisplayNode
private let contentTextContainer: ASDisplayNode
private let adNode: TextNode
private let titleNode: TextNode
private let textNode: TextNodeWithEntities
private let removeButtonNode: HighlightTrackingButtonNode
private let removeBackgroundNode: ASImageNode
private let removeTextNode: ImmediateTextNode
private let closeButton: HighlightableButtonNode
private let imageNode: TransformImageNode
private let imageNodeContainer: ASDisplayNode
private var currentLayout: (CGFloat, CGFloat, CGFloat)?
private var previousMediaReference: AnyMediaReference?
private let fetchDisposable = MetaDisposable()
init(
context: AccountContext,
action: @escaping (EngineMessage) -> Void,
contextAction: @escaping (EngineMessage, ASDisplayNode, ContextGesture?) -> Void,
close: @escaping () -> Void
) {
self.context = context
self.action = action
self.contextAction = contextAction
self.close = close
self.tapButton = HighlightTrackingButtonNode()
self.contextContainer = ContextControllerSourceNode()
self.clippingContainer = ASDisplayNode()
self.clippingContainer.clipsToBounds = true
self.contentContainer = ASDisplayNode()
self.contentTextContainer = ASDisplayNode()
self.adNode = TextNode()
self.adNode.displaysAsynchronously = false
self.adNode.isUserInteractionEnabled = false
self.removeButtonNode = HighlightTrackingButtonNode()
self.removeBackgroundNode = ASImageNode()
self.removeTextNode = ImmediateTextNode()
self.removeTextNode.displaysAsynchronously = false
self.removeTextNode.isUserInteractionEnabled = false
self.titleNode = TextNode()
self.titleNode.displaysAsynchronously = false
self.titleNode.isUserInteractionEnabled = false
self.textNode = TextNodeWithEntities()
self.textNode.textNode.displaysAsynchronously = false
self.textNode.textNode.isUserInteractionEnabled = false
self.imageNode = TransformImageNode()
self.imageNode.contentAnimations = [.subsequentUpdates]
self.imageNodeContainer = ASDisplayNode()
self.closeButton = HighlightableButtonNode()
self.closeButton.hitTestSlop = UIEdgeInsets(top: -8.0, left: -8.0, bottom: -8.0, right: -8.0)
self.closeButton.displaysAsynchronously = false
super.init()
self.addSubnode(self.contextContainer)
self.contextContainer.addSubnode(self.clippingContainer)
self.clippingContainer.addSubnode(self.contentContainer)
self.contentTextContainer.addSubnode(self.titleNode)
self.contentTextContainer.addSubnode(self.adNode)
self.contentTextContainer.addSubnode(self.textNode.textNode)
self.contentContainer.addSubnode(self.contentTextContainer)
self.imageNodeContainer.addSubnode(self.imageNode)
self.contentContainer.addSubnode(self.imageNodeContainer)
self.tapButton.addTarget(self, action: #selector(self.tapped), forControlEvents: [.touchUpInside])
self.tapButton.highligthedChanged = { [weak self] highlighted in
if let strongSelf = self {
if highlighted {
strongSelf.adNode.layer.removeAnimation(forKey: "opacity")
strongSelf.adNode.alpha = 0.4
strongSelf.titleNode.layer.removeAnimation(forKey: "opacity")
strongSelf.titleNode.alpha = 0.4
strongSelf.textNode.textNode.layer.removeAnimation(forKey: "opacity")
strongSelf.textNode.textNode.alpha = 0.4
strongSelf.imageNode.layer.removeAnimation(forKey: "opacity")
strongSelf.imageNode.alpha = 0.4
strongSelf.removeTextNode.layer.removeAnimation(forKey: "opacity")
strongSelf.removeTextNode.alpha = 0.4
strongSelf.removeBackgroundNode.layer.removeAnimation(forKey: "opacity")
strongSelf.removeBackgroundNode.alpha = 0.4
} else {
strongSelf.adNode.alpha = 1.0
strongSelf.adNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
strongSelf.titleNode.alpha = 1.0
strongSelf.titleNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
strongSelf.textNode.textNode.alpha = 1.0
strongSelf.textNode.textNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
strongSelf.imageNode.alpha = 1.0
strongSelf.imageNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
strongSelf.removeTextNode.alpha = 1.0
strongSelf.removeTextNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
strongSelf.removeBackgroundNode.alpha = 1.0
strongSelf.removeBackgroundNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
}
}
}
self.contextContainer.addSubnode(self.tapButton)
self.contextContainer.addSubnode(self.removeBackgroundNode)
self.contextContainer.addSubnode(self.removeTextNode)
self.contextContainer.addSubnode(self.removeButtonNode)
self.removeButtonNode.addTarget(self, action: #selector(self.removePressed), forControlEvents: [.touchUpInside])
self.removeButtonNode.highligthedChanged = { [weak self] highlighted in
if let strongSelf = self {
if highlighted {
strongSelf.removeTextNode.layer.removeAnimation(forKey: "opacity")
strongSelf.removeTextNode.alpha = 0.4
strongSelf.removeBackgroundNode.layer.removeAnimation(forKey: "opacity")
strongSelf.removeBackgroundNode.alpha = 0.4
} else {
strongSelf.removeTextNode.alpha = 1.0
strongSelf.removeTextNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
strongSelf.removeBackgroundNode.alpha = 1.0
strongSelf.removeBackgroundNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
}
}
}
self.contextContainer.activated = { [weak self] gesture, _ in
guard let self, let message = self.message else {
return
}
self.contextAction(message, self.contextContainer, gesture)
}
self.closeButton.addTarget(self, action: #selector(self.closePressed), forControlEvents: [.touchUpInside])
self.addSubnode(self.closeButton)
}
deinit {
self.fetchDisposable.dispose()
}
private var theme: PresentationTheme?
@objc private func closePressed() {
/*if self.context.isPremium, let adAttribute = self.message?.adAttribute {
self.controllerInteraction?.removeAd(adAttribute.opaqueId)
} else {
self.controllerInteraction?.openNoAdsDemo()
}*/
self.close()
}
func updateLayout(width: CGFloat, theme: PresentationTheme, strings: PresentationStrings, info: AdPanelHeaderPanelComponent.Info, transition: ContainedViewLayoutTransition) -> CGFloat {
let leftInset: CGFloat = 0.0
let rightInset: CGFloat = 0.0
self.message = info.message
if self.theme !== theme {
self.theme = theme
self.removeBackgroundNode.image = generateStretchableFilledCircleImage(diameter: 15.0, color: theme.chat.inputPanel.panelControlColor.withMultipliedAlpha(0.1))
self.removeTextNode.attributedText = NSAttributedString(string: strings.Chat_BotAd_WhatIsThis, font: Font.regular(11.0), textColor: theme.chat.inputPanel.panelControlColor)
self.closeButton.setImage(generateImage(CGSize(width: 12.0, height: 12.0), contextGenerator: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setStrokeColor(theme.chat.inputPanel.panelControlColor.cgColor)
context.setLineWidth(1.33)
context.setLineCap(.round)
context.move(to: CGPoint(x: 1.0, y: 1.0))
context.addLine(to: CGPoint(x: size.width - 1.0, y: size.height - 1.0))
context.strokePath()
context.move(to: CGPoint(x: size.width - 1.0, y: 1.0))
context.addLine(to: CGPoint(x: 1.0, y: size.height - 1.0))
context.strokePath()
}), for: [])
}
self.contextContainer.isGestureEnabled = false
let panelHeight: CGFloat
var hasCloseButton = true
let presentationData = self.context.sharedContext.currentPresentationData.with({ $0 })
panelHeight = self.enqueueTransition(width: width, leftInset: leftInset, rightInset: rightInset, transition: .immediate, animation: nil, message: info.message, theme: theme, strings: strings, nameDisplayOrder: presentationData.nameDisplayOrder, dateTimeFormat: presentationData.dateTimeFormat, accountPeerId: self.context.account.peerId, firstTime: false, isReplyThread: false, translateToLanguage: nil)
hasCloseButton = info.message.media.isEmpty
self.contextContainer.frame = CGRect(origin: CGPoint(), size: CGSize(width: width, height: panelHeight))
self.tapButton.frame = CGRect(origin: CGPoint(), size: CGSize(width: width, height: panelHeight))
self.clippingContainer.frame = CGRect(origin: CGPoint(), size: CGSize(width: width, height: panelHeight))
self.contentContainer.frame = CGRect(origin: CGPoint(), size: CGSize(width: width, height: panelHeight))
let contentRightInset: CGFloat = 14.0 + rightInset
let closeButtonSize = self.closeButton.measure(CGSize(width: 100.0, height: 100.0))
self.closeButton.frame = CGRect(origin: CGPoint(x: width - contentRightInset - closeButtonSize.width, y: floorToScreenPixels((panelHeight - closeButtonSize.height) / 2.0)), size: closeButtonSize)
self.closeButton.isHidden = !hasCloseButton
self.currentLayout = (width, leftInset, rightInset)
return panelHeight
}
private func enqueueTransition(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition, animation: PinnedMessageAnimation?, message: EngineMessage, theme: PresentationTheme, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, dateTimeFormat: PresentationDateTimeFormat, accountPeerId: PeerId, firstTime: Bool, isReplyThread: Bool, translateToLanguage: String?) -> CGFloat {
var animationTransition: ContainedViewLayoutTransition = .immediate
if let animation = animation {
animationTransition = .animated(duration: 0.2, curve: .easeInOut)
if let copyView = self.textNode.textNode.view.snapshotView(afterScreenUpdates: false) {
let offset: CGFloat
switch animation {
case .slideToTop:
offset = -10.0
case .slideToBottom:
offset = 10.0
}
copyView.frame = self.textNode.textNode.frame
self.textNode.textNode.view.superview?.addSubview(copyView)
copyView.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: offset), duration: 0.2, removeOnCompletion: false, additive: true)
copyView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak copyView] _ in
copyView?.removeFromSuperview()
})
self.textNode.textNode.layer.animatePosition(from: CGPoint(x: 0.0, y: -offset), to: CGPoint(), duration: 0.2, additive: true)
self.textNode.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
}
let makeAdLayout = TextNode.asyncLayout(self.adNode)
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
let makeTextLayout = TextNodeWithEntities.asyncLayout(self.textNode)
let imageNodeLayout = self.imageNode.asyncLayout()
let previousMediaReference = self.previousMediaReference
let context = self.context
let contentLeftInset: CGFloat = leftInset + 18.0
let contentRightInset: CGFloat = rightInset + 9.0
var textRightInset: CGFloat = 0.0
var updatedMediaReference: AnyMediaReference?
var imageDimensions: CGSize?
if !message._asMessage().containsSecretMedia {
for media in message.media {
if let image = media as? TelegramMediaImage {
updatedMediaReference = .message(message: MessageReference(message._asMessage()), media: image)
if let representation = largestRepresentationForPhoto(image) {
imageDimensions = representation.dimensions.cgSize
}
break
} else if let file = media as? TelegramMediaFile {
updatedMediaReference = .message(message: MessageReference(message._asMessage()), media: file)
if !file.isInstantVideo && !file.isSticker, let representation = largestImageRepresentation(file.previewRepresentations) {
imageDimensions = representation.dimensions.cgSize
} else if file.isAnimated, let dimensions = file.dimensions {
imageDimensions = dimensions.cgSize
}
break
} else if let paidContent = media as? TelegramMediaPaidContent, let firstMedia = paidContent.extendedMedia.first {
switch firstMedia {
case let .preview(dimensions, immediateThumbnailData, _):
let thumbnailMedia = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: [], immediateThumbnailData: immediateThumbnailData, reference: nil, partialReference: nil, flags: [])
if let dimensions {
imageDimensions = dimensions.cgSize
}
updatedMediaReference = .standalone(media: thumbnailMedia)
case let .full(fullMedia):
updatedMediaReference = .message(message: MessageReference(message._asMessage()), media: fullMedia)
if let image = fullMedia as? TelegramMediaImage {
if let representation = largestRepresentationForPhoto(image) {
imageDimensions = representation.dimensions.cgSize
}
break
} else if let file = fullMedia as? TelegramMediaFile {
if let dimensions = file.dimensions {
imageDimensions = dimensions.cgSize
}
break
}
}
}
}
}
let imageBoundingSize = CGSize(width: 48.0, height: 48.0)
var applyImage: (() -> Void)?
if let imageDimensions {
applyImage = imageNodeLayout(TransformImageArguments(corners: ImageCorners(radius: 10.0), imageSize: imageDimensions.aspectFilled(imageBoundingSize), boundingSize: imageBoundingSize, intrinsicInsets: UIEdgeInsets()))
textRightInset += imageBoundingSize.width + 18.0
} else {
textRightInset = 27.0
}
var mediaUpdated = false
if let updatedMediaReference = updatedMediaReference, let previousMediaReference = previousMediaReference {
mediaUpdated = !updatedMediaReference.media.isEqual(to: previousMediaReference.media)
} else if (updatedMediaReference != nil) != (previousMediaReference != nil) {
mediaUpdated = true
}
var updateImageSignal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>?
var updatedFetchMediaSignal: Signal<FetchResourceSourceType, FetchResourceError>?
if mediaUpdated {
if let updatedMediaReference = updatedMediaReference, imageDimensions != nil {
if let imageReference = updatedMediaReference.concrete(TelegramMediaImage.self) {
if imageReference.media.representations.isEmpty {
updateImageSignal = chatSecretPhoto(account: context.account, userLocation: .peer(message.id.peerId), photoReference: imageReference, ignoreFullSize: true, synchronousLoad: true)
} else {
updateImageSignal = chatMessagePhotoThumbnail(account: context.account, userLocation: .peer(message.id.peerId), photoReference: imageReference, blurred: false)
}
} else if let fileReference = updatedMediaReference.concrete(TelegramMediaFile.self) {
if fileReference.media.isAnimatedSticker {
let dimensions = fileReference.media.dimensions ?? PixelDimensions(width: 512, height: 512)
updateImageSignal = chatMessageAnimatedSticker(postbox: context.account.postbox, userLocation: .peer(message.id.peerId), file: fileReference.media, small: false, size: dimensions.cgSize.aspectFitted(CGSize(width: 160.0, height: 160.0)))
updatedFetchMediaSignal = fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, userLocation: .peer(message.id.peerId), userContentType: MediaResourceUserContentType(file: fileReference.media), reference: fileReference.resourceReference(fileReference.media.resource))
} else if fileReference.media.isVideo || fileReference.media.isAnimated {
updateImageSignal = chatMessageVideoThumbnail(account: context.account, userLocation: .peer(message.id.peerId), fileReference: fileReference, blurred: false)
} else if let iconImageRepresentation = smallestImageRepresentation(fileReference.media.previewRepresentations) {
updateImageSignal = chatWebpageSnippetFile(account: context.account, userLocation: .peer(message.id.peerId), mediaReference: fileReference.abstract, representation: iconImageRepresentation)
}
}
} else {
updateImageSignal = .single({ _ in return nil })
}
}
let (adLayout, adApply) = makeAdLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: strings.Chat_BotAd_Title, font: Font.semibold(14.0), textColor: theme.chat.inputPanel.panelControlColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: width, height: .greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: .zero))
let titleConstrainedSize = CGSize(width: width - contentLeftInset - contentRightInset - textRightInset - adLayout.size.width - 90.0, height: CGFloat.greatestFiniteMagnitude)
let textConstrainedSize = CGSize(width: width - contentLeftInset - contentRightInset - textRightInset, height: CGFloat.greatestFiniteMagnitude)
var titleText: String = ""
if let author = message.author {
titleText = author.compactDisplayTitle
}
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: titleText, font: Font.semibold(14.0), textColor: theme.chat.inputPanel.primaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: titleConstrainedSize, alignment: .natural, cutout: nil, insets: .zero))
let (textString, _, isText) = descriptionStringForMessage(contentSettings: context.currentContentSettings.with { $0 }, message: message, strings: strings, nameDisplayOrder: nameDisplayOrder, dateTimeFormat: dateTimeFormat, accountPeerId: accountPeerId)
let messageText: NSAttributedString
let textFont = Font.regular(14.0)
if isText {
var text = message.text
var messageEntities = message._asMessage().textEntitiesAttribute?.entities ?? []
if let translateToLanguage = translateToLanguage, !text.isEmpty {
for attribute in message.attributes {
if let attribute = attribute as? TranslationMessageAttribute, !attribute.text.isEmpty, attribute.toLang == translateToLanguage {
text = attribute.text
messageEntities = attribute.entities
break
}
}
}
let entities = messageEntities.filter { entity in
switch entity.type {
case .CustomEmoji:
return true
default:
return false
}
}
let textColor = theme.chat.inputPanel.primaryTextColor
if entities.count > 0 {
messageText = stringWithAppliedEntities(trimToLineCount(text, lineCount: 1), entities: entities, baseColor: textColor, linkColor: textColor, baseFont: textFont, linkFont: textFont, boldFont: textFont, italicFont: textFont, boldItalicFont: textFont, fixedFont: textFont, blockQuoteFont: textFont, underlineLinks: false, message: message._asMessage())
} else {
messageText = NSAttributedString(string: foldLineBreaks(text), font: textFont, textColor: textColor)
}
} else {
messageText = NSAttributedString(string: foldLineBreaks(textString.string), font: textFont, textColor: message.media.isEmpty || message.media.first is TelegramMediaWebpage ? theme.chat.inputPanel.primaryTextColor : theme.chat.inputPanel.secondaryTextColor)
}
let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: messageText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: textConstrainedSize, alignment: .natural, cutout: nil, insets: .zero))
var panelHeight: CGFloat = 0.0
if let _ = imageDimensions {
panelHeight = 9.0 + imageBoundingSize.height + 9.0
}
var textHeight: CGFloat
var titleOnSeparateLine = false
if textLayout.numberOfLines == 1 || contentLeftInset + adLayout.size.width + 2.0 + titleLayout.size.width > width - contentRightInset - textRightInset {
textHeight = adLayout.size.height + titleLayout.size.height + textLayout.size.height + 15.0
titleOnSeparateLine = true
} else {
textHeight = titleLayout.size.height + textLayout.size.height + 15.0
}
panelHeight = max(panelHeight, textHeight)
Queue.mainQueue().async {
let _ = adApply()
let _ = titleApply()
let textArguments = TextNodeWithEntities.Arguments(
context: self.context,
cache: self.context.animationCache,
renderer: self.context.animationRenderer,
placeholderColor: theme.list.mediaPlaceholderColor,
attemptSynchronous: false
)
let _ = textApply(textArguments)
self.previousMediaReference = updatedMediaReference
let textContainerFrame = CGRect(origin: CGPoint(x: contentLeftInset, y: 0.0), size: CGSize(width: width, height: panelHeight))
animationTransition.updateFrameAdditive(node: self.contentTextContainer, frame: textContainerFrame)
let removeTextSize = self.removeTextNode.updateLayout(CGSize(width: width, height: .greatestFiniteMagnitude))
if titleOnSeparateLine {
self.adNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 9.0), size: adLayout.size)
self.titleNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 26.0), size: titleLayout.size)
self.textNode.textNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 43.0), size: textLayout.size)
self.removeTextNode.frame = CGRect(origin: CGPoint(x: contentLeftInset + adLayout.size.width + 8.0, y: 11.0 - UIScreenPixel), size: removeTextSize)
self.removeBackgroundNode.frame = self.removeTextNode.frame.insetBy(dx: -5.0, dy: -1.0)
self.removeButtonNode.frame = self.removeBackgroundNode.frame
} else {
self.adNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 9.0), size: adLayout.size)
self.titleNode.frame = CGRect(origin: CGPoint(x: adLayout.size.width + 2.0, y: 9.0), size: titleLayout.size)
self.textNode.textNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 26.0), size: textLayout.size)
self.removeTextNode.frame = CGRect(origin: CGPoint(x: contentLeftInset + adLayout.size.width + 2.0 + titleLayout.size.width + 8.0, y: 11.0 - UIScreenPixel), size: removeTextSize)
self.removeBackgroundNode.frame = self.removeTextNode.frame.insetBy(dx: -5.0, dy: -1.0)
self.removeButtonNode.frame = self.removeBackgroundNode.frame
}
self.textNode.visibilityRect = CGRect.infinite
self.imageNodeContainer.frame = CGRect(origin: CGPoint(x: width - contentRightInset - imageBoundingSize.width, y: 9.0), size: imageBoundingSize)
self.imageNode.frame = CGRect(origin: CGPoint(), size: imageBoundingSize)
if let applyImage = applyImage {
applyImage()
animationTransition.updateSublayerTransformScale(node: self.imageNodeContainer, scale: 1.0)
animationTransition.updateAlpha(node: self.imageNodeContainer, alpha: 1.0, beginWithCurrentState: true)
} else {
animationTransition.updateSublayerTransformScale(node: self.imageNodeContainer, scale: 0.1)
animationTransition.updateAlpha(node: self.imageNodeContainer, alpha: 0.0, beginWithCurrentState: true)
}
if let updateImageSignal = updateImageSignal {
self.imageNode.setSignal(updateImageSignal)
}
if let updatedFetchMediaSignal = updatedFetchMediaSignal {
self.fetchDisposable.set(updatedFetchMediaSignal.startStrict())
}
}
return panelHeight
}
@objc func tapped() {
guard let message = self.message else {
return
}
self.action(message)
}
@objc func removePressed() {
guard let message = self.message else {
return
}
self.contextAction(message, self.contextContainer, nil)
}
}
@@ -0,0 +1,39 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "AdminUserActionsSheet",
module_name = "AdminUserActionsSheet",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/AsyncDisplayKit",
"//submodules/Display",
"//submodules/Postbox",
"//submodules/TelegramCore",
"//submodules/SSignalKit/SwiftSignalKit",
"//submodules/ComponentFlow",
"//submodules/Components/ViewControllerComponent",
"//submodules/Components/ComponentDisplayAdapters",
"//submodules/Components/MultilineTextComponent",
"//submodules/Components/BundleIconComponent",
"//submodules/TelegramPresentationData",
"//submodules/AccountContext",
"//submodules/AppBundle",
"//submodules/PresentationDataUtils",
"//submodules/TelegramUI/Components/ButtonComponent",
"//submodules/AvatarNode",
"//submodules/CheckNode",
"//submodules/UndoUI",
"//submodules/TelegramUI/Components/ListSectionComponent",
"//submodules/TelegramUI/Components/ListActionItemComponent",
"//submodules/TelegramUI/Components/PlainButtonComponent",
"//submodules/TelegramUI/Components/GlassBarButtonComponent",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,277 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import ComponentFlow
import SwiftSignalKit
import AccountContext
import TelegramCore
import MultilineTextComponent
import AvatarNode
import TelegramPresentationData
import CheckNode
import TelegramStringFormatting
import ListSectionComponent
private let avatarFont = avatarPlaceholderFont(size: 15.0)
private func cancelContextGestures(view: UIView) {
if let gestureRecognizers = view.gestureRecognizers {
for gesture in gestureRecognizers {
if let gesture = gesture as? ContextGesture {
gesture.cancel()
}
}
}
for subview in view.subviews {
cancelContextGestures(view: subview)
}
}
final class AdminUserActionsPeerComponent: Component {
enum SelectionState: Equatable {
case none
case editing(isSelected: Bool)
}
let context: AccountContext
let theme: PresentationTheme
let strings: PresentationStrings
let baseFontSize: CGFloat
let sideInset: CGFloat
let title: String
let peer: EnginePeer?
let selectionState: SelectionState
let action: (EnginePeer) -> Void
init(
context: AccountContext,
theme: PresentationTheme,
strings: PresentationStrings,
baseFontSize: CGFloat,
sideInset: CGFloat,
title: String,
peer: EnginePeer?,
selectionState: SelectionState,
action: @escaping (EnginePeer) -> Void
) {
self.context = context
self.theme = theme
self.strings = strings
self.baseFontSize = baseFontSize
self.sideInset = sideInset
self.title = title
self.peer = peer
self.selectionState = selectionState
self.action = action
}
static func ==(lhs: AdminUserActionsPeerComponent, rhs: AdminUserActionsPeerComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.theme !== rhs.theme {
return false
}
if lhs.strings !== rhs.strings {
return false
}
if lhs.baseFontSize != rhs.baseFontSize {
return false
}
if lhs.sideInset != rhs.sideInset {
return false
}
if lhs.title != rhs.title {
return false
}
if lhs.peer != rhs.peer {
return false
}
if lhs.selectionState != rhs.selectionState {
return false
}
return true
}
final class View: UIView, ListSectionComponent.ChildView {
private let containerButton: HighlightTrackingButton
private let title = ComponentView<Empty>()
private let label = ComponentView<Empty>()
private let avatarNode: AvatarNode
private var labelIconView: UIImageView?
private var checkLayer: CheckLayer?
private var component: AdminUserActionsPeerComponent?
private weak var state: EmptyComponentState?
public var customUpdateIsHighlighted: ((Bool) -> Void)?
public var enumerateSiblings: (((UIView) -> Void) -> Void)?
public var separatorInset: CGFloat = 0.0
override init(frame: CGRect) {
self.containerButton = HighlightTrackingButton()
self.avatarNode = AvatarNode(font: avatarFont)
self.avatarNode.isLayerBacked = true
super.init(frame: frame)
self.addSubview(self.containerButton)
self.containerButton.layer.addSublayer(self.avatarNode.layer)
self.containerButton.addTarget(self, action: #selector(self.pressed), for: .touchUpInside)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@objc private func pressed() {
guard let component = self.component, let peer = component.peer else {
return
}
component.action(peer)
}
func update(component: AdminUserActionsPeerComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
let themeUpdated = self.component?.theme !== component.theme
var hasSelectionUpdated = false
if let previousComponent = self.component {
switch previousComponent.selectionState {
case .none:
if case .none = component.selectionState {
} else {
hasSelectionUpdated = true
}
case .editing:
if case .editing = component.selectionState {
} else {
hasSelectionUpdated = true
}
}
}
self.component = component
self.state = state
let contextInset: CGFloat = 0.0
let height: CGFloat = 52.0
let verticalInset: CGFloat = 1.0
let leftInset: CGFloat = 30.0 + component.sideInset
var rightInset: CGFloat = contextInset * 2.0 + 8.0 + component.sideInset
var avatarLeftInset: CGFloat = component.sideInset + 10.0
if case let .editing(isSelected) = component.selectionState {
rightInset += 46.0
avatarLeftInset += 24.0
let checkSize: CGFloat = 22.0
let checkLayer: CheckLayer
if let current = self.checkLayer {
checkLayer = current
if themeUpdated {
checkLayer.theme = CheckNodeTheme(theme: component.theme, style: .plain)
}
checkLayer.setSelected(isSelected, animated: !transition.animation.isImmediate)
} else {
checkLayer = CheckLayer(theme: CheckNodeTheme(theme: component.theme, style: .plain))
self.checkLayer = checkLayer
self.containerButton.layer.addSublayer(checkLayer)
checkLayer.frame = CGRect(origin: CGPoint(x: -checkSize, y: floor((height - verticalInset * 2.0 - checkSize) / 2.0)), size: CGSize(width: checkSize, height: checkSize))
checkLayer.setSelected(isSelected, animated: false)
checkLayer.setNeedsDisplay()
}
transition.setFrame(layer: checkLayer, frame: CGRect(origin: CGPoint(x: floor((22.0 - checkSize) * 0.5), y: floor((height - verticalInset * 2.0 - checkSize) / 2.0)), size: CGSize(width: checkSize, height: checkSize)))
} else {
if let checkLayer = self.checkLayer {
self.checkLayer = nil
transition.setPosition(layer: checkLayer, position: CGPoint(x: -checkLayer.bounds.width * 0.5, y: checkLayer.position.y), completion: { [weak checkLayer] _ in
checkLayer?.removeFromSuperlayer()
})
}
}
let avatarSize: CGFloat = 30.0
let avatarFrame = CGRect(origin: CGPoint(x: avatarLeftInset, y: floor((height - verticalInset * 2.0 - avatarSize) / 2.0)), size: CGSize(width: avatarSize, height: avatarSize))
if self.avatarNode.bounds.isEmpty {
self.avatarNode.frame = avatarFrame
} else {
transition.setFrame(layer: self.avatarNode.layer, frame: avatarFrame)
}
if let peer = component.peer {
let clipStyle: AvatarNodeClipStyle
if case let .channel(channel) = peer, channel.isForumOrMonoForum {
clipStyle = .roundedRect
} else {
clipStyle = .round
}
self.avatarNode.setPeer(context: component.context, theme: component.theme, peer: peer, clipStyle: clipStyle, displayDimensions: CGSize(width: avatarSize, height: avatarSize))
}
let avatarTitleSpacing: CGFloat = 5.0
let maxTextSize = availableSize.width - avatarLeftInset - avatarSize - avatarTitleSpacing - rightInset
let previousTitleFrame = self.title.view?.frame
var previousTitleContents: UIView?
if hasSelectionUpdated && !"".isEmpty {
previousTitleContents = self.title.view?.snapshotView(afterScreenUpdates: false)
}
let titleSize = self.title.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: component.title, font: Font.semibold(component.baseFontSize), textColor: component.theme.list.itemPrimaryTextColor))
)),
environment: {},
containerSize: CGSize(width: maxTextSize, height: 100.0)
)
let centralContentHeight: CGFloat = titleSize.height
let titleFrame = CGRect(origin: CGPoint(x: avatarLeftInset + avatarSize + avatarTitleSpacing, y: floor((height - verticalInset * 2.0 - centralContentHeight) / 2.0)), size: titleSize)
if let titleView = self.title.view {
if titleView.superview == nil {
titleView.isUserInteractionEnabled = false
self.containerButton.addSubview(titleView)
}
titleView.frame = titleFrame
if let previousTitleFrame, previousTitleFrame.origin.x != titleFrame.origin.x {
transition.animatePosition(view: titleView, from: CGPoint(x: previousTitleFrame.origin.x - titleFrame.origin.x, y: 0.0), to: CGPoint(), additive: true)
}
if let previousTitleFrame, let previousTitleContents, previousTitleFrame.size != titleSize {
previousTitleContents.frame = CGRect(origin: previousTitleFrame.origin, size: previousTitleFrame.size)
self.addSubview(previousTitleContents)
transition.setFrame(view: previousTitleContents, frame: CGRect(origin: titleFrame.origin, size: previousTitleFrame.size))
transition.setAlpha(view: previousTitleContents, alpha: 0.0, completion: { [weak previousTitleContents] _ in
previousTitleContents?.removeFromSuperview()
})
transition.animateAlpha(view: titleView, from: 0.0, to: 1.0)
}
}
let containerFrame = CGRect(origin: CGPoint(x: contextInset, y: verticalInset), size: CGSize(width: availableSize.width - contextInset * 2.0, height: height - verticalInset * 2.0))
transition.setFrame(view: self.containerButton, frame: containerFrame)
self.separatorInset = leftInset
return CGSize(width: availableSize.width, height: height)
}
}
func makeView() -> View {
return View(frame: CGRect())
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
@@ -0,0 +1,39 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "AdsInfoScreen",
module_name = "AdsInfoScreen",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/AsyncDisplayKit",
"//submodules/Display",
"//submodules/Postbox",
"//submodules/TelegramCore",
"//submodules/SSignalKit/SwiftSignalKit",
"//submodules/ComponentFlow",
"//submodules/Components/ViewControllerComponent",
"//submodules/Components/ComponentDisplayAdapters",
"//submodules/Components/MultilineTextComponent",
"//submodules/Components/BalancedTextComponent",
"//submodules/Components/SolidRoundedButtonComponent",
"//submodules/Components/BundleIconComponent",
"//submodules/Components/BlurredBackgroundComponent",
"//submodules/TelegramUI/Components/ScrollComponent",
"//submodules/TelegramPresentationData",
"//submodules/AccountContext",
"//submodules/AppBundle",
"//submodules/TelegramStringFormatting",
"//submodules/PresentationDataUtils",
"//submodules/ContextUI",
"//submodules/UndoUI",
"//submodules/TelegramUI/Components/Ads/AdsReportScreen",
],
visibility = [
"//visibility:public",
],
)
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,38 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "AdsReportScreen",
module_name = "AdsReportScreen",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/AsyncDisplayKit",
"//submodules/Display",
"//submodules/Postbox",
"//submodules/TelegramCore",
"//submodules/SSignalKit/SwiftSignalKit",
"//submodules/ComponentFlow",
"//submodules/Components/ViewControllerComponent",
"//submodules/Components/ComponentDisplayAdapters",
"//submodules/Components/MultilineTextComponent",
"//submodules/Components/BalancedTextComponent",
"//submodules/TelegramPresentationData",
"//submodules/AccountContext",
"//submodules/AppBundle",
"//submodules/ItemListUI",
"//submodules/TelegramStringFormatting",
"//submodules/PresentationDataUtils",
"//submodules/Components/SheetComponent",
"//submodules/UndoUI",
"//submodules/TelegramUI/Components/ListSectionComponent",
"//submodules/TelegramUI/Components/ListActionItemComponent",
"//submodules/TelegramUI/Components/NavigationStackComponent",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,647 @@
import Foundation
import UIKit
import Display
import ComponentFlow
import SwiftSignalKit
import TelegramCore
import Markdown
import TextFormat
import TelegramPresentationData
import ViewControllerComponent
import SheetComponent
import BalancedTextComponent
import MultilineTextComponent
import ListSectionComponent
import ListActionItemComponent
import NavigationStackComponent
import ItemListUI
import UndoUI
import AccountContext
private enum ReportResult {
case reported
case hidden
case premiumRequired
}
private final class SheetPageContent: CombinedComponent {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
struct Item: Equatable {
let title: String
let option: Data
}
let context: AccountContext
let title: String?
let subtitle: String
let items: [Item]
let action: (Item) -> Void
let pop: () -> Void
init(
context: AccountContext,
title: String?,
subtitle: String,
items: [Item],
action: @escaping (Item) -> Void,
pop: @escaping () -> Void
) {
self.context = context
self.title = title
self.subtitle = subtitle
self.items = items
self.action = action
self.pop = pop
}
static func ==(lhs: SheetPageContent, rhs: SheetPageContent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.title != rhs.title {
return false
}
if lhs.subtitle != rhs.subtitle {
return false
}
if lhs.items != rhs.items {
return false
}
return true
}
final class State: ComponentState {
var backArrowImage: (UIImage, PresentationTheme)?
}
func makeState() -> State {
return State()
}
static var body: Body {
let background = Child(RoundedRectangle.self)
let back = Child(Button.self)
let title = Child(Text.self)
let subtitle = Child(MultilineTextComponent.self)
let section = Child(ListSectionComponent.self)
return { context in
let environment = context.environment[EnvironmentType.self]
let component = context.component
let state = context.state
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
let theme = environment.theme
let strings = environment.strings
let sideInset: CGFloat = 16.0 + environment.safeInsets.left
var contentSize = CGSize(width: context.availableSize.width, height: 18.0)
let background = background.update(
component: RoundedRectangle(color: theme.list.modalBlocksBackgroundColor, cornerRadius: 8.0),
availableSize: CGSize(width: context.availableSize.width, height: 1000.0),
transition: .immediate
)
context.add(background
.position(CGPoint(x: context.availableSize.width / 2.0, y: background.size.height / 2.0))
)
let backArrowImage: UIImage
if let (cached, cachedTheme) = state.backArrowImage, cachedTheme === theme {
backArrowImage = cached
} else {
backArrowImage = NavigationBarTheme.generateBackArrowImage(color: theme.list.itemAccentColor)!
state.backArrowImage = (backArrowImage, theme)
}
let backContents: AnyComponent<Empty>
if component.title == nil {
backContents = AnyComponent(Text(text: strings.Common_Cancel, font: Font.regular(17.0), color: theme.list.itemAccentColor))
} else {
backContents = AnyComponent(
HStack([
AnyComponentWithIdentity(id: "arrow", component: AnyComponent(Image(image: backArrowImage, contentMode: .center))),
AnyComponentWithIdentity(id: "label", component: AnyComponent(Text(text: strings.Common_Back, font: Font.regular(17.0), color: theme.list.itemAccentColor)))
], spacing: 6.0)
)
}
let back = back.update(
component: Button(
content: backContents,
action: {
component.pop()
}
),
availableSize: CGSize(width: context.availableSize.width, height: context.availableSize.height),
transition: .immediate
)
context.add(back
.position(CGPoint(x: sideInset + back.size.width / 2.0 - (component.title != nil ? 8.0 : 0.0), y: contentSize.height + back.size.height / 2.0))
)
let constrainedTitleWidth = context.availableSize.width - (back.size.width + 16.0) * 2.0
let title = title.update(
component: Text(text: strings.ReportAd_Title, font: Font.semibold(17.0), color: theme.list.itemPrimaryTextColor),
availableSize: CGSize(width: constrainedTitleWidth, height: context.availableSize.height),
transition: .immediate
)
if let subtitleText = component.title {
let subtitle = subtitle.update(
component: MultilineTextComponent(text: .plain(NSAttributedString(string: subtitleText, font: Font.regular(13.0), textColor: theme.list.itemSecondaryTextColor)), truncationType: .end, maximumNumberOfLines: 1),
availableSize: CGSize(width: constrainedTitleWidth, height: context.availableSize.height),
transition: .immediate
)
context.add(title
.position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + title.size.height / 2.0 - 8.0))
)
contentSize.height += title.size.height
context.add(subtitle
.position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + subtitle.size.height / 2.0 - 9.0))
)
contentSize.height += subtitle.size.height
contentSize.height += 8.0
} else {
context.add(title
.position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + title.size.height / 2.0))
)
contentSize.height += title.size.height
contentSize.height += 24.0
}
var items: [AnyComponentWithIdentity<Empty>] = []
for item in component.items {
items.append(AnyComponentWithIdentity(id: item.title, component: AnyComponent(ListActionItemComponent(
theme: theme,
title: AnyComponent(VStack([
AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: item.title,
font: Font.regular(presentationData.listsFontSize.baseDisplaySize),
textColor: theme.list.itemPrimaryTextColor
)),
maximumNumberOfLines: 1
))),
], alignment: .left, spacing: 2.0)),
accessory: .arrow,
action: { _ in
component.action(item)
}
))))
}
let section = section.update(
component: ListSectionComponent(
theme: theme,
header: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: component.subtitle.uppercased(),
font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize),
textColor: theme.list.freeTextColor
)),
maximumNumberOfLines: 0
)),
footer: AnyComponent(MultilineTextComponent(
text: .markdown(
text: strings.ReportAd_Help,
attributes: MarkdownAttributes(
body: MarkdownAttributeSet(font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), textColor: theme.list.freeTextColor),
bold: MarkdownAttributeSet(font: Font.semibold(presentationData.listsFontSize.itemListBaseHeaderFontSize), textColor: theme.list.freeTextColor),
link: MarkdownAttributeSet(font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), textColor: theme.list.itemAccentColor),
linkAttribute: { contents in
return (TelegramTextAttributes.URL, contents)
}
)
),
maximumNumberOfLines: 0,
highlightColor: theme.list.itemAccentColor.withAlphaComponent(0.2),
highlightAction: { attributes in
if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] {
return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)
} else {
return nil
}
},
tapAction: { _, _ in
component.context.sharedContext.openExternalUrl(context: component.context, urlContext: .generic, url: strings.ReportAd_Help_URL, forceExternal: true, presentationData: presentationData, navigationController: nil, dismissInput: {})
}
)),
items: items,
isModal: true
),
environment: {},
availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: .greatestFiniteMagnitude),
transition: context.transition
)
context.add(section
.position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + section.size.height / 2.0))
)
contentSize.height += section.size.height
contentSize.height += 54.0
return contentSize
}
}
}
private final class SheetContent: CombinedComponent {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
let context: AccountContext
let opaqueId: Data
let title: String
let options: [ReportAdMessageResult.Option]
let pts: Int
let openMore: () -> Void
let complete: (ReportResult) -> Void
let dismiss: () -> Void
let update: (ComponentTransition) -> Void
init(
context: AccountContext,
opaqueId: Data,
title: String,
options: [ReportAdMessageResult.Option],
pts: Int,
openMore: @escaping () -> Void,
complete: @escaping (ReportResult) -> Void,
dismiss: @escaping () -> Void,
update: @escaping (ComponentTransition) -> Void
) {
self.context = context
self.opaqueId = opaqueId
self.title = title
self.options = options
self.pts = pts
self.openMore = openMore
self.complete = complete
self.dismiss = dismiss
self.update = update
}
static func ==(lhs: SheetContent, rhs: SheetContent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.opaqueId != rhs.opaqueId {
return false
}
if lhs.title != rhs.title {
return false
}
if lhs.options != rhs.options {
return false
}
if lhs.pts != rhs.pts {
return false
}
return true
}
final class State: ComponentState {
var pushedOptions: [(title: String, subtitle: String, options: [ReportAdMessageResult.Option])] = []
let disposable = MetaDisposable()
deinit {
self.disposable.dispose()
}
}
func makeState() -> State {
return State()
}
static var body: Body {
let navigation = Child(NavigationStackComponent<EnvironmentType>.self)
return { context in
let environment = context.environment[EnvironmentType.self]
let component = context.component
let state = context.state
let update = component.update
let accountContext = component.context
let opaqueId = component.opaqueId
let complete = component.complete
let action: (SheetPageContent.Item) -> Void = { [weak state] item in
guard let state else {
return
}
state.disposable.set(
(accountContext.engine.messages.reportAdMessage(opaqueId: opaqueId, option: item.option)
|> deliverOnMainQueue).start(next: { [weak state] result in
switch result {
case let .options(title, options):
state?.pushedOptions.append((item.title, title, options))
state?.updated(transition: .spring(duration: 0.45))
case .adsHidden:
complete(.hidden)
case .reported:
complete(.reported)
}
}, error: { error in
if case .premiumRequired = error {
complete(.premiumRequired)
}
})
)
}
var items: [AnyComponentWithIdentity<EnvironmentType>] = []
items.append(AnyComponentWithIdentity(id: items.count, component: AnyComponent(
SheetPageContent(
context: component.context,
title: nil,
subtitle: component.title,
items: component.options.map {
SheetPageContent.Item(title: $0.text, option: $0.option)
},
action: { item in
action(item)
},
pop: {
component.dismiss()
}
)
)))
for pushedOption in state.pushedOptions {
items.append(AnyComponentWithIdentity(id: items.count, component: AnyComponent(
SheetPageContent(
context: component.context,
title: pushedOption.title,
subtitle: pushedOption.subtitle,
items: pushedOption.options.map {
SheetPageContent.Item(title: $0.text, option: $0.option)
},
action: { item in
action(item)
},
pop: { [weak state] in
state?.pushedOptions.removeLast()
update(.spring(duration: 0.45))
}
)
)))
}
var contentSize = CGSize(width: context.availableSize.width, height: 0.0)
let navigation = navigation.update(
component: NavigationStackComponent(
items: items,
clipContent: false,
requestPop: { [weak state] in
state?.pushedOptions.removeLast()
update(.spring(duration: 0.45))
}
),
environment: { environment },
availableSize: CGSize(width: context.availableSize.width, height: context.availableSize.height),
transition: context.transition
)
context.add(navigation
.position(CGPoint(x: context.availableSize.width / 2.0, y: navigation.size.height / 2.0))
.clipsToBounds(true)
.cornerRadius(8.0)
)
contentSize.height += navigation.size.height
return contentSize
}
}
}
private final class SheetContainerComponent: CombinedComponent {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
let context: AccountContext
let opaqueId: Data
let title: String
let options: [ReportAdMessageResult.Option]
let openMore: () -> Void
let complete: (ReportResult) -> Void
init(
context: AccountContext,
opaqueId: Data,
title: String,
options: [ReportAdMessageResult.Option],
openMore: @escaping () -> Void,
complete: @escaping (ReportResult) -> Void
) {
self.context = context
self.opaqueId = opaqueId
self.title = title
self.options = options
self.openMore = openMore
self.complete = complete
}
static func ==(lhs: SheetContainerComponent, rhs: SheetContainerComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.opaqueId != rhs.opaqueId {
return false
}
if lhs.title != rhs.title {
return false
}
if lhs.options != rhs.options {
return false
}
return true
}
final class State: ComponentState {
var pts: Int = 0
}
func makeState() -> State {
return State()
}
static var body: Body {
let sheet = Child(SheetComponent<EnvironmentType>.self)
let animateOut = StoredActionSlot(Action<Void>.self)
let sheetExternalState = SheetComponent<EnvironmentType>.ExternalState()
return { context in
let environment = context.environment[EnvironmentType.self]
let state = context.state
let controller = environment.controller
let sheet = sheet.update(
component: SheetComponent<EnvironmentType>(
content: AnyComponent<EnvironmentType>(SheetContent(
context: context.component.context,
opaqueId: context.component.opaqueId,
title: context.component.title,
options: context.component.options,
pts: state.pts,
openMore: context.component.openMore,
complete: context.component.complete,
dismiss: {
animateOut.invoke(Action { _ in
if let controller = controller() {
controller.dismiss(completion: nil)
}
})
},
update: { [weak state] transition in
state?.pts += 1
state?.updated(transition: transition)
}
)),
backgroundColor: .color(environment.theme.list.modalBlocksBackgroundColor),
followContentSizeChanges: true,
externalState: sheetExternalState,
animateOut: animateOut
),
environment: {
environment
SheetComponentEnvironment(
metrics: environment.metrics,
deviceMetrics: environment.deviceMetrics,
isDisplaying: environment.value.isVisible,
isCentered: environment.metrics.widthClass == .regular,
hasInputHeight: !environment.inputHeight.isZero,
regularMetricsSize: CGSize(width: 430.0, height: 900.0),
dismiss: { animated in
if animated {
animateOut.invoke(Action { _ in
if let controller = controller() {
controller.dismiss(completion: nil)
}
})
} else {
if let controller = controller() {
controller.dismiss(completion: nil)
}
}
}
)
},
availableSize: context.availableSize,
transition: context.transition
)
context.add(sheet
.position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0))
)
if let controller = controller(), !controller.automaticallyControlPresentationContextLayout {
let layout = ContainerViewLayout(
size: context.availableSize,
metrics: environment.metrics,
deviceMetrics: environment.deviceMetrics,
intrinsicInsets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: max(environment.safeInsets.bottom, sheetExternalState.contentHeight), right: 0.0),
safeInsets: UIEdgeInsets(top: 0.0, left: environment.safeInsets.left, bottom: 0.0, right: environment.safeInsets.right),
additionalInsets: .zero,
statusBarHeight: environment.statusBarHeight,
inputHeight: nil,
inputHeightIsInteractivellyChanging: false,
inVoiceOver: false
)
controller.presentationContext.containerLayoutUpdated(layout, transition: context.transition.containedViewLayoutTransition)
}
return context.availableSize
}
}
}
public final class AdsReportScreen: ViewControllerComponentContainer {
private let context: AccountContext
public init(
context: AccountContext,
opaqueId: Data,
title: String,
options: [ReportAdMessageResult.Option],
forceDark: Bool = false,
completed: @escaping () -> Void
) {
self.context = context
var completeImpl: ((ReportResult) -> Void)?
super.init(
context: context,
component: SheetContainerComponent(
context: context,
opaqueId: opaqueId,
title: title,
options: options,
openMore: {},
complete: { hidden in
completeImpl?(hidden)
}
),
navigationBarAppearance: .none,
statusBarStyle: .ignore,
theme: forceDark ? .dark : .default
)
self.navigationPresentation = .flatModal
completeImpl = { [weak self] result in
guard let self else {
return
}
let navigationController = self.navigationController
self.dismissAnimated()
switch result {
case .reported, .hidden:
completed()
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let text: String
if case .reported = result {
text = presentationData.strings.ReportAd_Reported
} else {
text = presentationData.strings.ReportAd_Hidden
}
Queue.mainQueue().after(0.4, {
(navigationController?.viewControllers.last as? ViewController)?.present(UndoOverlayController(presentationData: presentationData, content: .actionSucceeded(title: nil, text: text, cancel: nil, destructive: false), elevatedLayout: false, action: { action in
if case .info = action, case .reported = result {
context.sharedContext.openExternalUrl(context: context, urlContext: .generic, url: presentationData.strings.ReportAd_Help_URL, forceExternal: true, presentationData: presentationData, navigationController: nil, dismissInput: {})
}
return true
}), in: .current)
})
case .premiumRequired:
var replaceImpl: ((ViewController) -> Void)?
let controller = context.sharedContext.makePremiumDemoController(context: context, subject: .noAds, forceDark: false, action: {
let controller = context.sharedContext.makePremiumIntroController(context: context, source: .ads, forceDark: false, dismissed: nil)
replaceImpl?(controller)
}, dismissed: nil)
replaceImpl = { [weak controller] c in
controller?.replace(with: c)
}
Queue.mainQueue().after(0.4, {
navigationController?.pushViewController(controller, animated: true)
})
}
}
}
required public init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public override func viewDidLoad() {
super.viewDidLoad()
self.view.disablesInteractiveModalDismiss = true
}
public func dismissAnimated() {
if let view = self.node.hostView.findTaggedView(tag: SheetComponent<ViewControllerComponentContainer.Environment>.View.Tag()) as? SheetComponent<ViewControllerComponentContainer.Environment>.View {
view.dismissAnimated()
}
}
}
@@ -0,0 +1,28 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "AlertCheckComponent",
module_name = "AlertCheckComponent",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/AsyncDisplayKit",
"//submodules/Display",
"//submodules/ComponentFlow",
"//submodules/TelegramPresentationData",
"//submodules/SSignalKit/SwiftSignalKit",
"//submodules/TextFormat",
"//submodules/Markdown",
"//submodules/TelegramUI/Components/AlertComponent",
"//submodules/TelegramUI/Components/CheckComponent",
"//submodules/Components/MultilineTextComponent",
"//submodules/TelegramUI/Components/PlainButtonComponent",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,186 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import ComponentFlow
import SwiftSignalKit
import TelegramPresentationData
import AlertComponent
import PlainButtonComponent
import MultilineTextComponent
import CheckComponent
import TextFormat
import Markdown
public final class AlertCheckComponent: Component {
public typealias EnvironmentType = AlertComponentEnvironment
public class ExternalState {
public fileprivate(set) var value: Bool
fileprivate var valuePromise = Promise<Bool>()
public var valueSignal: Signal<Bool, NoError>
public init() {
self.value = false
self.valueSignal = self.valuePromise.get()
}
}
let title: String
let initialValue: Bool
let externalState: ExternalState
let linkAction: (() -> Void)?
public init(
title: String,
initialValue: Bool,
externalState: ExternalState,
linkAction: (() -> Void)? = nil
) {
self.title = title
self.initialValue = initialValue
self.externalState = externalState
self.linkAction = linkAction
}
public static func ==(lhs: AlertCheckComponent, rhs: AlertCheckComponent) -> Bool {
return true
}
public final class View: UIView {
private let button = ComponentView<Empty>()
private var component: AlertCheckComponent?
private weak var state: EmptyComponentState?
private var isUpdating = false
private var valuePromise = ValuePromise<Bool>(false)
public override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
func findTextView(view: UIView?) -> ImmediateTextView? {
if let view {
if let view = view as? ImmediateTextView {
return view
}
for view in view.subviews {
if let result = findTextView(view: view) {
return result
}
}
}
return nil
}
let result = super.hitTest(point, with: event)
if let textView = findTextView(view: result) {
if let (_, attributes) = textView.attributesAtPoint(self.convert(point, to: textView)) {
if attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] != nil {
return textView
}
}
}
return result
}
func update(component: AlertCheckComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<AlertComponentEnvironment>, transition: ComponentTransition) -> CGSize {
self.isUpdating = true
defer {
self.isUpdating = false
}
if self.component == nil {
component.externalState.value = component.initialValue
component.externalState.valuePromise.set(self.valuePromise.get())
}
self.component = component
self.state = state
let environment = environment[AlertComponentEnvironment.self]
let checkTheme = CheckComponent.Theme(
backgroundColor: environment.theme.list.itemCheckColors.fillColor,
strokeColor: environment.theme.list.itemCheckColors.foregroundColor,
borderColor: environment.theme.actionSheet.primaryTextColor.withMultipliedAlpha(0.15),
overlayBorder: false,
hasInset: false,
hasShadow: false
)
let textFont = Font.regular(15.0)
let boldTextFont = Font.semibold(15.0)
let textColor = environment.theme.actionSheet.primaryTextColor
let linkColor = environment.theme.actionSheet.controlAccentColor
let markdownAttributes = MarkdownAttributes(
body: MarkdownAttributeSet(font: textFont, textColor: textColor),
bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor),
link: MarkdownAttributeSet(font: textFont, textColor: linkColor),
linkAttribute: { contents in
return (TelegramTextAttributes.URL, contents)
}
)
let buttonSize = self.button.update(
transition: transition,
component: AnyComponent(PlainButtonComponent(
content: AnyComponent(HStack([
AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(CheckComponent(
theme: checkTheme,
size: CGSize(width: 18.0, height: 18.0),
selected: component.externalState.value
))),
AnyComponentWithIdentity(id: AnyHashable(1), component: AnyComponent(MultilineTextComponent(
text: .markdown(text: component.title, attributes: markdownAttributes),
maximumNumberOfLines: 2,
highlightColor: linkColor.withAlphaComponent(0.1),
highlightAction: { attributes in
if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] {
return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)
} else {
return nil
}
},
tapAction: { attributes, _ in
if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String {
component.linkAction?()
}
}
)))
], spacing: 10.0)),
effectAlignment: .center,
action: { [weak self] in
guard let self, let component = self.component else {
return
}
component.externalState.value = !component.externalState.value
self.valuePromise.set(component.externalState.value)
if !self.isUpdating {
self.state?.updated(transition: .spring(duration: 0.4))
}
},
animateAlpha: false,
animateScale: false
)),
environment: {
},
containerSize: CGSize(width: availableSize.width + 20.0, height: 1000.0)
)
let buttonFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - buttonSize.width) / 2.0), y: 7.0), size: buttonSize)
if let buttonView = self.button.view {
if buttonView.superview == nil {
self.addSubview(buttonView)
}
transition.setFrame(view: buttonView, frame: buttonFrame)
}
return CGSize(width: availableSize.width, height: buttonSize.height + 7.0)
}
}
public func makeView() -> View {
return View(frame: CGRect())
}
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<AlertComponentEnvironment>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
@@ -0,0 +1,28 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "AlertInputFieldComponent",
module_name = "AlertInputFieldComponent",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/AsyncDisplayKit",
"//submodules/Display",
"//submodules/AccountContext",
"//submodules/ComponentFlow",
"//submodules/SSignalKit/SwiftSignalKit",
"//submodules/TelegramPresentationData",
"//submodules/TextFormat",
"//submodules/Components/MultilineTextComponent",
"//submodules/Components/BundleIconComponent",
"//submodules/TelegramUI/Components/AlertComponent",
"//submodules/TelegramUI/Components/PlainButtonComponent",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,353 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import ComponentFlow
import SwiftSignalKit
import TelegramCore
import TelegramPresentationData
import AlertComponent
import MultilineTextComponent
import AccountContext
import TextFormat
import PlainButtonComponent
import BundleIconComponent
public final class AlertInputFieldComponent: Component {
public typealias EnvironmentType = AlertComponentEnvironment
public class ExternalState {
public fileprivate(set) var value: String = ""
public fileprivate(set) var animateError: () -> Void = {}
public fileprivate(set) var activateInput: () -> Void = {}
fileprivate let valuePromise = ValuePromise<String>("")
public var valueSignal: Signal<String, NoError> {
return self.valuePromise.get()
}
public init() {
}
}
let context: AccountContext
let initialValue: String?
let placeholder: String
let characterLimit: Int?
let hasClearButton: Bool
let isSecureTextEntry: Bool
let returnKeyType: UIReturnKeyType
let keyboardType: UIKeyboardType
let autocapitalizationType: UITextAutocapitalizationType
let autocorrectionType: UITextAutocorrectionType
let isInitiallyFocused: Bool
let externalState: ExternalState
let shouldChangeText: ((String) -> Bool)?
let returnKeyAction: (() -> Void)?
public init(
context: AccountContext,
initialValue: String? = nil,
placeholder: String,
characterLimit: Int? = nil,
hasClearButton: Bool = false,
isSecureTextEntry: Bool = false,
returnKeyType: UIReturnKeyType = .done,
keyboardType: UIKeyboardType = .default,
autocapitalizationType: UITextAutocapitalizationType = .sentences,
autocorrectionType: UITextAutocorrectionType = .default,
isInitiallyFocused: Bool = false,
externalState: ExternalState,
shouldChangeText: ((String) -> Bool)? = nil,
returnKeyAction: (() -> Void)? = nil
) {
self.context = context
self.initialValue = initialValue
self.placeholder = placeholder
self.characterLimit = characterLimit
self.hasClearButton = hasClearButton
self.isSecureTextEntry = isSecureTextEntry
self.returnKeyType = returnKeyType
self.keyboardType = keyboardType
self.autocapitalizationType = autocapitalizationType
self.autocorrectionType = autocorrectionType
self.isInitiallyFocused = isInitiallyFocused
self.externalState = externalState
self.shouldChangeText = shouldChangeText
self.returnKeyAction = returnKeyAction
}
public static func ==(lhs: AlertInputFieldComponent, rhs: AlertInputFieldComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.initialValue != rhs.initialValue {
return false
}
if lhs.placeholder != rhs.placeholder {
return false
}
if lhs.characterLimit != rhs.characterLimit {
return false
}
if lhs.hasClearButton != rhs.hasClearButton {
return false
}
if lhs.isSecureTextEntry != rhs.isSecureTextEntry {
return false
}
if lhs.returnKeyType != rhs.returnKeyType {
return false
}
if lhs.keyboardType != rhs.keyboardType {
return false
}
if lhs.autocapitalizationType != rhs.autocapitalizationType {
return false
}
if lhs.autocorrectionType != rhs.autocorrectionType {
return false
}
if lhs.isInitiallyFocused != rhs.isInitiallyFocused {
return false
}
return true
}
private final class TextField: UITextField {
var sideInset: CGFloat = 0.0
override func textRect(forBounds bounds: CGRect) -> CGRect {
return CGRect(origin: CGPoint(x: self.sideInset, y: 0.0), size: CGSize(width: bounds.width - self.sideInset * 2.0, height: bounds.height))
}
override func editingRect(forBounds bounds: CGRect) -> CGRect {
return CGRect(origin: CGPoint(x: self.sideInset, y: 0.0), size: CGSize(width: bounds.width - self.sideInset * 2.0, height: bounds.height))
}
}
public final class View: UIView, UITextFieldDelegate {
private let background = ComponentView<Empty>()
private let textField = TextField()
private let placeholder = ComponentView<Empty>()
private let clearButton = ComponentView<Empty>()
private var component: AlertInputFieldComponent?
private weak var state: EmptyComponentState?
private var isUpdating = false
var currentText: String {
return self.textField.text ?? ""
}
private var clearOnce: Bool = false
func activateInput() {
self.textField.becomeFirstResponder()
}
func animateError() {
if let component = self.component, component.isInitiallyFocused {
self.clearOnce = true
}
self.textField.layer.addShakeAnimation()
HapticFeedback().error()
}
public func textFieldShouldReturn(_ textField: UITextField) -> Bool {
self.component?.returnKeyAction?()
return false
}
@objc private func textDidChange() {
if !self.isUpdating {
self.state?.updated(transition: .immediate)
}
}
public func textFieldDidBeginEditing(_ textField: UITextField) {
self.clearButton.view?.isHidden = self.currentText.isEmpty
}
public func textFieldDidEndEditing(_ textField: UITextField) {
self.clearButton.view?.isHidden = true
}
public func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
guard let component = self.component else {
return true
}
if self.clearOnce {
self.clearOnce = false
if range.length > string.count {
textField.text = ""
return false
}
}
let updatedText = ((textField.text ?? "") as NSString).replacingCharacters(in: range, with: string)
if let shouldChangeText = component.shouldChangeText {
return shouldChangeText(updatedText)
}
return true
}
public func setText(text: String) {
self.textField.text = text
if !self.isUpdating {
self.state?.updated(transition: .immediate, isLocal: true)
}
}
func update(component: AlertInputFieldComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<AlertComponentEnvironment>, transition: ComponentTransition) -> CGSize {
self.isUpdating = true
defer {
self.isUpdating = false
}
var resetText: String?
if self.component == nil {
resetText = component.initialValue
component.externalState.animateError = { [weak self] in
self?.animateError()
}
component.externalState.activateInput = { [weak self] in
self?.activateInput()
}
}
let isFirstTime = self.component == nil
self.component = component
self.state = state
let environment = environment[AlertComponentEnvironment.self]
let topInset: CGFloat = 15.0
if self.textField.superview == nil {
self.addSubview(self.textField)
self.textField.delegate = self
self.textField.addTarget(self, action: #selector(self.textDidChange), for: .editingChanged)
}
if self.textField.autocapitalizationType != component.autocapitalizationType {
self.textField.autocapitalizationType = component.autocapitalizationType
}
if self.textField.autocorrectionType != component.autocorrectionType {
self.textField.autocorrectionType = component.autocorrectionType
}
if self.textField.isSecureTextEntry != component.isSecureTextEntry {
self.textField.isSecureTextEntry = component.isSecureTextEntry
}
if self.textField.returnKeyType != component.returnKeyType {
self.textField.returnKeyType = component.returnKeyType
}
self.textField.keyboardAppearance = environment.theme.overallDarkAppearance ? .dark : .light
if let resetText {
self.textField.text = resetText
}
self.textField.font = Font.regular(17.0)
self.textField.textColor = environment.theme.actionSheet.primaryTextColor
self.textField.tintColor = environment.theme.actionSheet.controlAccentColor
self.textField.sideInset = 16.0
let backgroundPadding: CGFloat = 14.0
let size = CGSize(width: availableSize.width, height: 50.0)
let backgroundSize = self.background.update(
transition: transition,
component: AnyComponent(
FilledRoundedRectangleComponent(color: environment.theme.actionSheet.primaryTextColor.withMultipliedAlpha(0.1), cornerRadius: .value(25.0), smoothCorners: false)
),
environment: {},
containerSize: CGSize(width: size.width + backgroundPadding * 2.0, height: size.height)
)
let backgroundFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - backgroundSize.width) / 2.0), y: topInset ), size: backgroundSize)
if let backgroundView = self.background.view {
if backgroundView.superview == nil {
self.addSubview(backgroundView)
}
transition.setFrame(view: backgroundView, frame: backgroundFrame)
}
let textFieldSize = CGSize(width: availableSize.width - 24.0, height: 50.0)
let textFieldFrame = CGRect(origin: CGPoint(x: -12.0, y: topInset), size: textFieldSize)
transition.setFrame(view: self.textField, frame: textFieldFrame)
let placeholderSize = self.placeholder.update(
transition: .immediate,
component: AnyComponent(
MultilineTextComponent(text: .plain(NSAttributedString(
string: component.placeholder,
font: Font.regular(17.0),
textColor: environment.theme.actionSheet.primaryTextColor.withMultipliedAlpha(0.4)
)))
),
environment: {},
containerSize: CGSize(width: size.width, height: 50.0)
)
let placeholderFrame = CGRect(origin: CGPoint(x: 4.0, y: floorToScreenPixels(textFieldFrame.midY - placeholderSize.height / 2.0)), size: placeholderSize)
if let placeholderView = self.placeholder.view {
if placeholderView.superview == nil {
placeholderView.isUserInteractionEnabled = false
self.addSubview(placeholderView)
}
placeholderView.frame = placeholderFrame
placeholderView.isHidden = !self.currentText.isEmpty
}
if component.hasClearButton {
let clearButtonSize = self.clearButton.update(
transition: transition,
component: AnyComponent(PlainButtonComponent(
content: AnyComponent(BundleIconComponent(
name: "Components/Search Bar/Clear",
tintColor: environment.theme.list.itemPrimaryTextColor.withMultipliedAlpha(0.4)
)),
effectAlignment: .center,
minSize: CGSize(width: 44.0, height: 44.0),
action: { [weak self] in
guard let self else {
return
}
self.setText(text: "")
},
animateAlpha: false,
animateScale: true
)),
environment: {},
containerSize: CGSize(width: 44.0, height: 44.0)
)
if let clearButtonView = self.clearButton.view {
if clearButtonView.superview == nil {
self.addSubview(clearButtonView)
}
transition.setFrame(view: clearButtonView, frame: CGRect(origin: CGPoint(x: availableSize.width - clearButtonSize.width + 11.0, y: topInset + floor((size.height - clearButtonSize.height) * 0.5)), size: clearButtonSize))
clearButtonView.isHidden = self.currentText.isEmpty || !self.textField.isFirstResponder
}
} else if let clearButtonView = self.clearButton.view, clearButtonView.superview != nil {
clearButtonView.removeFromSuperview()
}
if isFirstTime && component.isInitiallyFocused {
self.activateInput()
}
component.externalState.value = self.currentText
component.externalState.valuePromise.set(self.currentText)
return CGSize(width: availableSize.width, height: size.height + topInset)
}
}
public func makeView() -> View {
return View(frame: CGRect())
}
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<AlertComponentEnvironment>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
@@ -0,0 +1,27 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "AlertMultilineInputFieldComponent",
module_name = "AlertMultilineInputFieldComponent",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/AsyncDisplayKit",
"//submodules/Display",
"//submodules/AccountContext",
"//submodules/ComponentFlow",
"//submodules/SSignalKit/SwiftSignalKit",
"//submodules/TelegramPresentationData",
"//submodules/TextFormat",
"//submodules/Components/MultilineTextComponent",
"//submodules/TelegramUI/Components/AlertComponent",
"//submodules/TelegramUI/Components/TextFieldComponent",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,361 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import ComponentFlow
import SwiftSignalKit
import TelegramCore
import TelegramPresentationData
import AlertComponent
import TextFieldComponent
import MultilineTextComponent
import AccountContext
import TextFormat
public final class AlertMultilineInputFieldComponent: Component {
public typealias EnvironmentType = AlertComponentEnvironment
public class ExternalState {
public fileprivate(set) var value: NSAttributedString = NSAttributedString()
public fileprivate(set) var animateError: () -> Void = {}
public fileprivate(set) var activateInput: () -> Void = {}
fileprivate let valuePromise = ValuePromise<NSAttributedString>(NSAttributedString())
public var valueSignal: Signal<NSAttributedString, NoError> {
return self.valuePromise.get()
}
public var textAndEntities: (String, [MessageTextEntity]) {
let text = self.value.string
let entities = generateChatInputTextEntities(self.value)
return (text, entities)
}
public init() {
}
}
public enum FormatMenuAvailability: Equatable {
public enum Action: CaseIterable {
case bold
case italic
case monospace
case link
case strikethrough
case underline
case spoiler
case quote
case code
public static var all: [Action] = [
.bold,
.italic,
.monospace,
.link,
.strikethrough,
.underline,
.spoiler,
.quote,
.code
]
var textFieldValue: TextFieldComponent.FormatMenuAvailability.Action {
switch self {
case .bold:
return .bold
case .italic:
return .italic
case .monospace:
return .monospace
case .link:
return .link
case .strikethrough:
return .strikethrough
case .underline:
return .underline
case .spoiler:
return .spoiler
case .quote:
return .quote
case .code:
return .code
}
}
}
case available([Action])
case none
var textFieldValue: TextFieldComponent.FormatMenuAvailability {
switch self {
case let .available(actions):
return .available(actions.map { $0.textFieldValue })
case .none:
return .none
}
}
}
public enum EmptyLineHandling {
case allowed
case oneConsecutive
case notAllowed
var textFieldValue: TextFieldComponent.EmptyLineHandling {
switch self {
case .allowed:
return .allowed
case .oneConsecutive:
return .oneConsecutive
case .notAllowed:
return .notAllowed
}
}
}
let context: AccountContext
let initialValue: NSAttributedString?
let placeholder: String
let prefix: NSAttributedString?
let characterLimit: Int?
let returnKeyType: UIReturnKeyType
let keyboardType: UIKeyboardType
let autocapitalizationType: UITextAutocapitalizationType
let autocorrectionType: UITextAutocorrectionType
let formatMenuAvailability: FormatMenuAvailability
let emptyLineHandling: EmptyLineHandling
let isInitiallyFocused: Bool
let externalState: ExternalState
let present: (ViewController) -> Void
let returnKeyAction: (() -> Void)?
public init(
context: AccountContext,
initialValue: NSAttributedString? = nil,
placeholder: String,
prefix: NSAttributedString? = nil,
characterLimit: Int? = nil,
returnKeyType: UIReturnKeyType = .default,
keyboardType: UIKeyboardType = .default,
autocapitalizationType: UITextAutocapitalizationType = .sentences,
autocorrectionType: UITextAutocorrectionType = .default,
formatMenuAvailability: FormatMenuAvailability = .none,
emptyLineHandling: EmptyLineHandling = .allowed,
isInitiallyFocused: Bool = false,
externalState: ExternalState,
present: @escaping (ViewController) -> Void = { _ in },
returnKeyAction: (() -> Void)? = nil
) {
self.context = context
self.initialValue = initialValue
self.placeholder = placeholder
self.prefix = prefix
self.characterLimit = characterLimit
self.returnKeyType = returnKeyType
self.keyboardType = keyboardType
self.autocapitalizationType = autocapitalizationType
self.autocorrectionType = autocorrectionType
self.formatMenuAvailability = formatMenuAvailability
self.emptyLineHandling = emptyLineHandling
self.isInitiallyFocused = isInitiallyFocused
self.externalState = externalState
self.present = present
self.returnKeyAction = returnKeyAction
}
public static func ==(lhs: AlertMultilineInputFieldComponent, rhs: AlertMultilineInputFieldComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.initialValue != rhs.initialValue {
return false
}
if lhs.placeholder != rhs.placeholder {
return false
}
if lhs.prefix != rhs.prefix {
return false
}
if lhs.returnKeyType != rhs.returnKeyType {
return false
}
if lhs.characterLimit != rhs.characterLimit {
return false
}
if lhs.keyboardType != rhs.keyboardType {
return false
}
if lhs.autocapitalizationType != rhs.autocapitalizationType {
return false
}
if lhs.autocorrectionType != rhs.autocorrectionType {
return false
}
if lhs.formatMenuAvailability != rhs.formatMenuAvailability {
return false
}
if lhs.emptyLineHandling != rhs.emptyLineHandling {
return false
}
if lhs.isInitiallyFocused != rhs.isInitiallyFocused {
return false
}
return true
}
public final class View: UIView {
private let background = ComponentView<Empty>()
private let textField = ComponentView<Empty>()
private let textFieldExternalState = TextFieldComponent.ExternalState()
private let placeholder = ComponentView<Empty>()
private var component: AlertMultilineInputFieldComponent?
private weak var state: EmptyComponentState?
func activateInput() {
if let textFieldView = self.textField.view as? TextFieldComponent.View {
textFieldView.activateInput()
}
}
func animateError() {
if let textFieldView = self.textField.view {
textFieldView.layer.addShakeAnimation()
}
HapticFeedback().error()
}
func update(component: AlertMultilineInputFieldComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<AlertComponentEnvironment>, transition: ComponentTransition) -> CGSize {
var resetText: NSAttributedString?
if self.component == nil {
resetText = component.initialValue
component.externalState.animateError = { [weak self] in
self?.animateError()
}
component.externalState.activateInput = { [weak self] in
self?.activateInput()
}
}
let isFirstTime = self.component == nil
self.component = component
self.state = state
let environment = environment[AlertComponentEnvironment.self]
let topInset: CGFloat = 15.0
let horizontalInset: CGFloat = 4.0
let verticalInset: CGFloat = 11.0 - UIScreenPixel
let textFieldSize = self.textField.update(
transition: transition,
component: AnyComponent(TextFieldComponent(
context: component.context,
theme: environment.theme,
strings: environment.strings,
externalState: self.textFieldExternalState,
fontSize: 17.0,
textColor: environment.theme.actionSheet.primaryTextColor,
accentColor: environment.theme.actionSheet.controlAccentColor,
insets: UIEdgeInsets(top: 4.0, left: 0.0, bottom: 4.0, right: 0.0),
hideKeyboard: false,
customInputView: nil,
resetText: resetText,
isOneLineWhenUnfocused: false,
characterLimit: component.characterLimit,
emptyLineHandling: component.emptyLineHandling.textFieldValue,
formatMenuAvailability: component.formatMenuAvailability.textFieldValue,
returnKeyType: component.returnKeyType,
keyboardType: component.keyboardType,
autocapitalizationType: component.autocapitalizationType,
autocorrectionType: component.autocorrectionType,
lockedFormatAction: {
},
present: { [weak self] c in
guard let self, let component = self.component else {
return
}
component.present(c)
},
paste: { _ in
},
returnKeyAction: { [weak self] in
guard let self, let component = self.component else {
return
}
component.returnKeyAction?()
},
backspaceKeyAction: {
}
)),
environment: {},
containerSize: CGSize(width: availableSize.width + horizontalInset * 2.0, height: availableSize.height)
)
component.externalState.value = self.textFieldExternalState.text
component.externalState.valuePromise.set(component.externalState.value)
let backgroundPadding: CGFloat = 14.0
let size = CGSize(width: availableSize.width, height: max(50.0, floor(textFieldSize.height + verticalInset * 2.0)))
let backgroundSize = self.background.update(
transition: transition,
component: AnyComponent(
FilledRoundedRectangleComponent(color: environment.theme.actionSheet.primaryTextColor.withMultipliedAlpha(0.1), cornerRadius: .value(25.0), smoothCorners: false)
),
environment: {},
containerSize: CGSize(width: size.width + backgroundPadding * 2.0, height: size.height)
)
let backgroundFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - backgroundSize.width) / 2.0), y: topInset ), size: backgroundSize)
if let backgroundView = self.background.view {
if backgroundView.superview == nil {
self.addSubview(backgroundView)
}
transition.setFrame(view: backgroundView, frame: backgroundFrame)
}
let textFieldFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - textFieldSize.width) / 2.0), y: topInset + 11.0 - UIScreenPixel), size: textFieldSize)
if let textFieldView = self.textField.view {
if textFieldView.superview == nil {
self.addSubview(textFieldView)
self.textField.parentState = state
}
transition.setFrame(view: textFieldView, frame: textFieldFrame)
}
let placeholderSize = self.placeholder.update(
transition: .immediate,
component: AnyComponent(
MultilineTextComponent(text: .plain(NSAttributedString(
string: component.placeholder,
font: Font.regular(17.0),
textColor: environment.theme.actionSheet.primaryTextColor.withMultipliedAlpha(0.4)
)))
),
environment: {},
containerSize: CGSize(width: size.width, height: 50.0)
)
let placeholderFrame = CGRect(origin: CGPoint(x: 4.0, y: floorToScreenPixels(textFieldFrame.midY - placeholderSize.height / 2.0)), size: placeholderSize)
if let placeholderView = self.placeholder.view {
if placeholderView.superview == nil {
placeholderView.isUserInteractionEnabled = false
self.addSubview(placeholderView)
}
placeholderView.frame = placeholderFrame
placeholderView.isHidden = self.textFieldExternalState.hasText
}
if isFirstTime && component.isInitiallyFocused {
self.activateInput()
}
return CGSize(width: availableSize.width, height: size.height + topInset)
}
}
public func makeView() -> View {
return View(frame: CGRect())
}
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<AlertComponentEnvironment>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
@@ -0,0 +1,23 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "AlertTableComponent",
module_name = "AlertTableComponent",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/AsyncDisplayKit",
"//submodules/Display",
"//submodules/ComponentFlow",
"//submodules/TelegramPresentationData",
"//submodules/TelegramUI/Components/AlertComponent",
"//submodules/TelegramUI/Components/Gifts/TableComponent",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,70 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import ComponentFlow
import TelegramPresentationData
import AlertComponent
import TableComponent
public final class AlertTableComponent: Component {
public typealias EnvironmentType = AlertComponentEnvironment
let items: [TableComponent.Item]
public init(
items: [TableComponent.Item]
) {
self.items = items
}
public static func ==(lhs: AlertTableComponent, rhs: AlertTableComponent) -> Bool {
if lhs.items != rhs.items {
return false
}
return true
}
public final class View: UIView {
private let table = ComponentView<Empty>()
private var component: AlertTableComponent?
private weak var state: EmptyComponentState?
func update(component: AlertTableComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<AlertComponentEnvironment>, transition: ComponentTransition) -> CGSize {
self.component = component
self.state = state
let environment = environment[AlertComponentEnvironment.self]
let tableSize = self.table.update(
transition: transition,
component: AnyComponent(
TableComponent(
theme: environment.theme,
items: component.items,
semiTransparent: true
)
),
environment: {},
containerSize: CGSize(width: availableSize.width + 20.0, height: availableSize.height)
)
let tableFrame = CGRect(origin: CGPoint(x: -10.0, y: 5.0), size: tableSize)
if let tableView = self.table.view {
if tableView.superview == nil {
self.addSubview(tableView)
}
transition.setFrame(view: tableView, frame: tableFrame)
}
return CGSize(width: availableSize.width, height: tableSize.height + 10.0)
}
}
public func makeView() -> View {
return View(frame: CGRect())
}
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<AlertComponentEnvironment>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
@@ -0,0 +1,23 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "AlertTransferHeaderComponent",
module_name = "AlertTransferHeaderComponent",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/AsyncDisplayKit",
"//submodules/Display",
"//submodules/ComponentFlow",
"//submodules/TelegramPresentationData",
"//submodules/Components/BundleIconComponent",
"//submodules/TelegramUI/Components/AlertComponent",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,126 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import ComponentFlow
import TelegramPresentationData
import AlertComponent
import BundleIconComponent
public final class AlertTransferHeaderComponent: Component {
public typealias EnvironmentType = AlertComponentEnvironment
public enum IconType {
case transfer
case take
}
let fromComponent: AnyComponentWithIdentity<Empty>
let toComponent: AnyComponentWithIdentity<Empty>
let type: IconType
public init(
fromComponent: AnyComponentWithIdentity<Empty>,
toComponent: AnyComponentWithIdentity<Empty>,
type: IconType
) {
self.fromComponent = fromComponent
self.toComponent = toComponent
self.type = type
}
public static func ==(lhs: AlertTransferHeaderComponent, rhs: AlertTransferHeaderComponent) -> Bool {
if lhs.fromComponent != rhs.fromComponent {
return false
}
if lhs.toComponent != rhs.toComponent {
return false
}
if lhs.type != rhs.type {
return false
}
return true
}
public final class View: UIView {
private let from = ComponentView<Empty>()
private let to = ComponentView<Empty>()
private let arrow = ComponentView<Empty>()
private var component: AlertTransferHeaderComponent?
private weak var state: EmptyComponentState?
func update(component: AlertTransferHeaderComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<AlertComponentEnvironment>, transition: ComponentTransition) -> CGSize {
self.component = component
self.state = state
let environment = environment[AlertComponentEnvironment.self]
let size: CGSize
let iconName: String
switch component.type {
case .transfer:
iconName = "Peer Info/AlertArrow"
size = CGSize(width: 148.0, height: 60.0)
case .take:
iconName = "Media Editor/CutoutUndo"
size = CGSize(width: 154.0, height: 60.0)
}
let sideInset = floorToScreenPixels((availableSize.width - size.width) / 2.0)
let fromSize = self.from.update(
transition: transition,
component: component.fromComponent.component,
environment: {},
containerSize: CGSize(width: 60.0, height: 60.0)
)
let fromFrame = CGRect(origin: CGPoint(x: sideInset, y: 0.0), size: fromSize)
if let fromView = self.from.view {
if fromView.superview == nil {
self.addSubview(fromView)
}
transition.setFrame(view: fromView, frame: fromFrame)
}
let arrowSize = self.arrow.update(
transition: transition,
component: AnyComponent(
BundleIconComponent(name: iconName, tintColor: environment.theme.actionSheet.primaryTextColor.withMultipliedAlpha(0.2))
),
environment: {},
containerSize: availableSize
)
let arrowFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - arrowSize.width) / 2.0), y: floorToScreenPixels((size.height - arrowSize.height) / 2.0)), size: arrowSize)
if let arrowView = self.arrow.view {
if arrowView.superview == nil {
self.addSubview(arrowView)
}
transition.setFrame(view: arrowView, frame: arrowFrame)
}
let toSize = self.to.update(
transition: transition,
component: component.toComponent.component,
environment: {},
containerSize: CGSize(width: 60.0, height: 60.0)
)
let toFrame = CGRect(origin: CGPoint(x: availableSize.width - toSize.width - sideInset, y: 0.0), size: toSize)
if let toView = self.to.view {
if toView.superview == nil {
self.addSubview(toView)
}
transition.setFrame(view: toView, frame: toFrame)
}
return CGSize(width: availableSize.width, height: size.height + 11.0)
}
}
public func makeView() -> View {
return View(frame: CGRect())
}
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<AlertComponentEnvironment>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
@@ -0,0 +1,32 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "AlertComponent",
module_name = "AlertComponent",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/AsyncDisplayKit",
"//submodules/Display",
"//submodules/ComponentFlow",
"//submodules/SSignalKit/SwiftSignalKit",
"//submodules/TelegramPresentationData",
"//submodules/TelegramCore",
"//submodules/AccountContext",
"//submodules/Markdown",
"//submodules/TextFormat",
"//submodules/Components/ComponentDisplayAdapters",
"//submodules/Components/ViewControllerComponent",
"//submodules/Components/MultilineTextComponent",
"//submodules/Components/MultilineTextWithEntitiesComponent",
"//submodules/Components/ActivityIndicatorComponent",
"//submodules/TelegramUI/Components/GlassBackgroundComponent",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,228 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import ComponentFlow
import SwiftSignalKit
import AccountContext
import TelegramPresentationData
import MultilineTextComponent
import GlassBackgroundComponent
import ActivityIndicatorComponent
private let titleFont = Font.medium(17.0)
private let boldTitleFont = Font.semibold(17.0)
final class AlertActionComponent: Component {
typealias EnvironmentType = AlertComponentEnvironment
static let actionHeight: CGFloat = 48.0
struct Theme: Equatable {
enum Font {
case regular
case bold
}
let background: UIColor
let foreground: UIColor
let secondary: UIColor
let font: Font
}
let theme: Theme
let title: String
let isHighlighted: Bool
let isEnabled: Signal<Bool, NoError>
let progress: Signal<Bool, NoError>
init(
theme: Theme,
title: String,
isHighlighted: Bool,
isEnabled: Signal<Bool, NoError>,
progress: Signal<Bool, NoError>
) {
self.theme = theme
self.title = title
self.isHighlighted = isHighlighted
self.isEnabled = isEnabled
self.progress = progress
}
static func ==(lhs: AlertActionComponent, rhs: AlertActionComponent) -> Bool {
if lhs.theme != rhs.theme {
return false
}
if lhs.title != rhs.title {
return false
}
if lhs.isHighlighted != rhs.isHighlighted {
return false
}
return true
}
final class View: UIView {
private let backgroundView = UIView()
private let title = ComponentView<Empty>()
private var activity: ComponentView<Empty>?
private var component: AlertActionComponent?
private weak var state: EmptyComponentState?
private var isEnabledDisposable: Disposable?
private var isEnabled = true
private var progressDisposable: Disposable?
private var hasProgress = false
private var isUpdating = false
override init(frame: CGRect) {
super.init(frame: frame)
self.backgroundView.clipsToBounds = true
self.addSubview(self.backgroundView)
}
required init?(coder: NSCoder) {
preconditionFailure()
}
deinit {
self.isEnabledDisposable?.dispose()
self.progressDisposable?.dispose()
}
func update(component: AlertActionComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<AlertComponentEnvironment>, transition: ComponentTransition) -> CGSize {
self.isUpdating = true
defer {
self.isUpdating = false
}
if self.component == nil {
self.isEnabledDisposable = (component.isEnabled
|> deliverOnMainQueue).start(next: { [weak self] isEnabled in
guard let self else {
return
}
self.isEnabled = isEnabled
if !self.isUpdating {
self.state?.updated(transition: .easeInOut(duration: 0.25))
}
})
self.progressDisposable = (component.progress
|> deliverOnMainQueue).start(next: { [weak self] hasProgress in
guard let self else {
return
}
self.hasProgress = hasProgress
if !self.isUpdating {
self.state?.updated(transition: .easeInOut(duration: 0.25))
}
})
}
self.component = component
self.state = state
let attributedString = NSMutableAttributedString(string: component.title, font: component.theme.font == .bold ? boldTitleFont : titleFont, textColor: .white, paragraphAlignment: .center)
if let range = attributedString.string.range(of: "$") {
attributedString.addAttribute(.attachment, value: UIImage(bundleImageName: "Item List/PremiumIcon")!, range: NSRange(range, in: attributedString.string))
attributedString.addAttribute(.foregroundColor, value: UIColor.white, range: NSRange(range, in: attributedString.string))
attributedString.addAttribute(.baselineOffset, value: 2.0, range: NSRange(range, in: attributedString.string))
}
let titlePadding: CGFloat = 16.0
let titleSize = self.title.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(
text: .plain(attributedString),
horizontalAlignment: .center,
maximumNumberOfLines: 1,
tintColor: component.theme.foreground
)),
environment: {},
containerSize: CGSize(width: availableSize.width - titlePadding * 2.0, height: availableSize.height)
)
if let titleView = self.title.view {
if titleView.superview == nil {
self.addSubview(titleView)
}
titleView.bounds = CGRect(origin: .zero, size: titleSize)
transition.setAlpha(view: titleView, alpha: self.hasProgress ? 0.0 : 1.0)
}
if self.hasProgress {
let activity: ComponentView<Empty>
if let current = self.activity {
activity = current
} else {
activity = ComponentView()
self.activity = activity
}
let activitySize = CGSize(width: 18.0, height: 18.0)
let _ = activity.update(
transition: transition,
component: AnyComponent(ActivityIndicatorComponent(color: component.theme.secondary)),
environment: {},
containerSize: activitySize
)
if let activityView = activity.view {
activityView.bounds = CGRect(origin: .zero, size: activitySize)
}
} else if let activity = self.activity {
self.activity = nil
if let activityView = activity.view {
transition.setAlpha(view: activityView, alpha: 0.0, completion: { _ in
activityView.removeFromSuperview()
})
}
}
let buttonAlpha: CGFloat
if self.isEnabled {
buttonAlpha = component.isHighlighted ? 0.35 : 1.0
} else {
buttonAlpha = 0.2
}
transition.setBackgroundColor(view: self.backgroundView, color: component.theme.background)
transition.setAlpha(view: self.backgroundView, alpha: buttonAlpha)
self.backgroundView.layer.cornerRadius = availableSize.height * 0.5
return CGSize(width: titleSize.width + titlePadding * 2.0, height: availableSize.height)
}
func applySize(size: CGSize, transition: ComponentTransition) {
transition.setFrame(view: self.backgroundView, frame: CGRect(origin: .zero, size: size))
if let titleView = self.title.view {
let titleSize = titleView.bounds.size
let titleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - titleSize.width) / 2.0), y: floorToScreenPixels((size.height - titleSize.height) / 2.0)), size: titleSize)
transition.setFrame(view: titleView, frame: titleFrame)
}
if let activityView = self.activity?.view {
var activityTransition = transition
if activityView.superview == nil {
self.addSubview(activityView)
transition.animateAlpha(view: activityView, from: 0.0, to: 1.0)
activityTransition = .immediate
}
let activitySize = activityView.bounds.size
let activityFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - activitySize.width) / 2.0), y: floorToScreenPixels((size.height - activitySize.height) / 2.0)), size: activitySize)
activityTransition.setPosition(view: activityView, position: activityFrame.center)
activityView.transform = CGAffineTransformMakeScale(0.7, 0.7)
}
}
}
func makeView() -> View {
return View(frame: CGRect())
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<AlertComponentEnvironment>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
@@ -0,0 +1,952 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import ComponentFlow
import SwiftSignalKit
import AccountContext
import TelegramPresentationData
import MultilineTextComponent
import ViewControllerComponent
import ComponentDisplayAdapters
import GlassBackgroundComponent
public final class AlertComponentEnvironment: Equatable {
public let theme: PresentationTheme
public let strings: PresentationStrings
public init(
theme: PresentationTheme,
strings: PresentationStrings
) {
self.theme = theme
self.strings = strings
}
public static func ==(lhs: AlertComponentEnvironment, rhs: AlertComponentEnvironment) -> Bool {
if lhs.theme !== rhs.theme {
return false
}
if lhs.strings !== rhs.strings {
return false
}
return true
}
}
private final class AlertScreenComponent: Component {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
let configuration: AlertScreen.Configuration
let content: Signal<[AnyComponentWithIdentity<AlertComponentEnvironment>], NoError>
let actions: Signal<[AlertScreen.Action], NoError>
let ready: Promise<Bool>
init(
configuration: AlertScreen.Configuration,
content: Signal<[AnyComponentWithIdentity<AlertComponentEnvironment>], NoError>,
actions: Signal<[AlertScreen.Action], NoError>,
ready: Promise<Bool>
) {
self.configuration = configuration
self.content = content
self.actions = actions
self.ready = ready
}
static func ==(lhs: AlertScreenComponent, rhs: AlertScreenComponent) -> Bool {
return true
}
enum KeyCommand {
case up
case down
case left
case right
case escape
case enter
}
final class View: UIView, UIGestureRecognizerDelegate {
private let dimView = UIView()
private let containerView = GlassBackgroundContainerView()
private let backgroundView = GlassBackgroundView()
private var disposable: Disposable?
private var content: [AnyComponentWithIdentity<AlertComponentEnvironment>]?
private var actions: [AlertScreen.Action]?
private var contentItems: [AnyHashable: ComponentView<AlertComponentEnvironment>] = [:]
private var actionItems: [AnyHashable: ComponentView<AlertComponentEnvironment>] = [:]
private var highlightedAction: AnyHashable?
private let hapticFeedback = HapticFeedback()
private enum ActionLayout {
case horizontal
case vertical
case verticalReversed
var isVertical: Bool {
switch self {
case .vertical, .verticalReversed:
return true
default:
return false
}
}
}
private var effectiveActionLayout: ActionLayout = .horizontal
fileprivate var dismissedByTapOutside = false
private var isUpdating: Bool = false
private var component: AlertScreenComponent?
private var environment: EnvironmentType?
private weak var state: EmptyComponentState?
override init(frame: CGRect) {
super.init(frame: frame)
self.dimView.alpha = 0.0
self.dimView.backgroundColor = UIColor(rgb: 0x000000, alpha: 0.2)
self.addSubview(self.dimView)
self.addSubview(self.containerView)
self.containerView.contentView.addSubview(self.backgroundView)
self.dimView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimTapped)))
let tapRecognizer = ActionSelectionGestureRecognizer(target: self, action: #selector(self.actionTapped(_:)))
tapRecognizer.delegate = self
self.backgroundView.addGestureRecognizer(tapRecognizer)
}
required init?(coder: NSCoder) {
preconditionFailure()
}
deinit {
self.disposable?.dispose()
}
override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
if gestureRecognizer is ActionSelectionGestureRecognizer {
let location = gestureRecognizer.location(in: self.backgroundView)
for (_, action) in self.actionItems {
if let actionView = action.view, actionView.frame.contains(location) {
return true
}
}
return false
} else {
return super.gestureRecognizerShouldBegin(gestureRecognizer)
}
}
@objc private func actionTapped(_ gestureRecognizer: ActionSelectionGestureRecognizer) {
let location = gestureRecognizer.location(in: self.backgroundView)
switch gestureRecognizer.state {
case .began, .changed:
var highlightedActionId: AnyHashable?
for (actionId, action) in self.actionItems {
if let actionView = action.view, actionView.frame.contains(location) {
highlightedActionId = actionId
break
}
}
if self.highlightedAction != highlightedActionId {
self.highlightedAction = highlightedActionId
self.state?.updated(transition: .easeInOut(duration: 0.2))
if case .changed = gestureRecognizer.state, highlightedActionId != nil {
self.hapticFeedback.tap()
}
}
case .ended:
if let _ = self.highlightedAction {
self.performHighlightedAction()
self.highlightedAction = nil
self.state?.updated(transition: .easeInOut(duration: 0.2))
}
case .cancelled:
self.highlightedAction = nil
self.state?.updated(transition: .easeInOut(duration: 0.2))
default:
break
}
}
@objc private func dimTapped() {
guard let component = self.component, component.configuration.dismissOnOutsideTap else {
return
}
self.dismissedByTapOutside = true
self.requestDismiss()
}
func animateIn() {
let alphaTransition = ComponentTransition(animation: .curve(duration: 0.2, curve: .linear))
let scaleTransition = ComponentTransition(animation: .curve(duration: 0.4, curve: .spring))
alphaTransition.setAlpha(view: self.dimView, alpha: 1.0)
scaleTransition.animateScale(view: self.backgroundView, from: 1.15, to: 1.0)
alphaTransition.animateAlpha(view: self.containerView, from: 0.0, to: 1.0)
}
func animateOut(completion: @escaping () -> Void) {
let transition = ComponentTransition(animation: .curve(duration: 0.2, curve: .linear))
transition.setAlpha(view: self.dimView, alpha: 0.0, completion: { _ in
completion()
})
var initialAlpha: CGFloat = 1.0
if let presentationLayer = self.containerView.layer.presentation() {
initialAlpha = CGFloat(presentationLayer.opacity)
}
self.containerView.layer.animateAlpha(from: initialAlpha, to: 0.0, duration: 0.2, removeOnCompletion: false)
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let result = super.hitTest(point, with: event)
if result === self.containerView.contentView {
return self.dimView
}
return result
}
func requestDismiss() {
guard let controller = self.environment?.controller() as? AlertScreen else {
return
}
controller.dismiss(completion: nil)
}
func handleKeyCommand(_ command: KeyCommand) {
switch command {
case .up:
guard self.effectiveActionLayout.isVertical else {
return
}
self.updateActionHighlight(previous: false)
case .down:
guard self.effectiveActionLayout.isVertical else {
return
}
self.updateActionHighlight(previous: true)
case .left:
guard !self.effectiveActionLayout.isVertical else {
return
}
self.updateActionHighlight(previous: true)
case .right:
guard !self.effectiveActionLayout.isVertical else {
return
}
self.updateActionHighlight(previous: false)
case .escape:
self.requestDismiss()
case .enter:
self.performHighlightedAction()
}
}
func updateActionHighlight(previous: Bool) {
guard let actions = self.actions else {
return
}
guard let highlightedAction = self.highlightedAction else {
if let action = actions.first(where: { $0.type == .default }) {
self.highlightedAction = action.id
} else if let action = actions.first(where: { $0.type == .defaultDestructive }) {
self.highlightedAction = action.id
} else if case .verticalReversed = self.effectiveActionLayout, let action = actions.last {
self.highlightedAction = action.id
} else if let action = actions.first {
self.highlightedAction = action.id
}
self.state?.updated(transition: .easeInOut(duration: 0.2))
return
}
let sequence = previous ? actions.reversed() : actions
var selectNext = false
var newHighlightedAction: AnyHashable?
for action in sequence {
let id = AnyHashable(action.id)
if selectNext {
newHighlightedAction = id
break
} else if id == highlightedAction {
selectNext = true
}
}
guard let newHighlightedAction else {
return
}
self.highlightedAction = newHighlightedAction
self.state?.updated(transition: .easeInOut(duration: 0.2))
}
func performHighlightedAction() {
guard let actions = self.actions else {
return
}
guard let highlightedAction = self.highlightedAction else {
return
}
guard let action = actions.first(where: { AnyHashable($0.id) == highlightedAction }) else {
return
}
action.action()
if action.autoDismiss {
self.requestDismiss()
}
}
func update(component: AlertScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<ViewControllerComponentContainer.Environment>, transition: ComponentTransition) -> CGSize {
self.isUpdating = true
defer {
self.isUpdating = false
}
let environment = environment[ViewControllerComponentContainer.Environment.self].value
self.environment = environment
self.state = state
if self.component == nil {
self.disposable = (combineLatest(
queue: Queue.mainQueue(),
component.content,
component.actions
) |> deliverOnMainQueue).start(next: { [weak self] content, actions in
guard let self else {
return
}
self.content = content
self.actions = actions
if !self.isUpdating {
self.state?.updated(transition: .easeInOut(duration: 0.25))
}
})
}
self.component = component
var alertHeight: CGFloat = 0.0
let alertWidth: CGFloat = 300.0
let contentTopInset: CGFloat = 22.0
let contentBottomInset: CGFloat = 21.0
let contentSideInset: CGFloat = 30.0
let contentSpacing: CGFloat = 8.0
let actionSideInset: CGFloat = 16.0
let actionSpacing: CGFloat = 8.0
let fullWidthActionSize = CGSize(width: alertWidth - actionSideInset * 2.0, height: AlertActionComponent.actionHeight)
let halfWidthActionSize = CGSize(width: (alertWidth - actionSideInset * 2.0 - actionSpacing) / 2.0, height: AlertActionComponent.actionHeight)
let alertEnvironment = AlertComponentEnvironment(theme: environment.theme, strings: environment.strings)
var contentOriginY: CGFloat = 0.0
var validContentIds: Set<AnyHashable> = Set()
if let content = self.content {
for content in content {
if contentOriginY.isZero {
contentOriginY += contentTopInset
} else {
contentOriginY += contentSpacing
}
validContentIds.insert(content.id)
let item: ComponentView<AlertComponentEnvironment>
var itemTransition = transition
if let current = self.contentItems[content.id] {
item = current
} else {
item = ComponentView()
if !transition.animation.isImmediate {
itemTransition = .immediate
}
self.contentItems[content.id] = item
}
let itemSize = item.update(
transition: itemTransition,
component: content.component,
environment: { alertEnvironment },
containerSize: CGSize(width: alertWidth - contentSideInset * 2.0, height: availableSize.height)
)
let itemFrame = CGRect(origin: CGPoint(x: contentSideInset, y: contentOriginY), size: itemSize)
if let itemView = item.view {
if itemView.superview == nil {
self.backgroundView.contentView.addSubview(itemView)
item.parentState = state
}
transition.setFrame(view: itemView, frame: itemFrame)
}
contentOriginY += itemSize.height
}
}
if !contentOriginY.isZero {
alertHeight += contentOriginY
alertHeight += contentBottomInset
}
if let actions = self.actions {
let genericActionTheme = AlertActionComponent.Theme(
background: environment.theme.actionSheet.primaryTextColor.withMultipliedAlpha(0.1),
foreground: environment.theme.actionSheet.primaryTextColor,
secondary: environment.theme.actionSheet.secondaryTextColor,
font: .regular
)
let defaultActionTheme = AlertActionComponent.Theme(
background: environment.theme.actionSheet.controlAccentColor,
foreground: environment.theme.list.itemCheckColors.foregroundColor,
secondary: environment.theme.list.itemCheckColors.foregroundColor.withMultipliedAlpha(0.85),
font: .bold
)
let destructiveActionTheme = AlertActionComponent.Theme(
background: environment.theme.list.itemDestructiveColor,
foreground: .white,
secondary: .white.withMultipliedAlpha(0.6),
font: .regular
)
let defaultDestructiveActionTheme = AlertActionComponent.Theme(
background: environment.theme.list.itemDestructiveColor,
foreground: .white,
secondary: .white.withMultipliedAlpha(0.6),
font: .bold
)
var effectiveActionLayout: ActionLayout = .horizontal
if case .vertical = component.configuration.actionAlignment {
effectiveActionLayout = .vertical
} else if actions.count == 1 {
effectiveActionLayout = .vertical
}
var actionTransitions: [AnyHashable: ComponentTransition] = [:]
var validActionIds: Set<AnyHashable> = Set()
for action in actions {
validActionIds.insert(action.id)
let item: ComponentView<AlertComponentEnvironment>
var itemTransition = transition
if let current = self.actionItems[action.id] {
item = current
} else {
item = ComponentView()
if !transition.animation.isImmediate {
itemTransition = .immediate
}
self.actionItems[action.id] = item
}
actionTransitions[action.id] = itemTransition
let actionTheme: AlertActionComponent.Theme
switch action.type {
case .generic:
actionTheme = genericActionTheme
case .default:
actionTheme = defaultActionTheme
case .destructive:
actionTheme = destructiveActionTheme
case .defaultDestructive:
actionTheme = defaultDestructiveActionTheme
}
let itemSize = item.update(
transition: itemTransition,
component: AnyComponent(AlertActionComponent(
theme: actionTheme,
title: action.title,
isHighlighted: AnyHashable(action.id) == self.highlightedAction,
isEnabled: action.isEnabled,
progress: action.progress
)),
environment: { alertEnvironment },
containerSize: fullWidthActionSize
)
if let itemView = item.view {
if itemView.superview == nil {
self.backgroundView.contentView.addSubview(itemView)
}
}
if case .horizontal = effectiveActionLayout, itemSize.width > halfWidthActionSize.width {
effectiveActionLayout = .verticalReversed
}
}
self.effectiveActionLayout = effectiveActionLayout
if !actions.isEmpty {
let actionsHeight: CGFloat
if self.effectiveActionLayout.isVertical {
actionsHeight = fullWidthActionSize.height * CGFloat(actions.count) + actionSpacing * CGFloat(actions.count - 1)
} else {
actionsHeight = fullWidthActionSize.height
}
alertHeight += actionsHeight
alertHeight += actionSideInset
}
var actionOriginX: CGFloat = actionSideInset
var actionOriginY: CGFloat
switch self.effectiveActionLayout {
case .horizontal, .verticalReversed:
actionOriginY = alertHeight - actionSideInset - fullWidthActionSize.height
case .vertical:
actionOriginY = alertHeight - actionSideInset - fullWidthActionSize.height * CGFloat( actions.count) - actionSpacing * CGFloat(actions.count - 1)
}
for action in actions {
guard let item = self.actionItems[action.id], let itemView = item.view as? AlertActionComponent.View else {
continue
}
let itemTransition = actionTransitions[action.id] ?? transition
let itemFrame: CGRect
switch self.effectiveActionLayout {
case .horizontal:
itemFrame = CGRect(origin: CGPoint(x: actionOriginX, y: actionOriginY), size: halfWidthActionSize)
actionOriginX += halfWidthActionSize.width + actionSpacing
case .vertical:
itemFrame = CGRect(origin: CGPoint(x: actionOriginX, y: actionOriginY), size: fullWidthActionSize)
actionOriginY += fullWidthActionSize.height + actionSpacing
case .verticalReversed:
itemFrame = CGRect(origin: CGPoint(x: actionOriginX, y: actionOriginY), size: fullWidthActionSize)
actionOriginY -= fullWidthActionSize.height + actionSpacing
}
itemView.applySize(size: itemFrame.size, transition: itemTransition)
itemTransition.setFrame(view: itemView, frame: itemFrame)
}
var removeActionIds: [AnyHashable] = []
for (id, item) in self.actionItems {
if !validActionIds.contains(id) {
removeActionIds.append(id)
if let itemView = item.view {
if !transition.animation.isImmediate {
itemView.layer.animateScale(from: 1.0, to: 0.01, duration: 0.25, removeOnCompletion: false)
itemView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { _ in
itemView.removeFromSuperview()
})
} else {
itemView.removeFromSuperview()
}
}
}
}
for id in removeActionIds {
self.actionItems.removeValue(forKey: id)
}
}
let alertSize = CGSize(width: alertWidth, height: alertHeight)
let bounds = CGRect(origin: .zero, size: availableSize)
transition.setFrame(view: self.dimView, frame: bounds)
transition.setFrame(view: self.containerView, frame: bounds)
self.containerView.update(size: availableSize, isDark: environment.theme.overallDarkAppearance, transition: transition)
var availableHeight = availableSize.height
availableHeight -= environment.statusBarHeight
if component.configuration.allowInputInset, environment.inputHeight > 0.0 {
availableHeight -= environment.inputHeight
}
transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - alertSize.width) / 2.0), y: environment.statusBarHeight + floorToScreenPixels((availableHeight - alertSize.height) / 2.0)), size: alertSize))
self.backgroundView.update(size: alertSize, cornerRadius: 35.0, isDark: environment.theme.overallDarkAppearance, tintColor: .init(kind: .panel), isInteractive: true, transition: transition)
return availableSize
}
}
func makeView() -> View {
return View(frame: CGRect())
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<ViewControllerComponentContainer.Environment>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
open class AlertScreen: ViewControllerComponentContainer, KeyShortcutResponder {
public enum ActionAligmnent: Equatable {
case `default`
case vertical
}
public struct Configuration: Equatable {
let actionAlignment: ActionAligmnent
let dismissOnOutsideTap: Bool
let allowInputInset: Bool
public init(
actionAlignment: ActionAligmnent = .default,
dismissOnOutsideTap: Bool = true,
allowInputInset: Bool = false
) {
self.actionAlignment = actionAlignment
self.dismissOnOutsideTap = dismissOnOutsideTap
self.allowInputInset = allowInputInset
}
}
public struct Action: Equatable {
public enum ActionType: Equatable {
case generic
case `default`
case destructive
case defaultDestructive
}
public let title: String
public let type: ActionType
public let action: () -> Void
public let autoDismiss: Bool
public let isEnabled: Signal<Bool, NoError>
public let progress: Signal<Bool, NoError>
public init(
id: AnyHashable? = nil,
title: String,
type: ActionType = .generic,
action: @escaping () -> Void = {},
autoDismiss: Bool = true,
isEnabled: Signal<Bool, NoError> = .single(true),
progress: Signal<Bool, NoError> = .single(false)
) {
self.type = type
self.title = title
self.action = action
self.autoDismiss = autoDismiss
self.isEnabled = isEnabled
self.progress = progress
if let id {
self.id = id
} else {
self.id = title
}
}
public static func ==(lhs: Action, rhs: Action) -> Bool {
if lhs.title != rhs.title {
return false
}
if lhs.type != rhs.type {
return false
}
if lhs.autoDismiss != rhs.autoDismiss {
return false
}
return true
}
fileprivate let id: AnyHashable
}
private var processedDidAppear: Bool = false
private var processedDidDisappear: Bool = false
private let readyValue = Promise<Bool>(true)
override public var ready: Promise<Bool> {
return self.readyValue
}
public var dismissed: ((Bool) -> Void)?
public init(
configuration: Configuration = Configuration(),
contentSignal: Signal<[AnyComponentWithIdentity<AlertComponentEnvironment>], NoError>,
actionsSignal: Signal<[Action], NoError>,
updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)
) {
let componentReady = Promise<Bool>()
super.init(
component: AlertScreenComponent(
configuration: configuration,
content: contentSignal,
actions: actionsSignal,
ready: componentReady
),
navigationBarAppearance: .none,
statusBarStyle: .ignore,
presentationMode: .default,
updatedPresentationData: updatedPresentationData
)
self.navigationPresentation = .flatModal
//self.readyValue.set(componentReady.get() |> timeout(1.0, queue: .mainQueue(), alternate: .single(true)))
}
public convenience init(
configuration: Configuration = Configuration(),
content: [AnyComponentWithIdentity<AlertComponentEnvironment>],
actions: [Action],
updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)
) {
self.init(
configuration: configuration,
contentSignal: .single(content),
actionsSignal: .single(actions),
updatedPresentationData: updatedPresentationData
)
}
public convenience init(
context: AccountContext,
configuration: Configuration = Configuration(),
content: [AnyComponentWithIdentity<AlertComponentEnvironment>],
actions: [Action]
) {
self.init(
sharedContext: context.sharedContext,
configuration: configuration,
content: content,
actions: actions,
)
}
public convenience init(
sharedContext: SharedAccountContext,
configuration: Configuration = Configuration(),
content: [AnyComponentWithIdentity<AlertComponentEnvironment>],
actions: [Action]
) {
let presentationData = sharedContext.currentPresentationData.with { $0 }
let updatedPresentationDataSignal = sharedContext.presentationData
self.init(
configuration: configuration,
content: content,
actions: actions,
updatedPresentationData: (initial: presentationData, signal: updatedPresentationDataSignal)
)
}
public convenience init(
configuration: Configuration = Configuration(),
title: String? = nil,
text: String,
textAction: @escaping ([NSAttributedString.Key: Any]) -> Void = { _ in },
actions: [Action],
updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)
) {
var content: [AnyComponentWithIdentity<AlertComponentEnvironment>] = []
if let title {
content.append(AnyComponentWithIdentity(
id: "title",
component: AnyComponent(
AlertTitleComponent(title: title)
)
))
}
if !text.isEmpty {
content.append(AnyComponentWithIdentity(
id: "text",
component: AnyComponent(
AlertTextComponent(content: .plain(text), action: textAction)
)
))
}
if content.isEmpty {
content.append(AnyComponentWithIdentity(
id: "text",
component: AnyComponent(
AlertTextComponent(content: .plain(" "), action: textAction)
)
))
}
self.init(
configuration: configuration,
content: content,
actions: actions,
updatedPresentationData: updatedPresentationData
)
}
public convenience init(
context: AccountContext,
configuration: Configuration = Configuration(),
title: String? = nil,
text: String,
actions: [Action]
) {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let updatedPresentationDataSignal = context.sharedContext.presentationData
self.init(
configuration: configuration,
title: title,
text: text,
actions: actions,
updatedPresentationData: (initial: presentationData, signal: updatedPresentationDataSignal)
)
}
required public init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
}
override public func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
if !self.processedDidAppear {
self.processedDidAppear = true
if let componentView = self.node.hostView.componentView as? AlertScreenComponent.View {
componentView.animateIn()
}
}
}
private func superDismiss() {
super.dismiss()
}
override open func dismiss(completion: (() -> Void)? = nil) {
if !self.processedDidDisappear {
self.processedDidDisappear = true
self.view.window?.endEditing(true)
if let componentView = self.node.hostView.componentView as? AlertScreenComponent.View {
let dismissedByTapOutside = componentView.dismissedByTapOutside
componentView.animateOut(completion: { [weak self] in
if let self {
self.dismissed?(dismissedByTapOutside)
self.superDismiss()
}
completion?()
})
} else {
super.dismiss(completion: completion)
}
}
}
public var keyShortcuts: [KeyShortcut] {
return [
KeyShortcut(
input: UIKeyCommand.inputEscape,
modifiers: [],
action: { [weak self] in
if let componentView = self?.node.hostView.componentView as? AlertScreenComponent.View {
componentView.handleKeyCommand(.escape)
}
}
),
KeyShortcut(
input: "W",
modifiers: [.command],
action: { [weak self] in
if let componentView = self?.node.hostView.componentView as? AlertScreenComponent.View {
componentView.handleKeyCommand(.escape)
}
}
),
KeyShortcut(
input: "\r",
modifiers: [],
action: { [weak self] in
if let componentView = self?.node.hostView.componentView as? AlertScreenComponent.View {
componentView.handleKeyCommand(.enter)
}
}
),
KeyShortcut(
input: UIKeyCommand.inputUpArrow,
modifiers: [],
action: { [weak self] in
if let componentView = self?.node.hostView.componentView as? AlertScreenComponent.View {
componentView.handleKeyCommand(.up)
}
}
),
KeyShortcut(
input: UIKeyCommand.inputDownArrow,
modifiers: [],
action: { [weak self] in
if let componentView = self?.node.hostView.componentView as? AlertScreenComponent.View {
componentView.handleKeyCommand(.down)
}
}
),
KeyShortcut(
input: UIKeyCommand.inputLeftArrow,
modifiers: [],
action: { [weak self] in
if let componentView = self?.node.hostView.componentView as? AlertScreenComponent.View {
componentView.handleKeyCommand(.left)
}
}
),
KeyShortcut(
input: UIKeyCommand.inputRightArrow,
modifiers: [],
action: { [weak self] in
if let componentView = self?.node.hostView.componentView as? AlertScreenComponent.View {
componentView.handleKeyCommand(.right)
}
}
)
]
}
}
public final class ActionSelectionGestureRecognizer: UIGestureRecognizer {
private var initialLocation: CGPoint?
private var currentLocation: CGPoint?
public override init(target: Any?, action: Selector?) {
super.init(target: target, action: action)
self.delaysTouchesBegan = false
self.delaysTouchesEnded = false
}
public override func reset() {
super.reset()
self.initialLocation = nil
}
public override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesBegan(touches, with: event)
if self.initialLocation == nil {
self.initialLocation = touches.first?.location(in: self.view)
}
self.currentLocation = self.initialLocation
self.state = .began
}
public override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesEnded(touches, with: event)
self.state = .ended
}
public override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesCancelled(touches, with: event)
self.state = .cancelled
}
public override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesMoved(touches, with: event)
self.currentLocation = touches.first?.location(in: self.view)
self.state = .changed
}
public func translation(in: UIView?) -> CGPoint {
if let initialLocation = self.initialLocation, let currentLocation = self.currentLocation {
return CGPoint(x: currentLocation.x - initialLocation.x, y: currentLocation.y - initialLocation.y)
}
return CGPoint()
}
}
@@ -0,0 +1,369 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import ComponentFlow
import TelegramCore
import TelegramPresentationData
import MultilineTextComponent
import MultilineTextWithEntitiesComponent
import Markdown
import TextFormat
import AccountContext
private let titleFont = Font.bold(17.0)
private let defaultTextFont = Font.regular(15.0)
private let defaultBoldTextFont = Font.semibold(15.0)
private let defaultItalicTextFont = Font.italic(15.0)
private let defaultBoldItalicTextFont = Font.with(size: 15.0, weight: .semibold, traits: [.italic])
private let defaultFixedTextFont = Font.monospace(15.0)
private let smallTextFont = Font.regular(14.0)
private let smallBoldTextFont = Font.semibold(14.0)
private let smallItalicTextFont = Font.italic(14.0)
private let smallBoldItalicTextFont = Font.with(size: 14.0, weight: .semibold, traits: [.italic])
private let smallFixedTextFont = Font.monospace(14.0)
private let backgroundInset: CGFloat = 8.0
public final class AlertTitleComponent: Component {
public typealias EnvironmentType = AlertComponentEnvironment
public enum Alignment {
case `default`
case center
}
let title: String
let alignment: Alignment
public init(
title: String,
alignment: Alignment = .default
) {
self.title = title
self.alignment = alignment
}
public static func ==(lhs: AlertTitleComponent, rhs: AlertTitleComponent) -> Bool {
if lhs.title != rhs.title {
return false
}
if lhs.alignment != rhs.alignment {
return false
}
return true
}
public final class View: UIView {
private let title = ComponentView<Empty>()
private var component: AlertTitleComponent?
private weak var state: EmptyComponentState?
func update(component: AlertTitleComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<AlertComponentEnvironment>, transition: ComponentTransition) -> CGSize {
self.component = component
self.state = state
let environment = environment[AlertComponentEnvironment.self]
let inset: CGFloat = -6.0
let titleConstrainedSize = CGSize(width: availableSize.width - inset * 2.0, height: availableSize.height)
let titleSize = self.title.update(
transition: transition,
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: component.title,
font: titleFont,
textColor: environment.theme.actionSheet.primaryTextColor
)),
horizontalAlignment: component.alignment == .center ? .center : .natural,
maximumNumberOfLines: 0
)),
environment: {},
containerSize: titleConstrainedSize
)
let titleOriginX: CGFloat
switch component.alignment {
case .default:
titleOriginX = inset
case .center:
titleOriginX = floorToScreenPixels((availableSize.width - titleSize.width) / 2.0)
}
let titleFrame = CGRect(origin: CGPoint(x: titleOriginX, y: 0.0), size: titleSize)
if let titleView = self.title.view {
if titleView.superview == nil {
self.addSubview(titleView)
}
transition.setFrame(view: titleView, frame: titleFrame)
}
return CGSize(width: availableSize.width, height: titleSize.height)
}
}
public func makeView() -> View {
return View(frame: CGRect())
}
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<AlertComponentEnvironment>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
public final class AlertTextComponent: Component {
public typealias EnvironmentType = AlertComponentEnvironment
public enum Content: Equatable {
case plain(String)
case attributed(NSAttributedString)
case textWithEntities(AccountContext, String, [MessageTextEntity])
public static func ==(lhs: Content, rhs: Content) -> Bool {
switch lhs {
case let .plain(text):
if case .plain(text) = rhs {
return true
} else {
return false
}
case let .attributed(text):
if case .attributed(text) = rhs {
return true
} else {
return false
}
case let .textWithEntities(_, lhsText, lhsEntities):
if case let .textWithEntities(_, rhsText, rhsEntities) = rhs {
return lhsText == rhsText && lhsEntities == rhsEntities
} else {
return false
}
}
}
}
public enum Alignment: Equatable {
case `default`
case center
}
public enum Color: Equatable {
case primary
case secondary
case destructive
}
public enum TextStyle: Equatable {
case `default`
case small
case bold
}
public enum Style: Equatable {
case plain(TextStyle)
case background(TextStyle)
}
let content: Content
let alignment: Alignment
let color: Color
let style: Style
let insets: UIEdgeInsets
let action: ([NSAttributedString.Key: Any]) -> Void
public init(
content: Content,
alignment: Alignment = .default,
color: Color = .primary,
style: Style = .plain(.default),
insets: UIEdgeInsets = .zero,
action: @escaping ([NSAttributedString.Key: Any]) -> Void = { _ in }
) {
self.content = content
self.alignment = alignment
self.color = color
self.style = style
self.insets = insets
self.action = action
}
public static func ==(lhs: AlertTextComponent, rhs: AlertTextComponent) -> Bool {
if lhs.content != rhs.content {
return false
}
if lhs.alignment != rhs.alignment {
return false
}
if lhs.color != rhs.color {
return false
}
if lhs.style != rhs.style {
return false
}
if lhs.insets != rhs.insets {
return false
}
return true
}
public final class View: UIView {
private let background = ComponentView<Empty>()
private let text = ComponentView<Empty>()
private var component: AlertTextComponent?
private weak var state: EmptyComponentState?
func update(component: AlertTextComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<AlertComponentEnvironment>, transition: ComponentTransition) -> CGSize {
self.component = component
self.state = state
let environment = environment[AlertComponentEnvironment.self]
let textColor: UIColor
switch component.color {
case .primary:
textColor = environment.theme.actionSheet.primaryTextColor
case .secondary:
textColor = environment.theme.actionSheet.primaryTextColor.withMultipliedAlpha(0.35)
case .destructive:
textColor = environment.theme.actionSheet.destructiveActionTextColor
}
let linkColor = environment.theme.actionSheet.controlAccentColor
let textFont: UIFont
let boldTextFont: UIFont
let italicTextFont: UIFont
let fixedTextFont: UIFont
switch component.style {
case let .plain(textStyle), let .background(textStyle):
switch textStyle {
case .default:
textFont = defaultTextFont
boldTextFont = defaultBoldTextFont
italicTextFont = defaultItalicTextFont
fixedTextFont = defaultFixedTextFont
case .small:
textFont = smallTextFont
boldTextFont = smallBoldTextFont
italicTextFont = smallItalicTextFont
fixedTextFont = smallFixedTextFont
case .bold:
textFont = defaultBoldTextFont
boldTextFont = defaultBoldTextFont
italicTextFont = defaultBoldItalicTextFont
fixedTextFont = defaultFixedTextFont
}
}
var finalText: NSAttributedString
var context: AccountContext?
switch component.content {
case let .plain(text):
let markdownAttributes = MarkdownAttributes(
body: MarkdownAttributeSet(font: textFont, textColor: textColor),
bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor),
link: MarkdownAttributeSet(font: textFont, textColor: linkColor),
linkAttribute: { contents in
return (TelegramTextAttributes.URL, contents)
}
)
finalText = parseMarkdownIntoAttributedString(text, attributes: markdownAttributes)
case let .attributed(attributedText):
finalText = attributedText
case let .textWithEntities(accountContext, text, entities):
context = accountContext
finalText = stringWithAppliedEntities(text, entities: entities, baseColor: textColor, linkColor: linkColor, baseFont: textFont, linkFont: textFont, boldFont: boldTextFont, italicFont: italicTextFont, boldItalicFont: italicTextFont, fixedFont: fixedTextFont, blockQuoteFont: textFont, message: nil)
}
var hasCenterAlignment = component.alignment == .center
switch component.style {
case .background:
hasCenterAlignment = true
default:
break
}
let inset: CGFloat = -6.0
let textConstrainedSize = CGSize(width: availableSize.width - inset * 2.0, height: availableSize.height)
let textSize = self.text.update(
transition: transition,
component: AnyComponent(
MultilineTextWithEntitiesComponent(
context: context,
animationCache: context?.animationCache,
animationRenderer: context?.animationRenderer,
placeholderColor: textColor.withMultipliedAlpha(0.1),
text: .plain(finalText),
horizontalAlignment: hasCenterAlignment ? .center : .natural,
maximumNumberOfLines: 0,
lineSpacing: 0.2,
spoilerColor: textColor,
highlightColor: linkColor.withAlphaComponent(0.2),
manualVisibilityControl: true,
resetAnimationsOnVisibilityChange: true,
highlightAction: { attributes in
if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] {
return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)
} else {
return nil
}
},
tapAction: { attributes, _ in
component.action(attributes)
}
)
),
environment: {},
containerSize: textConstrainedSize
)
var textOffset = CGPoint(x: inset, y: 0.0)
if hasCenterAlignment {
textOffset.x = floorToScreenPixels((availableSize.width - textSize.width) / 2.0)
}
var size = CGSize(width: availableSize.width, height: textSize.height)
if case .background = component.style {
let backgroundSize = CGSize(width: availableSize.width + 20.0, height: textSize.height + backgroundInset * 2.0)
size = backgroundSize
textOffset = CGPoint(x: textOffset.x, y: backgroundInset)
let _ = self.background.update(
transition: transition,
component: AnyComponent(
FilledRoundedRectangleComponent(
color: textColor.withMultipliedAlpha(0.1),
cornerRadius: .value(10.0),
smoothCorners: true
)
),
environment: {},
containerSize: backgroundSize
)
let backgroundFrame = CGRect(origin: CGPoint(x: -10.0, y: component.insets.top), size: backgroundSize)
if let backgroundView = self.background.view {
if backgroundView.superview == nil {
self.addSubview(backgroundView)
}
transition.setFrame(view: backgroundView, frame: backgroundFrame)
}
}
let textFrame = CGRect(origin: textOffset.offsetBy(dx: 0.0, dy: component.insets.top), size: textSize)
if let textView = self.text.view {
if textView.superview == nil {
self.addSubview(textView)
}
transition.setFrame(view: textView, frame: textFrame)
}
return CGSize(width: size.width, height: size.height + component.insets.top + component.insets.bottom)
}
}
public func makeView() -> View {
return View(frame: CGRect())
}
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<AlertComponentEnvironment>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
@@ -0,0 +1,19 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "AnimatedCounterComponent",
module_name = "AnimatedCounterComponent",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/Display",
"//submodules/ComponentFlow",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,279 @@
import Foundation
import UIKit
import Display
import ComponentFlow
final class AnimatedCounterItemComponent: Component {
public let font: UIFont
public let color: UIColor
public let text: String
public let numericValue: Int
public let alignment: CGFloat
public init(
font: UIFont,
color: UIColor,
text: String,
numericValue: Int,
alignment: CGFloat
) {
self.font = font
self.color = color
self.text = text
self.numericValue = numericValue
self.alignment = alignment
}
public static func ==(lhs: AnimatedCounterItemComponent, rhs: AnimatedCounterItemComponent) -> Bool {
if lhs.font != rhs.font {
return false
}
if lhs.color != rhs.color {
return false
}
if lhs.text != rhs.text {
return false
}
if lhs.numericValue != rhs.numericValue {
return false
}
if lhs.alignment != rhs.alignment {
return false
}
return true
}
public final class View: UIView {
private let contentView: UIImageView
private var component: AnimatedCounterItemComponent?
private weak var state: EmptyComponentState?
override init(frame: CGRect) {
self.contentView = UIImageView()
super.init(frame: frame)
self.addSubview(self.contentView)
}
required init(coder: NSCoder) {
preconditionFailure()
}
func update(component: AnimatedCounterItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
let previousNumericValue = self.component?.numericValue
self.component = component
self.state = state
let text = NSAttributedString(string: component.text, font: component.font, textColor: component.color)
let textBounds = text.boundingRect(with: availableSize, options: [.usesLineFragmentOrigin], context: nil)
let size = CGSize(width: ceil(textBounds.width), height: ceil(textBounds.height))
let previousContentImage = self.contentView.image
let previousContentFrame = self.contentView.frame
self.contentView.image = generateImage(size, rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
UIGraphicsPushContext(context)
text.draw(at: textBounds.origin)
UIGraphicsPopContext()
})
self.contentView.frame = CGRect(origin: CGPoint(), size: size)
if !transition.animation.isImmediate, let previousContentImage, !previousContentFrame.isEmpty, let previousNumericValue, previousNumericValue != component.numericValue {
let previousContentView = UIImageView()
previousContentView.image = previousContentImage
previousContentView.frame = CGRect(origin: CGPoint(x: size.width * component.alignment - previousContentFrame.width * component.alignment, y: previousContentFrame.minY), size: previousContentFrame.size)
self.addSubview(previousContentView)
let offsetY: CGFloat = size.height * 0.6 * (previousNumericValue < component.numericValue ? -1.0 : 1.0)
let subTransition = ComponentTransition(animation: .curve(duration: 0.16, curve: .easeInOut))
subTransition.animatePosition(view: self.contentView, from: CGPoint(x: 0.0, y: offsetY), to: CGPoint(), additive: true)
subTransition.animateAlpha(view: self.contentView, from: 0.0, to: 1.0)
subTransition.setPosition(view: previousContentView, position: CGPoint(x: previousContentView.layer.position.x, y: previousContentView.layer.position.y - offsetY))
subTransition.setAlpha(view: previousContentView, alpha: 0.0, completion: { [weak previousContentView] _ in
previousContentView?.removeFromSuperview()
})
}
return size
}
}
public func makeView() -> View {
return View(frame: CGRect())
}
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
public final class AnimatedCounterComponent: Component {
public enum Alignment {
case left
case right
}
public struct Item: Equatable {
public var id: AnyHashable
public var text: String
public var numericValue: Int
public init(id: AnyHashable, text: String, numericValue: Int) {
self.id = id
self.text = text
self.numericValue = numericValue
}
}
public let font: UIFont
public let color: UIColor
public let alignment: Alignment
public let items: [Item]
public init(
font: UIFont,
color: UIColor,
alignment: Alignment,
items: [Item]
) {
self.font = font
self.color = color
self.alignment = alignment
self.items = items
}
public static func ==(lhs: AnimatedCounterComponent, rhs: AnimatedCounterComponent) -> Bool {
if lhs.font != rhs.font {
return false
}
if lhs.color != rhs.color {
return false
}
if lhs.alignment != rhs.alignment {
return false
}
if lhs.items != rhs.items {
return false
}
return true
}
private final class ItemView {
let view = ComponentView<Empty>()
}
public final class View: UIView {
private var itemViews: [AnyHashable: ItemView] = [:]
private var component: AnimatedCounterComponent?
private weak var state: EmptyComponentState?
private var measuredSpaceWidth: CGFloat?
override init(frame: CGRect) {
super.init(frame: frame)
}
required init(coder: NSCoder) {
preconditionFailure()
}
func update(component: AnimatedCounterComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
let spaceWidth: CGFloat
if let measuredSpaceWidth = self.measuredSpaceWidth, let previousComponent = self.component, previousComponent.font.pointSize == component.font.pointSize {
spaceWidth = measuredSpaceWidth
} else {
spaceWidth = ceil(NSAttributedString(string: " ", font: component.font, textColor: .black).boundingRect(with: CGSize(width: 100.0, height: 100.0), options: .usesLineFragmentOrigin, context: nil).width)
self.measuredSpaceWidth = spaceWidth
}
self.component = component
self.state = state
var size = CGSize()
var validIds: [AnyHashable] = []
for item in component.items {
if size.width != 0.0 {
size.width += spaceWidth
}
validIds.append(item.id)
let itemView: ItemView
var itemTransition = transition
if let current = self.itemViews[item.id] {
itemView = current
} else {
itemTransition = .immediate
itemView = ItemView()
self.itemViews[item.id] = itemView
}
let itemSize = itemView.view.update(
transition: itemTransition,
component: AnyComponent(AnimatedCounterItemComponent(
font: component.font,
color: component.color,
text: item.text,
numericValue: item.numericValue,
alignment: component.alignment == .left ? 0.0 : 1.0
)),
environment: {},
containerSize: CGSize(width: 100.0, height: 100.0)
)
if let itemComponentView = itemView.view.view {
if itemComponentView.superview == nil {
self.addSubview(itemComponentView)
}
let itemFrame = CGRect(origin: CGPoint(x: size.width, y: 0.0), size: itemSize)
switch component.alignment {
case .left:
itemComponentView.layer.anchorPoint = CGPoint(x: 0.0, y: 0.5)
itemTransition.setPosition(view: itemComponentView, position: CGPoint(x: itemFrame.minX, y: itemFrame.midY))
case .right:
itemComponentView.layer.anchorPoint = CGPoint(x: 1.0, y: 0.5)
itemTransition.setPosition(view: itemComponentView, position: CGPoint(x: itemFrame.maxX, y: itemFrame.midY))
}
itemComponentView.bounds = CGRect(origin: CGPoint(), size: itemFrame.size)
}
size.width += itemSize.width
size.height = max(size.height, itemSize.height)
}
var removeIds: [AnyHashable] = []
for (id, itemView) in self.itemViews {
if !validIds.contains(id) {
removeIds.append(id)
if let componentView = itemView.view.view {
transition.setAlpha(view: componentView, alpha: 0.0, completion: { [weak componentView] _ in
componentView?.removeFromSuperview()
})
}
}
}
return size
}
}
public func makeView() -> View {
return View(frame: CGRect())
}
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
@@ -0,0 +1,23 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "AnimatedTextComponent",
module_name = "AnimatedTextComponent",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/SSignalKit/SwiftSignalKit",
"//submodules/Display",
"//submodules/ComponentFlow",
"//submodules/TelegramPresentationData",
"//submodules/Components/BundleIconComponent",
"//submodules/Components/MultilineTextComponent",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,444 @@
import Foundation
import UIKit
import Display
import ComponentFlow
import TelegramPresentationData
import BundleIconComponent
import MultilineTextComponent
extension ComponentTransition {
func animateBlur(layer: CALayer, from: CGFloat, to: CGFloat, delay: Double = 0.0, removeOnCompletion: Bool = true, completion: ((Bool) -> Void)? = nil) {
if let blurFilter = CALayer.blur() {
blurFilter.setValue(to as NSNumber, forKey: "inputRadius")
layer.filters = [blurFilter]
layer.animate(from: from as NSNumber, to: to as NSNumber, keyPath: "filters.gaussianBlur.inputRadius", timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, duration: 0.3, delay: delay, removeOnCompletion: removeOnCompletion, completion: { [weak layer] _ in
guard let layer else {
return
}
if to == 0.0 && removeOnCompletion {
layer.filters = nil
}
})
}
}
}
public final class AnimatedTextComponent: Component {
public struct Item: Equatable {
public enum Content: Equatable {
case text(String)
case number(Int, minDigits: Int)
case icon(String, tint: Bool, offset: CGPoint)
}
public var id: AnyHashable
public var isUnbreakable: Bool
public var content: Content
public init(id: AnyHashable, isUnbreakable: Bool = false, content: Content) {
self.id = id
self.isUnbreakable = isUnbreakable
self.content = content
}
}
public let font: UIFont
public let color: UIColor
public let items: [Item]
public let noDelay: Bool
public let animateScale: Bool
public let animateSlide: Bool
public let preferredDirectionIsDown: Bool
public let blur: Bool
public init(
font: UIFont,
color: UIColor,
items: [Item],
noDelay: Bool = false,
animateScale: Bool = true,
animateSlide: Bool = true,
preferredDirectionIsDown: Bool = false,
blur: Bool = false
) {
self.font = font
self.color = color
self.items = items
self.noDelay = noDelay
self.animateScale = animateScale
self.animateSlide = animateSlide
self.preferredDirectionIsDown = preferredDirectionIsDown
self.blur = blur
}
public static func ==(lhs: AnimatedTextComponent, rhs: AnimatedTextComponent) -> Bool {
if lhs.font != rhs.font {
return false
}
if lhs.color != rhs.color {
return false
}
if lhs.items != rhs.items {
return false
}
if lhs.noDelay != rhs.noDelay {
return false
}
if lhs.animateScale != rhs.animateScale {
return false
}
if lhs.animateSlide != rhs.animateSlide {
return false
}
if lhs.preferredDirectionIsDown != rhs.preferredDirectionIsDown {
return false
}
if lhs.blur != rhs.blur {
return false
}
return true
}
private struct CharacterKey: Hashable {
var itemId: AnyHashable
var index: Int
var value: String
}
public final class View: UIView {
private var characters: [CharacterKey: ComponentView<Empty>] = [:]
private var spaceSize: CGSize?
private var component: AnimatedTextComponent?
private weak var state: EmptyComponentState?
override init(frame: CGRect) {
super.init(frame: frame)
}
required init(coder: NSCoder) {
preconditionFailure()
}
func update(component: AnimatedTextComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
let spaceSize: CGSize
if let current = self.spaceSize, self.component?.font == component.font {
spaceSize = current
} else {
let spaceSizeValue = NSAttributedString(string: " ", font: component.font, textColor: .black).boundingRect(with: CGSize(width: 100.0, height: 100.0), options: .usesLineFragmentOrigin, context: nil).size
spaceSize = CGSize(width: ceil(spaceSizeValue.width), height: ceil(spaceSizeValue.height))
self.spaceSize = spaceSize
}
self.component = component
self.state = state
var size = CGSize()
let delayNorm: CGFloat = 0.002
var offsetNorm: CGFloat = 0.4
let transitionBlurRadius: CGFloat = 6.0
var firstDelayWidth: CGFloat?
if component.preferredDirectionIsDown {
firstDelayWidth = 0.0
offsetNorm = 0.8
}
var validKeys: [CharacterKey] = []
for item in component.items {
var itemText: [String] = []
switch item.content {
case let .text(text):
if item.isUnbreakable {
itemText = [text]
} else {
itemText = text.map(String.init)
}
case let .number(value, minDigits):
var valueText: String = "\(value)"
while valueText.count < minDigits {
valueText.insert("0", at: valueText.startIndex)
}
if item.isUnbreakable {
itemText = [valueText]
} else {
itemText = valueText.map(String.init)
}
case let .icon(iconName, _, _):
let characterKey = CharacterKey(itemId: item.id, index: 0, value: iconName)
validKeys.append(characterKey)
}
var index = 0
for character in itemText {
let characterKey = CharacterKey(itemId: item.id, index: index, value: character)
index += 1
validKeys.append(characterKey)
}
}
var outLastDelayWidth: CGFloat?
var outFirstDelayWidth: CGFloat?
if component.preferredDirectionIsDown {
for (key, characterView) in self.characters {
if !validKeys.contains(key), let characterView = characterView.view {
if let outFirstDelayWidthValue = outFirstDelayWidth {
outFirstDelayWidth = max(outFirstDelayWidthValue, characterView.frame.center.x)
} else {
outFirstDelayWidth = characterView.frame.center.x
}
if let outLastDelayWidthValue = outLastDelayWidth {
outLastDelayWidth = min(outLastDelayWidthValue, characterView.frame.center.x)
} else {
outLastDelayWidth = characterView.frame.center.x
}
}
}
}
if outLastDelayWidth != nil {
firstDelayWidth = outLastDelayWidth
}
for item in component.items {
enum AnimatedTextCharacter {
case text(String)
case icon(String, Bool, CGPoint)
var value: String {
switch self {
case let .text(value), let .icon(value, _, _):
return value
}
}
}
var itemText: [AnimatedTextCharacter] = []
switch item.content {
case let .text(text):
if item.isUnbreakable {
itemText = [.text(text)]
} else {
itemText = text.map { .text(String($0)) }
}
case let .number(value, minDigits):
var valueText: String = "\(value)"
while valueText.count < minDigits {
valueText.insert("0", at: valueText.startIndex)
}
if item.isUnbreakable {
itemText = [.text(valueText)]
} else {
itemText = valueText.map { .text(String($0)) }
}
case let .icon(iconName, tint, offset):
itemText = [.icon(iconName, tint, offset)]
}
var index = 0
characterLoop: for character in itemText {
let characterKey = CharacterKey(itemId: item.id, index: index, value: character.value)
index += 1
var characterTransition = transition
let characterView: ComponentView<Empty>
if let current = self.characters[characterKey] {
characterView = current
} else {
characterTransition = .immediate
characterView = ComponentView()
self.characters[characterKey] = characterView
}
let characterComponent: AnyComponent<Empty>
var characterOffset: CGPoint = .zero
var addTrailingSpace = false
switch character {
case let .text(text):
if text == " " {
size.height = max(size.height, ceil(spaceSize.height))
size.width += max(0.0, ceil(spaceSize.width))
continue characterLoop
} else {
if text.hasSuffix(" ") {
addTrailingSpace = true
}
characterComponent = AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: text, font: component.font, textColor: component.color))
))
}
case let .icon(iconName, tint, offset):
characterComponent = AnyComponent(BundleIconComponent(
name: iconName,
tintColor: tint ? component.color : nil
))
characterOffset = offset
}
let characterSize = characterView.update(
transition: characterTransition,
component: characterComponent,
environment: {},
containerSize: CGSize(width: availableSize.width, height: 100.0)
)
let characterFrame = CGRect(origin: CGPoint(x: size.width + characterOffset.x, y: characterOffset.y), size: characterSize)
if let characterComponentView = characterView.view {
var animateIn = false
if characterComponentView.superview == nil {
characterComponentView.layer.rasterizationScale = UIScreenScale
self.addSubview(characterComponentView)
characterComponentView.layer.anchorPoint = CGPoint()
animateIn = true
}
if characterComponentView.frame != characterFrame {
if characterTransition.animation.isImmediate {
characterComponentView.frame = characterFrame
} else {
var delayWidth: Double = 0.0
if let firstDelayWidth {
if characterFrame.midX > characterComponentView.frame.midX {
delayWidth = 0.0
} else {
delayWidth = abs(size.width - firstDelayWidth)
}
} else {
firstDelayWidth = size.width
}
characterComponentView.bounds = CGRect(origin: CGPoint(), size: characterFrame.size)
let deltaPosition = CGPoint(x: characterFrame.minX - characterComponentView.frame.minX, y: characterFrame.minY - characterComponentView.frame.minY)
characterComponentView.center = characterFrame.origin
characterComponentView.layer.animatePosition(from: CGPoint(x: -deltaPosition.x, y: -deltaPosition.y), to: CGPoint(), duration: 0.4, delay: delayNorm * delayWidth, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
}
}
characterTransition.setFrame(view: characterComponentView, frame: characterFrame)
if animateIn, !transition.animation.isImmediate {
var delayWidth: Double = 0.0
if !component.noDelay || component.preferredDirectionIsDown {
if let firstDelayWidth {
delayWidth = size.width - firstDelayWidth
} else {
firstDelayWidth = size.width
}
}
if component.animateScale {
characterComponentView.layer.animateScale(from: 0.001, to: 1.0, duration: 0.4, delay: delayNorm * delayWidth, timingFunction: kCAMediaTimingFunctionSpring)
}
if component.blur {
ComponentTransition.easeInOut(duration: 0.2).animateBlur(layer: characterComponentView.layer, from: transitionBlurRadius, to: 0.0, delay: delayNorm * delayWidth)
}
if component.animateSlide {
characterComponentView.layer.animatePosition(from: CGPoint(x: 0.0, y: characterSize.height * offsetNorm), to: CGPoint(), duration: 0.4, delay: delayNorm * delayWidth, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
}
characterComponentView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.18, delay: delayNorm * delayWidth)
}
}
size.height = max(size.height, characterSize.height)
size.width += max(0.0, characterSize.width - UIScreenPixel)
if addTrailingSpace {
size.height = max(size.height, ceil(spaceSize.height))
size.width += max(0.0, ceil(spaceSize.width))
}
}
}
let outScaleTransition: ComponentTransition = .spring(duration: 0.4)
let outAlphaTransition: ComponentTransition = .easeInOut(duration: 0.18)
var removedKeys: [CharacterKey] = []
for (key, characterView) in self.characters {
if !validKeys.contains(key) {
removedKeys.append(key)
if let characterComponentView = characterView.view {
if !transition.animation.isImmediate {
var delayWidth: Double = 0.0
if let outFirstDelayWidth {
delayWidth = abs(characterComponentView.frame.midX - outFirstDelayWidth)
} else {
outFirstDelayWidth = characterComponentView.frame.midX
}
delayWidth = max(0.0, delayWidth)
if component.animateScale {
outScaleTransition.setScale(view: characterComponentView, scale: 0.01, delay: delayNorm * delayWidth)
}
let targetY: CGFloat
if component.preferredDirectionIsDown {
targetY = characterComponentView.center.y - characterComponentView.bounds.height * offsetNorm
} else {
targetY = characterComponentView.center.y - characterComponentView.bounds.height * offsetNorm
}
if component.animateSlide {
outScaleTransition.setPosition(view: characterComponentView, position: CGPoint(x: characterComponentView.center.x, y: targetY), delay: delayNorm * delayWidth)
}
outAlphaTransition.setAlpha(view: characterComponentView, alpha: 0.0, delay: delayNorm * delayWidth, completion: { [weak characterComponentView] _ in
characterComponentView?.removeFromSuperview()
})
if component.blur {
outAlphaTransition.animateBlur(layer: characterComponentView.layer, from: 0.0, to: transitionBlurRadius, delay: delayNorm * delayWidth, removeOnCompletion: false)
}
} else {
characterComponentView.removeFromSuperview()
}
}
}
}
for removedKey in removedKeys {
self.characters.removeValue(forKey: removedKey)
}
return size
}
}
public func makeView() -> View {
return View(frame: CGRect())
}
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
public extension AnimatedTextComponent {
static func extractAnimatedTextString(string: PresentationStrings.FormattedString, id: String, mapping: [Int: AnimatedTextComponent.Item.Content]) -> [AnimatedTextComponent.Item] {
var textItems: [AnimatedTextComponent.Item] = []
var previousIndex = 0
let nsString = string.string as NSString
for range in string.ranges.sorted(by: { $0.range.lowerBound < $1.range.lowerBound }) {
if range.range.lowerBound > previousIndex {
textItems.append(AnimatedTextComponent.Item(id: AnyHashable("\(id)_text_before_\(range.index)"), isUnbreakable: true, content: .text(nsString.substring(with: NSRange(location: previousIndex, length: range.range.lowerBound - previousIndex)))))
}
if let value = mapping[range.index] {
let isUnbreakable: Bool
switch value {
case .text:
isUnbreakable = true
case .number:
isUnbreakable = false
case .icon:
isUnbreakable = true
}
textItems.append(AnimatedTextComponent.Item(id: AnyHashable("\(id)_item_\(range.index)"), isUnbreakable: isUnbreakable, content: value))
}
previousIndex = range.range.upperBound
}
if nsString.length > previousIndex {
textItems.append(AnimatedTextComponent.Item(id: AnyHashable("\(id)_text_end"), isUnbreakable: true, content: .text(nsString.substring(with: NSRange(location: previousIndex, length: nsString.length - previousIndex)))))
}
return textItems
}
}
@@ -0,0 +1,21 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "AnimationCache",
module_name = "AnimationCache",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
"//submodules/CryptoUtils:CryptoUtils",
"//submodules/ManagedFile:ManagedFile",
"//submodules/TelegramUI/Components/AnimationCache/ImageDCT:ImageDCT",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,28 @@
objc_library(
name = "ImageDCT",
enable_modules = True,
module_name = "ImageDCT",
srcs = glob([
"Sources/**/*.m",
"Sources/**/*.mm",
"Sources/**/*.c",
"Sources/**/*.cpp",
"Sources/**/*.h",
]),
hdrs = glob([
"PublicHeaders/**/*.h",
]),
includes = [
"PublicHeaders",
],
copts = [
],
sdk_frameworks = [
"Foundation",
"Accelerate",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,37 @@
#ifndef DctImageTransform_h
#define DctImageTransform_h
#import <Foundation/Foundation.h>
#import <ImageDCT/YuvConversion.h>
typedef NS_ENUM(NSUInteger, ImageDCTTableType) {
ImageDCTTableTypeLuma,
ImageDCTTableTypeChroma,
ImageDCTTableTypeDelta
};
@interface ImageDCTTable : NSObject
- (instancetype _Nonnull)initWithQuality:(NSInteger)quality type:(ImageDCTTableType)type;
- (instancetype _Nullable)initWithData:(NSData * _Nonnull)data;
- (NSData * _Nonnull)serializedData;
@end
@interface ImageDCT : NSObject
- (instancetype _Nonnull)initWithTable:(ImageDCTTable * _Nonnull)table;
- (void)forwardWithPixels:(uint8_t const * _Nonnull)pixels coefficients:(int16_t * _Nonnull)coefficients width:(NSInteger)width height:(NSInteger)height bytesPerRow:(NSInteger)bytesPerRow __attribute__((objc_direct));
- (void)inverseWithCoefficients:(int16_t const * _Nonnull)coefficients pixels:(uint8_t * _Nonnull)pixels width:(NSInteger)width height:(NSInteger)height coefficientsPerRow:(NSInteger)coefficientsPerRow bytesPerRow:(NSInteger)bytesPerRow __attribute__((objc_direct));
#if defined(__aarch64__)
- (void)forward4x4:(int16_t const * _Nonnull)normalizedCoefficients coefficients:(int16_t * _Nonnull)coefficients width:(NSInteger)width height:(NSInteger)height __attribute__((objc_direct));
- (void)inverse4x4Add:(int16_t const * _Nonnull)coefficients normalizedCoefficients:(int16_t * _Nonnull)normalizedCoefficients width:(NSInteger)width height:(NSInteger)height __attribute__((objc_direct));
#endif
@end
#endif /* DctImageTransform_h */
@@ -0,0 +1,25 @@
#ifndef YuvConversion_h
#define YuvConversion_h
#import <Foundation/Foundation.h>
#ifdef __cplusplus__
extern "C" {
#endif
void splitRGBAIntoYUVAPlanes(uint8_t const *argb, uint8_t *outY, uint8_t *outU, uint8_t *outV, uint8_t *outA, int width, int height, int bytesPerRow, bool restrictedRange, bool keepColorsOrder);
void combineYUVAPlanesIntoARGB(uint8_t *argb, uint8_t const *inY, uint8_t const *inU, uint8_t const *inV, uint8_t const *inA, int width, int height, int bytesPerRow);
void scaleImagePlane(uint8_t *outPlane, int outWidth, int outHeight, int outBytesPerRow, uint8_t const *inPlane, int inWidth, int inHeight, int inBytesPerRow);
void convertUInt8toInt16(uint8_t const *source, int16_t *dest, int length);
void convertInt16toUInt8(int16_t const *source, uint8_t *dest, int length);
void subtractArraysInt16(int16_t const *a, int16_t const *b, int16_t *dest, int length);
void addArraysInt16(int16_t const *a, int16_t const *b, int16_t *dest, int length);
void subtractArraysUInt8Int16(uint8_t const *a, int16_t const *b, uint8_t *dest, int length);
void addArraysUInt8Int16(uint8_t const *a, int16_t const *b, uint8_t *dest, int length);
#ifdef __cplusplus__
}
#endif
#endif /* YuvConversion_h */
@@ -0,0 +1,791 @@
#import "DCT.h"
#include "DCTCommon.h"
#include <vector>
#include <Accelerate/Accelerate.h>
#define DCTSIZE 8 /* The basic DCT block is 8x8 samples */
#define DCTSIZE2 64 /* DCTSIZE squared; # of elements in a block */
typedef unsigned short UDCTELEM;
typedef unsigned int UDCTELEM2;
typedef long JLONG;
#define MULTIPLIER short /* prefer 16-bit with SIMD for parellelism */
typedef MULTIPLIER IFAST_MULT_TYPE; /* 16 bits is OK, use short if faster */
#define IFAST_SCALE_BITS 2 /* fractional bits in scale factors */
#define CENTERJSAMPLE 128
namespace {
int flss(uint16_t val) {
int bit;
bit = 16;
if (!val)
return 0;
if (!(val & 0xff00)) {
bit -= 8;
val <<= 8;
}
if (!(val & 0xf000)) {
bit -= 4;
val <<= 4;
}
if (!(val & 0xc000)) {
bit -= 2;
val <<= 2;
}
if (!(val & 0x8000)) {
bit -= 1;
val <<= 1;
}
return bit;
}
int compute_reciprocal(uint16_t divisor, DCTELEM *dtbl) {
UDCTELEM2 fq, fr;
UDCTELEM c;
int b, r;
if (divisor == 1) {
/* divisor == 1 means unquantized, so these reciprocal/correction/shift
* values will cause the C quantization algorithm to act like the
* identity function. Since only the C quantization algorithm is used in
* these cases, the scale value is irrelevant.
*/
dtbl[DCTSIZE2 * 0] = (DCTELEM)1; /* reciprocal */
dtbl[DCTSIZE2 * 1] = (DCTELEM)0; /* correction */
dtbl[DCTSIZE2 * 2] = (DCTELEM)1; /* scale */
dtbl[DCTSIZE2 * 3] = -(DCTELEM)(sizeof(DCTELEM) * 8); /* shift */
return 0;
}
b = flss(divisor) - 1;
r = sizeof(DCTELEM) * 8 + b;
fq = ((UDCTELEM2)1 << r) / divisor;
fr = ((UDCTELEM2)1 << r) % divisor;
c = divisor / 2; /* for rounding */
if (fr == 0) { /* divisor is power of two */
/* fq will be one bit too large to fit in DCTELEM, so adjust */
fq >>= 1;
r--;
} else if (fr <= (divisor / 2U)) { /* fractional part is < 0.5 */
c++;
} else { /* fractional part is > 0.5 */
fq++;
}
dtbl[DCTSIZE2 * 0] = (DCTELEM)fq; /* reciprocal */
dtbl[DCTSIZE2 * 1] = (DCTELEM)c; /* correction + roundfactor */
#ifdef WITH_SIMD
dtbl[DCTSIZE2 * 2] = (DCTELEM)(1 << (sizeof(DCTELEM) * 8 * 2 - r)); /* scale */
#else
dtbl[DCTSIZE2 * 2] = 1;
#endif
dtbl[DCTSIZE2 * 3] = (DCTELEM)r - sizeof(DCTELEM) * 8; /* shift */
if (r <= 16) return 0;
else return 1;
}
#define DESCALE(x, n) RIGHT_SHIFT(x, n)
/* Multiply a DCTELEM variable by an JLONG constant, and immediately
* descale to yield a DCTELEM result.
*/
#define MULTIPLY(var, const) ((DCTELEM)DESCALE((var) * (const), CONST_BITS))
#define MULTIPLY16V16(var1, var2) ((var1) * (var2))
static DCTELEM std_luminance_quant_tbl[DCTSIZE2] = {
16, 11, 10, 16, 24, 40, 51, 61,
12, 12, 14, 19, 26, 58, 60, 55,
14, 13, 16, 24, 40, 57, 69, 56,
14, 17, 22, 29, 51, 87, 80, 62,
18, 22, 37, 56, 68, 109, 103, 77,
24, 35, 55, 64, 81, 104, 113, 92,
49, 64, 78, 87, 103, 121, 120, 101,
72, 92, 95, 98, 112, 100, 103, 99
};
static DCTELEM std_chrominance_quant_tbl[DCTSIZE2] = {
17, 18, 24, 47, 99, 99, 99, 99,
18, 21, 26, 66, 99, 99, 99, 99,
24, 26, 56, 99, 99, 99, 99, 99,
47, 66, 99, 99, 99, 99, 99, 99,
99, 99, 99, 99, 99, 99, 99, 99,
99, 99, 99, 99, 99, 99, 99, 99,
99, 99, 99, 99, 99, 99, 99, 99,
99, 99, 99, 99, 99, 99, 99, 99
};
static DCTELEM std_delta_quant_tbl[DCTSIZE2] = {
16, 16, 16, 16, 16, 16, 16, 16,
16, 16, 16, 16, 16, 16, 16, 16,
16, 16, 16, 16, 16, 16, 16, 16,
16, 16, 16, 16, 16, 16, 16, 16,
16, 16, 16, 16, 16, 16, 16, 16,
16, 16, 16, 16, 16, 16, 16, 16,
16, 16, 16, 16, 16, 16, 16, 16,
16, 16, 16, 16, 16, 16, 16, 16
};
int jpeg_quality_scaling(int quality)
/* Convert a user-specified quality rating to a percentage scaling factor
* for an underlying quantization table, using our recommended scaling curve.
* The input 'quality' factor should be 0 (terrible) to 100 (very good).
*/
{
/* Safety limit on quality factor. Convert 0 to 1 to avoid zero divide. */
if (quality <= 0) quality = 1;
if (quality > 100) quality = 100;
/* The basic table is used as-is (scaling 100) for a quality of 50.
* Qualities 50..100 are converted to scaling percentage 200 - 2*Q;
* note that at Q=100 the scaling is 0, which will cause jpeg_add_quant_table
* to make all the table entries 1 (hence, minimum quantization loss).
* Qualities 1..50 are converted to scaling percentage 5000/Q.
*/
if (quality < 50)
quality = 5000 / quality;
else
quality = 200 - quality * 2;
return quality;
}
void jpeg_add_quant_table(DCTELEM *qtable, DCTELEM const *basicTable, int scale_factor, bool forceBaseline)
/* Define a quantization table equal to the basic_table times
* a scale factor (given as a percentage).
* If force_baseline is TRUE, the computed quantization table entries
* are limited to 1..255 for JPEG baseline compatibility.
*/
{
int i;
long temp;
for (i = 0; i < DCTSIZE2; i++) {
temp = ((long)basicTable[i] * scale_factor + 50L) / 100L;
/* limit the values to the valid range */
if (temp <= 0L) temp = 1L;
if (temp > 32767L) temp = 32767L; /* max quantizer needed for 12 bits */
if (forceBaseline && temp > 255L)
temp = 255L; /* limit to baseline range if requested */
qtable[i] = (uint16_t)temp;
}
}
void jpeg_set_quality(DCTELEM *qtable, DCTELEM const *basicTable, int quality)
/* Set or change the 'quality' (quantization) setting, using default tables.
* This is the standard quality-adjusting entry point for typical user
* interfaces; only those who want detailed control over quantization tables
* would use the preceding three routines directly.
*/
{
/* Convert user 0-100 rating to percentage scaling */
quality = jpeg_quality_scaling(quality);
/* Set up standard quality tables */
jpeg_add_quant_table(qtable, basicTable, quality, false);
}
void getDivisors(DCTELEM *dtbl, DCTELEM const *qtable) {
#define CONST_BITS 14
#define RIGHT_SHIFT(x, shft) ((x) >> (shft))
static const int16_t aanscales[DCTSIZE2] = {
/* precomputed values scaled up by 14 bits */
16384, 22725, 21407, 19266, 16384, 12873, 8867, 4520,
22725, 31521, 29692, 26722, 22725, 17855, 12299, 6270,
21407, 29692, 27969, 25172, 21407, 16819, 11585, 5906,
19266, 26722, 25172, 22654, 19266, 15137, 10426, 5315,
16384, 22725, 21407, 19266, 16384, 12873, 8867, 4520,
12873, 17855, 16819, 15137, 12873, 10114, 6967, 3552,
8867, 12299, 11585, 10426, 8867, 6967, 4799, 2446,
4520, 6270, 5906, 5315, 4520, 3552, 2446, 1247
};
for (int i = 0; i < DCTSIZE2; i++) {
if (!compute_reciprocal(
DESCALE(MULTIPLY16V16((JLONG)qtable[i],
(JLONG)aanscales[i]),
CONST_BITS - 3), &dtbl[i])) {
}
}
}
void quantize(JCOEFPTR coef_block, DCTELEM *divisors, DCTELEM *workspace)
{
int i;
DCTELEM temp;
JCOEFPTR output_ptr = coef_block;
UDCTELEM recip, corr;
int shift;
UDCTELEM2 product;
for (i = 0; i < DCTSIZE2; i++) {
temp = workspace[i];
recip = divisors[i + DCTSIZE2 * 0];
corr = divisors[i + DCTSIZE2 * 1];
shift = divisors[i + DCTSIZE2 * 3];
if (temp < 0) {
temp = -temp;
product = (UDCTELEM2)(temp + corr) * recip;
product >>= shift + sizeof(DCTELEM) * 8;
temp = (DCTELEM)product;
temp = -temp;
} else {
product = (UDCTELEM2)(temp + corr) * recip;
product >>= shift + sizeof(DCTELEM) * 8;
temp = (DCTELEM)product;
}
output_ptr[i] = (JCOEF)temp;
}
}
void generateForwardDctData(DCTELEM const *qtable, std::vector<uint8_t> &data) {
data.resize(DCTSIZE2 * 4 * sizeof(DCTELEM));
getDivisors((DCTELEM *)data.data(), qtable);
}
void generateInverseDctData(DCTELEM const *qtable, std::vector<uint8_t> &data) {
data.resize(DCTSIZE2 * sizeof(IFAST_MULT_TYPE));
IFAST_MULT_TYPE *ifmtbl = (IFAST_MULT_TYPE *)data.data();
#define CONST_BITS 14
static const int16_t aanscales[DCTSIZE2] = {
/* precomputed values scaled up by 14 bits */
16384, 22725, 21407, 19266, 16384, 12873, 8867, 4520,
22725, 31521, 29692, 26722, 22725, 17855, 12299, 6270,
21407, 29692, 27969, 25172, 21407, 16819, 11585, 5906,
19266, 26722, 25172, 22654, 19266, 15137, 10426, 5315,
16384, 22725, 21407, 19266, 16384, 12873, 8867, 4520,
12873, 17855, 16819, 15137, 12873, 10114, 6967, 3552,
8867, 12299, 11585, 10426, 8867, 6967, 4799, 2446,
4520, 6270, 5906, 5315, 4520, 3552, 2446, 1247
};
for (int i = 0; i < DCTSIZE2; i++) {
ifmtbl[i] = (IFAST_MULT_TYPE)
DESCALE(MULTIPLY16V16((JLONG)qtable[i],
(JLONG)aanscales[i]),
CONST_BITS - IFAST_SCALE_BITS);
}
}
static const int zigZagInv[DCTSIZE2] = {
0,1,8,16,9,2,3,10,
17,24,32,25,18,11,4,5,
12,19,26,33,40,48,41,34,
27,20,13,6,7,14,21,28,
35,42,49,56,57,50,43,36,
29,22,15,23,30,37,44,51,
58,59,52,45,38,31,39,46,
53,60,61,54,47,55,62,63
};
static const int zigZag4x4Inv[4 * 4] = {
0, 1, 4, 8, 5, 2, 3, 6, 9, 12, 13, 10, 7, 11, 14, 15
};
void performForwardDct(uint8_t const *pixels, int16_t *coefficients, int width, int height, int bytesPerRow, DCTELEM *divisors) {
DCTELEM block[DCTSIZE2];
JCOEF coefBlock[DCTSIZE2];
int acOffset = (width / DCTSIZE) * (height / DCTSIZE);
for (int y = 0; y < height; y += DCTSIZE) {
for (int x = 0; x < width; x += DCTSIZE) {
for (int blockY = 0; blockY < DCTSIZE; blockY++) {
for (int blockX = 0; blockX < DCTSIZE; blockX++) {
block[blockY * DCTSIZE + blockX] = ((DCTELEM)pixels[(y + blockY) * bytesPerRow + (x + blockX)]) - CENTERJSAMPLE;
}
}
dct_jpeg_fdct_ifast(block);
quantize(coefBlock, divisors, block);
coefficients[(y / DCTSIZE) * (width / DCTSIZE) + x / DCTSIZE] = coefBlock[0];
for (int blockY = 0; blockY < DCTSIZE; blockY++) {
for (int blockX = 0; blockX < DCTSIZE; blockX++) {
if (blockX == 0 && blockY == 0) {
continue;
}
int16_t element = coefBlock[zigZagInv[blockY * DCTSIZE + blockX]];
//coefficients[(y + blockY) * bytesPerRow + (x + blockX)] = element;
coefficients[acOffset] = element;
acOffset++;
}
}
}
}
}
void performInverseDct(int16_t const * coefficients, uint8_t *pixels, int width, int height, int coefficientsPerRow, int bytesPerRow, DctAuxiliaryData *auxiliaryData, IFAST_MULT_TYPE *ifmtbl) {
DCTELEM coefficientBlock[DCTSIZE2];
JSAMPLE pixelBlock[DCTSIZE2];
int acOffset = (width / DCTSIZE) * (height / DCTSIZE);
for (int y = 0; y < height; y += DCTSIZE) {
for (int x = 0; x < width; x += DCTSIZE) {
coefficientBlock[0] = coefficients[(y / DCTSIZE) * (width / DCTSIZE) + x / DCTSIZE];
for (int blockY = 0; blockY < DCTSIZE; blockY++) {
for (int blockX = 0; blockX < DCTSIZE; blockX++) {
if (blockX == 0 && blockY == 0) {
continue;
}
int16_t element = coefficients[acOffset];
acOffset++;
coefficientBlock[zigZagInv[blockY * DCTSIZE + blockX]] = element;
}
}
dct_jpeg_idct_ifast(auxiliaryData, ifmtbl, coefficientBlock, pixelBlock);
for (int blockY = 0; blockY < DCTSIZE; blockY++) {
for (int blockX = 0; blockX < DCTSIZE; blockX++) {
pixels[(y + blockY) * bytesPerRow + (x + blockX)] = pixelBlock[blockY * DCTSIZE + blockX];
}
}
}
}
}
typedef int16_t tran_low_t;
typedef int32_t tran_high_t;
typedef int16_t tran_coef_t;
static const tran_coef_t cospi_8_64 = 15137;
static const tran_coef_t cospi_16_64 = 11585;
static const tran_coef_t cospi_24_64 = 6270;
#define DCT_CONST_BITS 14
#define DCT_CONST_ROUNDING (1 << (DCT_CONST_BITS - 1))
#define ROUND_POWER_OF_TWO(value, n) (((value) + (1 << ((n)-1))) >> (n))
static inline tran_high_t fdct_round_shift(tran_high_t input) {
tran_high_t rv = ROUND_POWER_OF_TWO(input, DCT_CONST_BITS);
// TODO(debargha, peter.derivaz): Find new bounds for this assert
// and make the bounds consts.
// assert(INT16_MIN <= rv && rv <= INT16_MAX);
return rv;
}
void vpx_fdct4x4_c(const int16_t *input, tran_low_t *output, int stride) {
// The 2D transform is done with two passes which are actually pretty
// similar. In the first one, we transform the columns and transpose
// the results. In the second one, we transform the rows. To achieve that,
// as the first pass results are transposed, we transpose the columns (that
// is the transposed rows) and transpose the results (so that it goes back
// in normal/row positions).
int pass;
// We need an intermediate buffer between passes.
tran_low_t intermediate[4 * 4];
const tran_low_t *in_low = NULL;
tran_low_t *out = intermediate;
// Do the two transform/transpose passes
for (pass = 0; pass < 2; ++pass) {
tran_high_t in_high[4]; // canbe16
tran_high_t step[4]; // canbe16
tran_high_t temp1, temp2; // needs32
int i;
for (i = 0; i < 4; ++i) {
// Load inputs.
if (pass == 0) {
in_high[0] = input[0 * stride] * 16;
in_high[1] = input[1 * stride] * 16;
in_high[2] = input[2 * stride] * 16;
in_high[3] = input[3 * stride] * 16;
if (i == 0 && in_high[0]) {
++in_high[0];
}
} else {
assert(in_low != NULL);
in_high[0] = in_low[0 * 4];
in_high[1] = in_low[1 * 4];
in_high[2] = in_low[2 * 4];
in_high[3] = in_low[3 * 4];
++in_low;
}
// Transform.
step[0] = in_high[0] + in_high[3];
step[1] = in_high[1] + in_high[2];
step[2] = in_high[1] - in_high[2];
step[3] = in_high[0] - in_high[3];
temp1 = (step[0] + step[1]) * cospi_16_64;
temp2 = (step[0] - step[1]) * cospi_16_64;
out[0] = (tran_low_t)fdct_round_shift(temp1);
out[2] = (tran_low_t)fdct_round_shift(temp2);
temp1 = step[2] * cospi_24_64 + step[3] * cospi_8_64;
temp2 = -step[2] * cospi_8_64 + step[3] * cospi_24_64;
out[1] = (tran_low_t)fdct_round_shift(temp1);
out[3] = (tran_low_t)fdct_round_shift(temp2);
// Do next column (which is a transposed row in second/horizontal pass)
++input;
out += 4;
}
// Setup in/out for next pass.
in_low = intermediate;
out = output;
}
{
int i, j;
for (i = 0; i < 4; ++i) {
for (j = 0; j < 4; ++j) output[j + i * 4] = (output[j + i * 4] + 1) >> 2;
}
}
}
#define ROUND_POWER_OF_TWO(value, n) (((value) + (1 << ((n)-1))) >> (n))
/*static inline tran_high_t dct_const_round_shift(tran_high_t input) {
tran_high_t rv = ROUND_POWER_OF_TWO(input, DCT_CONST_BITS);
return (tran_high_t)rv;
}
static inline tran_high_t check_range(tran_high_t input) {
#ifdef CONFIG_COEFFICIENT_RANGE_CHECKING
// For valid VP9 input streams, intermediate stage coefficients should always
// stay within the range of a signed 16 bit integer. Coefficients can go out
// of this range for invalid/corrupt VP9 streams. However, strictly checking
// this range for every intermediate coefficient can burdensome for a decoder,
// therefore the following assertion is only enabled when configured with
// --enable-coefficient-range-checking.
assert(INT16_MIN <= input);
assert(input <= INT16_MAX);
#endif // CONFIG_COEFFICIENT_RANGE_CHECKING
return input;
}*/
#define WRAPLOW(x) ((int32_t)check_range(x))
/*void idct4_c(const tran_low_t *input, tran_low_t *output) {
int16_t step[4];
tran_high_t temp1, temp2;
// stage 1
temp1 = ((int16_t)input[0] + (int16_t)input[2]) * cospi_16_64;
temp2 = ((int16_t)input[0] - (int16_t)input[2]) * cospi_16_64;
step[0] = WRAPLOW(dct_const_round_shift(temp1));
step[1] = WRAPLOW(dct_const_round_shift(temp2));
temp1 = (int16_t)input[1] * cospi_24_64 - (int16_t)input[3] * cospi_8_64;
temp2 = (int16_t)input[1] * cospi_8_64 + (int16_t)input[3] * cospi_24_64;
step[2] = WRAPLOW(dct_const_round_shift(temp1));
step[3] = WRAPLOW(dct_const_round_shift(temp2));
// stage 2
output[0] = WRAPLOW(step[0] + step[3]);
output[1] = WRAPLOW(step[1] + step[2]);
output[2] = WRAPLOW(step[1] - step[2]);
output[3] = WRAPLOW(step[0] - step[3]);
}
void vpx_idct4x4_16_add_c(const tran_low_t *input, tran_low_t *dest, int stride) {
int i, j;
tran_low_t out[4 * 4];
tran_low_t *outptr = out;
tran_low_t temp_in[4], temp_out[4];
// Rows
for (i = 0; i < 4; ++i) {
idct4_c(input, outptr);
input += 4;
outptr += 4;
}
// Columns
for (i = 0; i < 4; ++i) {
for (j = 0; j < 4; ++j) temp_in[j] = out[j * 4 + i];
idct4_c(temp_in, temp_out);
for (j = 0; j < 4; ++j) {
dest[j * stride + i] = ROUND_POWER_OF_TWO(temp_out[j], 4);
}
}
}*/
#if defined(__aarch64__)
static inline void transpose_s16_4x4q(int16x8_t *a0, int16x8_t *a1) {
// Swap 32 bit elements. Goes from:
// a0: 00 01 02 03 10 11 12 13
// a1: 20 21 22 23 30 31 32 33
// to:
// b0.val[0]: 00 01 20 21 10 11 30 31
// b0.val[1]: 02 03 22 23 12 13 32 33
const int32x4x2_t b0 =
vtrnq_s32(vreinterpretq_s32_s16(*a0), vreinterpretq_s32_s16(*a1));
// Swap 64 bit elements resulting in:
// c0: 00 01 20 21 02 03 22 23
// c1: 10 11 30 31 12 13 32 33
const int32x4_t c0 =
vcombine_s32(vget_low_s32(b0.val[0]), vget_low_s32(b0.val[1]));
const int32x4_t c1 =
vcombine_s32(vget_high_s32(b0.val[0]), vget_high_s32(b0.val[1]));
// Swap 16 bit elements resulting in:
// d0.val[0]: 00 10 20 30 02 12 22 32
// d0.val[1]: 01 11 21 31 03 13 23 33
const int16x8x2_t d0 =
vtrnq_s16(vreinterpretq_s16_s32(c0), vreinterpretq_s16_s32(c1));
*a0 = d0.val[0];
*a1 = d0.val[1];
}
static inline int16x8_t dct_const_round_shift_low_8(const int32x4_t *const in) {
return vcombine_s16(vrshrn_n_s32(in[0], DCT_CONST_BITS),
vrshrn_n_s32(in[1], DCT_CONST_BITS));
}
static inline void dct_const_round_shift_low_8_dual(const int32x4_t *const t32,
int16x8_t *const d0,
int16x8_t *const d1) {
*d0 = dct_const_round_shift_low_8(t32 + 0);
*d1 = dct_const_round_shift_low_8(t32 + 2);
}
static const int16_t kCospi[16] = {
16384 /* cospi_0_64 */, 15137 /* cospi_8_64 */,
11585 /* cospi_16_64 */, 6270 /* cospi_24_64 */,
16069 /* cospi_4_64 */, 13623 /* cospi_12_64 */,
-9102 /* -cospi_20_64 */, 3196 /* cospi_28_64 */,
16305 /* cospi_2_64 */, 1606 /* cospi_30_64 */,
14449 /* cospi_10_64 */, 7723 /* cospi_22_64 */,
15679 /* cospi_6_64 */, -4756 /* -cospi_26_64 */,
12665 /* cospi_14_64 */, -10394 /* -cospi_18_64 */
};
static inline void idct4x4_16_kernel_bd8(int16x8_t *const a) {
const int16x4_t cospis = vld1_s16(kCospi);
int16x4_t b[4];
int32x4_t c[4];
int16x8_t d[2];
b[0] = vget_low_s16(a[0]);
b[1] = vget_high_s16(a[0]);
b[2] = vget_low_s16(a[1]);
b[3] = vget_high_s16(a[1]);
c[0] = vmull_lane_s16(b[0], cospis, 2);
c[2] = vmull_lane_s16(b[1], cospis, 2);
c[1] = vsubq_s32(c[0], c[2]);
c[0] = vaddq_s32(c[0], c[2]);
c[3] = vmull_lane_s16(b[2], cospis, 3);
c[2] = vmull_lane_s16(b[2], cospis, 1);
c[3] = vmlsl_lane_s16(c[3], b[3], cospis, 1);
c[2] = vmlal_lane_s16(c[2], b[3], cospis, 3);
dct_const_round_shift_low_8_dual(c, &d[0], &d[1]);
a[0] = vaddq_s16(d[0], d[1]);
a[1] = vsubq_s16(d[0], d[1]);
}
static inline void transpose_idct4x4_16_bd8(int16x8_t *const a) {
transpose_s16_4x4q(&a[0], &a[1]);
idct4x4_16_kernel_bd8(a);
}
inline void vpx_idct4x4_16_add_neon(const int16x8_t &top64, const int16x8_t &bottom64, const int16x4_t &current0, const int16x4_t &current1, const int16x4_t &current2, const int16x4_t &current3, int16_t multiplier, int16_t *dest, int destRowIncrement) {
int16x8_t a[2];
assert(!((intptr_t)dest % sizeof(uint32_t)));
int16x8_t mul = vdupq_n_s16(multiplier);
// Rows
a[0] = vmulq_s16(top64, mul);
a[1] = vmulq_s16(bottom64, mul);
transpose_idct4x4_16_bd8(a);
// Columns
a[1] = vcombine_s16(vget_high_s16(a[1]), vget_low_s16(a[1]));
transpose_idct4x4_16_bd8(a);
a[0] = vrshrq_n_s16(a[0], 4);
a[1] = vrshrq_n_s16(a[1], 4);
a[0] = vaddq_s16(a[0], vcombine_s16(current0, current1));
a[1] = vaddq_s16(a[1], vcombine_s16(current3, current2));
vst1_s16(dest + destRowIncrement * 0, vget_low_s16(a[0]));
vst1_s16(dest + destRowIncrement * 1, vget_high_s16(a[0]));
vst1_s16(dest + destRowIncrement * 2, vget_high_s16(a[1]));
vst1_s16(dest + destRowIncrement * 3, vget_low_s16(a[1]));
}
#endif
static int dct4x4QuantDC = 58;
static int dct4x4QuantAC = 58;
#if defined(__aarch64__)
void performForward4x4Dct(int16_t const *normalizedCoefficients, int16_t *coefficients, int width, int height, DCTELEM *divisors) {
DCTELEM block[4 * 4];
DCTELEM coefBlock[4 * 4];
for (int y = 0; y < height; y += 4) {
for (int x = 0; x < width; x += 4) {
for (int blockY = 0; blockY < 4; blockY++) {
for (int blockX = 0; blockX < 4; blockX++) {
block[blockY * 4 + blockX] = normalizedCoefficients[(y + blockY) * width + (x + blockX)];
}
}
vpx_fdct4x4_c(block, coefBlock, 4);
coefBlock[0] /= dct4x4QuantDC;
for (int blockY = 0; blockY < 4; blockY++) {
for (int blockX = 0; blockX < 4; blockX++) {
if (blockX == 0 && blockY == 0) {
continue;
}
coefBlock[blockY * 4 + blockX] /= dct4x4QuantAC;
}
}
for (int blockY = 0; blockY < 4; blockY++) {
for (int blockX = 0; blockX < 4; blockX++) {
coefficients[(y + blockY) * width + (x + blockX)] = coefBlock[zigZag4x4Inv[blockY * 4 + blockX]];
}
}
}
}
}
void performInverse4x4DctAdd(int16_t const *coefficients, int16_t *normalizedCoefficients, int width, int height, DctAuxiliaryData *auxiliaryData, IFAST_MULT_TYPE *ifmtbl) {
for (int y = 0; y < height; y += 4) {
for (int x = 0; x < width; x += 4) {
int16x4_t current0 = vld1_s16(&normalizedCoefficients[(y + 0) * width + x]);
int16x4_t current1 = vld1_s16(&normalizedCoefficients[(y + 1) * width + x]);
int16x4_t current2 = vld1_s16(&normalizedCoefficients[(y + 2) * width + x]);
int16x4_t current3 = vld1_s16(&normalizedCoefficients[(y + 3) * width + x]);
uint32x2_t sa = vld1_u32((uint32_t *)&coefficients[(y + 0) * width + x]);
uint32x2_t sb = vld1_u32((uint32_t *)&coefficients[(y + 1) * width + x]);
uint32x2_t sc = vld1_u32((uint32_t *)&coefficients[(y + 2) * width + x]);
uint32x2_t sd = vld1_u32((uint32_t *)&coefficients[(y + 3) * width + x]);
uint8x16_t top = vreinterpretq_u8_u32(vcombine_u32(sa, sb));
uint8x16_t bottom = vreinterpretq_u8_u32(vcombine_u32(sc, sd));
uint8x16x2_t quad = vzipq_u8(top, bottom);
uint8_t topReorderIndices[16] = {0, 2, 4, 6, 20, 22, 24, 26, 8, 10, 16, 18, 28, 30, 17, 19};
uint8_t bottomReorderIndices[16] = {12, 14, 1, 3, 13, 15, 21, 23, 5, 7, 9, 11, 25, 27, 29, 31};
uint8x16_t qtop = vqtbl2q_u8(quad, vld1q_u8(topReorderIndices));
uint8x16_t qbottom = vqtbl2q_u8(quad, vld1q_u8(bottomReorderIndices));
uint16x8_t qtop16 = vreinterpretq_s16_u8(qtop);
uint16x8_t qbottom16 = vreinterpretq_s16_u8(qbottom);
int16x8_t top64 = vreinterpretq_s16_u16(qtop16);
int16x8_t bottom64 = vreinterpretq_s16_u16(qbottom16);
vpx_idct4x4_16_add_neon(top64, bottom64, current0, current1, current2, current3, dct4x4QuantAC, normalizedCoefficients + y * width + x, width);
}
}
}
#endif
}
namespace dct {
DCTTable DCTTable::generate(int quality, DCTTable::Type type) {
DCTTable result;
result.table.resize(DCTSIZE2);
switch (type) {
case DCTTable::Type::Luma:
jpeg_set_quality(result.table.data(), std_luminance_quant_tbl, quality);
break;
case DCTTable::Type::Chroma:
jpeg_set_quality(result.table.data(), std_chrominance_quant_tbl, quality);
break;
case DCTTable::Type::Delta:
jpeg_set_quality(result.table.data(), std_delta_quant_tbl, quality);
break;
default:
jpeg_set_quality(result.table.data(), std_luminance_quant_tbl, quality);
break;
}
return result;
}
DCTTable DCTTable::initializeEmpty() {
DCTTable result;
result.table.resize(DCTSIZE2);
return result;
}
class DCTInternal {
public:
DCTInternal(DCTTable const &dctTable) {
auxiliaryData = createDctAuxiliaryData();
generateForwardDctData(dctTable.table.data(), forwardDctData);
generateInverseDctData(dctTable.table.data(), inverseDctData);
}
~DCTInternal() {
freeDctAuxiliaryData(auxiliaryData);
}
public:
struct DctAuxiliaryData *auxiliaryData = nullptr;
std::vector<uint8_t> forwardDctData;
std::vector<uint8_t> inverseDctData;
};
DCT::DCT(DCTTable const &dctTable) {
_internal = new DCTInternal(dctTable);
}
DCT::~DCT() {
delete _internal;
}
void DCT::forward(uint8_t const *pixels, int16_t *coefficients, int width, int height, int bytesPerRow) {
performForwardDct(pixels, coefficients, width, height, bytesPerRow, (DCTELEM *)_internal->forwardDctData.data());
}
void DCT::inverse(int16_t const *coefficients, uint8_t *pixels, int width, int height, int coefficientsPerRow, int bytesPerRow) {
performInverseDct(coefficients, pixels, width, height, coefficientsPerRow, bytesPerRow, _internal->auxiliaryData, (IFAST_MULT_TYPE *)_internal->inverseDctData.data());
}
#if defined(__aarch64__)
void DCT::forward4x4(int16_t const *normalizedCoefficients, int16_t *coefficients, int width, int height) {
performForward4x4Dct(normalizedCoefficients, coefficients, width, height, (DCTELEM *)_internal->forwardDctData.data());
}
void DCT::inverse4x4Add(int16_t const *coefficients, int16_t *normalizedCoefficients, int width, int height) {
performInverse4x4DctAdd(coefficients, normalizedCoefficients, width, height, _internal->auxiliaryData, (IFAST_MULT_TYPE *)_internal->inverseDctData.data());
}
#endif
}
@@ -0,0 +1,45 @@
#ifndef DCT_H
#define DCT_H
#include "DCTCommon.h"
#include <vector>
#include <stdint.h>
namespace dct {
class DCTInternal;
struct DCTTable {
enum class Type {
Luma,
Chroma,
Delta
};
static DCTTable generate(int quality, Type type);
static DCTTable initializeEmpty();
std::vector<int16_t> table;
};
class DCT {
public:
DCT(DCTTable const &dctTable);
~DCT();
void forward(uint8_t const *pixels, int16_t *coefficients, int width, int height, int bytesPerRow);
void inverse(int16_t const *coefficients, uint8_t *pixels, int width, int height, int coefficientsPerRow, int bytesPerRow);
#if defined(__aarch64__)
void forward4x4(int16_t const *normalizedCoefficients, int16_t *coefficients, int width, int height);
void inverse4x4Add(int16_t const *coefficients, int16_t *normalizedCoefficients, int width, int height);
#endif
private:
DCTInternal *_internal;
};
}
#endif
@@ -0,0 +1,27 @@
#ifndef DCT_COMMON_H
#define DCT_COMMON_H
#ifdef __cplusplus
extern "C" {
#endif
typedef short DCTELEM;
typedef short JCOEF;
typedef JCOEF *JCOEFPTR;
typedef unsigned char JSAMPLE;
typedef JSAMPLE *JSAMPROW;
struct DctAuxiliaryData;
struct DctAuxiliaryData *createDctAuxiliaryData();
void freeDctAuxiliaryData(struct DctAuxiliaryData *data);
void dct_jpeg_idct_ifast(struct DctAuxiliaryData *auxiliaryData, void *dct_table, JCOEFPTR coef_block, JSAMPROW output_buf);
void dct_jpeg_fdct_ifast(DCTELEM *data);
#ifdef __cplusplus
}
#endif
#endif
@@ -0,0 +1,399 @@
#import "DCTCommon.h"
#if !defined(__aarch64__)
#include <string.h>
#include <stdlib.h>
typedef long JLONG;
#define CONST_BITS 8
#define PASS1_BITS 2
#define DCTSIZE 8 /* The basic DCT block is 8x8 samples */
#define DCTSIZE2 64 /* DCTSIZE squared; # of elements in a block */
#define FIX_0_382683433 ((JLONG)98) /* FIX(0.382683433) */
#define FIX_0_541196100 ((JLONG)139) /* FIX(0.541196100) */
#define FIX_0_707106781 ((JLONG)181) /* FIX(0.707106781) */
#define FIX_1_306562965 ((JLONG)334) /* FIX(1.306562965) */
#define FIX_1_082392200 ((JLONG)277) /* FIX(1.082392200) */
#define FIX_1_414213562 ((JLONG)362) /* FIX(1.414213562) */
#define FIX_1_847759065 ((JLONG)473) /* FIX(1.847759065) */
#define FIX_2_613125930 ((JLONG)669) /* FIX(2.613125930) */
#define RIGHT_SHIFT(x, shft) ((x) >> (shft))
#define IRIGHT_SHIFT(x, shft) ((x) >> (shft))
#define DESCALE(x, n) RIGHT_SHIFT(x, n)
#define IDESCALE(x, n) ((int)IRIGHT_SHIFT(x, n))
#define MULTIPLY(var, const) ((DCTELEM)DESCALE((var) * (const), CONST_BITS))
#define MULTIPLIER short /* prefer 16-bit with SIMD for parellelism */
typedef MULTIPLIER IFAST_MULT_TYPE; /* 16 bits is OK, use short if faster */
#define DEQUANTIZE(coef, quantval) (((IFAST_MULT_TYPE)(coef)) * (quantval))
#define RANGE_MASK (MAXJSAMPLE * 4 + 3) /* 2 bits wider than legal samples */
#define MAXJSAMPLE 255
#define CENTERJSAMPLE 128
typedef JSAMPROW *JSAMPARRAY; /* ptr to some rows (a 2-D sample array) */
typedef JSAMPARRAY *JSAMPIMAGE; /* a 3-D sample array: top index is color */
#define IDCT_range_limit(cinfo) ((cinfo)->sample_range_limit + CENTERJSAMPLE)
void dct_jpeg_fdct_ifast(DCTELEM *data)
{
DCTELEM tmp0, tmp1, tmp2, tmp3, tmp4, tmp5, tmp6, tmp7;
DCTELEM tmp10, tmp11, tmp12, tmp13;
DCTELEM z1, z2, z3, z4, z5, z11, z13;
DCTELEM *dataptr;
int ctr;
/* Pass 1: process rows. */
dataptr = data;
for (ctr = DCTSIZE - 1; ctr >= 0; ctr--) {
tmp0 = dataptr[0] + dataptr[7];
tmp7 = dataptr[0] - dataptr[7];
tmp1 = dataptr[1] + dataptr[6];
tmp6 = dataptr[1] - dataptr[6];
tmp2 = dataptr[2] + dataptr[5];
tmp5 = dataptr[2] - dataptr[5];
tmp3 = dataptr[3] + dataptr[4];
tmp4 = dataptr[3] - dataptr[4];
/* Even part */
tmp10 = tmp0 + tmp3; /* phase 2 */
tmp13 = tmp0 - tmp3;
tmp11 = tmp1 + tmp2;
tmp12 = tmp1 - tmp2;
dataptr[0] = tmp10 + tmp11; /* phase 3 */
dataptr[4] = tmp10 - tmp11;
z1 = MULTIPLY(tmp12 + tmp13, FIX_0_707106781); /* c4 */
dataptr[2] = tmp13 + z1; /* phase 5 */
dataptr[6] = tmp13 - z1;
/* Odd part */
tmp10 = tmp4 + tmp5; /* phase 2 */
tmp11 = tmp5 + tmp6;
tmp12 = tmp6 + tmp7;
/* The rotator is modified from fig 4-8 to avoid extra negations. */
z5 = MULTIPLY(tmp10 - tmp12, FIX_0_382683433); /* c6 */
z2 = MULTIPLY(tmp10, FIX_0_541196100) + z5; /* c2-c6 */
z4 = MULTIPLY(tmp12, FIX_1_306562965) + z5; /* c2+c6 */
z3 = MULTIPLY(tmp11, FIX_0_707106781); /* c4 */
z11 = tmp7 + z3; /* phase 5 */
z13 = tmp7 - z3;
dataptr[5] = z13 + z2; /* phase 6 */
dataptr[3] = z13 - z2;
dataptr[1] = z11 + z4;
dataptr[7] = z11 - z4;
dataptr += DCTSIZE; /* advance pointer to next row */
}
/* Pass 2: process columns. */
dataptr = data;
for (ctr = DCTSIZE - 1; ctr >= 0; ctr--) {
tmp0 = dataptr[DCTSIZE * 0] + dataptr[DCTSIZE * 7];
tmp7 = dataptr[DCTSIZE * 0] - dataptr[DCTSIZE * 7];
tmp1 = dataptr[DCTSIZE * 1] + dataptr[DCTSIZE * 6];
tmp6 = dataptr[DCTSIZE * 1] - dataptr[DCTSIZE * 6];
tmp2 = dataptr[DCTSIZE * 2] + dataptr[DCTSIZE * 5];
tmp5 = dataptr[DCTSIZE * 2] - dataptr[DCTSIZE * 5];
tmp3 = dataptr[DCTSIZE * 3] + dataptr[DCTSIZE * 4];
tmp4 = dataptr[DCTSIZE * 3] - dataptr[DCTSIZE * 4];
/* Even part */
tmp10 = tmp0 + tmp3; /* phase 2 */
tmp13 = tmp0 - tmp3;
tmp11 = tmp1 + tmp2;
tmp12 = tmp1 - tmp2;
dataptr[DCTSIZE * 0] = tmp10 + tmp11; /* phase 3 */
dataptr[DCTSIZE * 4] = tmp10 - tmp11;
z1 = MULTIPLY(tmp12 + tmp13, FIX_0_707106781); /* c4 */
dataptr[DCTSIZE * 2] = tmp13 + z1; /* phase 5 */
dataptr[DCTSIZE * 6] = tmp13 - z1;
/* Odd part */
tmp10 = tmp4 + tmp5; /* phase 2 */
tmp11 = tmp5 + tmp6;
tmp12 = tmp6 + tmp7;
/* The rotator is modified from fig 4-8 to avoid extra negations. */
z5 = MULTIPLY(tmp10 - tmp12, FIX_0_382683433); /* c6 */
z2 = MULTIPLY(tmp10, FIX_0_541196100) + z5; /* c2-c6 */
z4 = MULTIPLY(tmp12, FIX_1_306562965) + z5; /* c2+c6 */
z3 = MULTIPLY(tmp11, FIX_0_707106781); /* c4 */
z11 = tmp7 + z3; /* phase 5 */
z13 = tmp7 - z3;
dataptr[DCTSIZE * 5] = z13 + z2; /* phase 6 */
dataptr[DCTSIZE * 3] = z13 - z2;
dataptr[DCTSIZE * 1] = z11 + z4;
dataptr[DCTSIZE * 7] = z11 - z4;
dataptr++; /* advance pointer to next column */
}
}
struct DctAuxiliaryData {
JSAMPLE *allocated_sample_range_limit;
JSAMPLE *sample_range_limit;
};
static void prepare_range_limit_table(struct DctAuxiliaryData *data)
/* Allocate and fill in the sample_range_limit table */
{
JSAMPLE *table;
int i;
table = (JSAMPLE *)malloc((5 * (MAXJSAMPLE + 1) + CENTERJSAMPLE) * sizeof(JSAMPLE));
data->allocated_sample_range_limit = table;
table += (MAXJSAMPLE + 1); /* allow negative subscripts of simple table */
data->sample_range_limit = table;
/* First segment of "simple" table: limit[x] = 0 for x < 0 */
memset(table - (MAXJSAMPLE + 1), 0, (MAXJSAMPLE + 1) * sizeof(JSAMPLE));
/* Main part of "simple" table: limit[x] = x */
for (i = 0; i <= MAXJSAMPLE; i++)
table[i] = (JSAMPLE)i;
table += CENTERJSAMPLE; /* Point to where post-IDCT table starts */
/* End of simple table, rest of first half of post-IDCT table */
for (i = CENTERJSAMPLE; i < 2 * (MAXJSAMPLE + 1); i++)
table[i] = MAXJSAMPLE;
/* Second half of post-IDCT table */
memset(table + (2 * (MAXJSAMPLE + 1)), 0,
(2 * (MAXJSAMPLE + 1) - CENTERJSAMPLE) * sizeof(JSAMPLE));
memcpy(table + (4 * (MAXJSAMPLE + 1) - CENTERJSAMPLE),
data->sample_range_limit, CENTERJSAMPLE * sizeof(JSAMPLE));
}
struct DctAuxiliaryData *createDctAuxiliaryData() {
struct DctAuxiliaryData *result = malloc(sizeof(struct DctAuxiliaryData));
memset(result, 0, sizeof(struct DctAuxiliaryData));
prepare_range_limit_table(result);
return result;
}
void freeDctAuxiliaryData(struct DctAuxiliaryData *data) {
if (data) {
free(data->allocated_sample_range_limit);
free(data);
}
}
void dct_jpeg_idct_ifast(struct DctAuxiliaryData *auxiliaryData, void *dct_table, JCOEFPTR coef_block, JSAMPROW output_buf) {
DCTELEM tmp0, tmp1, tmp2, tmp3, tmp4, tmp5, tmp6, tmp7;
DCTELEM tmp10, tmp11, tmp12, tmp13;
DCTELEM z5, z10, z11, z12, z13;
JCOEFPTR inptr;
IFAST_MULT_TYPE *quantptr;
int *wsptr;
JSAMPROW outptr;
JSAMPLE *range_limit = IDCT_range_limit(auxiliaryData);
int ctr;
int workspace[DCTSIZE2]; /* buffers data between passes */
/* Pass 1: process columns from input, store into work array. */
inptr = coef_block;
quantptr = dct_table;
wsptr = workspace;
for (ctr = DCTSIZE; ctr > 0; ctr--) {
/* Due to quantization, we will usually find that many of the input
* coefficients are zero, especially the AC terms. We can exploit this
* by short-circuiting the IDCT calculation for any column in which all
* the AC terms are zero. In that case each output is equal to the
* DC coefficient (with scale factor as needed).
* With typical images and quantization tables, half or more of the
* column DCT calculations can be simplified this way.
*/
if (inptr[DCTSIZE * 1] == 0 && inptr[DCTSIZE * 2] == 0 &&
inptr[DCTSIZE * 3] == 0 && inptr[DCTSIZE * 4] == 0 &&
inptr[DCTSIZE * 5] == 0 && inptr[DCTSIZE * 6] == 0 &&
inptr[DCTSIZE * 7] == 0) {
/* AC terms all zero */
int dcval = (int)DEQUANTIZE(inptr[DCTSIZE * 0], quantptr[DCTSIZE * 0]);
wsptr[DCTSIZE * 0] = dcval;
wsptr[DCTSIZE * 1] = dcval;
wsptr[DCTSIZE * 2] = dcval;
wsptr[DCTSIZE * 3] = dcval;
wsptr[DCTSIZE * 4] = dcval;
wsptr[DCTSIZE * 5] = dcval;
wsptr[DCTSIZE * 6] = dcval;
wsptr[DCTSIZE * 7] = dcval;
inptr++; /* advance pointers to next column */
quantptr++;
wsptr++;
continue;
}
/* Even part */
tmp0 = DEQUANTIZE(inptr[DCTSIZE * 0], quantptr[DCTSIZE * 0]);
tmp1 = DEQUANTIZE(inptr[DCTSIZE * 2], quantptr[DCTSIZE * 2]);
tmp2 = DEQUANTIZE(inptr[DCTSIZE * 4], quantptr[DCTSIZE * 4]);
tmp3 = DEQUANTIZE(inptr[DCTSIZE * 6], quantptr[DCTSIZE * 6]);
tmp10 = tmp0 + tmp2; /* phase 3 */
tmp11 = tmp0 - tmp2;
tmp13 = tmp1 + tmp3; /* phases 5-3 */
tmp12 = MULTIPLY(tmp1 - tmp3, FIX_1_414213562) - tmp13; /* 2*c4 */
tmp0 = tmp10 + tmp13; /* phase 2 */
tmp3 = tmp10 - tmp13;
tmp1 = tmp11 + tmp12;
tmp2 = tmp11 - tmp12;
/* Odd part */
tmp4 = DEQUANTIZE(inptr[DCTSIZE * 1], quantptr[DCTSIZE * 1]);
tmp5 = DEQUANTIZE(inptr[DCTSIZE * 3], quantptr[DCTSIZE * 3]);
tmp6 = DEQUANTIZE(inptr[DCTSIZE * 5], quantptr[DCTSIZE * 5]);
tmp7 = DEQUANTIZE(inptr[DCTSIZE * 7], quantptr[DCTSIZE * 7]);
z13 = tmp6 + tmp5; /* phase 6 */
z10 = tmp6 - tmp5;
z11 = tmp4 + tmp7;
z12 = tmp4 - tmp7;
tmp7 = z11 + z13; /* phase 5 */
tmp11 = MULTIPLY(z11 - z13, FIX_1_414213562); /* 2*c4 */
z5 = MULTIPLY(z10 + z12, FIX_1_847759065); /* 2*c2 */
tmp10 = MULTIPLY(z12, FIX_1_082392200) - z5; /* 2*(c2-c6) */
tmp12 = MULTIPLY(z10, -FIX_2_613125930) + z5; /* -2*(c2+c6) */
tmp6 = tmp12 - tmp7; /* phase 2 */
tmp5 = tmp11 - tmp6;
tmp4 = tmp10 + tmp5;
wsptr[DCTSIZE * 0] = (int)(tmp0 + tmp7);
wsptr[DCTSIZE * 7] = (int)(tmp0 - tmp7);
wsptr[DCTSIZE * 1] = (int)(tmp1 + tmp6);
wsptr[DCTSIZE * 6] = (int)(tmp1 - tmp6);
wsptr[DCTSIZE * 2] = (int)(tmp2 + tmp5);
wsptr[DCTSIZE * 5] = (int)(tmp2 - tmp5);
wsptr[DCTSIZE * 4] = (int)(tmp3 + tmp4);
wsptr[DCTSIZE * 3] = (int)(tmp3 - tmp4);
inptr++; /* advance pointers to next column */
quantptr++;
wsptr++;
}
/* Pass 2: process rows from work array, store into output array. */
/* Note that we must descale the results by a factor of 8 == 2**3, */
/* and also undo the PASS1_BITS scaling. */
wsptr = workspace;
for (ctr = 0; ctr < DCTSIZE; ctr++) {
outptr = output_buf + ctr * DCTSIZE;
/* Rows of zeroes can be exploited in the same way as we did with columns.
* However, the column calculation has created many nonzero AC terms, so
* the simplification applies less often (typically 5% to 10% of the time).
* On machines with very fast multiplication, it's possible that the
* test takes more time than it's worth. In that case this section
* may be commented out.
*/
#ifndef NO_ZERO_ROW_TEST
if (wsptr[1] == 0 && wsptr[2] == 0 && wsptr[3] == 0 && wsptr[4] == 0 &&
wsptr[5] == 0 && wsptr[6] == 0 && wsptr[7] == 0) {
/* AC terms all zero */
JSAMPLE dcval =
range_limit[IDESCALE(wsptr[0], PASS1_BITS + 3) & RANGE_MASK];
outptr[0] = dcval;
outptr[1] = dcval;
outptr[2] = dcval;
outptr[3] = dcval;
outptr[4] = dcval;
outptr[5] = dcval;
outptr[6] = dcval;
outptr[7] = dcval;
wsptr += DCTSIZE; /* advance pointer to next row */
continue;
}
#endif
/* Even part */
tmp10 = ((DCTELEM)wsptr[0] + (DCTELEM)wsptr[4]);
tmp11 = ((DCTELEM)wsptr[0] - (DCTELEM)wsptr[4]);
tmp13 = ((DCTELEM)wsptr[2] + (DCTELEM)wsptr[6]);
tmp12 =
MULTIPLY((DCTELEM)wsptr[2] - (DCTELEM)wsptr[6], FIX_1_414213562) - tmp13;
tmp0 = tmp10 + tmp13;
tmp3 = tmp10 - tmp13;
tmp1 = tmp11 + tmp12;
tmp2 = tmp11 - tmp12;
/* Odd part */
z13 = (DCTELEM)wsptr[5] + (DCTELEM)wsptr[3];
z10 = (DCTELEM)wsptr[5] - (DCTELEM)wsptr[3];
z11 = (DCTELEM)wsptr[1] + (DCTELEM)wsptr[7];
z12 = (DCTELEM)wsptr[1] - (DCTELEM)wsptr[7];
tmp7 = z11 + z13; /* phase 5 */
tmp11 = MULTIPLY(z11 - z13, FIX_1_414213562); /* 2*c4 */
z5 = MULTIPLY(z10 + z12, FIX_1_847759065); /* 2*c2 */
tmp10 = MULTIPLY(z12, FIX_1_082392200) - z5; /* 2*(c2-c6) */
tmp12 = MULTIPLY(z10, -FIX_2_613125930) + z5; /* -2*(c2+c6) */
tmp6 = tmp12 - tmp7; /* phase 2 */
tmp5 = tmp11 - tmp6;
tmp4 = tmp10 + tmp5;
/* Final output stage: scale down by a factor of 8 and range-limit */
outptr[0] =
range_limit[IDESCALE(tmp0 + tmp7, PASS1_BITS + 3) & RANGE_MASK];
outptr[7] =
range_limit[IDESCALE(tmp0 - tmp7, PASS1_BITS + 3) & RANGE_MASK];
outptr[1] =
range_limit[IDESCALE(tmp1 + tmp6, PASS1_BITS + 3) & RANGE_MASK];
outptr[6] =
range_limit[IDESCALE(tmp1 - tmp6, PASS1_BITS + 3) & RANGE_MASK];
outptr[2] =
range_limit[IDESCALE(tmp2 + tmp5, PASS1_BITS + 3) & RANGE_MASK];
outptr[5] =
range_limit[IDESCALE(tmp2 - tmp5, PASS1_BITS + 3) & RANGE_MASK];
outptr[4] =
range_limit[IDESCALE(tmp3 + tmp4, PASS1_BITS + 3) & RANGE_MASK];
outptr[3] =
range_limit[IDESCALE(tmp3 - tmp4, PASS1_BITS + 3) & RANGE_MASK];
wsptr += DCTSIZE; /* advance pointer to next row */
}
}
#endif
@@ -0,0 +1,698 @@
#import "DCTCommon.h"
#include <stdlib.h>
#if defined(__aarch64__)
typedef long JLONG;
#define GETJSAMPLE(value) ((int)(value))
#define MAXJSAMPLE 255
#define CENTERJSAMPLE 128
typedef unsigned int JDIMENSION;
#define JPEG_MAX_DIMENSION 65500L /* a tad under 64K to prevent overflows */
#define MULTIPLIER short /* prefer 16-bit with SIMD for parellelism */
typedef MULTIPLIER IFAST_MULT_TYPE; /* 16 bits is OK, use short if faster */
#define IFAST_SCALE_BITS 2 /* fractional bits in scale factors */
/* Various constants determining the sizes of things.
* All of these are specified by the JPEG standard, so don't change them
* if you want to be compatible.
*/
#define DCTSIZE 8 /* The basic DCT block is 8x8 samples */
#define DCTSIZE2 64 /* DCTSIZE squared; # of elements in a block */
#define NUM_QUANT_TBLS 4 /* Quantization tables are numbered 0..3 */
#define NUM_HUFF_TBLS 4 /* Huffman tables are numbered 0..3 */
#define NUM_ARITH_TBLS 16 /* Arith-coding tables are numbered 0..15 */
#define MAX_COMPS_IN_SCAN 4 /* JPEG limit on # of components in one scan */
#define MAX_SAMP_FACTOR 4 /* JPEG limit on sampling factors */
/* Unfortunately, some bozo at Adobe saw no reason to be bound by the standard;
* the PostScript DCT filter can emit files with many more than 10 blocks/MCU.
* If you happen to run across such a file, you can up D_MAX_BLOCKS_IN_MCU
* to handle it. We even let you do this from the jconfig.h file. However,
* we strongly discourage changing C_MAX_BLOCKS_IN_MCU; just because Adobe
* sometimes emits noncompliant files doesn't mean you should too.
*/
#define C_MAX_BLOCKS_IN_MCU 10 /* compressor's limit on blocks per MCU */
#ifndef D_MAX_BLOCKS_IN_MCU
#define D_MAX_BLOCKS_IN_MCU 10 /* decompressor's limit on blocks per MCU */
#endif
/* Data structures for images (arrays of samples and of DCT coefficients).
*/
typedef JSAMPROW *JSAMPARRAY; /* ptr to some rows (a 2-D sample array) */
typedef JSAMPARRAY *JSAMPIMAGE; /* a 3-D sample array: top index is color */
typedef JCOEF JBLOCK[DCTSIZE2]; /* one block of coefficients */
typedef JBLOCK *JBLOCKROW; /* pointer to one row of coefficient blocks */
typedef JBLOCKROW *JBLOCKARRAY; /* a 2-D array of coefficient blocks */
typedef JBLOCKARRAY *JBLOCKIMAGE; /* a 3-D array of coefficient blocks */
#include <arm_neon.h>
/* jsimd_idct_ifast_neon() performs dequantization and a fast, not so accurate
* inverse DCT (Discrete Cosine Transform) on one block of coefficients. It
* uses the same calculations and produces exactly the same output as IJG's
* original jpeg_idct_ifast() function, which can be found in jidctfst.c.
*
* Scaled integer constants are used to avoid floating-point arithmetic:
* 0.082392200 = 2688 * 2^-15
* 0.414213562 = 13568 * 2^-15
* 0.847759065 = 27776 * 2^-15
* 0.613125930 = 20096 * 2^-15
*
* See jidctfst.c for further details of the IDCT algorithm. Where possible,
* the variable names and comments here in jsimd_idct_ifast_neon() match up
* with those in jpeg_idct_ifast().
*/
#define PASS1_BITS 2
#define F_0_082 2688
#define F_0_414 13568
#define F_0_847 27776
#define F_0_613 20096
__attribute__((aligned(16))) static const int16_t jsimd_idct_ifast_neon_consts[] = {
F_0_082, F_0_414, F_0_847, F_0_613
};
#define F_0_382 12544
#define F_0_541 17792
#define F_0_707 23168
#define F_0_306 9984
__attribute__((aligned(16))) static const int16_t jsimd_fdct_ifast_neon_consts[] = {
F_0_382, F_0_541, F_0_707, F_0_306
};
#define FIX_0_382683433 ((JLONG)98) /* FIX(0.382683433) */
#define FIX_0_541196100 ((JLONG)139) /* FIX(0.541196100) */
#define FIX_0_707106781 ((JLONG)181) /* FIX(0.707106781) */
#define FIX_1_306562965 ((JLONG)334) /* FIX(1.306562965) */
#define FIX_1_082392200 ((JLONG)277) /* FIX(1.082392200) */
#define FIX_1_414213562 ((JLONG)362) /* FIX(1.414213562) */
#define FIX_1_847759065 ((JLONG)473) /* FIX(1.847759065) */
#define FIX_2_613125930 ((JLONG)669) /* FIX(2.613125930) */
#define CONST_BITS 8
#define RIGHT_SHIFT(x, shft) ((x) >> (shft))
#define IRIGHT_SHIFT(x, shft) ((x) >> (shft))
#define DESCALE(x, n) RIGHT_SHIFT(x, n)
#define IDESCALE(x, n) ((int)IRIGHT_SHIFT(x, n))
#define MULTIPLY(var, const) ((DCTELEM)DESCALE((var) * (const), CONST_BITS))
#define DEQUANTIZE(coef, quantval) (((IFAST_MULT_TYPE)(coef)) * (quantval))
#define NO_ZERO_ROW_TEST
void dct_jpeg_fdct_ifast(DCTELEM *data) {
/* Load an 8x8 block of samples into Neon registers. De-interleaving loads
* are used, followed by vuzp to transpose the block such that we have a
* column of samples per vector - allowing all rows to be processed at once.
*/
int16x8x4_t data1 = vld4q_s16(data);
int16x8x4_t data2 = vld4q_s16(data + 4 * DCTSIZE);
int16x8x2_t cols_04 = vuzpq_s16(data1.val[0], data2.val[0]);
int16x8x2_t cols_15 = vuzpq_s16(data1.val[1], data2.val[1]);
int16x8x2_t cols_26 = vuzpq_s16(data1.val[2], data2.val[2]);
int16x8x2_t cols_37 = vuzpq_s16(data1.val[3], data2.val[3]);
int16x8_t col0 = cols_04.val[0];
int16x8_t col1 = cols_15.val[0];
int16x8_t col2 = cols_26.val[0];
int16x8_t col3 = cols_37.val[0];
int16x8_t col4 = cols_04.val[1];
int16x8_t col5 = cols_15.val[1];
int16x8_t col6 = cols_26.val[1];
int16x8_t col7 = cols_37.val[1];
/* Pass 1: process rows. */
/* Load DCT conversion constants. */
const int16x4_t consts = vld1_s16(jsimd_fdct_ifast_neon_consts);
int16x8_t tmp0 = vaddq_s16(col0, col7);
int16x8_t tmp7 = vsubq_s16(col0, col7);
int16x8_t tmp1 = vaddq_s16(col1, col6);
int16x8_t tmp6 = vsubq_s16(col1, col6);
int16x8_t tmp2 = vaddq_s16(col2, col5);
int16x8_t tmp5 = vsubq_s16(col2, col5);
int16x8_t tmp3 = vaddq_s16(col3, col4);
int16x8_t tmp4 = vsubq_s16(col3, col4);
/* Even part */
int16x8_t tmp10 = vaddq_s16(tmp0, tmp3); /* phase 2 */
int16x8_t tmp13 = vsubq_s16(tmp0, tmp3);
int16x8_t tmp11 = vaddq_s16(tmp1, tmp2);
int16x8_t tmp12 = vsubq_s16(tmp1, tmp2);
col0 = vaddq_s16(tmp10, tmp11); /* phase 3 */
col4 = vsubq_s16(tmp10, tmp11);
int16x8_t z1 = vqdmulhq_lane_s16(vaddq_s16(tmp12, tmp13), consts, 2);
col2 = vaddq_s16(tmp13, z1); /* phase 5 */
col6 = vsubq_s16(tmp13, z1);
/* Odd part */
tmp10 = vaddq_s16(tmp4, tmp5); /* phase 2 */
tmp11 = vaddq_s16(tmp5, tmp6);
tmp12 = vaddq_s16(tmp6, tmp7);
int16x8_t z5 = vqdmulhq_lane_s16(vsubq_s16(tmp10, tmp12), consts, 0);
int16x8_t z2 = vqdmulhq_lane_s16(tmp10, consts, 1);
z2 = vaddq_s16(z2, z5);
int16x8_t z4 = vqdmulhq_lane_s16(tmp12, consts, 3);
z5 = vaddq_s16(tmp12, z5);
z4 = vaddq_s16(z4, z5);
int16x8_t z3 = vqdmulhq_lane_s16(tmp11, consts, 2);
int16x8_t z11 = vaddq_s16(tmp7, z3); /* phase 5 */
int16x8_t z13 = vsubq_s16(tmp7, z3);
col5 = vaddq_s16(z13, z2); /* phase 6 */
col3 = vsubq_s16(z13, z2);
col1 = vaddq_s16(z11, z4);
col7 = vsubq_s16(z11, z4);
/* Transpose to work on columns in pass 2. */
int16x8x2_t cols_01 = vtrnq_s16(col0, col1);
int16x8x2_t cols_23 = vtrnq_s16(col2, col3);
int16x8x2_t cols_45 = vtrnq_s16(col4, col5);
int16x8x2_t cols_67 = vtrnq_s16(col6, col7);
int32x4x2_t cols_0145_l = vtrnq_s32(vreinterpretq_s32_s16(cols_01.val[0]),
vreinterpretq_s32_s16(cols_45.val[0]));
int32x4x2_t cols_0145_h = vtrnq_s32(vreinterpretq_s32_s16(cols_01.val[1]),
vreinterpretq_s32_s16(cols_45.val[1]));
int32x4x2_t cols_2367_l = vtrnq_s32(vreinterpretq_s32_s16(cols_23.val[0]),
vreinterpretq_s32_s16(cols_67.val[0]));
int32x4x2_t cols_2367_h = vtrnq_s32(vreinterpretq_s32_s16(cols_23.val[1]),
vreinterpretq_s32_s16(cols_67.val[1]));
int32x4x2_t rows_04 = vzipq_s32(cols_0145_l.val[0], cols_2367_l.val[0]);
int32x4x2_t rows_15 = vzipq_s32(cols_0145_h.val[0], cols_2367_h.val[0]);
int32x4x2_t rows_26 = vzipq_s32(cols_0145_l.val[1], cols_2367_l.val[1]);
int32x4x2_t rows_37 = vzipq_s32(cols_0145_h.val[1], cols_2367_h.val[1]);
int16x8_t row0 = vreinterpretq_s16_s32(rows_04.val[0]);
int16x8_t row1 = vreinterpretq_s16_s32(rows_15.val[0]);
int16x8_t row2 = vreinterpretq_s16_s32(rows_26.val[0]);
int16x8_t row3 = vreinterpretq_s16_s32(rows_37.val[0]);
int16x8_t row4 = vreinterpretq_s16_s32(rows_04.val[1]);
int16x8_t row5 = vreinterpretq_s16_s32(rows_15.val[1]);
int16x8_t row6 = vreinterpretq_s16_s32(rows_26.val[1]);
int16x8_t row7 = vreinterpretq_s16_s32(rows_37.val[1]);
/* Pass 2: process columns. */
tmp0 = vaddq_s16(row0, row7);
tmp7 = vsubq_s16(row0, row7);
tmp1 = vaddq_s16(row1, row6);
tmp6 = vsubq_s16(row1, row6);
tmp2 = vaddq_s16(row2, row5);
tmp5 = vsubq_s16(row2, row5);
tmp3 = vaddq_s16(row3, row4);
tmp4 = vsubq_s16(row3, row4);
/* Even part */
tmp10 = vaddq_s16(tmp0, tmp3); /* phase 2 */
tmp13 = vsubq_s16(tmp0, tmp3);
tmp11 = vaddq_s16(tmp1, tmp2);
tmp12 = vsubq_s16(tmp1, tmp2);
row0 = vaddq_s16(tmp10, tmp11); /* phase 3 */
row4 = vsubq_s16(tmp10, tmp11);
z1 = vqdmulhq_lane_s16(vaddq_s16(tmp12, tmp13), consts, 2);
row2 = vaddq_s16(tmp13, z1); /* phase 5 */
row6 = vsubq_s16(tmp13, z1);
/* Odd part */
tmp10 = vaddq_s16(tmp4, tmp5); /* phase 2 */
tmp11 = vaddq_s16(tmp5, tmp6);
tmp12 = vaddq_s16(tmp6, tmp7);
z5 = vqdmulhq_lane_s16(vsubq_s16(tmp10, tmp12), consts, 0);
z2 = vqdmulhq_lane_s16(tmp10, consts, 1);
z2 = vaddq_s16(z2, z5);
z4 = vqdmulhq_lane_s16(tmp12, consts, 3);
z5 = vaddq_s16(tmp12, z5);
z4 = vaddq_s16(z4, z5);
z3 = vqdmulhq_lane_s16(tmp11, consts, 2);
z11 = vaddq_s16(tmp7, z3); /* phase 5 */
z13 = vsubq_s16(tmp7, z3);
row5 = vaddq_s16(z13, z2); /* phase 6 */
row3 = vsubq_s16(z13, z2);
row1 = vaddq_s16(z11, z4);
row7 = vsubq_s16(z11, z4);
vst1q_s16(data + 0 * DCTSIZE, row0);
vst1q_s16(data + 1 * DCTSIZE, row1);
vst1q_s16(data + 2 * DCTSIZE, row2);
vst1q_s16(data + 3 * DCTSIZE, row3);
vst1q_s16(data + 4 * DCTSIZE, row4);
vst1q_s16(data + 5 * DCTSIZE, row5);
vst1q_s16(data + 6 * DCTSIZE, row6);
vst1q_s16(data + 7 * DCTSIZE, row7);
}
struct DctAuxiliaryData {
};
struct DctAuxiliaryData *createDctAuxiliaryData() {
struct DctAuxiliaryData *result = malloc(sizeof(struct DctAuxiliaryData));
return result;
}
void freeDctAuxiliaryData(struct DctAuxiliaryData *data) {
if (data) {
free(data);
}
}
void dct_jpeg_idct_ifast(struct DctAuxiliaryData *auxiliaryData, void *dct_table, JCOEFPTR coef_block, JSAMPROW output_buf)
{
IFAST_MULT_TYPE *quantptr = dct_table;
/* Load DCT coefficients. */
int16x8_t row0 = vld1q_s16(coef_block + 0 * DCTSIZE);
int16x8_t row1 = vld1q_s16(coef_block + 1 * DCTSIZE);
int16x8_t row2 = vld1q_s16(coef_block + 2 * DCTSIZE);
int16x8_t row3 = vld1q_s16(coef_block + 3 * DCTSIZE);
int16x8_t row4 = vld1q_s16(coef_block + 4 * DCTSIZE);
int16x8_t row5 = vld1q_s16(coef_block + 5 * DCTSIZE);
int16x8_t row6 = vld1q_s16(coef_block + 6 * DCTSIZE);
int16x8_t row7 = vld1q_s16(coef_block + 7 * DCTSIZE);
/* Load quantization table values for DC coefficients. */
int16x8_t quant_row0 = vld1q_s16(quantptr + 0 * DCTSIZE);
/* Dequantize DC coefficients. */
row0 = vmulq_s16(row0, quant_row0);
/* Construct bitmap to test if all AC coefficients are 0. */
int16x8_t bitmap = vorrq_s16(row1, row2);
bitmap = vorrq_s16(bitmap, row3);
bitmap = vorrq_s16(bitmap, row4);
bitmap = vorrq_s16(bitmap, row5);
bitmap = vorrq_s16(bitmap, row6);
bitmap = vorrq_s16(bitmap, row7);
int64_t left_ac_bitmap = vgetq_lane_s64(vreinterpretq_s64_s16(bitmap), 0);
int64_t right_ac_bitmap = vgetq_lane_s64(vreinterpretq_s64_s16(bitmap), 1);
/* Load IDCT conversion constants. */
const int16x4_t consts = vld1_s16(jsimd_idct_ifast_neon_consts);
if (left_ac_bitmap == 0 && right_ac_bitmap == 0) {
/* All AC coefficients are zero.
* Compute DC values and duplicate into vectors.
*/
int16x8_t dcval = row0;
row1 = dcval;
row2 = dcval;
row3 = dcval;
row4 = dcval;
row5 = dcval;
row6 = dcval;
row7 = dcval;
} else if (left_ac_bitmap == 0) {
/* AC coefficients are zero for columns 0, 1, 2, and 3.
* Use DC values for these columns.
*/
int16x4_t dcval = vget_low_s16(row0);
/* Commence regular fast IDCT computation for columns 4, 5, 6, and 7. */
/* Load quantization table. */
int16x4_t quant_row1 = vld1_s16(quantptr + 1 * DCTSIZE + 4);
int16x4_t quant_row2 = vld1_s16(quantptr + 2 * DCTSIZE + 4);
int16x4_t quant_row3 = vld1_s16(quantptr + 3 * DCTSIZE + 4);
int16x4_t quant_row4 = vld1_s16(quantptr + 4 * DCTSIZE + 4);
int16x4_t quant_row5 = vld1_s16(quantptr + 5 * DCTSIZE + 4);
int16x4_t quant_row6 = vld1_s16(quantptr + 6 * DCTSIZE + 4);
int16x4_t quant_row7 = vld1_s16(quantptr + 7 * DCTSIZE + 4);
/* Even part: dequantize DCT coefficients. */
int16x4_t tmp0 = vget_high_s16(row0);
int16x4_t tmp1 = vmul_s16(vget_high_s16(row2), quant_row2);
int16x4_t tmp2 = vmul_s16(vget_high_s16(row4), quant_row4);
int16x4_t tmp3 = vmul_s16(vget_high_s16(row6), quant_row6);
int16x4_t tmp10 = vadd_s16(tmp0, tmp2); /* phase 3 */
int16x4_t tmp11 = vsub_s16(tmp0, tmp2);
int16x4_t tmp13 = vadd_s16(tmp1, tmp3); /* phases 5-3 */
int16x4_t tmp1_sub_tmp3 = vsub_s16(tmp1, tmp3);
int16x4_t tmp12 = vqdmulh_lane_s16(tmp1_sub_tmp3, consts, 1);
tmp12 = vadd_s16(tmp12, tmp1_sub_tmp3);
tmp12 = vsub_s16(tmp12, tmp13);
tmp0 = vadd_s16(tmp10, tmp13); /* phase 2 */
tmp3 = vsub_s16(tmp10, tmp13);
tmp1 = vadd_s16(tmp11, tmp12);
tmp2 = vsub_s16(tmp11, tmp12);
/* Odd part: dequantize DCT coefficients. */
int16x4_t tmp4 = vmul_s16(vget_high_s16(row1), quant_row1);
int16x4_t tmp5 = vmul_s16(vget_high_s16(row3), quant_row3);
int16x4_t tmp6 = vmul_s16(vget_high_s16(row5), quant_row5);
int16x4_t tmp7 = vmul_s16(vget_high_s16(row7), quant_row7);
int16x4_t z13 = vadd_s16(tmp6, tmp5); /* phase 6 */
int16x4_t neg_z10 = vsub_s16(tmp5, tmp6);
int16x4_t z11 = vadd_s16(tmp4, tmp7);
int16x4_t z12 = vsub_s16(tmp4, tmp7);
tmp7 = vadd_s16(z11, z13); /* phase 5 */
int16x4_t z11_sub_z13 = vsub_s16(z11, z13);
tmp11 = vqdmulh_lane_s16(z11_sub_z13, consts, 1);
tmp11 = vadd_s16(tmp11, z11_sub_z13);
int16x4_t z10_add_z12 = vsub_s16(z12, neg_z10);
int16x4_t z5 = vqdmulh_lane_s16(z10_add_z12, consts, 2);
z5 = vadd_s16(z5, z10_add_z12);
tmp10 = vqdmulh_lane_s16(z12, consts, 0);
tmp10 = vadd_s16(tmp10, z12);
tmp10 = vsub_s16(tmp10, z5);
tmp12 = vqdmulh_lane_s16(neg_z10, consts, 3);
tmp12 = vadd_s16(tmp12, vadd_s16(neg_z10, neg_z10));
tmp12 = vadd_s16(tmp12, z5);
tmp6 = vsub_s16(tmp12, tmp7); /* phase 2 */
tmp5 = vsub_s16(tmp11, tmp6);
tmp4 = vadd_s16(tmp10, tmp5);
row0 = vcombine_s16(dcval, vadd_s16(tmp0, tmp7));
row7 = vcombine_s16(dcval, vsub_s16(tmp0, tmp7));
row1 = vcombine_s16(dcval, vadd_s16(tmp1, tmp6));
row6 = vcombine_s16(dcval, vsub_s16(tmp1, tmp6));
row2 = vcombine_s16(dcval, vadd_s16(tmp2, tmp5));
row5 = vcombine_s16(dcval, vsub_s16(tmp2, tmp5));
row4 = vcombine_s16(dcval, vadd_s16(tmp3, tmp4));
row3 = vcombine_s16(dcval, vsub_s16(tmp3, tmp4));
} else if (right_ac_bitmap == 0) {
/* AC coefficients are zero for columns 4, 5, 6, and 7.
* Use DC values for these columns.
*/
int16x4_t dcval = vget_high_s16(row0);
/* Commence regular fast IDCT computation for columns 0, 1, 2, and 3. */
/* Load quantization table. */
int16x4_t quant_row1 = vld1_s16(quantptr + 1 * DCTSIZE);
int16x4_t quant_row2 = vld1_s16(quantptr + 2 * DCTSIZE);
int16x4_t quant_row3 = vld1_s16(quantptr + 3 * DCTSIZE);
int16x4_t quant_row4 = vld1_s16(quantptr + 4 * DCTSIZE);
int16x4_t quant_row5 = vld1_s16(quantptr + 5 * DCTSIZE);
int16x4_t quant_row6 = vld1_s16(quantptr + 6 * DCTSIZE);
int16x4_t quant_row7 = vld1_s16(quantptr + 7 * DCTSIZE);
/* Even part: dequantize DCT coefficients. */
int16x4_t tmp0 = vget_low_s16(row0);
int16x4_t tmp1 = vmul_s16(vget_low_s16(row2), quant_row2);
int16x4_t tmp2 = vmul_s16(vget_low_s16(row4), quant_row4);
int16x4_t tmp3 = vmul_s16(vget_low_s16(row6), quant_row6);
int16x4_t tmp10 = vadd_s16(tmp0, tmp2); /* phase 3 */
int16x4_t tmp11 = vsub_s16(tmp0, tmp2);
int16x4_t tmp13 = vadd_s16(tmp1, tmp3); /* phases 5-3 */
int16x4_t tmp1_sub_tmp3 = vsub_s16(tmp1, tmp3);
int16x4_t tmp12 = vqdmulh_lane_s16(tmp1_sub_tmp3, consts, 1);
tmp12 = vadd_s16(tmp12, tmp1_sub_tmp3);
tmp12 = vsub_s16(tmp12, tmp13);
tmp0 = vadd_s16(tmp10, tmp13); /* phase 2 */
tmp3 = vsub_s16(tmp10, tmp13);
tmp1 = vadd_s16(tmp11, tmp12);
tmp2 = vsub_s16(tmp11, tmp12);
/* Odd part: dequantize DCT coefficients. */
int16x4_t tmp4 = vmul_s16(vget_low_s16(row1), quant_row1);
int16x4_t tmp5 = vmul_s16(vget_low_s16(row3), quant_row3);
int16x4_t tmp6 = vmul_s16(vget_low_s16(row5), quant_row5);
int16x4_t tmp7 = vmul_s16(vget_low_s16(row7), quant_row7);
int16x4_t z13 = vadd_s16(tmp6, tmp5); /* phase 6 */
int16x4_t neg_z10 = vsub_s16(tmp5, tmp6);
int16x4_t z11 = vadd_s16(tmp4, tmp7);
int16x4_t z12 = vsub_s16(tmp4, tmp7);
tmp7 = vadd_s16(z11, z13); /* phase 5 */
int16x4_t z11_sub_z13 = vsub_s16(z11, z13);
tmp11 = vqdmulh_lane_s16(z11_sub_z13, consts, 1);
tmp11 = vadd_s16(tmp11, z11_sub_z13);
int16x4_t z10_add_z12 = vsub_s16(z12, neg_z10);
int16x4_t z5 = vqdmulh_lane_s16(z10_add_z12, consts, 2);
z5 = vadd_s16(z5, z10_add_z12);
tmp10 = vqdmulh_lane_s16(z12, consts, 0);
tmp10 = vadd_s16(tmp10, z12);
tmp10 = vsub_s16(tmp10, z5);
tmp12 = vqdmulh_lane_s16(neg_z10, consts, 3);
tmp12 = vadd_s16(tmp12, vadd_s16(neg_z10, neg_z10));
tmp12 = vadd_s16(tmp12, z5);
tmp6 = vsub_s16(tmp12, tmp7); /* phase 2 */
tmp5 = vsub_s16(tmp11, tmp6);
tmp4 = vadd_s16(tmp10, tmp5);
row0 = vcombine_s16(vadd_s16(tmp0, tmp7), dcval);
row7 = vcombine_s16(vsub_s16(tmp0, tmp7), dcval);
row1 = vcombine_s16(vadd_s16(tmp1, tmp6), dcval);
row6 = vcombine_s16(vsub_s16(tmp1, tmp6), dcval);
row2 = vcombine_s16(vadd_s16(tmp2, tmp5), dcval);
row5 = vcombine_s16(vsub_s16(tmp2, tmp5), dcval);
row4 = vcombine_s16(vadd_s16(tmp3, tmp4), dcval);
row3 = vcombine_s16(vsub_s16(tmp3, tmp4), dcval);
} else {
/* Some AC coefficients are non-zero; full IDCT calculation required. */
/* Load quantization table. */
int16x8_t quant_row1 = vld1q_s16(quantptr + 1 * DCTSIZE);
int16x8_t quant_row2 = vld1q_s16(quantptr + 2 * DCTSIZE);
int16x8_t quant_row3 = vld1q_s16(quantptr + 3 * DCTSIZE);
int16x8_t quant_row4 = vld1q_s16(quantptr + 4 * DCTSIZE);
int16x8_t quant_row5 = vld1q_s16(quantptr + 5 * DCTSIZE);
int16x8_t quant_row6 = vld1q_s16(quantptr + 6 * DCTSIZE);
int16x8_t quant_row7 = vld1q_s16(quantptr + 7 * DCTSIZE);
/* Even part: dequantize DCT coefficients. */
int16x8_t tmp0 = row0;
int16x8_t tmp1 = vmulq_s16(row2, quant_row2);
int16x8_t tmp2 = vmulq_s16(row4, quant_row4);
int16x8_t tmp3 = vmulq_s16(row6, quant_row6);
int16x8_t tmp10 = vaddq_s16(tmp0, tmp2); /* phase 3 */
int16x8_t tmp11 = vsubq_s16(tmp0, tmp2);
int16x8_t tmp13 = vaddq_s16(tmp1, tmp3); /* phases 5-3 */
int16x8_t tmp1_sub_tmp3 = vsubq_s16(tmp1, tmp3);
int16x8_t tmp12 = vqdmulhq_lane_s16(tmp1_sub_tmp3, consts, 1);
tmp12 = vaddq_s16(tmp12, tmp1_sub_tmp3);
tmp12 = vsubq_s16(tmp12, tmp13);
tmp0 = vaddq_s16(tmp10, tmp13); /* phase 2 */
tmp3 = vsubq_s16(tmp10, tmp13);
tmp1 = vaddq_s16(tmp11, tmp12);
tmp2 = vsubq_s16(tmp11, tmp12);
/* Odd part: dequantize DCT coefficients. */
int16x8_t tmp4 = vmulq_s16(row1, quant_row1);
int16x8_t tmp5 = vmulq_s16(row3, quant_row3);
int16x8_t tmp6 = vmulq_s16(row5, quant_row5);
int16x8_t tmp7 = vmulq_s16(row7, quant_row7);
int16x8_t z13 = vaddq_s16(tmp6, tmp5); /* phase 6 */
int16x8_t neg_z10 = vsubq_s16(tmp5, tmp6);
int16x8_t z11 = vaddq_s16(tmp4, tmp7);
int16x8_t z12 = vsubq_s16(tmp4, tmp7);
tmp7 = vaddq_s16(z11, z13); /* phase 5 */
int16x8_t z11_sub_z13 = vsubq_s16(z11, z13);
tmp11 = vqdmulhq_lane_s16(z11_sub_z13, consts, 1);
tmp11 = vaddq_s16(tmp11, z11_sub_z13);
int16x8_t z10_add_z12 = vsubq_s16(z12, neg_z10);
int16x8_t z5 = vqdmulhq_lane_s16(z10_add_z12, consts, 2);
z5 = vaddq_s16(z5, z10_add_z12);
tmp10 = vqdmulhq_lane_s16(z12, consts, 0);
tmp10 = vaddq_s16(tmp10, z12);
tmp10 = vsubq_s16(tmp10, z5);
tmp12 = vqdmulhq_lane_s16(neg_z10, consts, 3);
tmp12 = vaddq_s16(tmp12, vaddq_s16(neg_z10, neg_z10));
tmp12 = vaddq_s16(tmp12, z5);
tmp6 = vsubq_s16(tmp12, tmp7); /* phase 2 */
tmp5 = vsubq_s16(tmp11, tmp6);
tmp4 = vaddq_s16(tmp10, tmp5);
row0 = vaddq_s16(tmp0, tmp7);
row7 = vsubq_s16(tmp0, tmp7);
row1 = vaddq_s16(tmp1, tmp6);
row6 = vsubq_s16(tmp1, tmp6);
row2 = vaddq_s16(tmp2, tmp5);
row5 = vsubq_s16(tmp2, tmp5);
row4 = vaddq_s16(tmp3, tmp4);
row3 = vsubq_s16(tmp3, tmp4);
}
/* Transpose rows to work on columns in pass 2. */
int16x8x2_t rows_01 = vtrnq_s16(row0, row1);
int16x8x2_t rows_23 = vtrnq_s16(row2, row3);
int16x8x2_t rows_45 = vtrnq_s16(row4, row5);
int16x8x2_t rows_67 = vtrnq_s16(row6, row7);
int32x4x2_t rows_0145_l = vtrnq_s32(vreinterpretq_s32_s16(rows_01.val[0]),
vreinterpretq_s32_s16(rows_45.val[0]));
int32x4x2_t rows_0145_h = vtrnq_s32(vreinterpretq_s32_s16(rows_01.val[1]),
vreinterpretq_s32_s16(rows_45.val[1]));
int32x4x2_t rows_2367_l = vtrnq_s32(vreinterpretq_s32_s16(rows_23.val[0]),
vreinterpretq_s32_s16(rows_67.val[0]));
int32x4x2_t rows_2367_h = vtrnq_s32(vreinterpretq_s32_s16(rows_23.val[1]),
vreinterpretq_s32_s16(rows_67.val[1]));
int32x4x2_t cols_04 = vzipq_s32(rows_0145_l.val[0], rows_2367_l.val[0]);
int32x4x2_t cols_15 = vzipq_s32(rows_0145_h.val[0], rows_2367_h.val[0]);
int32x4x2_t cols_26 = vzipq_s32(rows_0145_l.val[1], rows_2367_l.val[1]);
int32x4x2_t cols_37 = vzipq_s32(rows_0145_h.val[1], rows_2367_h.val[1]);
int16x8_t col0 = vreinterpretq_s16_s32(cols_04.val[0]);
int16x8_t col1 = vreinterpretq_s16_s32(cols_15.val[0]);
int16x8_t col2 = vreinterpretq_s16_s32(cols_26.val[0]);
int16x8_t col3 = vreinterpretq_s16_s32(cols_37.val[0]);
int16x8_t col4 = vreinterpretq_s16_s32(cols_04.val[1]);
int16x8_t col5 = vreinterpretq_s16_s32(cols_15.val[1]);
int16x8_t col6 = vreinterpretq_s16_s32(cols_26.val[1]);
int16x8_t col7 = vreinterpretq_s16_s32(cols_37.val[1]);
/* 1-D IDCT, pass 2 */
/* Even part */
int16x8_t tmp10 = vaddq_s16(col0, col4);
int16x8_t tmp11 = vsubq_s16(col0, col4);
int16x8_t tmp13 = vaddq_s16(col2, col6);
int16x8_t col2_sub_col6 = vsubq_s16(col2, col6);
int16x8_t tmp12 = vqdmulhq_lane_s16(col2_sub_col6, consts, 1);
tmp12 = vaddq_s16(tmp12, col2_sub_col6);
tmp12 = vsubq_s16(tmp12, tmp13);
int16x8_t tmp0 = vaddq_s16(tmp10, tmp13);
int16x8_t tmp3 = vsubq_s16(tmp10, tmp13);
int16x8_t tmp1 = vaddq_s16(tmp11, tmp12);
int16x8_t tmp2 = vsubq_s16(tmp11, tmp12);
/* Odd part */
int16x8_t z13 = vaddq_s16(col5, col3);
int16x8_t neg_z10 = vsubq_s16(col3, col5);
int16x8_t z11 = vaddq_s16(col1, col7);
int16x8_t z12 = vsubq_s16(col1, col7);
int16x8_t tmp7 = vaddq_s16(z11, z13); /* phase 5 */
int16x8_t z11_sub_z13 = vsubq_s16(z11, z13);
tmp11 = vqdmulhq_lane_s16(z11_sub_z13, consts, 1);
tmp11 = vaddq_s16(tmp11, z11_sub_z13);
int16x8_t z10_add_z12 = vsubq_s16(z12, neg_z10);
int16x8_t z5 = vqdmulhq_lane_s16(z10_add_z12, consts, 2);
z5 = vaddq_s16(z5, z10_add_z12);
tmp10 = vqdmulhq_lane_s16(z12, consts, 0);
tmp10 = vaddq_s16(tmp10, z12);
tmp10 = vsubq_s16(tmp10, z5);
tmp12 = vqdmulhq_lane_s16(neg_z10, consts, 3);
tmp12 = vaddq_s16(tmp12, vaddq_s16(neg_z10, neg_z10));
tmp12 = vaddq_s16(tmp12, z5);
int16x8_t tmp6 = vsubq_s16(tmp12, tmp7); /* phase 2 */
int16x8_t tmp5 = vsubq_s16(tmp11, tmp6);
int16x8_t tmp4 = vaddq_s16(tmp10, tmp5);
col0 = vaddq_s16(tmp0, tmp7);
col7 = vsubq_s16(tmp0, tmp7);
col1 = vaddq_s16(tmp1, tmp6);
col6 = vsubq_s16(tmp1, tmp6);
col2 = vaddq_s16(tmp2, tmp5);
col5 = vsubq_s16(tmp2, tmp5);
col4 = vaddq_s16(tmp3, tmp4);
col3 = vsubq_s16(tmp3, tmp4);
/* Scale down by a factor of 8, narrowing to 8-bit. */
int8x16_t cols_01_s8 = vcombine_s8(vqshrn_n_s16(col0, PASS1_BITS + 3),
vqshrn_n_s16(col1, PASS1_BITS + 3));
int8x16_t cols_45_s8 = vcombine_s8(vqshrn_n_s16(col4, PASS1_BITS + 3),
vqshrn_n_s16(col5, PASS1_BITS + 3));
int8x16_t cols_23_s8 = vcombine_s8(vqshrn_n_s16(col2, PASS1_BITS + 3),
vqshrn_n_s16(col3, PASS1_BITS + 3));
int8x16_t cols_67_s8 = vcombine_s8(vqshrn_n_s16(col6, PASS1_BITS + 3),
vqshrn_n_s16(col7, PASS1_BITS + 3));
/* Clamp to range [0-255]. */
uint8x16_t cols_01 =
vreinterpretq_u8_s8
(vaddq_s8(cols_01_s8, vreinterpretq_s8_u8(vdupq_n_u8(CENTERJSAMPLE))));
uint8x16_t cols_45 =
vreinterpretq_u8_s8
(vaddq_s8(cols_45_s8, vreinterpretq_s8_u8(vdupq_n_u8(CENTERJSAMPLE))));
uint8x16_t cols_23 =
vreinterpretq_u8_s8
(vaddq_s8(cols_23_s8, vreinterpretq_s8_u8(vdupq_n_u8(CENTERJSAMPLE))));
uint8x16_t cols_67 =
vreinterpretq_u8_s8
(vaddq_s8(cols_67_s8, vreinterpretq_s8_u8(vdupq_n_u8(CENTERJSAMPLE))));
/* Transpose block to prepare for store. */
uint32x4x2_t cols_0415 = vzipq_u32(vreinterpretq_u32_u8(cols_01),
vreinterpretq_u32_u8(cols_45));
uint32x4x2_t cols_2637 = vzipq_u32(vreinterpretq_u32_u8(cols_23),
vreinterpretq_u32_u8(cols_67));
uint8x16x2_t cols_0145 = vtrnq_u8(vreinterpretq_u8_u32(cols_0415.val[0]),
vreinterpretq_u8_u32(cols_0415.val[1]));
uint8x16x2_t cols_2367 = vtrnq_u8(vreinterpretq_u8_u32(cols_2637.val[0]),
vreinterpretq_u8_u32(cols_2637.val[1]));
uint16x8x2_t rows_0426 = vtrnq_u16(vreinterpretq_u16_u8(cols_0145.val[0]),
vreinterpretq_u16_u8(cols_2367.val[0]));
uint16x8x2_t rows_1537 = vtrnq_u16(vreinterpretq_u16_u8(cols_0145.val[1]),
vreinterpretq_u16_u8(cols_2367.val[1]));
uint8x16_t rows_04 = vreinterpretq_u8_u16(rows_0426.val[0]);
uint8x16_t rows_15 = vreinterpretq_u8_u16(rows_1537.val[0]);
uint8x16_t rows_26 = vreinterpretq_u8_u16(rows_0426.val[1]);
uint8x16_t rows_37 = vreinterpretq_u8_u16(rows_1537.val[1]);
JSAMPROW outptr0 = output_buf + DCTSIZE * 0;
JSAMPROW outptr1 = output_buf + DCTSIZE * 1;
JSAMPROW outptr2 = output_buf + DCTSIZE * 2;
JSAMPROW outptr3 = output_buf + DCTSIZE * 3;
JSAMPROW outptr4 = output_buf + DCTSIZE * 4;
JSAMPROW outptr5 = output_buf + DCTSIZE * 5;
JSAMPROW outptr6 = output_buf + DCTSIZE * 6;
JSAMPROW outptr7 = output_buf + DCTSIZE * 7;
/* Store DCT block to memory. */
vst1q_lane_u64((uint64_t *)outptr0, vreinterpretq_u64_u8(rows_04), 0);
vst1q_lane_u64((uint64_t *)outptr1, vreinterpretq_u64_u8(rows_15), 0);
vst1q_lane_u64((uint64_t *)outptr2, vreinterpretq_u64_u8(rows_26), 0);
vst1q_lane_u64((uint64_t *)outptr3, vreinterpretq_u64_u8(rows_37), 0);
vst1q_lane_u64((uint64_t *)outptr4, vreinterpretq_u64_u8(rows_04), 1);
vst1q_lane_u64((uint64_t *)outptr5, vreinterpretq_u64_u8(rows_15), 1);
vst1q_lane_u64((uint64_t *)outptr6, vreinterpretq_u64_u8(rows_26), 1);
vst1q_lane_u64((uint64_t *)outptr7, vreinterpretq_u64_u8(rows_37), 1);
}
#endif
@@ -0,0 +1,93 @@
#import <ImageDCT/ImageDCT.h>
#import <memory>
#include "DCT.h"
@interface ImageDCTTable () {
@public
dct::DCTTable _table;
}
@end
@implementation ImageDCTTable
- (instancetype _Nonnull)initWithQuality:(NSInteger)quality type:(ImageDCTTableType)type; {
self = [super init];
if (self != nil) {
dct::DCTTable::Type mappedType;
switch (type) {
case ImageDCTTableTypeLuma:
mappedType = dct::DCTTable::Type::Luma;
break;
case ImageDCTTableTypeChroma:
mappedType = dct::DCTTable::Type::Chroma;
break;
case ImageDCTTableTypeDelta:
mappedType = dct::DCTTable::Type::Delta;
break;
default:
mappedType = dct::DCTTable::Type::Luma;
break;
}
_table = dct::DCTTable::generate((int)quality, mappedType);
}
return self;
}
- (instancetype _Nullable)initWithData:(NSData * _Nonnull)data {
self = [super init];
if (self != nil) {
_table = dct::DCTTable::initializeEmpty();
if (data.length != _table.table.size() * 2) {
return nil;
}
memcpy(_table.table.data(), data.bytes, data.length);
}
return self;
}
- (NSData * _Nonnull)serializedData {
return [[NSData alloc] initWithBytes:_table.table.data() length:_table.table.size() * 2];
}
@end
@interface ImageDCT () {
std::unique_ptr<dct::DCT> _dct;
}
@end
@implementation ImageDCT
- (instancetype _Nonnull)initWithTable:(ImageDCTTable * _Nonnull)table {
self = [super init];
if (self != nil) {
_dct = std::unique_ptr<dct::DCT>(new dct::DCT(table->_table));
}
return self;
}
- (void)forwardWithPixels:(uint8_t const * _Nonnull)pixels coefficients:(int16_t * _Nonnull)coefficients width:(NSInteger)width height:(NSInteger)height bytesPerRow:(NSInteger)bytesPerRow {
_dct->forward(pixels, coefficients, (int)width, (int)height, (int)bytesPerRow);
}
- (void)inverseWithCoefficients:(int16_t const * _Nonnull)coefficients pixels:(uint8_t * _Nonnull)pixels width:(NSInteger)width height:(NSInteger)height coefficientsPerRow:(NSInteger)coefficientsPerRow bytesPerRow:(NSInteger)bytesPerRow {
_dct->inverse(coefficients, pixels, (int)width, (int)height, (int)coefficientsPerRow, (int)bytesPerRow);
}
#if defined(__aarch64__)
- (void)forward4x4:(int16_t const * _Nonnull)normalizedCoefficients coefficients:(int16_t * _Nonnull)coefficients width:(NSInteger)width height:(NSInteger)height {
_dct->forward4x4(normalizedCoefficients, coefficients, (int)width, (int)height);
}
- (void)inverse4x4Add:(int16_t const * _Nonnull)coefficients normalizedCoefficients:(int16_t * _Nonnull)normalizedCoefficients width:(NSInteger)width height:(NSInteger)height {
_dct->inverse4x4Add(coefficients, normalizedCoefficients, (int)width, (int)height);
}
#endif
@end
@@ -0,0 +1,298 @@
#import <ImageDCT/YuvConversion.h>
#import <Foundation/Foundation.h>
#import <Accelerate/Accelerate.h>
static uint8_t permuteMap[4] = { 3, 2, 1, 0 };
static uint8_t invertedPermuteMap[4] = { 3, 0, 1, 2 };
void splitRGBAIntoYUVAPlanes(uint8_t const *argb, uint8_t *outY, uint8_t *outU, uint8_t *outV, uint8_t *outA, int width, int height, int bytesPerRow, bool restrictedRange, bool keepColorsOrder) {
static vImage_ARGBToYpCbCr info;
static vImage_ARGBToYpCbCr restrictedInfo;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
vImage_YpCbCrPixelRange pixelRange = (vImage_YpCbCrPixelRange){ 0, 128, 255, 255, 255, 1, 255, 0 };
vImage_YpCbCrPixelRange restrictedPixelRange = (vImage_YpCbCrPixelRange){ 16, 128, 235, 240, 255, 0, 255, 0 };
vImageConvert_ARGBToYpCbCr_GenerateConversion(kvImage_ARGBToYpCbCrMatrix_ITU_R_709_2, &pixelRange, &info, kvImageARGB8888, kvImage420Yp8_Cb8_Cr8, 0);
vImageConvert_ARGBToYpCbCr_GenerateConversion(kvImage_ARGBToYpCbCrMatrix_ITU_R_709_2, &restrictedPixelRange, &restrictedInfo, kvImageARGB8888, kvImage420Yp8_Cb8_Cr8, 0);
});
vImage_Error error = kvImageNoError;
vImage_Buffer src;
src.data = (void *)argb;
src.width = width;
src.height = height;
src.rowBytes = bytesPerRow;
vImage_Buffer destYp;
destYp.data = outY;
destYp.width = width;
destYp.height = height;
destYp.rowBytes = width;
vImage_Buffer destCr;
destCr.data = outU;
destCr.width = width / 2;
destCr.height = height / 2;
destCr.rowBytes = width / 2;
vImage_Buffer destCb;
destCb.data = outV;
destCb.width = width / 2;
destCb.height = height / 2;
destCb.rowBytes = width / 2;
vImage_Buffer destA;
destA.data = outA;
destA.width = width;
destA.height = height;
destA.rowBytes = width;
error = vImageConvert_ARGB8888To420Yp8_Cb8_Cr8(&src, &destYp, &destCb, &destCr, restrictedRange ? &restrictedInfo : &info, keepColorsOrder ? invertedPermuteMap : permuteMap, kvImageDoNotTile);
if (error != kvImageNoError) {
return;
}
vImageExtractChannel_ARGB8888(&src, &destA, 3, kvImageDoNotTile);
}
void combineYUVAPlanesIntoARGB(uint8_t *argb, uint8_t const *inY, uint8_t const *inU, uint8_t const *inV, uint8_t const *inA, int width, int height, int bytesPerRow) {
static vImage_YpCbCrToARGB info;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
vImage_YpCbCrPixelRange pixelRange = (vImage_YpCbCrPixelRange){ 0, 128, 255, 255, 255, 1, 255, 0 };
vImageConvert_YpCbCrToARGB_GenerateConversion(kvImage_YpCbCrToARGBMatrix_ITU_R_709_2, &pixelRange, &info, kvImage420Yp8_Cb8_Cr8, kvImageARGB8888, 0);
});
vImage_Buffer destArgb;
destArgb.data = (void *)argb;
destArgb.width = width;
destArgb.height = height;
destArgb.rowBytes = bytesPerRow;
vImage_Buffer srcYp;
srcYp.data = (void *)inY;
srcYp.width = width;
srcYp.height = height;
srcYp.rowBytes = width;
vImage_Buffer srcCr;
srcCr.data = (void *)inU;
srcCr.width = width / 2;
srcCr.height = height / 2;
srcCr.rowBytes = width / 2;
vImage_Buffer srcCb;
srcCb.data = (void *)inV;
srcCb.width = width / 2;
srcCb.height = height / 2;
srcCb.rowBytes = width / 2;
vImage_Buffer srcA;
srcA.data = (void *)inA;
srcA.width = width;
srcA.height = height;
srcA.rowBytes = width;
vImageConvert_420Yp8_Cb8_Cr8ToARGB8888(&srcYp, &srcCb, &srcCr, &destArgb, &info, permuteMap, 255, kvImageDoNotTile);
vImageOverwriteChannels_ARGB8888(&srcA, &destArgb, &destArgb, 1 << 0, kvImageDoNotTile);
}
void scaleImagePlane(uint8_t *outPlane, int outWidth, int outHeight, int outBytesPerRow, uint8_t const *inPlane, int inWidth, int inHeight, int inBytesPerRow) {
vImage_Buffer src;
src.data = (void *)inPlane;
src.width = inWidth;
src.height = inHeight;
src.rowBytes = inBytesPerRow;
vImage_Buffer dst;
dst.data = (void *)outPlane;
dst.width = outWidth;
dst.height = outHeight;
dst.rowBytes = outBytesPerRow;
vImageScale_Planar8(&src, &dst, nil, kvImageDoNotTile);
}
void convertUInt8toInt16(uint8_t const *source, int16_t *dest, int length) {
#if defined(__aarch64__)
#if DEBUG
assert(!((intptr_t)source % sizeof(uint64_t)));
assert(!((intptr_t)dest % sizeof(uint64_t)));
#endif
for (int i = 0; i < length; i += 8 * 4) {
#pragma unroll
for (int j = 0; j < 4; j++) {
uint8x8_t lhs8 = vld1_u8(&source[i + j * 8]);
int16x8_t lhs = vreinterpretq_s16_u16(vmovl_u8(lhs8));
vst1q_s16(&dest[i + j * 8], lhs);
}
}
if (length % (8 * 4) != 0) {
for (int i = length - (length % (8 * 4)); i < length; i++) {
dest[i] = (int16_t)source[i];
}
}
#else
for (int i = 0; i < length; i++) {
dest[i] = (int16_t)source[i];
}
#endif
}
void convertInt16toUInt8(int16_t const *source, uint8_t *dest, int length) {
#if defined(__aarch64__)
for (int i = 0; i < length; i += 8) {
int16x8_t lhs16 = vld1q_s16(&source[i]);
int8x8_t lhs = vqmovun_s16(lhs16);
vst1_u8(&dest[i], lhs);
}
if (length % 8 != 0) {
for (int i = length - (length % 8); i < length; i++) {
int16_t result = source[i];
if (result < 0) {
result = 0;
}
if (result > 255) {
result = 255;
}
dest[i] = (int8_t)result;
}
}
#else
for (int i = 0; i < length; i++) {
int16_t result = source[i];
if (result < 0) {
result = 0;
}
if (result > 255) {
result = 255;
}
dest[i] = (int8_t)result;
}
#endif
}
void subtractArraysInt16(int16_t const *a, int16_t const *b, int16_t *dest, int length) {
#if defined(__aarch64__)
for (int i = 0; i < length; i += 8) {
int16x8_t lhs = vld1q_s16((int16_t *)&a[i]);
int16x8_t rhs = vld1q_s16((int16_t *)&b[i]);
int16x8_t result = vsubq_s16(lhs, rhs);
vst1q_s16((int16_t *)&dest[i], result);
}
if (length % 8 != 0) {
for (int i = length - (length % 8); i < length; i++) {
dest[i] = a[i] - b[i];
}
}
#else
for (int i = 0; i < length; i++) {
dest[i] = a[i] - b[i];
}
#endif
}
void addArraysInt16(int16_t const *a, int16_t const *b, int16_t *dest, int length) {
#if defined(__aarch64__)
for (int i = 0; i < length; i += 8 * 4) {
#pragma unroll
for (int j = 0; j < 4; j++) {
int16x8_t lhs = vld1q_s16((int16_t *)&a[i + j * 8]);
int16x8_t rhs = vld1q_s16((int16_t *)&b[i + j * 8]);
int16x8_t result = vaddq_s16(lhs, rhs);
vst1q_s16((int16_t *)&dest[i + j * 8], result);
}
}
if (length % (8 * 4) != 0) {
for (int i = length - (length % (8 * 4)); i < length; i++) {
dest[i] = a[i] - b[i];
}
}
#else
for (int i = 0; i < length; i++) {
dest[i] = a[i] - b[i];
}
#endif
}
void subtractArraysUInt8Int16(uint8_t const *a, int16_t const *b, uint8_t *dest, int length) {
#if defined(__aarch64__)
for (int i = 0; i < length; i += 8) {
uint8x8_t lhs8 = vld1_u8(&a[i]);
int16x8_t lhs = vreinterpretq_s16_u16(vmovl_u8(lhs8));
int16x8_t rhs = vld1q_s16((int16_t *)&b[i]);
int16x8_t result = vsubq_s16(lhs, rhs);
uint8x8_t result8 = vqmovun_s16(result);
vst1_u8(&dest[i], result8);
}
if (length % 8 != 0) {
for (int i = length - (length % 8); i < length; i++) {
int16_t result = ((int16_t)a[i]) - b[i];
if (result < 0) {
result = 0;
}
if (result > 255) {
result = 255;
}
dest[i] = (int8_t)result;
}
}
#else
for (int i = 0; i < length; i++) {
int16_t result = ((int16_t)a[i]) - b[i];
if (result < 0) {
result = 0;
}
if (result > 255) {
result = 255;
}
dest[i] = (int8_t)result;
}
#endif
}
void addArraysUInt8Int16(uint8_t const *a, int16_t const *b, uint8_t *dest, int length) {
#if defined(__aarch64__)
for (int i = 0; i < length; i += 8) {
uint8x8_t lhs8 = vld1_u8(&a[i]);
int16x8_t lhs = vreinterpretq_s16_u16(vmovl_u8(lhs8));
int16x8_t rhs = vld1q_s16((int16_t *)&b[i]);
int16x8_t result = vaddq_s16(lhs, rhs);
uint8x8_t result8 = vqmovun_s16(result);
vst1_u8(&dest[i], result8);
}
if (length % 8 != 0) {
for (int i = length - (length % 8); i < length; i++) {
int16_t result = ((int16_t)a[i]) + b[i];
if (result < 0) {
result = 0;
}
if (result > 255) {
result = 255;
}
dest[i] = (int8_t)result;
}
}
#else
for (int i = 0; i < length; i++) {
int16_t result = ((int16_t)a[i]) + b[i];
if (result < 0) {
result = 0;
}
if (result > 255) {
result = 255;
}
dest[i] = (int8_t)result;
}
#endif
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,736 @@
import Foundation
import UIKit
import ImageDCT
import Accelerate
private func alignUp(size: Int, align: Int) -> Int {
precondition(((align - 1) & align) == 0, "Align must be a power of two")
let alignmentMask = align - 1
return (size + alignmentMask) & ~alignmentMask
}
final class ImagePlane: CustomStringConvertible {
let width: Int
let height: Int
let bytesPerRow: Int
let rowAlignment: Int
let components: Int
var data: Data
init(width: Int, height: Int, components: Int, rowAlignment: Int?) {
self.width = width
self.height = height
self.rowAlignment = rowAlignment ?? 1
self.bytesPerRow = alignUp(size: width * components, align: self.rowAlignment)
self.components = components
self.data = Data(count: self.bytesPerRow * height)
}
var description: String {
return self.data.withUnsafeBytes { bytes -> String in
let pixels = bytes.baseAddress!.assumingMemoryBound(to: UInt8.self)
var result = ""
for y in 0 ..< self.height {
if y != 0 {
result.append("\n")
}
for x in 0 ..< self.width {
if x != 0 {
result.append(" ")
}
result.append(String(format: "%03d", Int(pixels[y * self.bytesPerRow + x])))
}
}
return result
}
}
}
extension ImagePlane {
func copyScaled(fromPlane plane: AnimationCacheItemFrame.Plane) {
self.data.withUnsafeMutableBytes { destBytes in
plane.data.withUnsafeBytes { srcBytes in
scaleImagePlane(destBytes.baseAddress!.assumingMemoryBound(to: UInt8.self), Int32(self.width), Int32(self.height), Int32(self.bytesPerRow), srcBytes.baseAddress!.assumingMemoryBound(to: UInt8.self), Int32(plane.width), Int32(plane.height), Int32(plane.bytesPerRow))
}
}
}
func subtract(other: DctCoefficientPlane) {
self.data.withUnsafeMutableBytes { bytes in
other.data.withUnsafeBytes { otherBytes in
subtractArraysUInt8Int16(bytes.baseAddress!.assumingMemoryBound(to: UInt8.self), otherBytes.baseAddress!.assumingMemoryBound(to: Int16.self), bytes.baseAddress!.assumingMemoryBound(to: UInt8.self), Int32(bytes.count))
}
}
}
func add(other: DctCoefficientPlane) {
self.data.withUnsafeMutableBytes { bytes in
other.data.withUnsafeBytes { otherBytes in
addArraysUInt8Int16(bytes.baseAddress!.assumingMemoryBound(to: UInt8.self), otherBytes.baseAddress!.assumingMemoryBound(to: Int16.self), bytes.baseAddress!.assumingMemoryBound(to: UInt8.self), Int32(bytes.count))
}
}
}
}
final class ImageARGB {
let argbPlane: ImagePlane
init(width: Int, height: Int, rowAlignment: Int?) {
self.argbPlane = ImagePlane(width: width, height: height, components: 4, rowAlignment: rowAlignment)
}
}
final class ImageYUVA420 {
let yPlane: ImagePlane
let uPlane: ImagePlane
let vPlane: ImagePlane
let aPlane: ImagePlane
init(width: Int, height: Int, rowAlignment: Int?) {
self.yPlane = ImagePlane(width: width, height: height, components: 1, rowAlignment: rowAlignment)
self.uPlane = ImagePlane(width: width / 2, height: height / 2, components: 1, rowAlignment: rowAlignment)
self.vPlane = ImagePlane(width: width / 2, height: height / 2, components: 1, rowAlignment: rowAlignment)
self.aPlane = ImagePlane(width: width, height: height, components: 1, rowAlignment: rowAlignment)
}
}
final class DctCoefficientPlane: CustomStringConvertible {
let width: Int
let height: Int
var data: Data
init(width: Int, height: Int) {
self.width = width
self.height = height
self.data = Data(count: width * 2 * height)
}
var description: String {
return self.data.withUnsafeBytes { bytes -> String in
let pixels = bytes.baseAddress!.assumingMemoryBound(to: Int16.self)
var result = ""
for y in 0 ..< self.height {
if y != 0 {
result.append("\n")
}
for x in 0 ..< self.width {
if x != 0 {
result.append(" ")
}
result.append(String(format: "%03d", Int(pixels[y * self.width + x])))
}
}
return result
}
}
func subtract(other: DctCoefficientPlane) {
self.data.withUnsafeMutableBytes { bytes in
other.data.withUnsafeBytes { otherBytes in
subtractArraysInt16(bytes.baseAddress!.assumingMemoryBound(to: Int16.self), otherBytes.baseAddress!.assumingMemoryBound(to: Int16.self), bytes.baseAddress!.assumingMemoryBound(to: Int16.self), Int32(bytes.count / 2))
}
}
}
func add(other: DctCoefficientPlane) {
self.data.withUnsafeMutableBytes { bytes in
other.data.withUnsafeBytes { otherBytes in
addArraysInt16(bytes.baseAddress!.assumingMemoryBound(to: Int16.self), otherBytes.baseAddress!.assumingMemoryBound(to: Int16.self), bytes.baseAddress!.assumingMemoryBound(to: Int16.self), Int32(bytes.count / 2))
}
}
}
}
extension DctCoefficientPlane {
func toFloatCoefficients(target: FloatCoefficientPlane) {
self.data.withUnsafeBytes { bytes in
target.data.withUnsafeMutableBytes { otherBytes in
vDSP_vflt16(bytes.baseAddress!.assumingMemoryBound(to: Int16.self), 1, otherBytes.baseAddress!.assumingMemoryBound(to: Float32.self), 1, vDSP_Length(bytes.count / 2))
}
}
}
func toUInt8(target: ImagePlane) {
self.data.withUnsafeBytes { bytes in
target.data.withUnsafeMutableBytes { otherBytes in
convertInt16toUInt8(bytes.baseAddress!.assumingMemoryBound(to: Int16.self), otherBytes.baseAddress!.assumingMemoryBound(to: UInt8.self), Int32(bytes.count / 2))
}
}
}
}
final class DctCoefficientsYUVA420 {
let yPlane: DctCoefficientPlane
let uPlane: DctCoefficientPlane
let vPlane: DctCoefficientPlane
let aPlane: DctCoefficientPlane
init(width: Int, height: Int) {
self.yPlane = DctCoefficientPlane(width: width, height: height)
self.uPlane = DctCoefficientPlane(width: width / 2, height: height / 2)
self.vPlane = DctCoefficientPlane(width: width / 2, height: height / 2)
self.aPlane = DctCoefficientPlane(width: width, height: height)
}
}
final class FloatCoefficientPlane: CustomStringConvertible {
let width: Int
let height: Int
var data: Data
init(width: Int, height: Int) {
self.width = width
self.height = height
self.data = Data(count: width * 4 * height)
}
var description: String {
return self.data.withUnsafeBytes { bytes -> String in
let pixels = bytes.baseAddress!.assumingMemoryBound(to: Float32.self)
var result = ""
for y in 0 ..< self.height {
if y != 0 {
result.append("\n")
}
for x in 0 ..< self.width {
if x != 0 {
result.append(" ")
}
result.append(String(format: "%03.02f", Double(pixels[y * self.width + x])))
}
}
return result
}
}
}
final class FloatCoefficientsYUVA420 {
let yPlane: FloatCoefficientPlane
let uPlane: FloatCoefficientPlane
let vPlane: FloatCoefficientPlane
let aPlane: FloatCoefficientPlane
init(width: Int, height: Int) {
self.yPlane = FloatCoefficientPlane(width: width, height: height)
self.uPlane = FloatCoefficientPlane(width: width / 2, height: height / 2)
self.vPlane = FloatCoefficientPlane(width: width / 2, height: height / 2)
self.aPlane = FloatCoefficientPlane(width: width, height: height)
}
func copy(into other: FloatCoefficientsYUVA420) {
self.yPlane.copy(into: other.yPlane)
self.uPlane.copy(into: other.uPlane)
self.vPlane.copy(into: other.vPlane)
self.aPlane.copy(into: other.aPlane)
}
}
extension FloatCoefficientPlane {
func add(constant: Float32) {
let buffer = malloc(4 * self.data.count)!
memset(buffer, Int32(bitPattern: constant.bitPattern), 4 * self.data.count)
defer {
free(buffer)
}
var constant = constant
self.data.withUnsafeMutableBytes { bytes in
vDSP_vfill(&constant, buffer.assumingMemoryBound(to: Float32.self), 1, vDSP_Length(bytes.count / 4))
vDSP_vadd(bytes.baseAddress!.assumingMemoryBound(to: Float32.self), 1, buffer.assumingMemoryBound(to: Float32.self), 1, bytes.baseAddress!.assumingMemoryBound(to: Float32.self), 1, vDSP_Length(bytes.count / 4))
}
}
func add(other: FloatCoefficientPlane) {
self.data.withUnsafeMutableBytes { bytes in
other.data.withUnsafeBytes { otherBytes in
vDSP_vadd(bytes.baseAddress!.assumingMemoryBound(to: Float32.self), 1, otherBytes.baseAddress!.assumingMemoryBound(to: Float32.self), 1, bytes.baseAddress!.assumingMemoryBound(to: Float32.self), 1, vDSP_Length(bytes.count / 4))
}
}
}
func subtract(other: FloatCoefficientPlane) {
self.data.withUnsafeMutableBytes { bytes in
other.data.withUnsafeBytes { otherBytes in
vDSP_vsub(bytes.baseAddress!.assumingMemoryBound(to: Float32.self), 1, otherBytes.baseAddress!.assumingMemoryBound(to: Float32.self), 1, bytes.baseAddress!.assumingMemoryBound(to: Float32.self), 1, vDSP_Length(bytes.count / 4))
}
}
}
func clamp() {
self.data.withUnsafeMutableBytes { bytes in
let pixels = bytes.baseAddress!.assumingMemoryBound(to: Float32.self)
var low: Float32 = 0.0
var high: Float32 = 255.0
vDSP_vclip(pixels, 1, &low, &high, pixels, 1, vDSP_Length(bytes.count / 4))
}
}
func toDctCoefficients(target: DctCoefficientPlane) {
self.data.withUnsafeBytes { bytes in
target.data.withUnsafeMutableBytes { otherBytes in
vDSP_vfix16(bytes.baseAddress!.assumingMemoryBound(to: Float32.self), 1, otherBytes.baseAddress!.assumingMemoryBound(to: Int16.self), 1, vDSP_Length(bytes.count / 4))
}
}
}
func toUInt8(target: ImagePlane) {
self.data.withUnsafeBytes { bytes in
target.data.withUnsafeMutableBytes { otherBytes in
vDSP_vfix8(bytes.baseAddress!.assumingMemoryBound(to: Float32.self), 1, otherBytes.baseAddress!.assumingMemoryBound(to: Int8.self), 1, vDSP_Length(bytes.count / 4))
}
}
}
func copy(into other: FloatCoefficientPlane) {
assert(self.data.count == other.data.count)
self.data.withUnsafeBytes { bytes in
other.data.withUnsafeMutableBytes { otherBytes in
let _ = memcpy(otherBytes.baseAddress!, bytes.baseAddress!, bytes.count)
}
}
}
}
extension FloatCoefficientsYUVA420 {
func add(constant: Float32) {
self.yPlane.add(constant: constant)
self.uPlane.add(constant: constant)
self.vPlane.add(constant: constant)
self.aPlane.add(constant: constant)
}
func add(other: FloatCoefficientsYUVA420) {
self.yPlane.add(other: other.yPlane)
self.uPlane.add(other: other.uPlane)
self.vPlane.add(other: other.vPlane)
self.aPlane.add(other: other.aPlane)
}
func subtract(other: FloatCoefficientsYUVA420) {
self.yPlane.subtract(other: other.yPlane)
self.uPlane.subtract(other: other.uPlane)
self.vPlane.subtract(other: other.vPlane)
self.aPlane.subtract(other: other.aPlane)
}
func clamp() {
self.yPlane.clamp()
self.uPlane.clamp()
self.vPlane.clamp()
self.aPlane.clamp()
}
func toDctCoefficients(target: DctCoefficientsYUVA420) {
self.yPlane.toDctCoefficients(target: target.yPlane)
self.uPlane.toDctCoefficients(target: target.uPlane)
self.vPlane.toDctCoefficients(target: target.vPlane)
self.aPlane.toDctCoefficients(target: target.aPlane)
}
func toYUVA420(target: ImageYUVA420) {
assert(self.yPlane.width == target.yPlane.width && self.yPlane.height == target.yPlane.height)
self.yPlane.toUInt8(target: target.yPlane)
self.uPlane.toUInt8(target: target.uPlane)
self.vPlane.toUInt8(target: target.vPlane)
self.aPlane.toUInt8(target: target.aPlane)
}
}
extension ImageARGB {
func toYUVA420(target: ImageYUVA420) {
precondition(self.argbPlane.width == target.yPlane.width && self.argbPlane.height == target.yPlane.height)
self.argbPlane.data.withUnsafeBytes { argbBuffer -> Void in
target.yPlane.data.withUnsafeMutableBytes { yBuffer -> Void in
target.uPlane.data.withUnsafeMutableBytes { uBuffer -> Void in
target.vPlane.data.withUnsafeMutableBytes { vBuffer -> Void in
target.aPlane.data.withUnsafeMutableBytes { aBuffer -> Void in
splitRGBAIntoYUVAPlanes(
argbBuffer.baseAddress!.assumingMemoryBound(to: UInt8.self),
yBuffer.baseAddress!.assumingMemoryBound(to: UInt8.self),
uBuffer.baseAddress!.assumingMemoryBound(to: UInt8.self),
vBuffer.baseAddress!.assumingMemoryBound(to: UInt8.self),
aBuffer.baseAddress!.assumingMemoryBound(to: UInt8.self),
Int32(self.argbPlane.width),
Int32(self.argbPlane.height),
Int32(self.argbPlane.bytesPerRow),
false,
false
)
}
}
}
}
}
}
func toYUVA420(rowAlignment: Int?) -> ImageYUVA420 {
let resultImage = ImageYUVA420(width: self.argbPlane.width, height: self.argbPlane.height, rowAlignment: rowAlignment)
self.toYUVA420(target: resultImage)
return resultImage
}
}
extension ImageYUVA420 {
func toARGB(target: ImageARGB) {
precondition(self.yPlane.width == target.argbPlane.width && self.yPlane.height == target.argbPlane.height)
self.yPlane.data.withUnsafeBytes { yBuffer -> Void in
self.uPlane.data.withUnsafeBytes { uBuffer -> Void in
self.vPlane.data.withUnsafeBytes { vBuffer -> Void in
self.aPlane.data.withUnsafeBytes { aBuffer -> Void in
target.argbPlane.data.withUnsafeMutableBytes { argbBuffer -> Void in
combineYUVAPlanesIntoARGB(
argbBuffer.baseAddress!.assumingMemoryBound(to: UInt8.self),
yBuffer.baseAddress!.assumingMemoryBound(to: UInt8.self),
uBuffer.baseAddress!.assumingMemoryBound(to: UInt8.self),
vBuffer.baseAddress!.assumingMemoryBound(to: UInt8.self),
aBuffer.baseAddress!.assumingMemoryBound(to: UInt8.self),
Int32(target.argbPlane.width),
Int32(target.argbPlane.height),
Int32(target.argbPlane.bytesPerRow)
)
}
}
}
}
}
}
func toARGB(rowAlignment: Int?) -> ImageARGB {
let resultImage = ImageARGB(width: self.yPlane.width, height: self.yPlane.height, rowAlignment: rowAlignment)
self.toARGB(target: resultImage)
return resultImage
}
}
final class DctData {
let lumaTable: ImageDCTTable
let lumaDct: ImageDCT
let chromaTable: ImageDCTTable
let chromaDct: ImageDCT
let deltaTable: ImageDCTTable
let deltaDct: ImageDCT
init?(lumaTable: Data, chromaTable: Data, deltaTable: Data) {
guard let lumaTableData = ImageDCTTable(data: lumaTable) else {
return nil
}
guard let chromaTableData = ImageDCTTable(data: chromaTable) else {
return nil
}
guard let deltaTableData = ImageDCTTable(data: deltaTable) else {
return nil
}
self.lumaTable = lumaTableData
self.lumaDct = ImageDCT(table: lumaTableData)
self.chromaTable = chromaTableData
self.chromaDct = ImageDCT(table: chromaTableData)
self.deltaTable = deltaTableData
self.deltaDct = ImageDCT(table: deltaTableData)
}
init(generatingTablesAtQualityLuma lumaQuality: Int, chroma chromaQuality: Int, delta deltaQuality: Int) {
self.lumaTable = ImageDCTTable(quality: lumaQuality, type: .luma)
self.lumaDct = ImageDCT(table: self.lumaTable)
self.chromaTable = ImageDCTTable(quality: chromaQuality, type: .chroma)
self.chromaDct = ImageDCT(table: self.chromaTable)
self.deltaTable = ImageDCTTable(quality: deltaQuality, type: .delta)
self.deltaDct = ImageDCT(table: self.deltaTable)
}
}
extension ImageYUVA420 {
func toCoefficients(target: FloatCoefficientsYUVA420) {
precondition(self.yPlane.width == target.yPlane.width && self.yPlane.height == target.yPlane.height)
for i in 0 ..< 4 {
let sourcePlane: ImagePlane
let targetPlane: FloatCoefficientPlane
switch i {
case 0:
sourcePlane = self.yPlane
targetPlane = target.yPlane
case 1:
sourcePlane = self.uPlane
targetPlane = target.uPlane
case 2:
sourcePlane = self.vPlane
targetPlane = target.vPlane
case 3:
sourcePlane = self.aPlane
targetPlane = target.aPlane
default:
preconditionFailure()
}
sourcePlane.data.withUnsafeBytes { sourceBytes in
let sourcePixels = sourceBytes.baseAddress!.assumingMemoryBound(to: UInt8.self)
targetPlane.data.withUnsafeMutableBytes { bytes in
let coefficients = bytes.baseAddress!.assumingMemoryBound(to: Float32.self)
vDSP_vfltu8(sourcePixels, 1, coefficients, 1, vDSP_Length(sourceBytes.count))
}
}
}
}
func toCoefficients(target: DctCoefficientsYUVA420) {
precondition(self.yPlane.width == target.yPlane.width && self.yPlane.height == target.yPlane.height)
for i in 0 ..< 4 {
let sourcePlane: ImagePlane
let targetPlane: DctCoefficientPlane
switch i {
case 0:
sourcePlane = self.yPlane
targetPlane = target.yPlane
case 1:
sourcePlane = self.uPlane
targetPlane = target.uPlane
case 2:
sourcePlane = self.vPlane
targetPlane = target.vPlane
case 3:
sourcePlane = self.aPlane
targetPlane = target.aPlane
default:
preconditionFailure()
}
sourcePlane.data.withUnsafeBytes { sourceBytes in
let sourcePixels = sourceBytes.baseAddress!.assumingMemoryBound(to: UInt8.self)
targetPlane.data.withUnsafeMutableBytes { bytes in
let coefficients = bytes.baseAddress!.assumingMemoryBound(to: Int16.self)
convertUInt8toInt16(sourcePixels, coefficients, Int32(sourceBytes.count))
}
}
}
}
func dct8x8(dctData: DctData, target: DctCoefficientsYUVA420) {
precondition(self.yPlane.width == target.yPlane.width && self.yPlane.height == target.yPlane.height)
for i in 0 ..< 4 {
let sourcePlane: ImagePlane
let targetPlane: DctCoefficientPlane
let isChroma: Bool
switch i {
case 0:
sourcePlane = self.yPlane
targetPlane = target.yPlane
isChroma = false
case 1:
sourcePlane = self.uPlane
targetPlane = target.uPlane
isChroma = true
case 2:
sourcePlane = self.vPlane
targetPlane = target.vPlane
isChroma = true
case 3:
sourcePlane = self.aPlane
targetPlane = target.aPlane
isChroma = false
default:
preconditionFailure()
}
sourcePlane.data.withUnsafeBytes { sourceBytes in
let sourcePixels = sourceBytes.baseAddress!.assumingMemoryBound(to: UInt8.self)
targetPlane.data.withUnsafeMutableBytes { bytes in
let coefficients = bytes.baseAddress!.assumingMemoryBound(to: Int16.self)
let dct = isChroma ? dctData.chromaDct : dctData.lumaDct
dct.forward(withPixels: sourcePixels, coefficients: coefficients, width: sourcePlane.width, height: sourcePlane.height, bytesPerRow: sourcePlane.bytesPerRow)
}
}
}
}
func subtract(other: DctCoefficientsYUVA420) {
self.yPlane.subtract(other: other.yPlane)
self.uPlane.subtract(other: other.uPlane)
self.vPlane.subtract(other: other.vPlane)
self.aPlane.subtract(other: other.aPlane)
}
func add(other: DctCoefficientsYUVA420) {
self.yPlane.add(other: other.yPlane)
self.uPlane.add(other: other.uPlane)
self.vPlane.add(other: other.vPlane)
self.aPlane.add(other: other.aPlane)
}
}
extension DctCoefficientsYUVA420 {
func idct8x8(dctData: DctData, target: ImageYUVA420) {
precondition(self.yPlane.width == target.yPlane.width && self.yPlane.height == target.yPlane.height)
for i in 0 ..< 4 {
let sourcePlane: DctCoefficientPlane
let targetPlane: ImagePlane
let isChroma: Bool
switch i {
case 0:
sourcePlane = self.yPlane
targetPlane = target.yPlane
isChroma = false
case 1:
sourcePlane = self.uPlane
targetPlane = target.uPlane
isChroma = true
case 2:
sourcePlane = self.vPlane
targetPlane = target.vPlane
isChroma = true
case 3:
sourcePlane = self.aPlane
targetPlane = target.aPlane
isChroma = false
default:
preconditionFailure()
}
sourcePlane.data.withUnsafeBytes { sourceBytes in
let coefficients = sourceBytes.baseAddress!.assumingMemoryBound(to: Int16.self)
targetPlane.data.withUnsafeMutableBytes { bytes in
let pixels = bytes.baseAddress!.assumingMemoryBound(to: UInt8.self)
let dct = isChroma ? dctData.chromaDct : dctData.lumaDct
dct.inverse(withCoefficients: coefficients, pixels: pixels, width: sourcePlane.width, height: sourcePlane.height, coefficientsPerRow: targetPlane.width, bytesPerRow: targetPlane.bytesPerRow)
}
}
}
}
func dct4x4(dctData: DctData, target: DctCoefficientsYUVA420) {
#if arch(arm64)
precondition(self.yPlane.width == target.yPlane.width && self.yPlane.height == target.yPlane.height)
for i in 0 ..< 4 {
let sourcePlane: DctCoefficientPlane
let targetPlane: DctCoefficientPlane
switch i {
case 0:
sourcePlane = self.yPlane
targetPlane = target.yPlane
case 1:
sourcePlane = self.uPlane
targetPlane = target.uPlane
case 2:
sourcePlane = self.vPlane
targetPlane = target.vPlane
case 3:
sourcePlane = self.aPlane
targetPlane = target.aPlane
default:
preconditionFailure()
}
sourcePlane.data.withUnsafeBytes { sourceBytes in
let sourceCoefficients = sourceBytes.baseAddress!.assumingMemoryBound(to: Int16.self)
targetPlane.data.withUnsafeMutableBytes { bytes in
let coefficients = bytes.baseAddress!.assumingMemoryBound(to: Int16.self)
dctData.deltaDct.forward4x4(sourceCoefficients, coefficients: coefficients, width: sourcePlane.width, height: sourcePlane.height)
}
}
}
#endif
}
func idct4x4Add(dctData: DctData, target: DctCoefficientsYUVA420) {
#if arch(arm64)
precondition(self.yPlane.width == target.yPlane.width && self.yPlane.height == target.yPlane.height)
for i in 0 ..< 4 {
let sourcePlane: DctCoefficientPlane
let targetPlane: DctCoefficientPlane
switch i {
case 0:
sourcePlane = self.yPlane
targetPlane = target.yPlane
case 1:
sourcePlane = self.uPlane
targetPlane = target.uPlane
case 2:
sourcePlane = self.vPlane
targetPlane = target.vPlane
case 3:
sourcePlane = self.aPlane
targetPlane = target.aPlane
default:
preconditionFailure()
}
sourcePlane.data.withUnsafeBytes { sourceBytes in
let sourceCoefficients = sourceBytes.baseAddress!.assumingMemoryBound(to: Int16.self)
targetPlane.data.withUnsafeMutableBytes { bytes in
let coefficients = bytes.baseAddress!.assumingMemoryBound(to: Int16.self)
//memcpy(coefficients, sourceCoefficients, sourceBytes.count)
dctData.deltaDct.inverse4x4Add(sourceCoefficients, normalizedCoefficients: coefficients, width: sourcePlane.width, height: sourcePlane.height)
}
}
}
#endif
}
func subtract(other: DctCoefficientsYUVA420) {
self.yPlane.subtract(other: other.yPlane)
self.uPlane.subtract(other: other.uPlane)
self.vPlane.subtract(other: other.vPlane)
self.aPlane.subtract(other: other.aPlane)
}
func add(other: DctCoefficientsYUVA420) {
self.yPlane.add(other: other.yPlane)
self.uPlane.add(other: other.uPlane)
self.vPlane.add(other: other.vPlane)
self.aPlane.add(other: other.aPlane)
}
func toFloatCoefficients(target: FloatCoefficientsYUVA420) {
self.yPlane.toFloatCoefficients(target: target.yPlane)
self.uPlane.toFloatCoefficients(target: target.uPlane)
self.vPlane.toFloatCoefficients(target: target.vPlane)
self.aPlane.toFloatCoefficients(target: target.aPlane)
}
func toYUVA420(target: ImageYUVA420) {
assert(self.yPlane.width == target.yPlane.width && self.yPlane.height == target.yPlane.height)
self.yPlane.toUInt8(target: target.yPlane)
self.uPlane.toUInt8(target: target.uPlane)
self.vPlane.toUInt8(target: target.vPlane)
self.aPlane.toUInt8(target: target.aPlane)
}
}
@@ -0,0 +1,23 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "AsyncListComponent",
module_name = "AsyncListComponent",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/AsyncDisplayKit",
"//submodules/Display",
"//submodules/ComponentFlow",
"//submodules/SSignalKit/SwiftSignalKit",
"//submodules/MergeLists",
"//submodules/Components/ComponentDisplayAdapters",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,703 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import ComponentFlow
import SwiftSignalKit
import MergeLists
import ComponentDisplayAdapters
public final class AsyncListComponent: Component {
public protocol ItemView: UIView {
func isReorderable(at point: CGPoint) -> Bool
}
public final class OverlayContainerView: UIView {
public override init(frame: CGRect) {
super.init(frame: frame)
self.layer.anchorPoint = CGPoint()
}
required public init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public func updatePosition(position: CGPoint, transition: ComponentTransition) {
let previousPosition: CGPoint
var forceUpdate = false
if self.layer.animation(forKey: "positionUpdate") != nil, let presentation = self.layer.presentation() {
forceUpdate = true
previousPosition = presentation.position
if !transition.animation.isImmediate {
self.layer.removeAnimation(forKey: "positionUpdate")
}
} else {
previousPosition = self.layer.position
}
if previousPosition != position || forceUpdate {
self.center = position
if case let .curve(duration, curve) = transition.animation {
self.layer.animate(
from: NSValue(cgPoint: CGPoint(x: previousPosition.x - position.x, y: previousPosition.y - position.y)),
to: NSValue(cgPoint: CGPoint()),
keyPath: "position",
duration: duration,
delay: 0.0,
curve: curve,
removeOnCompletion: true,
additive: true,
completion: nil,
key: "positionUpdate"
)
}
}
}
}
final class ResetScrollingRequest: Equatable {
let requestId: Int
let id: AnyHashable
init(requestId: Int, id: AnyHashable) {
self.requestId = requestId
self.id = id
}
static func ==(lhs: ResetScrollingRequest, rhs: ResetScrollingRequest) -> Bool {
if lhs === rhs {
return true
}
if lhs.requestId != rhs.requestId {
return false
}
if lhs.id != rhs.id {
return false
}
return true
}
}
public final class ExternalState {
public struct Value: Equatable {
var resetScrollingRequest: ResetScrollingRequest?
public static func ==(lhs: Value, rhs: Value) -> Bool {
if lhs.resetScrollingRequest != rhs.resetScrollingRequest {
return false
}
return true
}
}
public private(set) var value: Value = Value()
private var nextId: Int = 0
public init() {
}
public func resetScrolling(id: AnyHashable) {
let requestId = self.nextId
self.nextId += 1
self.value.resetScrollingRequest = ResetScrollingRequest(requestId: requestId, id: id)
}
}
public enum Direction {
case vertical
case horizontal
}
public final class VisibleItem {
public let item: AnyComponentWithIdentity<Empty>
public let frame: CGRect
init(item: AnyComponentWithIdentity<Empty>, frame: CGRect) {
self.item = item
self.frame = frame
}
}
public final class VisibleItems: Sequence, IteratorProtocol {
private let view: AsyncListComponent.View
private var index: Int = 0
private let indices: [(Int, CGRect)]
init(view: AsyncListComponent.View, direction: Direction) {
self.view = view
var indices: [(Int, CGRect)] = []
view.listNode.forEachItemNode { itemNode in
if let itemNode = itemNode as? ListItemNodeImpl, let index = itemNode.index {
var itemFrame = itemNode.frame
itemFrame.origin.y -= itemNode.transitionOffset
if let animation = itemNode.animationForKey("height") {
if let height = animation.to as? CGFloat {
itemFrame.size.height = height
}
}
if case .horizontal = direction {
itemFrame = CGRect(origin: CGPoint(x: itemFrame.minY, y: itemFrame.minX), size: CGSize(width: itemFrame.height, height: itemFrame.width))
}
indices.append((index, itemFrame))
}
}
indices.sort(by: { $0.0 < $1.0 })
self.indices = indices
}
public func next() -> VisibleItem? {
if self.index >= self.indices.count {
return nil
}
let index = self.index
self.index += 1
if let component = self.view.component {
let (itemIndex, itemFrame) = self.indices[index]
return VisibleItem(item: component.items[itemIndex], frame: itemFrame)
}
return nil
}
}
public final class VisibleItemViews: Sequence, IteratorProtocol {
private var index: Int = 0
private let itemViews: [UIView]
init(view: AsyncListComponent.View) {
var itemViews: [(Int, UIView)] = []
view.listNode.forEachItemNode { itemNode in
if let itemNode = itemNode as? ListItemNodeImpl, let index = itemNode.index {
if let itemContentView = itemNode.contentsView.view {
itemViews.append((index, itemContentView))
}
}
}
itemViews.sort(by: { $0.0 < $1.0 })
self.itemViews = itemViews.map(\.1)
}
public func next() -> UIView? {
if self.index >= self.itemViews.count {
return nil
}
let index = self.index
self.index += 1
return self.itemViews[index]
}
}
public let externalState: ExternalState
public let externalStateValue: ExternalState.Value
public let items: [AnyComponentWithIdentity<Empty>]
public let itemSetId: AnyHashable // Changing itemSetId supresses update animations
public let direction: Direction
public let insets: UIEdgeInsets
public let reorderItems: ((Int, Int) -> Bool)?
public let onVisibleItemsUpdated: ((VisibleItems, ComponentTransition) -> Void)?
public init(
externalState: ExternalState,
items: [AnyComponentWithIdentity<Empty>],
itemSetId: AnyHashable,
direction: Direction,
insets: UIEdgeInsets,
reorderItems: ((Int, Int) -> Bool)? = nil,
onVisibleItemsUpdated: ((VisibleItems, ComponentTransition) -> Void)? = nil
) {
self.externalState = externalState
self.externalStateValue = externalState.value
self.items = items
self.itemSetId = itemSetId
self.direction = direction
self.insets = insets
self.reorderItems = reorderItems
self.onVisibleItemsUpdated = onVisibleItemsUpdated
}
public static func ==(lhs: AsyncListComponent, rhs: AsyncListComponent) -> Bool {
if lhs.externalState !== rhs.externalState {
return false
}
if lhs.externalStateValue != rhs.externalStateValue {
return false
}
if lhs.items != rhs.items {
return false
}
if lhs.itemSetId != rhs.itemSetId {
return false
}
if lhs.direction != rhs.direction {
return false
}
if lhs.insets != rhs.insets {
return false
}
if (lhs.reorderItems == nil) != (rhs.reorderItems == nil) {
return false
}
return true
}
private struct ItemEntry: Comparable, Identifiable {
let contents: AnyComponentWithIdentity<Empty>
let index: Int
var id: AnyHashable {
return self.contents.id
}
var stableId: AnyHashable {
return self.id
}
static func ==(lhs: ItemEntry, rhs: ItemEntry) -> Bool {
if lhs.contents != rhs.contents {
return false
}
if lhs.index != rhs.index {
return false
}
return true
}
static func <(lhs: ItemEntry, rhs: ItemEntry) -> Bool {
return lhs.index < rhs.index
}
func item(parentView: AsyncListComponent.View?, direction: Direction) -> ListViewItem {
return ListItemImpl(parentView: parentView, contents: self.contents, direction: direction)
}
}
private final class ListItemImpl: ListViewItem {
weak var parentView: AsyncListComponent.View?
let contents: AnyComponentWithIdentity<Empty>
let direction: Direction
let selectable: Bool = false
init(parentView: AsyncListComponent.View?, contents: AnyComponentWithIdentity<Empty>, direction: Direction) {
self.parentView = parentView
self.contents = contents
self.direction = direction
}
func nodeConfiguredForParams(
async: @escaping (@escaping () -> Void) -> Void,
params: ListViewItemLayoutParams,
synchronousLoads: Bool,
previousItem: ListViewItem?,
nextItem: ListViewItem?,
completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void
) {
async {
let impl: () -> Void = {
let node = ListItemNodeImpl()
let (nodeLayout, apply) = node.asyncLayout()(self, params)
node.insets = nodeLayout.insets
node.contentSize = nodeLayout.contentSize
Queue.mainQueue().async {
completion(node, {
return (nil, { _ in
apply(false)
})
})
}
}
if Thread.isMainThread {
impl()
} else {
assert(false)
Queue.mainQueue().async {
impl()
}
}
}
}
public func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
Queue.mainQueue().async {
assert(node() is ListItemNodeImpl)
if let nodeValue = node() as? ListItemNodeImpl {
let layout = nodeValue.asyncLayout()
async {
let impl: () -> Void = {
let (nodeLayout, apply) = layout(self, params)
Queue.mainQueue().async {
completion(nodeLayout, { _ in
apply(animation.isAnimated)
})
}
}
if Thread.isMainThread {
impl()
} else {
assert(false)
Queue.mainQueue().async {
impl()
}
}
}
}
}
}
}
private final class ListItemNodeImpl: ListViewItemNode {
private let contentContainer: UIView
let contentsView = ComponentView<Empty>()
private(set) var item: ListItemImpl?
init() {
self.contentContainer = UIView()
super.init(layerBacked: false, rotated: false, seeThrough: false)
self.view.addSubview(self.contentContainer)
self.scrollPositioningInsets = UIEdgeInsets(top: -24.0, left: 0.0, bottom: -24.0, right: 0.0)
}
deinit {
}
override func isReorderable(at point: CGPoint) -> Bool {
if let itemView = self.contentsView.view as? ItemView {
return itemView.isReorderable(at: self.view.convert(point, to: itemView))
}
return false
}
override func snapshotForReordering() -> UIView? {
return self.view.snapshotView(afterScreenUpdates: false)
}
func asyncLayout() -> (ListItemImpl, ListViewItemLayoutParams) -> (ListViewItemNodeLayout, (Bool) -> Void) {
return { item, params in
let containerSize: CGSize
switch item.direction {
case .vertical:
containerSize = CGSize(width: params.width, height: 100000.0)
case .horizontal:
containerSize = CGSize(width: 100000.0, height: params.width)
}
let contentsSize = self.contentsView.update(
transition: .immediate,
component: item.contents.component,
environment: {},
containerSize: containerSize
)
let mappedContentsSize: CGSize
switch item.direction {
case .vertical:
mappedContentsSize = CGSize(width: params.width, height: contentsSize.height)
case .horizontal:
mappedContentsSize = CGSize(width: params.width, height: contentsSize.width)
}
let itemLayout = ListViewItemNodeLayout(contentSize: mappedContentsSize, insets: UIEdgeInsets())
return (itemLayout, { animated in
self.item = item
switch item.direction {
case .vertical:
self.contentContainer.layer.sublayerTransform = CATransform3DIdentity
case .horizontal:
self.contentContainer.layer.sublayerTransform = CATransform3DMakeRotation(CGFloat.pi / 2.0, 0.0, 0.0, 1.0)
}
self.contentContainer.frame = CGRect(origin: CGPoint(), size: mappedContentsSize)
let contentsFrame = CGRect(origin: CGPoint(), size: contentsSize)
if let contentsComponentView = self.contentsView.view {
if contentsComponentView.superview == nil {
self.contentContainer.addSubview(contentsComponentView)
}
contentsComponentView.center = CGPoint(x: mappedContentsSize.width * 0.5, y: mappedContentsSize.height * 0.5)
contentsComponentView.bounds = CGRect(origin: CGPoint(), size: contentsFrame.size)
}
})
}
}
override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) {
super.animateInsertion(currentTimestamp, duration: duration, options: options)
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
super.animateRemoved(currentTimestamp, duration: duration)
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false)
}
override func animateAdded(_ currentTimestamp: Double, duration: Double) {
super.animateAdded(currentTimestamp, duration: duration)
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
}
public final class View: UIView {
let listNode: ListView
private var externalStateValue: ExternalState.Value?
private var isUpdating: Bool = false
public private(set) var component: AsyncListComponent?
private var currentEntries: [ItemEntry] = []
private var ignoreUpdateVisibleItems: Bool = false
public override init(frame: CGRect) {
self.listNode = ListView()
self.listNode.useMainQueueTransactions = true
self.listNode.scroller.delaysContentTouches = false
self.listNode.reorderedItemHasShadow = false
super.init(frame: frame)
self.addSubview(self.listNode.view)
self.listNode.onContentsUpdated = { [weak self] transition in
guard let self else {
return
}
self.updateVisibleItems(transition: ComponentTransition(transition))
}
}
required public init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
return super.hitTest(point, with: event)
}
public func stopScrolling() {
self.listNode.stopScrolling()
}
private func updateVisibleItems(transition: ComponentTransition) {
if self.ignoreUpdateVisibleItems {
return
}
guard let component = self.component else {
return
}
if let onVisibleItemsUpdated = component.onVisibleItemsUpdated {
onVisibleItemsUpdated(VisibleItems(view: self, direction: component.direction), transition)
}
}
public func visibleItems() -> VisibleItems? {
guard let component = self.component else {
return nil
}
return VisibleItems(view: self, direction: component.direction)
}
public func visibleItemView(id: AnyHashable) -> UIView? {
guard let component = self.component else {
return nil
}
guard let index = component.items.firstIndex(where: { $0.id == id }) else {
return nil
}
var foundItemNode: ListItemNodeImpl?
self.listNode.forEachItemNode { itemNode in
if let itemNode = itemNode as? ListItemNodeImpl, itemNode.index == index {
foundItemNode = itemNode
}
}
if let foundItemNode {
return foundItemNode.contentsView.view
}
return nil
}
public func visibleItemViews() -> VisibleItemViews {
return VisibleItemViews(view: self)
}
func update(component: AsyncListComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
let previousComponent = self.component
self.component = component
let listSize: CGSize
let listInsets: UIEdgeInsets
switch component.direction {
case .vertical:
self.listNode.transform = CATransform3DIdentity
listSize = CGSize(width: availableSize.width, height: availableSize.height)
listInsets = component.insets
case .horizontal:
self.listNode.transform = CATransform3DMakeRotation(-CGFloat.pi / 2.0, 0.0, 0.0, 1.0)
listSize = CGSize(width: availableSize.height, height: availableSize.width)
listInsets = UIEdgeInsets(top: component.insets.left, left: component.insets.top, bottom: component.insets.right, right: component.insets.bottom)
}
var updateSizeAndInsets = ListViewUpdateSizeAndInsets(
size: listSize,
insets: listInsets,
duration: 0.0,
curve: .Default(duration: nil)
)
var animateTransition = false
var transactionOptions: ListViewDeleteAndInsertOptions = []
if !transition.animation.isImmediate, let previousComponent {
if previousComponent.itemSetId == component.itemSetId {
transactionOptions.insert(.AnimateInsertion)
}
animateTransition = true
switch transition.animation {
case .none:
break
case let .curve(duration, curve):
updateSizeAndInsets.duration = duration
switch curve {
case .linear, .easeInOut:
updateSizeAndInsets.curve = .Default(duration: duration)
case .spring:
updateSizeAndInsets.curve = .Spring(duration: duration)
case let .custom(a, b, c, d):
updateSizeAndInsets.curve = .Custom(duration: duration, a, b, c, d)
case .bounce:
assertionFailure()
updateSizeAndInsets.curve = .Spring(duration: duration)
}
}
}
var entries: [ItemEntry] = []
for item in component.items {
entries.append(ItemEntry(
contents: item,
index: entries.count
))
}
var scrollToItem: ListViewScrollToItem?
if let resetScrollingRequest = component.externalStateValue.resetScrollingRequest, previousComponent?.externalStateValue.resetScrollingRequest != component.externalStateValue.resetScrollingRequest {
if let index = entries.firstIndex(where: { $0.id == resetScrollingRequest.id }) {
var directionHint: ListViewScrollToItemDirectionHint = .Down
var didSelectDirection = false
self.listNode.forEachItemNode { itemNode in
if didSelectDirection {
return
}
if let itemNode = itemNode as? ListItemNodeImpl, let itemIndex = itemNode.index {
if itemIndex <= index {
directionHint = .Up
} else {
directionHint = .Down
}
didSelectDirection = true
}
}
scrollToItem = ListViewScrollToItem(
index: index,
position: .visible,
animated: animateTransition,
curve: updateSizeAndInsets.curve,
directionHint: directionHint
)
}
}
self.ignoreUpdateVisibleItems = true
let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: self.currentEntries, rightList: entries)
self.currentEntries = entries
let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) }
let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(parentView: self, direction: component.direction), directionHint: .Down) }
let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(parentView: self, direction: component.direction), directionHint: nil) }
transactionOptions.insert(.Synchronous)
self.listNode.transaction(
deleteIndices: [],
insertIndicesAndItems: [],
updateIndicesAndItems: [],
options: transactionOptions,
scrollToItem: nil,
updateSizeAndInsets: updateSizeAndInsets,
stationaryItemRange: nil,
updateOpaqueState: nil,
completion: { _ in }
)
self.listNode.transaction(
deleteIndices: deletions,
insertIndicesAndItems: insertions,
updateIndicesAndItems: updates,
options: transactionOptions,
scrollToItem: scrollToItem,
updateSizeAndInsets: nil,
stationaryItemRange: nil,
updateOpaqueState: nil,
completion: { _ in }
)
let mappedListFrame: CGRect
switch component.direction {
case .vertical:
mappedListFrame = CGRect(origin: CGPoint(x: availableSize.width * 0.5, y: availableSize.height * 0.5), size: listSize)
case .horizontal:
mappedListFrame = CGRect(origin: CGPoint(x: availableSize.width * 0.5, y: availableSize.height * 0.5), size: listSize)
}
self.listNode.position = mappedListFrame.origin
self.listNode.bounds = CGRect(origin: CGPoint(), size: mappedListFrame.size)
self.listNode.reorderItem = { [weak self] fromIndex, toIndex, _ in
guard let self, let component = self.component else {
return .single(false)
}
guard let reorderItems = component.reorderItems else {
return .single(false)
}
if reorderItems(fromIndex, toIndex) {
return .single(true)
} else {
return .single(false)
}
}
self.ignoreUpdateVisibleItems = false
self.updateVisibleItems(transition: transition)
return availableSize
}
}
public func makeView() -> View {
return View(frame: CGRect())
}
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
@@ -0,0 +1,47 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "AttachmentFileController",
module_name = "AttachmentFileController",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/SSignalKit/SwiftSignalKit",
"//submodules/AsyncDisplayKit",
"//submodules/Display",
"//submodules/TelegramCore",
"//submodules/Postbox",
"//submodules/AccountContext",
"//submodules/TelegramPresentationData",
"//submodules/TelegramStringFormatting",
"//submodules/TelegramUIPreferences",
"//submodules/LegacyComponents",
"//submodules/SolidRoundedButtonNode",
"//submodules/PresentationDataUtils",
"//submodules/UIKitRuntimeUtils",
"//submodules/ComponentFlow",
"//submodules/ItemListPeerActionItem",
"//submodules/ListMessageItem",
"//submodules/AttachmentUI",
"//submodules/SearchBarNode",
"//submodules/MergeLists",
"//submodules/ChatListSearchItemHeader",
"//submodules/ItemListUI",
"//submodules/SearchUI",
"//submodules/ContextUI",
"//submodules/AnimatedStickerNode",
"//submodules/TelegramAnimatedStickerNode",
"//submodules/Components/BundleIconComponent",
"//submodules/TelegramUI/Components/GlassBarButtonComponent",
"//submodules/TelegramUI/Components/SearchInputPanelComponent",
"//submodules/TelegramUI/Components/EdgeEffect",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,704 @@
import Foundation
import UIKit
import Display
import SwiftSignalKit
import Postbox
import TelegramCore
import LegacyComponents
import TelegramPresentationData
import TelegramUIPreferences
import ItemListUI
import PresentationDataUtils
import AccountContext
import ItemListPeerActionItem
import AttachmentUI
import TelegramStringFormatting
import ListMessageItem
import ComponentFlow
import GlassBarButtonComponent
import BundleIconComponent
import EdgeEffect
import SaveToCameraRoll
private final class AttachmentFileControllerArguments {
let context: AccountContext
let isAudio: Bool
let openGallery: () -> Void
let openFiles: () -> Void
let scanDocument: () -> Void
let expandSavedMusic: () -> Void
let send: (Message) -> Void
init(context: AccountContext, isAudio: Bool, openGallery: @escaping () -> Void, openFiles: @escaping () -> Void, scanDocument: @escaping () -> Void, expandSavedMusic: @escaping () -> Void, send: @escaping (Message) -> Void) {
self.context = context
self.isAudio = isAudio
self.openGallery = openGallery
self.openFiles = openFiles
self.scanDocument = scanDocument
self.expandSavedMusic = expandSavedMusic
self.send = send
}
}
private enum AttachmentFileSection: Int32 {
case select
case savedMusic
case recent
}
private func areMessagesEqual(_ lhsMessage: Message?, _ rhsMessage: Message?) -> Bool {
guard let lhsMessage = lhsMessage, let rhsMessage = rhsMessage else {
return lhsMessage == nil && rhsMessage == nil
}
if lhsMessage.stableVersion != rhsMessage.stableVersion {
return false
}
if lhsMessage.id != rhsMessage.id || lhsMessage.flags != rhsMessage.flags {
return false
}
return true
}
private enum AttachmentFileEntry: ItemListNodeEntry {
case selectFromGallery(PresentationTheme, String)
case selectFromFiles(PresentationTheme, String)
case scanDocument(PresentationTheme, String)
case savedHeader(PresentationTheme, String)
case savedFile(Int32, PresentationTheme, Message?)
case showMore(PresentationTheme, String)
case recentHeader(PresentationTheme, String)
case file(Int32, PresentationTheme, Message?)
var section: ItemListSectionId {
switch self {
case .selectFromGallery, .selectFromFiles, .scanDocument:
return AttachmentFileSection.select.rawValue
case .savedHeader, .savedFile, .showMore:
return AttachmentFileSection.savedMusic.rawValue
case .recentHeader, .file:
return AttachmentFileSection.recent.rawValue
}
}
var stableId: Int32 {
switch self {
case .selectFromGallery:
return 0
case .selectFromFiles:
return 1
case .scanDocument:
return 2
case .savedHeader:
return 3
case let .savedFile(index, _, _):
return 4 + index
case .showMore:
return 9999
case .recentHeader:
return 10000
case let .file(index, _, _):
return 10001 + index
}
}
static func ==(lhs: AttachmentFileEntry, rhs: AttachmentFileEntry) -> Bool {
switch lhs {
case let .selectFromGallery(lhsTheme, lhsText):
if case let .selectFromGallery(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .selectFromFiles(lhsTheme, lhsText):
if case let .selectFromFiles(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .scanDocument(lhsTheme, lhsText):
if case let .scanDocument(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .savedHeader(lhsTheme, lhsText):
if case let .savedHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .savedFile(lhsIndex, lhsTheme, lhsMessage):
if case let .savedFile(rhsIndex, rhsTheme, rhsMessage) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, areMessagesEqual(lhsMessage, rhsMessage) {
return true
} else {
return false
}
case let .showMore(lhsTheme, lhsText):
if case let .showMore(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .recentHeader(lhsTheme, lhsText):
if case let .recentHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .file(lhsIndex, lhsTheme, lhsMessage):
if case let .file(rhsIndex, rhsTheme, rhsMessage) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, areMessagesEqual(lhsMessage, rhsMessage) {
return true
} else {
return false
}
}
}
static func <(lhs: AttachmentFileEntry, rhs: AttachmentFileEntry) -> Bool {
return lhs.stableId < rhs.stableId
}
func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem {
let arguments = arguments as! AttachmentFileControllerArguments
switch self {
case let .selectFromGallery(_, text):
return ItemListPeerActionItem(presentationData: presentationData, systemStyle: .glass, icon: PresentationResourcesItemList.imageIcon(presentationData.theme), title: text, alwaysPlain: false, sectionId: self.section, height: .generic, editing: false, action: {
arguments.openGallery()
})
case let .selectFromFiles(_, text):
return ItemListPeerActionItem(presentationData: presentationData, systemStyle: .glass, icon: PresentationResourcesItemList.cloudIcon(presentationData.theme), title: text, alwaysPlain: false, sectionId: self.section, height: .generic, editing: false, action: {
arguments.openFiles()
})
case let .scanDocument(_, text):
return ItemListPeerActionItem(presentationData: presentationData, systemStyle: .glass, icon: PresentationResourcesItemList.scanIcon(presentationData.theme), title: text, alwaysPlain: false, sectionId: self.section, height: .generic, editing: false, action: {
arguments.scanDocument()
})
case let .savedHeader(_, text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .savedFile(_, _, message):
let interaction = ListMessageItemInteraction(openMessage: { message, _ in
arguments.send(message)
return false
}, openMessageContextMenu: { _, _, _, _, _ in }, toggleMessagesSelection: { _, _ in }, openUrl: { _, _, _, _ in }, openInstantPage: { _, _ in }, longTap: { _, _ in }, getHiddenMedia: { return [:] })
let dateTimeFormat = arguments.context.sharedContext.currentPresentationData.with({$0}).dateTimeFormat
let chatPresentationData = ChatPresentationData(theme: ChatPresentationThemeData(theme: presentationData.theme, wallpaper: .color(0)), fontSize: presentationData.fontSize, strings: presentationData.strings, dateTimeFormat: dateTimeFormat, nameDisplayOrder: .firstLast, disableAnimations: false, largeEmoji: false, chatBubbleCorners: PresentationChatBubbleCorners(mainRadius: 0, auxiliaryRadius: 0, mergeBubbleCorners: false))
return ListMessageItem(presentationData: chatPresentationData, systemStyle: .glass, context: arguments.context, chatLocation: .peer(id: arguments.context.account.peerId), interaction: interaction, message: message, selection: .none, displayHeader: false, isDownloadList: arguments.isAudio, isStoryMusic: true, displayFileInfo: true, displayBackground: true, style: .blocks, sectionId: self.section)
case let .showMore(theme, text):
return ItemListPeerActionItem(presentationData: presentationData, systemStyle: .glass, icon: PresentationResourcesItemList.downArrowImage(theme), title: text, sectionId: self.section, editing: false, action: {
arguments.expandSavedMusic()
})
case let .recentHeader(_, text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .file(_, _, message):
let interaction = ListMessageItemInteraction(openMessage: { message, _ in
arguments.send(message)
return false
}, openMessageContextMenu: { _, _, _, _, _ in }, toggleMessagesSelection: { _, _ in }, openUrl: { _, _, _, _ in }, openInstantPage: { _, _ in }, longTap: { _, _ in }, getHiddenMedia: { return [:] })
let dateTimeFormat = arguments.context.sharedContext.currentPresentationData.with({$0}).dateTimeFormat
let chatPresentationData = ChatPresentationData(theme: ChatPresentationThemeData(theme: presentationData.theme, wallpaper: .color(0)), fontSize: presentationData.fontSize, strings: presentationData.strings, dateTimeFormat: dateTimeFormat, nameDisplayOrder: .firstLast, disableAnimations: false, largeEmoji: false, chatBubbleCorners: PresentationChatBubbleCorners(mainRadius: 0, auxiliaryRadius: 0, mergeBubbleCorners: false))
return ListMessageItem(presentationData: chatPresentationData, systemStyle: .glass, context: arguments.context, chatLocation: .peer(id: PeerId(0)), interaction: interaction, message: message, selection: .none, displayHeader: false, isDownloadList: arguments.isAudio, isStoryMusic: true, displayFileInfo: true, displayBackground: true, style: .blocks, sectionId: self.section)
}
}
}
private func attachmentFileControllerEntries(presentationData: PresentationData, mode: AttachmentFileControllerMode, state: AttachmentFileControllerState, savedMusic: [Message]?, recentDocuments: [Message]?, hasScan: Bool, empty: Bool) -> [AttachmentFileEntry] {
guard !empty else {
return []
}
var entries: [AttachmentFileEntry] = []
if case .recent = mode {
entries.append(.selectFromGallery(presentationData.theme, presentationData.strings.Attachment_SelectFromGallery))
}
entries.append(.selectFromFiles(presentationData.theme, presentationData.strings.Attachment_SelectFromFiles))
if hasScan {
entries.append(.scanDocument(presentationData.theme, "Scan Document"))
}
let listTitle: String
switch mode {
case .recent:
listTitle = presentationData.strings.Attachment_RecentlySentFiles
case .audio:
listTitle = presentationData.strings.Attachment_SharedAudio
}
if case .audio = mode {
if let savedMusic, savedMusic.count > 0 {
entries.append(.savedHeader(presentationData.theme, presentationData.strings.MediaEditor_Audio_SavedMusic.uppercased()))
var savedMusic = savedMusic
var showMore = false
if savedMusic.count > 4 && !state.savedMusicExpanded {
savedMusic = Array(savedMusic.prefix(3))
showMore = true
}
var i: Int32 = 0
for file in savedMusic {
entries.append(.savedFile(i, presentationData.theme, file))
i += 1
}
if showMore {
entries.append(.showMore(presentationData.theme, presentationData.strings.MediaEditor_Audio_ShowMore))
}
}
}
if let recentDocuments = recentDocuments {
if recentDocuments.count > 0 {
entries.append(.recentHeader(presentationData.theme, listTitle.uppercased()))
var i: Int32 = 0
for file in recentDocuments {
entries.append(.file(i, presentationData.theme, file))
i += 1
}
}
} else {
entries.append(.recentHeader(presentationData.theme, listTitle.uppercased()))
for i in 0 ..< 11 {
entries.append(.file(Int32(i), presentationData.theme, nil))
}
}
return entries
}
private final class AttachmentFileContext: AttachmentMediaPickerContext {
}
public class AttachmentFileControllerImpl: ItemListController, AttachmentFileController, AttachmentContainable {
public var requestAttachmentMenuExpansion: () -> Void = {}
public var updateNavigationStack: (@escaping ([AttachmentContainable]) -> ([AttachmentContainable], AttachmentMediaPickerContext?)) -> Void = { _ in }
public var parentController: () -> ViewController? = {
return nil
}
public var updateTabBarAlpha: (CGFloat, ContainedViewLayoutTransition) -> Void = { _, _ in }
public var updateTabBarVisibility: (Bool, ContainedViewLayoutTransition) -> Void = { _, _ in }
public var cancelPanGesture: () -> Void = { }
public var isContainerPanning: () -> Bool = { return false }
public var isContainerExpanded: () -> Bool = { return false }
public var isMinimized: Bool = false
var delayDisappear = false
var hasBottomEdgeEffect = true
var resetForReuseImpl: () -> Void = {}
public func resetForReuse() {
self.resetForReuseImpl()
self.scrollToTop?()
}
public func prepareForReuse() {
self.delayDisappear = true
self.visibleBottomContentOffsetChanged?(self.visibleBottomContentOffset)
self.delayDisappear = false
}
public var mediaPickerContext: AttachmentMediaPickerContext? {
return AttachmentFileContext()
}
private var topEdgeEffectView: EdgeEffectView?
private var bottomEdgeEffectView: EdgeEffectView?
var isSearching: Bool = false {
didSet {
self.requestLayout(transition: .animated(duration: 0.2, curve: .easeInOut))
}
}
public override func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition)
let topEdgeEffectView: EdgeEffectView
if let current = self.topEdgeEffectView {
topEdgeEffectView = current
} else {
topEdgeEffectView = EdgeEffectView()
if let navigationBar = self.navigationBar {
self.view.insertSubview(topEdgeEffectView, belowSubview: navigationBar.view)
}
self.topEdgeEffectView = topEdgeEffectView
}
let edgeEffectHeight: CGFloat = 88.0
let topEdgeEffectFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: layout.size.width, height: edgeEffectHeight))
transition.updateFrame(view: topEdgeEffectView, frame: topEdgeEffectFrame)
topEdgeEffectView.update(content: .clear, blur: true, alpha: 1.0, rect: topEdgeEffectFrame, edge: .top, edgeSize: topEdgeEffectFrame.height, transition: ComponentTransition(transition))
if self.hasBottomEdgeEffect {
let bottomEdgeEffectView: EdgeEffectView
if let current = self.bottomEdgeEffectView {
bottomEdgeEffectView = current
} else {
bottomEdgeEffectView = EdgeEffectView()
self.view.addSubview(bottomEdgeEffectView)
self.bottomEdgeEffectView = bottomEdgeEffectView
}
let bottomEdgeEffectFrame = CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - edgeEffectHeight - layout.additionalInsets.bottom), size: CGSize(width: layout.size.width, height: edgeEffectHeight))
transition.updateFrame(view: bottomEdgeEffectView, frame: bottomEdgeEffectFrame)
transition.updateAlpha(layer: bottomEdgeEffectView.layer, alpha: self.isSearching ? 0.0 : 1.0)
bottomEdgeEffectView.update(content: .clear, blur: true, alpha: 1.0, rect: bottomEdgeEffectFrame, edge: .bottom, edgeSize: bottomEdgeEffectFrame.height, transition: ComponentTransition(transition))
} else if let bottomEdgeEffectView = self.bottomEdgeEffectView {
bottomEdgeEffectView.removeFromSuperview()
}
}
}
private struct AttachmentFileControllerState: Equatable {
var searching: Bool
var savedMusicExpanded: Bool
}
public enum AttachmentFileControllerMode {
case recent
case audio
}
public func makeAttachmentFileControllerImpl(
context: AccountContext,
updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil,
mode: AttachmentFileControllerMode = .recent,
bannedSendMedia: (Int32, Bool)?,
presentGallery: @escaping () -> Void,
presentFiles: @escaping () -> Void,
presentDocumentScanner: (() -> Void)?,
send: @escaping (AnyMediaReference) -> Void
) -> AttachmentFileController {
let actionsDisposable = DisposableSet()
let statePromise = ValuePromise(AttachmentFileControllerState(searching: false, savedMusicExpanded: false), ignoreRepeated: true)
let stateValue = Atomic(value: AttachmentFileControllerState(searching: false, savedMusicExpanded: false))
let updateState: ((AttachmentFileControllerState) -> AttachmentFileControllerState) -> Void = { f in
statePromise.set(stateValue.modify { f($0) })
}
var updateTabBarVisibilityImpl: ((Bool) -> Void)?
var expandImpl: (() -> Void)?
var dismissImpl: (() -> Void)?
var dismissInputImpl: (() -> Void)?
var updateIsSearchingImpl: ((Bool) -> Void)?
let arguments = AttachmentFileControllerArguments(
context: context,
isAudio: mode == .audio,
openGallery: {
presentGallery()
},
openFiles: {
presentFiles()
},
scanDocument: {
presentDocumentScanner?()
dismissImpl?()
},
expandSavedMusic: {
updateState { state in
var updatedState = state
updatedState.savedMusicExpanded = true
return updatedState
}
},
send: { message in
if message.id.namespace == Namespaces.Message.Local {
if let file = message.media.first(where: { $0 is TelegramMediaFile }) as? TelegramMediaFile {
send(.standalone(media: file))
}
} else {
let _ = (context.engine.messages.getMessagesLoadIfNecessary([message.id], strategy: .cloud(skipLocal: true))
|> `catch` { _ in
return .single(.result([]))
}
|> mapToSignal { result -> Signal<[Message], NoError> in
guard case let .result(result) = result else {
return .complete()
}
return .single(result)
}
|> deliverOnMainQueue).startStandalone(next: { messages in
if let message = messages.first, let file = message.media.first(where: { $0 is TelegramMediaFile }) as? TelegramMediaFile {
send(.message(message: MessageReference(message), media: file))
}
dismissImpl?()
})
}
}
)
let recentDocuments: Signal<[Message]?, NoError>
let savedMusicContext: ProfileSavedMusicContext?
let savedMusic: Signal<[Message]?, NoError>
switch mode {
case .recent:
recentDocuments = .single(nil)
|> then(
context.engine.messages.searchMessages(location: .sentMedia(tags: [.file]), query: "", state: nil)
|> map { result -> [Message]? in
return result.0.messages
}
)
savedMusicContext = nil
savedMusic = .single(nil)
case .audio:
recentDocuments = .single(nil)
|> then(
context.engine.messages.searchMessages(location: .general(scope: .everywhere, tags: [.music], minDate: nil, maxDate: nil), query: "", state: nil)
|> map { result -> [Message]? in
return result.0.messages
}
)
savedMusicContext = ProfileSavedMusicContext(account: context.account, peerId: context.account.peerId)
savedMusic = .single(nil)
|> then(
savedMusicContext!.state
|> map { state in
let peerId = context.account.peerId
var messages: [Message] = []
let peers = SimpleDictionary<PeerId, Peer>()
for file in state.files {
let stableId = UInt32(clamping: file.fileId.id % Int64(Int32.max))
messages.append(Message(stableId: stableId, stableVersion: 0, id: MessageId(peerId: peerId, namespace: Namespaces.Message.Local, id: Int32(stableId)), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 0, flags: [], tags: [.music], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: nil, text: "", attributes: [], media: [file], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]))
}
return messages
}
)
}
let presentationData = updatedPresentationData?.signal ?? context.sharedContext.presentationData
let existingCloseButton = Atomic<BarComponentHostNode?>(value: nil)
let existingSearchButton = Atomic<BarComponentHostNode?>(value: nil)
let previousRecentDocuments = Atomic<[Message]?>(value: nil)
let signal = combineLatest(queue: Queue.mainQueue(),
presentationData,
recentDocuments,
savedMusic,
statePromise.get()
)
|> map { presentationData, recentDocuments, savedMusic, state -> (ItemListControllerState, (ItemListNodeState, Any)) in
var presentationData = presentationData
let updatedTheme = presentationData.theme.withModalBlocksBackground()
presentationData = presentationData.withUpdated(theme: updatedTheme)
let barButtonSize = CGSize(width: 44.0, height: 44.0)
let closeButton = GlassBarButtonComponent(
size: barButtonSize,
backgroundColor: nil,
isDark: presentationData.theme.overallDarkAppearance,
state: .generic,
animateScale: false,
component: AnyComponentWithIdentity(id: "close", component: AnyComponent(
BundleIconComponent(
name: "Navigation/Close",
tintColor: presentationData.theme.chat.inputPanel.panelControlColor
)
)),
action: { _ in
dismissImpl?()
}
)
let closeButtonComponent = AnyComponentWithIdentity(id: "close", component: AnyComponent(closeButton))
let closeButtonNode = existingCloseButton.modify { current in
let buttonNode: BarComponentHostNode
if let current {
buttonNode = current
buttonNode.component = closeButtonComponent
} else {
buttonNode = BarComponentHostNode(component: closeButtonComponent, size: barButtonSize)
}
return buttonNode
}
let searchButton = GlassBarButtonComponent(
size: barButtonSize,
backgroundColor: nil,
isDark: presentationData.theme.overallDarkAppearance,
state: .generic,
animateScale: false,
component: AnyComponentWithIdentity(id: "search", component: AnyComponent(
BundleIconComponent(
name: "Navigation/Search",
tintColor: presentationData.theme.chat.inputPanel.panelControlColor
)
)),
action: { _ in
updateState { state in
var updatedState = state
updatedState.searching = true
return updatedState
}
updateTabBarVisibilityImpl?(false)
updateIsSearchingImpl?(true)
}
)
let searchButtonComponent = state.searching ? nil : AnyComponentWithIdentity(id: "search", component: AnyComponent(searchButton))
let searchButtonNode: BarComponentHostNode? = !state.searching ? existingSearchButton.modify { current in
let buttonNode: BarComponentHostNode
if let current {
buttonNode = current
buttonNode.component = searchButtonComponent
} else {
buttonNode = BarComponentHostNode(component: searchButtonComponent, size: barButtonSize)
}
return buttonNode
} : nil
let previousRecentDocuments = previousRecentDocuments.swap(recentDocuments)
let crossfade = previousRecentDocuments == nil && recentDocuments != nil
var animateChanges = false
if let previousRecentDocuments = previousRecentDocuments,
let recentDocuments = recentDocuments,
!previousRecentDocuments.isEmpty && !recentDocuments.isEmpty,
!crossfade {
animateChanges = true
}
let leftNavigationButton = closeButtonNode.flatMap { ItemListNavigationButton(content: .node($0), style: .regular, enabled: true, action: {}) }
var rightNavigationButton: ItemListNavigationButton?
if bannedSendMedia == nil && (recentDocuments == nil || (recentDocuments?.count ?? 0) > 10) {
rightNavigationButton = searchButtonNode.flatMap { ItemListNavigationButton(content: .node($0), style: .regular, enabled: true, action: {}) }
}
let title: String
switch mode {
case .recent:
title = presentationData.strings.Attachment_File
case .audio:
title = presentationData.strings.MediaEditor_Audio_Title
}
let controllerState = ItemListControllerState(
presentationData: ItemListPresentationData(presentationData),
title: .text(title),
leftNavigationButton: leftNavigationButton,
rightNavigationButton: rightNavigationButton,
backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back),
animateChanges: true
)
var emptyItem: AttachmentFileEmptyStateItem?
if let (untilDate, personal) = bannedSendMedia {
let banDescription: String
if untilDate != 0 && untilDate != Int32.max {
banDescription = presentationData.strings.Conversation_RestrictedMediaTimed(stringForFullDate(timestamp: untilDate, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat)).string
} else if personal {
banDescription = presentationData.strings.Conversation_RestrictedMedia
} else {
banDescription = presentationData.strings.Conversation_DefaultRestrictedMedia
}
emptyItem = AttachmentFileEmptyStateItem(context: context, theme: presentationData.theme, strings: presentationData.strings, content: .bannedSendMedia(text: banDescription, canBoost: false))
} else if let recentDocuments = recentDocuments,
recentDocuments.isEmpty {
emptyItem = AttachmentFileEmptyStateItem(context: context, theme: presentationData.theme, strings: presentationData.strings, content: .intro)
}
var searchItem: ItemListControllerSearch?
if state.searching {
searchItem = AttachmentFileSearchItem(context: context, mode: mode, presentationData: presentationData, focus: {
expandImpl?()
}, cancel: {
updateState { state in
var updatedState = state
updatedState.searching = false
return updatedState
}
updateTabBarVisibilityImpl?(true)
updateIsSearchingImpl?(false)
}, send: { message in
arguments.send(message)
}, dismissInput: {
dismissInputImpl?()
})
}
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: attachmentFileControllerEntries(presentationData: presentationData, mode: mode, state: state, savedMusic: savedMusic, recentDocuments: recentDocuments, hasScan: presentDocumentScanner != nil, empty: bannedSendMedia != nil), style: .blocks, emptyStateItem: emptyItem, searchItem: searchItem, crossfadeState: crossfade, animateChanges: animateChanges)
return (controllerState, (listState, arguments))
} |> afterDisposed {
actionsDisposable.dispose()
let _ = savedMusicContext?.state
}
let controller = AttachmentFileControllerImpl(context: context, state: signal, hideNavigationBarBackground: true)
if case .audio = mode {
controller.hasBottomEdgeEffect = false
}
controller.delayDisappear = true
controller.visibleBottomContentOffsetChanged = { [weak controller] offset in
switch offset {
case let .known(value):
let backgroundAlpha: CGFloat = min(30.0, max(0.0, value)) / 30.0
if backgroundAlpha.isZero && controller?.delayDisappear == true {
Queue.mainQueue().after(0.25, {
controller?.updateTabBarAlpha(backgroundAlpha, .animated(duration: 0.1, curve: .easeInOut))
})
} else {
controller?.updateTabBarAlpha(backgroundAlpha, .immediate)
}
case .unknown, .none:
controller?.updateTabBarAlpha(1.0, .immediate)
controller?.delayDisappear = false
}
}
controller.resetForReuseImpl = {
updateState { state in
var updatedState = state
updatedState.searching = false
return updatedState
}
}
updateIsSearchingImpl = { [weak controller] isSearching in
controller?.isSearching = isSearching
}
dismissImpl = { [weak controller] in
controller?.dismiss(animated: true)
}
dismissInputImpl = { [weak controller] in
controller?.view.endEditing(true)
}
expandImpl = { [weak controller] in
controller?.requestAttachmentMenuExpansion()
}
updateTabBarVisibilityImpl = { [weak controller] isVisible in
controller?.updateTabBarVisibility(isVisible, .animated(duration: 0.4, curve: .spring))
}
return controller
}
public func storyAudioPickerController(
context: AccountContext,
selectFromFiles: @escaping () -> Void,
dismissed: @escaping () -> Void,
completion: @escaping (AnyMediaReference) -> Void,
) -> ViewController {
var dismissImpl: (() -> Void)?
let presentationData = context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: defaultDarkColorPresentationTheme)
let updatedPresentationData: (PresentationData, Signal<PresentationData, NoError>) = (presentationData, .single(presentationData))
let controller = AttachmentController(context: context, updatedPresentationData: updatedPresentationData, style: .glass, chatLocation: nil, buttons: [.standalone], initialButton: .standalone, fromMenu: false, hasTextInput: false)
controller.requestController = { _, present in
let filePickerController = makeAttachmentFileControllerImpl(context: context, updatedPresentationData: updatedPresentationData, mode: .audio, bannedSendMedia: nil, presentGallery: {}, presentFiles: {
selectFromFiles()
dismissImpl?()
}, presentDocumentScanner: nil, send: { file in
completion(file)
dismissImpl?()
}) as! AttachmentFileControllerImpl
present(filePickerController, filePickerController.mediaPickerContext)
}
controller.navigationPresentation = .flatModal
controller.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait)
controller.didDismiss = {
dismissed()
}
dismissImpl = { [weak controller] in
controller?.dismiss(animated: true)
}
return controller
}
@@ -0,0 +1,151 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import TelegramPresentationData
import ItemListUI
import PresentationDataUtils
import AnimatedStickerNode
import TelegramAnimatedStickerNode
import AccountContext
import SolidRoundedButtonNode
final class AttachmentFileEmptyStateItem: ItemListControllerEmptyStateItem {
enum Content: Equatable {
case intro
case bannedSendMedia(text: String, canBoost: Bool)
}
let context: AccountContext
let theme: PresentationTheme
let strings: PresentationStrings
let content: Content
init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, content: Content) {
self.context = context
self.theme = theme
self.strings = strings
self.content = content
}
func isEqual(to: ItemListControllerEmptyStateItem) -> Bool {
if let item = to as? AttachmentFileEmptyStateItem {
return self.theme === item.theme && self.strings === item.strings && self.content == item.content
} else {
return false
}
}
func node(current: ItemListControllerEmptyStateItemNode?) -> ItemListControllerEmptyStateItemNode {
if let current = current as? AttachmentFileEmptyStateItemNode {
current.item = self
return current
} else {
return AttachmentFileEmptyStateItemNode(item: self)
}
}
}
final class AttachmentFileEmptyStateItemNode: ItemListControllerEmptyStateItemNode {
private var animationNode: AnimatedStickerNode
private let textNode: ASTextNode
private let buttonNode: SolidRoundedButtonNode
private var validLayout: (ContainerViewLayout, CGFloat)?
var item: AttachmentFileEmptyStateItem {
didSet {
self.updateThemeAndStrings(theme: self.item.theme, strings: self.item.strings)
if let (layout, navigationHeight) = self.validLayout {
self.updateLayout(layout: layout, navigationBarHeight: navigationHeight, transition: .immediate)
}
}
}
init(item: AttachmentFileEmptyStateItem) {
self.item = item
let name: String
let playbackMode: AnimatedStickerPlaybackMode
switch item.content {
case .intro:
name = "Files"
playbackMode = .loop
case .bannedSendMedia:
name = "Banned"
playbackMode = .once
}
self.animationNode = DefaultAnimatedStickerNodeImpl()
self.animationNode.setup(source: AnimatedStickerNodeLocalFileSource(name: name), width: 320, height: 320, playbackMode: playbackMode, mode: .direct(cachePathPrefix: nil))
self.animationNode.visibility = true
self.textNode = ASTextNode()
self.textNode.isUserInteractionEnabled = false
self.textNode.lineSpacing = 0.1
self.textNode.textAlignment = .center
self.buttonNode = SolidRoundedButtonNode(theme: SolidRoundedButtonTheme(backgroundColor: .black, foregroundColor: .white), height: 52.0, cornerRadius: 26.0, isShimmering: true)
super.init()
self.isUserInteractionEnabled = false
self.addSubnode(self.animationNode)
self.addSubnode(self.textNode)
self.updateThemeAndStrings(theme: self.item.theme, strings: self.item.strings)
if case .bannedSendMedia(_, true) = item.content {
self.addSubnode(self.buttonNode)
}
}
private func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) {
let text: String
switch self.item.content {
case .intro:
text = strings.Attachment_FilesIntro
case let .bannedSendMedia(banDescription, _):
text = banDescription
}
self.textNode.attributedText = NSAttributedString(string: text.replacingOccurrences(of: "\n", with: " "), font: Font.regular(15.0), textColor: theme.list.freeTextColor, paragraphAlignment: .center)
self.buttonNode.title = strings.Attachment_OpenSettings
self.buttonNode.updateTheme(SolidRoundedButtonTheme(theme: theme))
}
override func updateLayout(layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
self.validLayout = (layout, navigationBarHeight)
var imageSize = CGSize(width: 144.0, height: 144.0)
var insets = layout.insets(options: [])
if layout.size.width == 320.0 {
insets.top += -60.0
imageSize = CGSize(width: 112.0, height: 112.0)
} else {
insets.top += 30.0 //-160.0
}
let imageSpacing: CGFloat = 12.0
let textSpacing: CGFloat = 12.0
let buttonSpacing: CGFloat = 15.0
let bottomSpacing: CGFloat = 33.0
let imageHeight = layout.size.width < layout.size.height ? imageSize.height + imageSpacing : 0.0
let buttonWidth: CGFloat = 248.0
let buttonHeight = self.buttonNode.updateLayout(width: buttonWidth, transition: transition)
let textSize = self.textNode.updateLayout(CGSize(width: layout.size.width - layout.safeInsets.left - layout.safeInsets.right - 40.0, height: max(1.0, layout.size.height - insets.top - insets.bottom)))
let totalHeight = imageHeight + textSpacing + textSize.height + buttonSpacing + buttonHeight + bottomSpacing
let topOffset = insets.top + floor((layout.size.height - insets.top - insets.bottom - totalHeight) / 2.0)
transition.updateAlpha(node: self.animationNode, alpha: imageHeight > 0.0 ? 1.0 : 0.0)
transition.updateFrame(node: self.animationNode, frame: CGRect(origin: CGPoint(x: floor((layout.size.width - imageSize.width) / 2.0), y: topOffset), size: imageSize))
self.animationNode.updateLayout(size: imageSize)
transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: layout.safeInsets.left + floor((layout.size.width - textSize.width - layout.safeInsets.left - layout.safeInsets.right) / 2.0), y: topOffset + imageHeight + textSpacing), size: textSize))
transition.updateFrame(node: self.buttonNode, frame: CGRect(origin: CGPoint(x: layout.safeInsets.left + floor((layout.size.width - buttonWidth - layout.safeInsets.left - layout.safeInsets.right) / 2.0), y: self.textNode.frame.maxY + buttonSpacing), size: CGSize(width: buttonWidth, height: buttonHeight)))
}
}
@@ -0,0 +1,573 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import Postbox
import TelegramCore
import SwiftSignalKit
import ItemListUI
import PresentationDataUtils
import TelegramPresentationData
import TelegramUIPreferences
import AccountContext
import MergeLists
import ChatListSearchItemHeader
import ItemListUI
import SearchUI
import ContextUI
import ListMessageItem
import ComponentFlow
import SearchInputPanelComponent
final class AttachmentFileSearchItem: ItemListControllerSearch {
let context: AccountContext
let mode: AttachmentFileControllerMode
let presentationData: PresentationData
let focus: () -> Void
let cancel: () -> Void
let send: (Message) -> Void
let dismissInput: () -> Void
private var updateActivity: ((Bool) -> Void)?
private var activity: ValuePromise<Bool> = ValuePromise(ignoreRepeated: false)
private let activityDisposable = MetaDisposable()
init(context: AccountContext, mode: AttachmentFileControllerMode, presentationData: PresentationData, focus: @escaping () -> Void, cancel: @escaping () -> Void, send: @escaping (Message) -> Void, dismissInput: @escaping () -> Void) {
self.context = context
self.mode = mode
self.presentationData = presentationData
self.focus = focus
self.cancel = cancel
self.send = send
self.dismissInput = dismissInput
self.activityDisposable.set((activity.get() |> mapToSignal { value -> Signal<Bool, NoError> in
if value {
return .single(value) |> delay(0.2, queue: Queue.mainQueue())
} else {
return .single(value)
}
}).startStrict(next: { [weak self] value in
self?.updateActivity?(value)
}))
}
deinit {
self.activityDisposable.dispose()
}
func isEqual(to: ItemListControllerSearch) -> Bool {
if let to = to as? AttachmentFileSearchItem {
if self.context !== to.context {
return false
}
return true
} else {
return false
}
}
func titleContentNode(current: (NavigationBarContentNode & ItemListControllerSearchNavigationContentNode)?) -> (NavigationBarContentNode & ItemListControllerSearchNavigationContentNode)? {
return nil
}
func node(current: ItemListControllerSearchNode?, titleContentNode: (NavigationBarContentNode & ItemListControllerSearchNavigationContentNode)?) -> ItemListControllerSearchNode {
return AttachmentFileSearchItemNode(context: self.context, mode: self.mode, presentationData: self.presentationData, focus: self.focus, send: self.send, cancel: self.cancel, updateActivity: { [weak self] value in
self?.activity.set(value)
}, dismissInput: self.dismissInput)
}
}
private final class AttachmentFileSearchItemNode: ItemListControllerSearchNode {
private let context: AccountContext
private let mode: AttachmentFileControllerMode
private let presentationData: PresentationData
private let focus: () -> Void
private let cancel: () -> Void
private let containerNode: AttachmentFileSearchContainerNode
private let searchInput = ComponentView<Empty>()
private var validLayout: ContainerViewLayout?
init(context: AccountContext, mode: AttachmentFileControllerMode, presentationData: PresentationData, focus: @escaping () -> Void, send: @escaping (Message) -> Void, cancel: @escaping () -> Void, updateActivity: @escaping(Bool) -> Void, dismissInput: @escaping () -> Void) {
self.context = context
self.mode = mode
self.presentationData = presentationData
self.focus = focus
self.cancel = cancel
self.containerNode = AttachmentFileSearchContainerNode(context: context, mode: mode, presentationData: presentationData, send: { message in
send(message)
}, updateActivity: updateActivity)
super.init()
self.addedUnderNavigationBar = true
self.addSubnode(self.containerNode)
self.containerNode.cancel = { [weak self] in
dismissInput()
cancel()
self?.deactivateInput()
}
self.containerNode.dismissInput = {
dismissInput()
}
}
override func queryUpdated(_ query: String) {
self.containerNode.searchTextUpdated(text: query)
}
override func scrollToTop() {
self.containerNode.scrollToTop()
}
private func deactivateInput() {
if let layout = self.validLayout, let searchInputView = self.searchInput.view as? SearchInputPanelComponent.View {
let transition = ComponentTransition.spring(duration: 0.4)
transition.setFrame(view: searchInputView, frame: CGRect(origin: CGPoint(x: searchInputView.frame.minX, y: layout.size.height), size: searchInputView.frame.size))
}
}
override func updateLayout(layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
self.validLayout = layout
transition.updateFrame(node: self.containerNode, frame: CGRect(origin: CGPoint(x: 0.0, y: navigationBarHeight), size: CGSize(width: layout.size.width, height: layout.size.height - navigationBarHeight)))
self.containerNode.containerLayoutUpdated(layout.withUpdatedSize(CGSize(width: layout.size.width, height: layout.size.height - navigationBarHeight)), navigationBarHeight: 0.0, transition: transition)
let searchInputSize = self.searchInput.update(
transition: .immediate,
component: AnyComponent(
SearchInputPanelComponent(
theme: self.presentationData.theme,
strings: self.presentationData.strings,
metrics: layout.metrics,
safeInsets: layout.safeInsets,
placeholder: self.mode == .audio ? self.presentationData.strings.Attachment_FilesSearchPlaceholder : self.presentationData.strings.Attachment_FilesSearchPlaceholder,
updated: { [weak self] query in
guard let self else {
return
}
self.queryUpdated(query)
},
cancel: { [weak self] in
guard let self else {
return
}
self.cancel()
self.deactivateInput()
}
)
),
environment: {},
containerSize: CGSize(width: layout.size.width, height: layout.size.height)
)
let bottomInset: CGFloat = layout.insets(options: .input).bottom
let searchInputFrame = CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - bottomInset - searchInputSize.height), size: searchInputSize)
if let searchInputView = self.searchInput.view as? SearchInputPanelComponent.View {
if searchInputView.superview == nil {
self.view.addSubview(searchInputView)
searchInputView.frame = CGRect(origin: CGPoint(x: searchInputFrame.minX, y: layout.size.height), size: searchInputFrame.size)
self.focus()
searchInputView.activateInput()
}
transition.updateFrame(view: searchInputView, frame: searchInputFrame)
}
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if let searchInputView = self.searchInput.view as? SearchInputPanelComponent.View {
if let result = searchInputView.hitTest(self.view.convert(point, to: searchInputView), with: event) {
return result
}
}
if let result = self.containerNode.hitTest(self.view.convert(point, to: self.containerNode.view), with: event) {
return result
}
return super.hitTest(point, with: event)
}
}
private final class AttachmentFileSearchContainerInteraction {
let context: AccountContext
let send: (Message) -> Void
init(context: AccountContext, send: @escaping (Message) -> Void) {
self.context = context
self.send = send
}
}
private enum AttachmentFileSearchEntryId: Hashable {
case placeholder(Int)
case message(MessageId)
}
private func areMessagesEqual(_ lhsMessage: Message?, _ rhsMessage: Message?) -> Bool {
guard let lhsMessage = lhsMessage, let rhsMessage = rhsMessage else {
return lhsMessage == nil && rhsMessage == nil
}
if lhsMessage.stableVersion != rhsMessage.stableVersion {
return false
}
if lhsMessage.id != rhsMessage.id || lhsMessage.flags != rhsMessage.flags {
return false
}
return true
}
private final class AttachmentFileSearchEntry: Comparable, Identifiable {
let index: Int
let message: Message?
init(index: Int, message: Message?) {
self.index = index
self.message = message
}
var stableId: AttachmentFileSearchEntryId {
if let message = self.message {
return .message(message.id)
} else {
return .placeholder(self.index)
}
}
static func ==(lhs: AttachmentFileSearchEntry, rhs: AttachmentFileSearchEntry) -> Bool {
return lhs.index == rhs.index && areMessagesEqual(lhs.message, rhs.message)
}
static func <(lhs: AttachmentFileSearchEntry, rhs: AttachmentFileSearchEntry) -> Bool {
return lhs.index < rhs.index
}
func item(context: AccountContext, presentationData: PresentationData, nameSortOrder: PresentationPersonNameOrder, nameDisplayOrder: PresentationPersonNameOrder, interaction: AttachmentFileSearchContainerInteraction) -> ListViewItem {
let itemInteraction = ListMessageItemInteraction(openMessage: { message, _ in
interaction.send(message)
return false
}, openMessageContextMenu: { _, _, _, _, _ in }, toggleMessagesSelection: { _, _ in }, openUrl: { _, _, _, _ in }, openInstantPage: { _, _ in }, longTap: { _, _ in }, getHiddenMedia: { return [:] })
return ListMessageItem(presentationData: ChatPresentationData(presentationData: presentationData), systemStyle: .glass, context: interaction.context, chatLocation: .peer(id: PeerId(0)), interaction: itemInteraction, message: message, selection: .none, displayHeader: true, displayFileInfo: false, displayBackground: true, style: .plain)
}
}
struct AttachmentFileSearchContainerTransition {
let deletions: [ListViewDeleteItem]
let insertions: [ListViewInsertItem]
let updates: [ListViewUpdateItem]
let isSearching: Bool
let isEmpty: Bool
let query: String
}
private func attachmentFileSearchContainerPreparedRecentTransition(from fromEntries: [AttachmentFileSearchEntry], to toEntries: [AttachmentFileSearchEntry], isSearching: Bool, isEmpty: Bool, query: String, context: AccountContext, presentationData: PresentationData, nameSortOrder: PresentationPersonNameOrder, nameDisplayOrder: PresentationPersonNameOrder, interaction: AttachmentFileSearchContainerInteraction) -> AttachmentFileSearchContainerTransition {
let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries)
let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) }
let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, nameSortOrder: nameSortOrder, nameDisplayOrder: nameDisplayOrder, interaction: interaction), directionHint: nil) }
let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, nameSortOrder: nameSortOrder, nameDisplayOrder: nameDisplayOrder, interaction: interaction), directionHint: nil) }
return AttachmentFileSearchContainerTransition(deletions: deletions, insertions: insertions, updates: updates, isSearching: isSearching, isEmpty: isEmpty, query: query)
}
public final class AttachmentFileSearchContainerNode: SearchDisplayControllerContentNode {
private let context: AccountContext
private let send: (Message) -> Void
private let dimNode: ASDisplayNode
private let backgroundNode: ASDisplayNode
private let listNode: ListView
private let emptyResultsTitleNode: ImmediateTextNode
private let emptyResultsTextNode: ImmediateTextNode
private var enqueuedTransitions: [(AttachmentFileSearchContainerTransition, Bool)] = []
private var validLayout: (ContainerViewLayout, CGFloat)?
private let searchQuery = Promise<String?>()
private let emptyQueryDisposable = MetaDisposable()
private let searchDisposable = MetaDisposable()
private var presentationData: PresentationData
private var presentationDataDisposable: Disposable?
private let presentationDataPromise: Promise<PresentationData>
private var _hasDim: Bool = false
override public var hasDim: Bool {
return _hasDim
}
public init(context: AccountContext, mode: AttachmentFileControllerMode, presentationData: PresentationData, send: @escaping (Message) -> Void, updateActivity: @escaping (Bool) -> Void) {
self.context = context
self.send = send
self.presentationData = presentationData
self.presentationDataPromise = Promise(self.presentationData)
self.dimNode = ASDisplayNode()
self.dimNode.backgroundColor = .clear
self.backgroundNode = ASDisplayNode()
self.listNode = ListView()
self.listNode.accessibilityPageScrolledString = { row, count in
return presentationData.strings.VoiceOver_ScrollStatus(row, count).string
}
self.emptyResultsTitleNode = ImmediateTextNode()
self.emptyResultsTitleNode.displaysAsynchronously = false
self.emptyResultsTitleNode.attributedText = NSAttributedString(string: self.presentationData.strings.ChatList_Search_NoResults, font: Font.semibold(17.0), textColor: self.presentationData.theme.list.freeTextColor)
self.emptyResultsTitleNode.textAlignment = .center
self.emptyResultsTitleNode.isHidden = true
self.emptyResultsTextNode = ImmediateTextNode()
self.emptyResultsTextNode.displaysAsynchronously = false
self.emptyResultsTextNode.maximumNumberOfLines = 0
self.emptyResultsTextNode.textAlignment = .center
self.emptyResultsTextNode.isHidden = true
super.init()
self.backgroundNode.backgroundColor = self.presentationData.theme.chatList.backgroundColor
self.backgroundNode.alpha = 0.0
self.listNode.backgroundColor = self.presentationData.theme.chatList.backgroundColor
self.listNode.alpha = 0.0
self._hasDim = true
self.addSubnode(self.dimNode)
self.addSubnode(self.backgroundNode)
self.addSubnode(self.listNode)
self.addSubnode(self.emptyResultsTitleNode)
self.addSubnode(self.emptyResultsTextNode)
let interaction = AttachmentFileSearchContainerInteraction(context: context, send: { [weak self] message in
send(message)
self?.listNode.clearHighlightAnimated(true)
})
let presentationDataPromise = self.presentationDataPromise
let searchQuery = self.searchQuery.get()
|> mapToSignal { query -> Signal<String?, NoError> in
if let query = query, !query.isEmpty {
return (.complete() |> delay(0.6, queue: Queue.mainQueue()))
|> then(.single(query))
} else {
return .single(query)
}
}
let foundItems = searchQuery
|> mapToSignal { query -> Signal<[AttachmentFileSearchEntry]?, NoError> in
guard let query = query, !query.isEmpty else {
return .single(nil)
}
let signal: Signal<[Message]?, NoError>
switch mode {
case .recent:
signal = .single(nil)
|> then(
context.engine.messages.searchMessages(location: .sentMedia(tags: [.file]), query: query, state: nil)
|> map { result -> [Message]? in
return result.0.messages
}
)
case .audio:
signal = .single(nil)
|> then(
context.engine.messages.searchMessages(location: .general(scope: .everywhere, tags: [.music], minDate: nil, maxDate: nil), query: query, state: nil)
|> map { result -> [Message]? in
return result.0.messages
}
)
}
updateActivity(true)
return combineLatest(signal, presentationDataPromise.get())
|> mapToSignal { messages, presentationData -> Signal<[AttachmentFileSearchEntry]?, NoError> in
var entries: [AttachmentFileSearchEntry] = []
var index = 0
if let messages = messages {
for message in messages {
entries.append(AttachmentFileSearchEntry(index: index, message: message))
index += 1
}
} else {
for _ in 0 ..< 16 {
entries.append(AttachmentFileSearchEntry(index: index, message: nil))
index += 1
}
}
return .single(entries)
}
}
let previousSearchItems = Atomic<[AttachmentFileSearchEntry]?>(value: nil)
self.searchDisposable.set((combineLatest(searchQuery, foundItems, self.presentationDataPromise.get())
|> deliverOnMainQueue).startStrict(next: { [weak self] query, entries, presentationData in
if let strongSelf = self {
let previousEntries = previousSearchItems.swap(entries)
updateActivity(false)
let firstTime = previousEntries == nil
let transition = attachmentFileSearchContainerPreparedRecentTransition(from: previousEntries ?? [], to: entries ?? [], isSearching: entries != nil, isEmpty: entries?.isEmpty ?? false, query: query ?? "", context: context, presentationData: presentationData, nameSortOrder: presentationData.nameSortOrder, nameDisplayOrder: presentationData.nameDisplayOrder, interaction: interaction)
strongSelf.enqueueTransition(transition, firstTime: firstTime)
}
}))
// self.presentationDataDisposable = (context.sharedContext.presentationData
// |> deliverOnMainQueue).startStrict(next: { [weak self] presentationData in
// if let strongSelf = self {
// let previousTheme = strongSelf.presentationData.theme
// let previousStrings = strongSelf.presentationData.strings
//
// strongSelf.presentationData = presentationData
//
// if previousTheme !== presentationData.theme || previousStrings !== presentationData.strings {
// strongSelf.updateThemeAndStrings(theme: presentationData.theme, strings: presentationData.strings)
// }
// }
// })
self.listNode.beganInteractiveDragging = { [weak self] _ in
self?.dismissInput?()
}
}
deinit {
self.searchDisposable.dispose()
self.presentationDataDisposable?.dispose()
}
override public func didLoad() {
super.didLoad()
self.dimNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimTapGesture(_:))))
}
private func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) {
self.backgroundNode.backgroundColor = theme.chatList.backgroundColor
self.listNode.backgroundColor = theme.chatList.backgroundColor
}
override public func searchTextUpdated(text: String) {
if text.isEmpty {
self.searchQuery.set(.single(nil))
} else {
self.searchQuery.set(.single(text))
}
}
private func enqueueTransition(_ transition: AttachmentFileSearchContainerTransition, firstTime: Bool) {
self.enqueuedTransitions.append((transition, firstTime))
if let _ = self.validLayout {
while !self.enqueuedTransitions.isEmpty {
self.dequeueTransition()
}
}
}
private func dequeueTransition() {
if let (transition, _) = self.enqueuedTransitions.first {
self.enqueuedTransitions.remove(at: 0)
var options = ListViewDeleteAndInsertOptions()
options.insert(.PreferSynchronousDrawing)
options.insert(.PreferSynchronousResourceLoading)
let isSearching = transition.isSearching
self.listNode.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, updateSizeAndInsets: nil, updateOpaqueState: nil, completion: { [weak self] _ in
guard let strongSelf = self else {
return
}
let containerTransition = ContainedViewLayoutTransition.animated(duration: 0.3, curve: .easeInOut)
containerTransition.updateAlpha(node: strongSelf.backgroundNode, alpha: isSearching ? 1.0 : 0.0)
containerTransition.updateAlpha(node: strongSelf.listNode, alpha: isSearching ? 1.0 : 0.0)
strongSelf.dimNode.isHidden = transition.isSearching
strongSelf.emptyResultsTextNode.attributedText = NSAttributedString(string: strongSelf.presentationData.strings.ChatList_Search_NoResultsQueryDescription(transition.query).string, font: Font.regular(15.0), textColor: strongSelf.presentationData.theme.list.freeTextColor)
let emptyResults = transition.isSearching && transition.isEmpty
strongSelf.emptyResultsTitleNode.isHidden = !emptyResults
strongSelf.emptyResultsTextNode.isHidden = !emptyResults
if let (layout, navigationBarHeight) = strongSelf.validLayout {
strongSelf.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .immediate)
}
})
}
}
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: transition)
let hadValidLayout = self.validLayout == nil
self.validLayout = (layout, navigationBarHeight)
let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition)
var insets = layout.insets(options: [.input])
insets.top += navigationBarHeight
let topInset = navigationBarHeight
transition.updateFrame(node: self.dimNode, frame: CGRect(origin: CGPoint(x: 0.0, y: topInset), size: CGSize(width: layout.size.width, height: layout.size.height - topInset)))
self.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -66.0), size: CGSize(width: layout.size.width, height: 66.0))
self.listNode.frame = CGRect(origin: CGPoint(), size: layout.size)
self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous], scrollToItem: nil, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: layout.size, insets: insets, duration: duration, curve: curve), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
let padding: CGFloat = 16.0
let emptyTitleSize = self.emptyResultsTitleNode.updateLayout(CGSize(width: layout.size.width - layout.safeInsets.left - layout.safeInsets.right - padding * 2.0, height: CGFloat.greatestFiniteMagnitude))
let emptyTextSize = self.emptyResultsTextNode.updateLayout(CGSize(width: layout.size.width - layout.safeInsets.left - layout.safeInsets.right - padding * 2.0, height: CGFloat.greatestFiniteMagnitude))
let emptyTextSpacing: CGFloat = 8.0
let emptyTotalHeight = emptyTitleSize.height + emptyTextSize.height + emptyTextSpacing
let emptyTitleY = navigationBarHeight + floorToScreenPixels((layout.size.height - navigationBarHeight - max(insets.bottom, layout.intrinsicInsets.bottom) - emptyTotalHeight) / 2.0)
transition.updateFrame(node: self.emptyResultsTitleNode, frame: CGRect(origin: CGPoint(x: layout.safeInsets.left + padding + (layout.size.width - layout.safeInsets.left - layout.safeInsets.right - padding * 2.0 - emptyTitleSize.width) / 2.0, y: emptyTitleY), size: emptyTitleSize))
transition.updateFrame(node: self.emptyResultsTextNode, frame: CGRect(origin: CGPoint(x: layout.safeInsets.left + padding + (layout.size.width - layout.safeInsets.left - layout.safeInsets.right - padding * 2.0 - emptyTextSize.width) / 2.0, y: emptyTitleY + emptyTitleSize.height + emptyTextSpacing), size: emptyTextSize))
if !hadValidLayout {
while !self.enqueuedTransitions.isEmpty {
self.dequeueTransition()
}
}
}
override public func scrollToTop() {
if self.listNode.alpha > 0.0 {
self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: ListViewScrollToItem(index: 0, position: .top(0.0), animated: true, curve: .Default(duration: nil), directionHint: .Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
}
}
@objc func dimTapGesture(_ recognizer: UITapGestureRecognizer) {
if case .ended = recognizer.state {
self.cancel?()
}
}
override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
guard let result = self.view.hitTest(point, with: event) else {
return nil
}
if result === self.view {
return nil
}
return result
}
}
@@ -0,0 +1,23 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "AudioTranscriptionButtonComponent",
module_name = "AudioTranscriptionButtonComponent",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/ComponentFlow:ComponentFlow",
"//submodules/AppBundle:AppBundle",
"//submodules/Display:Display",
"//submodules/TelegramPresentationData:TelegramPresentationData",
"//submodules/Components/LottieAnimationComponent:LottieAnimationComponent",
"//submodules/Components/BundleIconComponent:BundleIconComponent",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,279 @@
import Foundation
import UIKit
import ComponentFlow
import AppBundle
import Display
import TelegramPresentationData
import LottieAnimationComponent
import BundleIconComponent
public final class AudioTranscriptionButtonComponent: Component {
public enum Theme: Equatable {
public static func == (lhs: AudioTranscriptionButtonComponent.Theme, rhs: AudioTranscriptionButtonComponent.Theme) -> Bool {
switch lhs {
case let .bubble(lhsTheme):
if case let .bubble(rhsTheme) = rhs {
return lhsTheme === rhsTheme
} else {
return false
}
case let .custom(lhsBackgroundColor, lhsForegroundColor):
if case let .custom(rhsBackgroundColor, rhsForegroundColor) = rhs {
return lhsBackgroundColor == rhsBackgroundColor && lhsForegroundColor == rhsForegroundColor
} else {
return false
}
case let .freeform(lhsFreeform, lhsForeground):
if case let .freeform(rhsFreeform, rhsForeground) = rhs, lhsFreeform == rhsFreeform, lhsForeground == rhsForeground {
return true
} else {
return false
}
}
}
case bubble(PresentationThemePartedColors)
case custom(UIColor, UIColor)
case freeform((UIColor, Bool), UIColor)
}
public enum TranscriptionState {
case inProgress
case expanded
case collapsed
case locked
}
public let theme: AudioTranscriptionButtonComponent.Theme
public let transcriptionState: TranscriptionState
public let pressed: () -> Void
public init(
theme: AudioTranscriptionButtonComponent.Theme,
transcriptionState: TranscriptionState,
pressed: @escaping () -> Void
) {
self.theme = theme
self.transcriptionState = transcriptionState
self.pressed = pressed
}
public static func ==(lhs: AudioTranscriptionButtonComponent, rhs: AudioTranscriptionButtonComponent) -> Bool {
if lhs.theme != rhs.theme {
return false
}
if lhs.transcriptionState != rhs.transcriptionState {
return false
}
return true
}
public final class View: UIButton {
private var component: AudioTranscriptionButtonComponent?
private let blurredBackgroundNode: NavigationBackgroundNode
private let backgroundLayer: SimpleLayer
private var iconView: ComponentView<Empty>?
private var animationView: ComponentView<Empty>?
private var progressAnimationView: ComponentHostView<Empty>?
override init(frame: CGRect) {
self.blurredBackgroundNode = NavigationBackgroundNode(color: .clear)
self.backgroundLayer = SimpleLayer()
super.init(frame: frame)
self.backgroundLayer.masksToBounds = true
self.backgroundLayer.cornerRadius = 10.0
self.layer.addSublayer(self.backgroundLayer)
self.addTarget(self, action: #selector(self.pressed), for: .touchUpInside)
}
required public init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@objc private func pressed() {
self.component?.pressed()
}
func update(component: AudioTranscriptionButtonComponent, availableSize: CGSize, transition: ComponentTransition) -> CGSize {
let size = CGSize(width: 30.0, height: 30.0)
let foregroundColor: UIColor
let backgroundColor: UIColor
switch component.theme {
case let .bubble(theme):
foregroundColor = theme.bubble.withWallpaper.reactionActiveBackground
backgroundColor = theme.bubble.withWallpaper.reactionInactiveBackground
case let .custom(backgroundColorValue, foregroundColorValue):
foregroundColor = foregroundColorValue
backgroundColor = backgroundColorValue
case let .freeform(colorAndBlur, color):
foregroundColor = color
backgroundColor = .clear
if self.blurredBackgroundNode.view.superview == nil {
self.insertSubview(self.blurredBackgroundNode.view, at: 0)
}
self.blurredBackgroundNode.updateColor(color: colorAndBlur.0, enableBlur: colorAndBlur.1, transition: .immediate)
self.blurredBackgroundNode.update(size: size, cornerRadius: 10.0, transition: .immediate)
self.blurredBackgroundNode.frame = CGRect(origin: .zero, size: size)
}
if self.component?.transcriptionState != component.transcriptionState {
if case .locked = component.transcriptionState {
if let animationView = self.animationView {
self.animationView = nil
if let view = animationView.view {
view.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in
view.removeFromSuperview()
})
}
}
let iconView: ComponentView<Empty>
if let current = self.iconView {
iconView = current
} else {
iconView = ComponentView<Empty>()
self.iconView = iconView
}
let iconSize = iconView.update(
transition: transition,
component: AnyComponent(BundleIconComponent(
name: "Chat/Message/TranscriptionLocked",
tintColor: foregroundColor
)),
environment: {},
containerSize: CGSize(width: 30.0, height: 30.0)
)
if let view = iconView.view {
if view.superview == nil {
view.isUserInteractionEnabled = false
self.addSubview(view)
}
view.frame = CGRect(origin: CGPoint(x: floor((size.width - iconSize.width) / 2.0), y: floor((size.width - iconSize.height) / 2.0)), size: iconSize)
}
} else {
if let iconView = self.iconView {
self.iconView = nil
if let view = iconView.view {
view.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in
view.removeFromSuperview()
})
}
}
let animationView: ComponentView<Empty>
if let current = self.animationView {
animationView = current
} else {
animationView = ComponentView<Empty>()
self.animationView = animationView
}
switch component.transcriptionState {
case .inProgress:
if self.progressAnimationView == nil {
let progressAnimationView = ComponentHostView<Empty>()
self.progressAnimationView = progressAnimationView
self.addSubview(progressAnimationView)
}
default:
if let progressAnimationView = self.progressAnimationView {
self.progressAnimationView = nil
if case .none = transition.animation {
progressAnimationView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false, completion: { [weak progressAnimationView] _ in
progressAnimationView?.removeFromSuperview()
})
} else {
progressAnimationView.removeFromSuperview()
}
}
}
let animationName: String
switch component.transcriptionState {
case .inProgress:
animationName = "voiceToText"
case .collapsed:
animationName = "voiceToText"
case .expanded:
animationName = "textToVoice"
case .locked:
animationName = "voiceToText"
}
let animationSize = animationView.update(
transition: transition,
component: AnyComponent(LottieAnimationComponent(
animation: LottieAnimationComponent.AnimationItem(
name: animationName,
mode: .animateTransitionFromPrevious
),
colors: [
"icon.Group 3.Stroke 1": foregroundColor,
"icon.Group 1.Stroke 1": foregroundColor,
"icon.Group 4.Stroke 1": foregroundColor,
"icon.Group 2.Stroke 1": foregroundColor,
"Artboard Copy 2 Outlines.Group 5.Stroke 1": foregroundColor,
"Artboard Copy 2 Outlines.Group 1.Stroke 1": foregroundColor,
"Artboard Copy 2 Outlines.Group 4.Stroke 1": foregroundColor,
"Artboard Copy Outlines.Group 1.Stroke 1": foregroundColor,
],
size: CGSize(width: 30.0, height: 30.0)
)),
environment: {},
containerSize: CGSize(width: 30.0, height: 30.0)
)
if let view = animationView.view {
if view.superview == nil {
view.isUserInteractionEnabled = false
self.addSubview(view)
}
view.frame = CGRect(origin: CGPoint(x: floor((size.width - animationSize.width) / 2.0), y: floor((size.width - animationSize.height) / 2.0)), size: animationSize)
}
}
}
self.backgroundLayer.backgroundColor = backgroundColor.cgColor
self.component = component
self.backgroundLayer.frame = CGRect(origin: CGPoint(), size: size)
if let progressAnimationView = self.progressAnimationView {
let progressFrame = CGRect(origin: CGPoint(), size: size).insetBy(dx: -1.0, dy: -1.0)
let _ = progressAnimationView.update(
transition: transition,
component: AnyComponent(LottieAnimationComponent(
animation: LottieAnimationComponent.AnimationItem(
name: "voicets_progress",
mode: .animating(loop: true)
),
colors: [
"Rectangle 60.Rectangle 60.Stroke 1": foregroundColor
],
size: progressFrame.size
)),
environment: {},
containerSize: progressFrame.size
)
progressAnimationView.frame = progressFrame
}
return CGSize(width: min(availableSize.width, size.width), height: min(availableSize.height, size.height))
}
}
public func makeView() -> View {
return View(frame: CGRect())
}
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, transition: transition)
}
}
@@ -0,0 +1,21 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "AudioTranscriptionPendingIndicatorComponent",
module_name = "AudioTranscriptionPendingIndicatorComponent",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/ComponentFlow:ComponentFlow",
"//submodules/AppBundle:AppBundle",
"//submodules/Display:Display",
"//submodules/Components/LottieAnimationComponent:LottieAnimationComponent",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,178 @@
import Foundation
import UIKit
import ComponentFlow
import AppBundle
import Display
import LottieAnimationComponent
public final class AudioTranscriptionPendingIndicatorComponent: Component {
public let color: UIColor
public let font: UIFont
public init(color: UIColor, font: UIFont) {
self.color = color
self.font = font
}
public static func ==(lhs: AudioTranscriptionPendingIndicatorComponent, rhs: AudioTranscriptionPendingIndicatorComponent) -> Bool {
if lhs.color != rhs.color {
return false
}
if lhs.font != rhs.font {
return false
}
return true
}
public final class View: UIView {
private var component: AudioTranscriptionPendingIndicatorComponent?
private var dotLayers: [SimpleLayer] = []
override init(frame: CGRect) {
super.init(frame: frame)
for _ in 0 ..< 3 {
let dotLayer = SimpleLayer()
self.dotLayers.append(dotLayer)
self.layer.addSublayer(dotLayer)
}
self.dotLayers[0].didEnterHierarchy = { [weak self] in
self?.restartAnimations()
}
}
required public init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func restartAnimations() {
let beginTime = self.layer.convertTime(CACurrentMediaTime(), from: nil)
for i in 0 ..< self.dotLayers.count {
let delay = Double(i) * 0.07
let animation = CABasicAnimation(keyPath: "opacity")
animation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear)
animation.beginTime = beginTime + delay
animation.fromValue = 0.0 as NSNumber
animation.toValue = 1.0 as NSNumber
animation.repeatCount = Float.infinity
animation.autoreverses = true
animation.fillMode = .both
self.dotLayers[i].add(animation, forKey: "idle")
}
}
func update(component: AudioTranscriptionPendingIndicatorComponent, availableSize: CGSize, transition: ComponentTransition) -> CGSize {
let dotSize: CGFloat = 2.0
let spacing: CGFloat = 3.0
var stringSize = NSAttributedString(string: "...", font: component.font, textColor: .black).boundingRect(with: CGSize(width: 100.0, height: 100.0), options: .usesLineFragmentOrigin, context: nil).size
stringSize.width = ceil(stringSize.width)
stringSize.height = ceil(stringSize.height)
if self.component?.color != component.color {
if let dotImage = generateFilledCircleImage(diameter: dotSize, color: component.color) {
for dotLayer in self.dotLayers {
dotLayer.contents = dotImage.cgImage
}
}
}
self.component = component
let size = CGSize(width: dotSize * CGFloat(self.dotLayers.count) + spacing * CGFloat(self.dotLayers.count - 1), height: dotSize)
for i in 0 ..< self.dotLayers.count {
self.dotLayers[i].frame = CGRect(origin: CGPoint(x: CGFloat(i) * (dotSize + spacing), y: 0.0), size: CGSize(width: dotSize, height: dotSize))
}
return CGSize(width: min(availableSize.width, stringSize.width), height: min(availableSize.height, size.height))
}
}
public func makeView() -> View {
return View(frame: CGRect())
}
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, transition: transition)
}
}
public final class AudioTranscriptionPendingLottieIndicatorComponent: Component {
public let color: UIColor
public let font: UIFont
public init(color: UIColor, font: UIFont) {
self.color = color
self.font = font
}
public static func ==(lhs: AudioTranscriptionPendingLottieIndicatorComponent, rhs: AudioTranscriptionPendingLottieIndicatorComponent) -> Bool {
if lhs.color != rhs.color {
return false
}
if lhs.font != rhs.font {
return false
}
return true
}
public final class View: UIView {
private let animationView: ComponentHostView<Empty>
override init(frame: CGRect) {
self.animationView = ComponentHostView<Empty>()
super.init(frame: frame)
self.addSubview(self.animationView)
}
required public init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(component: AudioTranscriptionPendingLottieIndicatorComponent, availableSize: CGSize, transition: ComponentTransition) -> CGSize {
let originalSize = CGSize(width: 48.0, height: 66.0)
let animationSize = originalSize.aspectFitted(CGSize(width: 15.0, height: 100.0))
let _ = self.animationView.update(
transition: .immediate,
component: AnyComponent(LottieAnimationComponent(
animation: LottieAnimationComponent.AnimationItem(
name: "animated_text_dots",
mode: .animating(loop: true)
),
colors: [
"Comp 1.Point 3.Group 1.Fill 1": component.color,
"Comp 1.Point 2.Group 1.Fill 1": component.color,
"Comp 1.Point 1.Group 1.Fill 1": component.color
],
size: animationSize
)),
environment: {},
containerSize: animationSize
)
var stringSize = NSAttributedString(string: "...", font: component.font, textColor: .black).boundingRect(with: CGSize(width: 100.0, height: 100.0), options: .usesLineFragmentOrigin, context: nil).size
stringSize.width = ceil(stringSize.width)
stringSize.height = ceil(stringSize.height)
let size = CGSize(width: min(availableSize.width, stringSize.width), height: min(availableSize.height, 10.0))
self.animationView.frame = CGRect(origin: CGPoint(x: -2.0, y: size.height - animationSize.height + 4.0 + UIScreenPixel), size: animationSize)
return size
}
}
public func makeView() -> View {
return View(frame: CGRect())
}
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, transition: transition)
}
}
@@ -0,0 +1,23 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "AudioWaveformComponent",
module_name = "AudioWaveformComponent",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/ComponentFlow:ComponentFlow",
"//submodules/AppBundle:AppBundle",
"//submodules/Display:Display",
"//submodules/ShimmerEffect:ShimmerEffect",
"//submodules/MediaPlayer:UniversalMediaPlayer",
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,758 @@
import Foundation
import UIKit
import ComponentFlow
import Display
import ShimmerEffect
import UniversalMediaPlayer
import SwiftSignalKit
public final class AudioWaveformComponent: Component {
public enum Style {
case bottom
case middle
}
public let backgroundColor: UIColor
public let foregroundColor: UIColor
public let shimmerColor: UIColor?
public let style: Style
public let samples: Data
public let peak: Int32
public let status: Signal<MediaPlayerStatus, NoError>
public let isViewOnceMessage: Bool
public let seek: ((Double) -> Void)?
public let updateIsSeeking: ((Bool) -> Void)?
public init(
backgroundColor: UIColor,
foregroundColor: UIColor,
shimmerColor: UIColor?,
style: Style,
samples: Data,
peak: Int32,
status: Signal<MediaPlayerStatus, NoError>,
isViewOnceMessage: Bool,
seek: ((Double) -> Void)?,
updateIsSeeking: ((Bool) -> Void)?
) {
self.backgroundColor = backgroundColor
self.foregroundColor = foregroundColor
self.shimmerColor = shimmerColor
self.style = style
self.samples = samples
self.peak = peak
self.status = status
self.isViewOnceMessage = isViewOnceMessage
self.seek = seek
self.updateIsSeeking = updateIsSeeking
}
public static func ==(lhs: AudioWaveformComponent, rhs: AudioWaveformComponent) -> Bool {
if lhs.backgroundColor !== rhs.backgroundColor {
return false
}
if lhs.foregroundColor != rhs.foregroundColor {
return false
}
if lhs.shimmerColor != rhs.shimmerColor {
return false
}
if lhs.style != rhs.style {
return false
}
if lhs.samples != rhs.samples {
return false
}
if lhs.peak != rhs.peak {
return false
}
if lhs.isViewOnceMessage != rhs.isViewOnceMessage {
return false
}
return true
}
public final class View: UIView, UIGestureRecognizerDelegate {
private struct ShimmerParams: Equatable {
var backgroundColor: UIColor
var foregroundColor: UIColor
}
public final class CloneLayer: SimpleLayer {
}
private final class LayerImpl: SimpleLayer {
private var shimmerNode: ShimmerEffectNode?
private var shimmerMask: SimpleLayer?
var shimmerParams: ShimmerParams? {
didSet {
if (self.shimmerParams != nil) != (oldValue != nil) {
if self.shimmerParams != nil {
if self.shimmerNode == nil {
let shimmerNode = ShimmerEffectNode()
shimmerNode.isUserInteractionEnabled = false
self.shimmerNode = shimmerNode
self.addSublayer(shimmerNode.layer)
let shimmerMask = SimpleLayer()
shimmerNode.layer.mask = shimmerMask
shimmerMask.contents = self.contents
shimmerMask.frame = self.bounds
self.shimmerMask = shimmerMask
}
self.updateShimmer()
} else {
if let shimmerNode = self.shimmerNode {
self.shimmerNode = nil
shimmerNode.layer.removeFromSuperlayer()
self.shimmerMask = nil
}
}
}
}
}
private func updateShimmer() {
guard let shimmerNode = self.shimmerNode, !self.bounds.width.isZero, let shimmerParams = self.shimmerParams else {
return
}
shimmerNode.frame = self.bounds
shimmerNode.updateAbsoluteRect(self.bounds, within: CGSize(width: self.bounds.size.width + 60.0, height: self.bounds.size.height + 4.0))
var shapes: [ShimmerEffectNode.Shape] = []
shapes.append(.rect(rect: CGRect(origin: CGPoint(), size: self.bounds.size)))
shimmerNode.update(
backgroundColor: .clear,
foregroundColor: shimmerParams.backgroundColor,
shimmeringColor: shimmerParams.foregroundColor,
shapes: shapes,
horizontal: true,
effectSize: 60.0,
globalTimeOffset: false,
duration: 0.7,
size: self.bounds.size
)
}
override func display() {
if self.bounds.size.width.isZero {
return
}
UIGraphicsBeginImageContextWithOptions(self.bounds.size, false, 0.0)
if let view = self.delegate as? View {
view.draw(CGRect(origin: CGPoint(), size: self.bounds.size))
}
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
if let image = image {
let previousContents = self.contents
self.contents = image.cgImage
if let shimmerMask = self.shimmerMask {
shimmerMask.contents = image.cgImage
shimmerMask.frame = self.bounds
self.updateShimmer()
}
if let previousContents = previousContents, CFGetTypeID(previousContents as CFTypeRef) == CGImage.typeID, (previousContents as! CGImage).width != Int(image.size.width * image.scale), let contents = self.contents {
self.animate(from: previousContents as AnyObject, to: contents as AnyObject, keyPath: "contents", timingFunction: CAMediaTimingFunctionName.linear.rawValue, duration: 0.15)
}
}
}
weak var cloneLayer: CloneLayer? {
didSet {
if let cloneLayer = self.cloneLayer {
cloneLayer.contents = self.contents
}
}
}
override public var contents: Any? {
didSet {
if let cloneLayer = self.cloneLayer {
cloneLayer.contents = self.contents
}
}
}
}
override public static var layerClass: AnyClass {
return LayerImpl.self
}
private var panRecognizer: UIPanGestureRecognizer?
private var endScrubbing: ((Bool) -> Void)?
private var updateScrubbing: ((CGFloat, Double) -> Void)?
private var updateMultiplier: ((Double) -> Void)?
private var verticalPanEnabled = false
private var scrubbingMultiplier: Double = 1.0
private var scrubbingStartLocation: CGPoint?
private var component: AudioWaveformComponent?
private var validSize: CGSize?
private var playbackStatus: MediaPlayerStatus?
private var scrubbingBeginTimestamp: Double?
private var scrubbingTimestampValue: Double?
private var isAwaitingScrubbingApplication: Bool = false
private var statusDisposable: Disposable?
private var playbackStatusAnimator: ConstantDisplayLinkAnimator?
private var sparksView: SparksView?
private var progress: CGFloat = 0.0
private var lastHeight: CGFloat = 0.0
private var revealProgress: CGFloat = 1.0
private var animator: DisplayLinkAnimator?
public var enableScrubbing: Bool = false {
didSet {
if self.enableScrubbing != oldValue {
self.disablesInteractiveTransitionGestureRecognizer = self.enableScrubbing
self.panRecognizer?.isEnabled = self.enableScrubbing
}
}
}
override init(frame: CGRect) {
super.init(frame: frame)
self.backgroundColor = nil
self.isOpaque = false
(self.layer as! LayerImpl).didEnterHierarchy = { [weak self] in
self?.updatePlaybackAnimation()
}
(self.layer as! LayerImpl).didExitHierarchy = { [weak self] in
self?.updatePlaybackAnimation()
}
let panRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.panGesture(_:)))
panRecognizer.delegate = self
self.addGestureRecognizer(panRecognizer)
self.panRecognizer = panRecognizer
}
required public init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
self.statusDisposable?.dispose()
}
public var cloneLayer: CloneLayer? {
didSet {
(self.layer as! LayerImpl).cloneLayer = self.cloneLayer
}
}
@objc private func panGesture(_ recognizer: UIPanGestureRecognizer) {
var location = recognizer.location(in: self)
location.x -= self.bounds.minX
switch recognizer.state {
case .began:
self.scrubbingStartLocation = location
self.beginScrubbing()
case .changed:
if let scrubbingStartLocation = self.scrubbingStartLocation {
let delta = location.x - scrubbingStartLocation.x
var multiplier: Double = 1.0
var skipUpdate = false
if self.verticalPanEnabled, location.y > scrubbingStartLocation.y {
let verticalDelta = abs(location.y - scrubbingStartLocation.y)
if verticalDelta > 150.0 {
multiplier = 0.01
} else if verticalDelta > 100.0 {
multiplier = 0.25
} else if verticalDelta > 50.0 {
multiplier = 0.5
}
if multiplier != self.scrubbingMultiplier {
skipUpdate = true
self.scrubbingMultiplier = multiplier
self.scrubbingStartLocation = CGPoint(x: location.x, y: scrubbingStartLocation.y)
self.updateMultiplier?(multiplier)
}
}
if !skipUpdate {
self.updateScrubbing(addedFraction: delta / self.bounds.size.width, multiplier: multiplier)
}
}
case .ended, .cancelled:
if let scrubbingStartLocation = self.scrubbingStartLocation {
self.scrubbingStartLocation = nil
let delta = location.x - scrubbingStartLocation.x
self.updateScrubbing?(delta / self.bounds.size.width, self.scrubbingMultiplier)
self.endScrubbing(apply: recognizer.state == .ended)
//self.highlighted?(false)
self.scrubbingMultiplier = 1.0
}
default:
break
}
}
private func beginScrubbing() {
if let statusValue = self.playbackStatus, statusValue.duration > 0.0 {
self.scrubbingBeginTimestamp = statusValue.timestamp
self.scrubbingTimestampValue = statusValue.timestamp
self.component?.updateIsSeeking?(true)
self.setNeedsDisplay()
}
}
private func endScrubbing(apply: Bool) {
self.scrubbingBeginTimestamp = nil
let scrubbingTimestampValue = self.scrubbingTimestampValue
self.isAwaitingScrubbingApplication = true
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.2, execute: { [weak self] in
guard let strongSelf = self, strongSelf.isAwaitingScrubbingApplication else {
return
}
strongSelf.isAwaitingScrubbingApplication = false
strongSelf.scrubbingTimestampValue = nil
strongSelf.setNeedsDisplay()
})
if let scrubbingTimestampValue = scrubbingTimestampValue, apply {
self.component?.seek?(scrubbingTimestampValue)
self.component?.updateIsSeeking?(false)
}
}
private func updateScrubbing(addedFraction: CGFloat, multiplier: Double) {
if let statusValue = self.playbackStatus, let scrubbingBeginTimestamp = self.scrubbingBeginTimestamp, Double(0.0).isLess(than: statusValue.duration) {
self.scrubbingTimestampValue = scrubbingBeginTimestamp + (statusValue.duration * Double(addedFraction)) * multiplier
self.setNeedsDisplay()
}
}
public func animateIn() {
if self.animator == nil {
self.revealProgress = 0.0
self.setNeedsDisplay()
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.08, execute: {
self.animator = DisplayLinkAnimator(duration: 0.8, from: 0.0, to: 1.0, update: { [weak self] progress in
guard let strongSelf = self else {
return
}
strongSelf.revealProgress = progress
strongSelf.setNeedsDisplay()
}, completion: { [weak self] in
guard let strongSelf = self else {
return
}
strongSelf.animator?.invalidate()
strongSelf.animator = nil
})
})
}
}
func update(component: AudioWaveformComponent, availableSize: CGSize, transition: ComponentTransition) -> CGSize {
let size = CGSize(width: availableSize.width, height: availableSize.height)
if self.validSize != size || self.component != component {
self.setNeedsDisplay()
}
(self.layer as! LayerImpl).shimmerParams = component.shimmerColor.flatMap { shimmerColor in
return ShimmerParams(
backgroundColor: component.backgroundColor,
foregroundColor: shimmerColor
)
}
self.component = component
self.validSize = size
if self.statusDisposable == nil {
self.statusDisposable = (component.status
|> deliverOnMainQueue).start(next: { [weak self] value in
guard let strongSelf = self else {
return
}
if strongSelf.isAwaitingScrubbingApplication, value.duration > 0.0, let scrubbingTimestampValue = strongSelf.scrubbingTimestampValue, abs(value.timestamp - scrubbingTimestampValue) <= value.duration * 0.01 {
strongSelf.isAwaitingScrubbingApplication = false
strongSelf.scrubbingTimestampValue = nil
}
if strongSelf.playbackStatus != value {
strongSelf.playbackStatus = value
strongSelf.setNeedsDisplay()
strongSelf.updatePlaybackAnimation()
}
})
}
if component.isViewOnceMessage {
let sparksView: SparksView
if let current = self.sparksView {
sparksView = current
} else {
sparksView = SparksView()
self.addSubview(sparksView)
self.sparksView = sparksView
}
sparksView.frame = CGRect(origin: .zero, size: size).insetBy(dx: -10.0, dy: -15.0)
} else if let sparksView = self.sparksView {
self.sparksView = nil
sparksView.removeFromSuperview()
}
return size
}
private func updatePlaybackAnimation() {
var needsAnimation = false
if let playbackStatus = self.playbackStatus {
switch playbackStatus.status {
case .playing:
needsAnimation = true
default:
needsAnimation = false
}
}
if needsAnimation != (self.playbackStatusAnimator != nil) {
if needsAnimation {
self.playbackStatusAnimator = ConstantDisplayLinkAnimator(update: { [weak self] in
if let self, let component = self.component, let sparksView = self.sparksView {
sparksView.update(position: CGPoint(x: 10.0 + (sparksView.bounds.width - 20.0) * self.progress, y: sparksView.bounds.height / 2.0 + 8.0), sampleHeight: self.lastHeight, color: component.foregroundColor)
}
self?.setNeedsDisplay()
})
self.playbackStatusAnimator?.isPaused = false
if let sparksView = self.sparksView {
sparksView.alpha = 1.0
sparksView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
} else {
self.playbackStatusAnimator?.invalidate()
self.playbackStatusAnimator = nil
if let sparksView = self.sparksView {
sparksView.alpha = 0.0
sparksView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2)
}
}
}
}
override public func draw(_ rect: CGRect) {
guard let component = self.component else {
return
}
guard let context = UIGraphicsGetCurrentContext() else {
return
}
let timestampAndDuration: (timestamp: Double, duration: Double)?
var isPlaying = false
if let statusValue = self.playbackStatus, Double(0.0).isLess(than: statusValue.duration) {
switch statusValue.status {
case .playing:
isPlaying = true
default:
break
}
if let scrubbingTimestampValue = self.scrubbingTimestampValue {
timestampAndDuration = (max(0.0, min(scrubbingTimestampValue, statusValue.duration)), statusValue.duration)
} else {
timestampAndDuration = (statusValue.timestamp, statusValue.duration)
}
} else {
timestampAndDuration = nil
}
var playbackProgress: CGFloat
if let (timestamp, duration) = timestampAndDuration {
if let scrubbingTimestampValue = self.scrubbingTimestampValue {
var progress = CGFloat(scrubbingTimestampValue / duration)
if progress.isNaN || !progress.isFinite {
progress = 0.0
}
progress = max(0.0, min(1.0, progress))
playbackProgress = progress
} else if let statusValue = self.playbackStatus {
let actualTimestamp: Double
if statusValue.generationTimestamp.isZero || !isPlaying {
actualTimestamp = timestamp
} else {
let currentTimestamp = CACurrentMediaTime()
actualTimestamp = timestamp + (currentTimestamp - statusValue.generationTimestamp) * statusValue.baseRate
}
var progress = CGFloat(actualTimestamp / duration)
if progress.isNaN || !progress.isFinite {
progress = 0.0
}
progress = max(0.0, min(1.0, progress))
playbackProgress = progress
} else {
playbackProgress = 0.0
}
} else {
playbackProgress = 0.0
}
if component.isViewOnceMessage {
playbackProgress = 1.0 - playbackProgress
}
self.progress = playbackProgress
let sampleWidth: CGFloat = 2.0
let halfSampleWidth: CGFloat = 1.0
let distance: CGFloat = 2.0
let size = bounds.size
component.samples.withUnsafeBytes { rawSamples -> Void in
let samples = rawSamples.baseAddress!.assumingMemoryBound(to: UInt16.self)
let peakHeight: CGFloat = 18.0
let maxReadSamples = rawSamples.count / 2
var maxSample: UInt16 = 0
for i in 0 ..< maxReadSamples {
let sample = samples[i]
if maxSample < sample {
maxSample = sample
}
}
let numSamples = Int(floor(size.width / (sampleWidth + distance)))
let adjustedSamplesMemory = malloc(numSamples * 2)!
let adjustedSamples = adjustedSamplesMemory.assumingMemoryBound(to: UInt16.self)
defer {
free(adjustedSamplesMemory)
}
memset(adjustedSamplesMemory, 0, numSamples * 2)
var bins: [UInt16: Int] = [:]
for i in 0 ..< maxReadSamples {
let index = i * numSamples / maxReadSamples
let sample = samples[i]
if adjustedSamples[index] < sample {
adjustedSamples[index] = sample
}
if let count = bins[sample] {
bins[sample] = count + 1
} else {
bins[sample] = 1
}
}
var sortedSamples: [(UInt16, Int)] = []
var totalCount: Int = 0
for (sample, count) in bins {
if sample > 0 {
sortedSamples.append((sample, count))
totalCount += count
}
}
sortedSamples.sort { $0.1 > $1.1 }
let invScale = 1.0 / max(1.0, CGFloat(maxSample))
let commonRevealFraction = listViewAnimationCurveSystem(self.revealProgress)
var lastHeight: CGFloat = 0.0
for i in 0 ..< numSamples {
let offset = CGFloat(i) * (sampleWidth + distance)
let peakSample = adjustedSamples[i]
var sampleHeight = CGFloat(peakSample) * peakHeight * invScale
if abs(sampleHeight) > peakHeight {
sampleHeight = peakHeight
}
let startFraction = CGFloat(i) / CGFloat(numSamples)
let nextStartFraction = CGFloat(i + 1) / CGFloat(numSamples)
if startFraction < commonRevealFraction {
let currentVerticalProgress: CGFloat = max(0.0, min(1.0, max(0.0, commonRevealFraction - startFraction) / (1.0 - startFraction)))
sampleHeight *= currentVerticalProgress
} else {
sampleHeight *= 0.0
}
let colorMixFraction: CGFloat
if startFraction < playbackProgress {
colorMixFraction = max(0.0, min(1.0, (playbackProgress - startFraction) / (nextStartFraction - startFraction)))
lastHeight = sampleHeight
} else {
colorMixFraction = 0.0
}
let diff: CGFloat
diff = sampleWidth * 1.5
let gravityMultiplierY: CGFloat
switch component.style {
case .bottom:
gravityMultiplierY = 1.0
case .middle:
gravityMultiplierY = 0.5
}
if component.backgroundColor.alpha > 0.0 {
var backgroundColor = component.backgroundColor
if component.isViewOnceMessage {
backgroundColor = component.foregroundColor.withMultipliedAlpha(0.0)
}
context.setFillColor(backgroundColor.mixedWith(component.foregroundColor, alpha: colorMixFraction).cgColor)
} else {
context.setFillColor(component.foregroundColor.cgColor)
}
context.setBlendMode(.copy)
let adjustedSampleHeight = sampleHeight - diff
if adjustedSampleHeight.isLessThanOrEqualTo(sampleWidth) {
context.fillEllipse(in: CGRect(x: offset, y: (size.height - sampleWidth) * gravityMultiplierY, width: sampleWidth, height: sampleWidth))
} else {
let adjustedRect = CGRect(
x: offset,
y: (size.height - adjustedSampleHeight) * gravityMultiplierY,
width: sampleWidth,
height: adjustedSampleHeight - halfSampleWidth
)
context.fillEllipse(in: CGRect(x: adjustedRect.minX, y: adjustedRect.minY - halfSampleWidth, width: sampleWidth, height: sampleWidth))
context.fillEllipse(in: CGRect(x: adjustedRect.minX, y: adjustedRect.maxY - halfSampleWidth, width: sampleWidth, height: sampleWidth))
context.fill(adjustedRect)
}
}
self.lastHeight = lastHeight
}
}
}
public func makeView() -> View {
return View(frame: CGRect())
}
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, transition: transition)
}
}
private struct ContentParticle {
var position: CGPoint
var direction: CGPoint
var velocity: CGFloat
var alpha: CGFloat
var lifetime: Double
var beginTime: Double
init(position: CGPoint, direction: CGPoint, velocity: CGFloat, alpha: CGFloat, lifetime: Double, beginTime: Double) {
self.position = position
self.direction = direction
self.velocity = velocity
self.alpha = alpha
self.lifetime = lifetime
self.beginTime = beginTime
}
}
private class SparksView: UIView {
private var particles: [ContentParticle] = []
private var color: UIColor = .black
override init(frame: CGRect) {
super.init(frame: frame)
self.backgroundColor = nil
self.isOpaque = false
}
required public init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private var presentationSampleHeight: CGFloat = 0.0
private var sampleHeight: CGFloat = 0.0
func update(position: CGPoint, sampleHeight: CGFloat, color: UIColor) {
self.color = color
self.sampleHeight = sampleHeight
self.presentationSampleHeight = self.presentationSampleHeight * 0.9 + self.sampleHeight * 0.1
let v = CGPoint(x: 1.0, y: 0.0)
let c = CGPoint(x: position.x - 4.0, y: position.y + 1.0 - self.presentationSampleHeight * CGFloat(arc4random_uniform(100)) / 100.0)
let timestamp = CACurrentMediaTime()
let dt: CGFloat = 1.0 / 60.0
var removeIndices: [Int] = []
for i in 0 ..< self.particles.count {
let currentTime = timestamp - self.particles[i].beginTime
if currentTime > self.particles[i].lifetime {
removeIndices.append(i)
} else {
let input: CGFloat = CGFloat(currentTime / self.particles[i].lifetime)
let decelerated: CGFloat = (1.0 - (1.0 - input) * (1.0 - input))
self.particles[i].alpha = 1.0 - decelerated
var p = self.particles[i].position
let d = self.particles[i].direction
let v = self.particles[i].velocity
p = CGPoint(x: p.x + d.x * v * dt, y: p.y + d.y * v * dt)
self.particles[i].position = p
}
}
for i in removeIndices.reversed() {
self.particles.remove(at: i)
}
let newParticleCount = 3
for _ in 0 ..< newParticleCount {
let degrees: CGFloat = CGFloat(arc4random_uniform(100)) - 65.0
let angle: CGFloat = degrees * CGFloat.pi / 180.0
let direction = CGPoint(x: v.x * cos(angle) - v.y * sin(angle), y: v.x * sin(angle) + v.y * cos(angle))
let velocity = (80.0 + (CGFloat(arc4random()) / CGFloat(UINT32_MAX)) * 4.0) * 0.5
let lifetime = Double(0.65 + CGFloat(arc4random_uniform(100)) * 0.01)
let particle = ContentParticle(position: c, direction: direction, velocity: velocity, alpha: 1.0, lifetime: lifetime, beginTime: timestamp)
self.particles.append(particle)
}
self.setNeedsDisplay()
}
override public func draw(_ rect: CGRect) {
guard let context = UIGraphicsGetCurrentContext() else {
return
}
context.setFillColor(self.color.cgColor)
for particle in self.particles {
let size: CGFloat = 1.4
context.setAlpha(particle.alpha * 1.0)
context.fillEllipse(in: CGRect(origin: CGPoint(x: particle.position.x - size / 2.0, y: particle.position.y - size / 2.0), size: CGSize(width: size, height: size)))
}
}
}
@@ -0,0 +1,20 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "AudioWaveformNode",
module_name = "AudioWaveformNode",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/AsyncDisplayKit",
"//submodules/Display",
"//submodules/AudioWaveform",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,235 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import AudioWaveform
private final class AudioWaveformNodeParameters: NSObject {
let waveform: AudioWaveform?
let drawFakeSamplesIfNeeded: Bool
let color: UIColor?
let gravity: AudioWaveformNode.Gravity?
let progress: CGFloat?
let trimRange: Range<CGFloat>?
init(waveform: AudioWaveform?, drawFakeSamplesIfNeeded: Bool, color: UIColor?, gravity: AudioWaveformNode.Gravity?, progress: CGFloat?, trimRange: Range<CGFloat>?) {
self.waveform = waveform
self.drawFakeSamplesIfNeeded = drawFakeSamplesIfNeeded
self.color = color
self.gravity = gravity
self.progress = progress
self.trimRange = trimRange
super.init()
}
}
public final class AudioWaveformNode: ASDisplayNode {
public enum Gravity {
case bottom
case center
}
private var waveform: AudioWaveform?
private var color: UIColor?
private var gravity: Gravity?
public var drawFakeSamplesIfNeeded = false
public var progress: CGFloat? {
didSet {
if self.progress != oldValue {
self.setNeedsDisplay()
}
}
}
public var trimRange: Range<CGFloat>? {
didSet {
if self.trimRange != oldValue {
self.setNeedsDisplay()
}
}
}
override public init() {
super.init()
self.isOpaque = false
}
override public var frame: CGRect {
get {
return super.frame
} set(value) {
let redraw = value.size != self.frame.size
super.frame = value
if redraw {
self.setNeedsDisplay()
}
}
}
public func setup(color: UIColor, gravity: Gravity, waveform: AudioWaveform?) {
if self.color == nil || !self.color!.isEqual(color) || self.waveform != waveform || self.gravity != gravity {
self.color = color
self.gravity = gravity
self.waveform = waveform
self.setNeedsDisplay()
}
}
override public func drawParameters(forAsyncLayer layer: _ASDisplayLayer) -> NSObjectProtocol? {
return AudioWaveformNodeParameters(waveform: self.waveform, drawFakeSamplesIfNeeded: self.drawFakeSamplesIfNeeded, color: self.color, gravity: self.gravity, progress: self.progress, trimRange: self.trimRange)
}
@objc override public class func draw(_ bounds: CGRect, withParameters parameters: Any?, isCancelled: () -> Bool, isRasterizing: Bool) {
let context = UIGraphicsGetCurrentContext()!
if !isRasterizing {
context.setBlendMode(.copy)
context.setFillColor(UIColor.clear.cgColor)
context.fill(bounds)
}
if let parameters = parameters as? AudioWaveformNodeParameters {
let sampleWidth: CGFloat = 2.0
let halfSampleWidth: CGFloat = 1.0
let distance: CGFloat = 1.0
let size = bounds.size
if let color = parameters.color {
context.setFillColor(color.cgColor)
}
if let waveform = parameters.waveform {
waveform.samples.withUnsafeBytes { rawSamples -> Void in
let samples = rawSamples.baseAddress!.assumingMemoryBound(to: UInt16.self)
let peakHeight: CGFloat = 12.0
let maxReadSamples = waveform.samples.count / 2
var maxSample: UInt16 = 0
for i in 0 ..< maxReadSamples {
let sample = samples[i]
if maxSample < sample {
maxSample = sample
}
}
let numSamples = Int(floor(size.width / (sampleWidth + distance)))
var adjustedSamples = Array<UInt16>(repeating: 0, count: numSamples)
var generateFakeSamples = false
var bins: [UInt16: Int] = [:]
for i in 0 ..< maxReadSamples {
let index = min(i * numSamples / max(1, maxReadSamples), numSamples - 1)
let sample = samples[i]
if adjustedSamples[index] < sample {
adjustedSamples[index] = sample
}
if let count = bins[sample] {
bins[sample] = count + 1
} else {
bins[sample] = 1
}
}
var sortedSamples: [(UInt16, Int)] = []
var totalCount: Int = 0
for (sample, count) in bins {
if sample > 0 {
sortedSamples.append((sample, count))
totalCount += count
}
}
sortedSamples.sort { $0.1 > $1.1 }
let topSamples = sortedSamples.prefix(1)
let topCount = topSamples.map{ $0.1 }.reduce(.zero, +)
var topCountPercent: Float = 0.0
if bins.count > 0 {
topCountPercent = Float(topCount) / Float(totalCount)
}
if parameters.drawFakeSamplesIfNeeded && topCountPercent > 0.75 {
generateFakeSamples = true
}
if generateFakeSamples {
if maxSample < 10 {
maxSample = 20
}
for i in 0 ..< maxReadSamples {
let index = i * numSamples / maxReadSamples
adjustedSamples[index] = UInt16.random(in: 6...maxSample)
}
}
let invScale = 1.0 / max(1.0, CGFloat(maxSample))
var clipRange: Range<CGFloat>?
if let trimRange = parameters.trimRange {
clipRange = trimRange.lowerBound * size.width ..< trimRange.upperBound * size.width
}
for i in 0 ..< numSamples {
let offset = CGFloat(i) * (sampleWidth + distance)
if let clipRange {
if !clipRange.contains(offset) {
continue
}
}
let peakSample = adjustedSamples[i]
var sampleHeight = CGFloat(peakSample) * peakHeight * invScale
if abs(sampleHeight) > peakHeight {
sampleHeight = peakHeight
}
let diff: CGFloat
let samplePosition = CGFloat(i) / CGFloat(numSamples)
if let position = parameters.progress, abs(position - samplePosition) < 0.01 {
diff = sampleWidth * 1.5
} else {
diff = sampleWidth * 1.5
}
let gravityMultiplierY: CGFloat = {
switch parameters.gravity ?? .bottom {
case .bottom:
return 1
case .center:
return 0.5
}
}()
let adjustedSampleHeight = sampleHeight - diff
if adjustedSampleHeight.isLessThanOrEqualTo(sampleWidth) {
context.fillEllipse(in: CGRect(x: offset, y: (size.height - sampleWidth) * gravityMultiplierY, width: sampleWidth, height: sampleWidth))
context.fill(CGRect(x: offset, y: (size.height - halfSampleWidth) * gravityMultiplierY, width: sampleWidth, height: halfSampleWidth))
} else {
let adjustedRect = CGRect(
x: offset,
y: (size.height - adjustedSampleHeight) * gravityMultiplierY,
width: sampleWidth,
height: adjustedSampleHeight
)
context.fill(adjustedRect)
context.fillEllipse(in: CGRect(x: adjustedRect.minX, y: adjustedRect.minY - halfSampleWidth, width: sampleWidth, height: sampleWidth))
context.fillEllipse(in: CGRect(x: adjustedRect.minX, y: adjustedRect.maxY - halfSampleWidth, width: sampleWidth, height: sampleWidth))
}
}
}
} else {
context.fill(CGRect(x: halfSampleWidth, y: size.height - sampleWidth, width: size.width - sampleWidth, height: sampleWidth))
context.fillEllipse(in: CGRect(x: 0.0, y: size.height - sampleWidth, width: sampleWidth, height: sampleWidth))
context.fillEllipse(in: CGRect(x: size.width - sampleWidth, y: size.height - sampleWidth, width: sampleWidth, height: sampleWidth))
}
}
}
}
@@ -0,0 +1,45 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "AuthConfirmationScreen",
module_name = "AuthConfirmationScreen",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/AsyncDisplayKit",
"//submodules/Display",
"//submodules/Postbox",
"//submodules/TelegramCore",
"//submodules/SSignalKit/SwiftSignalKit",
"//submodules/ComponentFlow",
"//submodules/Components/ViewControllerComponent",
"//submodules/Components/ComponentDisplayAdapters",
"//submodules/Components/MultilineTextComponent",
"//submodules/TelegramPresentationData",
"//submodules/AccountContext",
"//submodules/TelegramUI/Components/ButtonComponent",
"//submodules/TelegramUI/Components/PlainButtonComponent",
"//submodules/TelegramUI/Components/GlassBarButtonComponent",
"//submodules/Components/SheetComponent",
"//submodules/PresentationDataUtils",
"//submodules/Components/BundleIconComponent",
"//submodules/TelegramUI/Components/ListSectionComponent",
"//submodules/TelegramUI/Components/ListActionItemComponent",
"//submodules/TelegramUI/Components/AvatarComponent",
"//submodules/Markdown",
"//submodules/PhoneNumberFormat",
"//submodules/ContextUI",
"//submodules/AccountUtils",
"//submodules/ActivityIndicator",
"//submodules/TelegramUI/Components/PeerInfo/AccountPeerContextItem",
"//submodules/TelegramUI/Components/LottieComponent",
"//submodules/TelegramUI/Components/LottieComponentResourceContent",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,140 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import ComponentFlow
import TelegramCore
import TelegramPresentationData
import GlassBackgroundComponent
import AvatarComponent
import BundleIconComponent
import AccountContext
final class AccountSwitchComponent: Component {
let context: AccountContext
let theme: PresentationTheme
let peer: EnginePeer
let canSwitch: Bool
let isVisible: Bool
let action: ((GlassContextExtractableContainer) -> Void)
init(
context: AccountContext,
theme: PresentationTheme,
peer: EnginePeer,
canSwitch: Bool,
isVisible: Bool,
action: @escaping ((GlassContextExtractableContainer) -> Void)
) {
self.context = context
self.theme = theme
self.peer = peer
self.canSwitch = canSwitch
self.isVisible = isVisible
self.action = action
}
static func ==(lhs: AccountSwitchComponent, rhs: AccountSwitchComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.theme !== rhs.theme {
return false
}
if lhs.peer != rhs.peer {
return false
}
if lhs.canSwitch != rhs.canSwitch {
return false
}
if lhs.isVisible != rhs.isVisible {
return false
}
return true
}
final class View: UIView {
private let backgroundView = GlassContextExtractableContainer()
private let avatar = ComponentView<Empty>()
private let arrow = ComponentView<Empty>()
private let button = HighlightTrackingButton()
private var component: AccountSwitchComponent?
override init(frame: CGRect) {
super.init(frame: frame)
self.addSubview(self.backgroundView)
self.button.addTarget(self, action: #selector(self.buttonPressed), for: .touchUpInside)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@objc private func buttonPressed() {
if let component = self.component {
component.action(self.backgroundView)
}
}
func update(component: AccountSwitchComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
self.component = component
let size = CGSize(width: component.canSwitch ? 76.0 : 44.0, height: 44.0)
let avatarSize = self.avatar.update(
transition: .immediate,
component: AnyComponent(
AvatarComponent(
context: component.context,
theme: component.theme,
peer: component.peer,
)
),
environment: {},
containerSize: CGSize(width: 36.0, height: 36.0)
)
if let avatarView = self.avatar.view {
if avatarView.superview == nil {
avatarView.isUserInteractionEnabled = false
self.backgroundView.contentView.addSubview(avatarView)
}
avatarView.frame = CGRect(origin: CGPoint(x: 4.0, y: 4.0), size: avatarSize)
}
let arrowSize = self.arrow.update(
transition: .immediate,
component: AnyComponent(
BundleIconComponent(name: "Navigation/Disclosure", tintColor: component.theme.rootController.navigationBar.secondaryTextColor)
),
environment: {},
containerSize: availableSize
)
if let arrowView = self.arrow.view {
if arrowView.superview == nil {
arrowView.isUserInteractionEnabled = false
self.backgroundView.contentView.addSubview(arrowView)
self.backgroundView.contentView.addSubview(self.button)
}
arrowView.frame = CGRect(origin: CGPoint(x: size.width - arrowSize.width - 8.0, y: floorToScreenPixels((size.height - arrowSize.height) / 2.0)), size: arrowSize)
transition.setAlpha(view: arrowView, alpha: component.canSwitch ? 1.0 : 0.0)
}
self.backgroundView.update(size: size, cornerRadius: size.height * 0.5, isDark: component.theme.overallDarkAppearance, tintColor: .init(kind: .panel), isInteractive: component.canSwitch, isVisible: component.isVisible, transition: transition)
transition.setFrame(view: self.backgroundView, frame: CGRect(origin: .zero, size: size))
transition.setFrame(view: self.button, frame: CGRect(origin: .zero, size: size))
return size
}
}
func makeView() -> View {
return View(frame: CGRect())
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
@@ -0,0 +1,20 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "AvatarBackground",
module_name = "AvatarBackground",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/AsyncDisplayKit:AsyncDisplayKit",
"//submodules/Display:Display",
"//submodules/GradientBackground:GradientBackground",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,95 @@
import Foundation
import UIKit
import Display
import GradientBackground
public enum AvatarBackground: Equatable {
public static let defaultBackgrounds: [AvatarBackground] = [
.gradient([0xFF5bd1ca, 0xFF538edb], false),
.gradient([0xFF61dba8, 0xFF52abd6], false),
.gradient([0xFFbdcb57, 0xFF4abe6e], false),
.gradient([0xFFd971bf, 0xFF986ce9], false),
.gradient([0xFFee8c56, 0xFFec628f], false),
.gradient([0xFFf2994f, 0xFFe76667], false),
.gradient([0xFFf0b948, 0xFFef7e4b], false),
.gradient([0xFF94A3B0, 0xFF6C7B87], true),
.gradient([0xFF949487, 0xFF707062], true),
.gradient([0xFFB09F99, 0xFF8F7E72], true),
.gradient([0xFFEBA15B, 0xFFA16730], true),
.gradient([0xFFE8B948, 0xFFB87C30], true),
.gradient([0xFF5E6F91, 0xFF415275], true),
.gradient([0xFF565D61, 0xFF3B4347], true),
.gradient([0xFF8F6655, 0xFF68443F], true),
.gradient([0xFF1B1B1B, 0xFF000000], true),
.gradient([0xFFAE72E3, 0xFF8854B5], true),
.gradient([0xFFC269BE, 0xFF8B4384], true),
.gradient([0xFF469CD3, 0xFF2E78A8], true),
.gradient([0xFF5BCEC5, 0xFF36928E], true),
.gradient([0xFF5FD66F, 0xFF319F76], true),
.gradient([0xFF66B27A, 0xFF33786D], true),
.gradient([0xFF6C9CF4, 0xFF5C6AEC], true),
.gradient([0xFFDA76A8, 0xFFAE5891], true),
.gradient([0xFFE66473, 0xFFA74559], true),
.gradient([0xFFAF75BC, 0xFF895196], true),
.gradient([0xFF438CB9, 0xFF2D6283], true),
.gradient([0xFF81B6B2, 0xFF4B9A96], true),
.gradient([0xFF66B27A, 0xFF33786D], true),
.gradient([0xFFCAB560, 0xFF8C803C], true),
.gradient([0xFFADB070, 0xFF6B7D54], true),
.gradient([0xFFBC7051, 0xFF975547], true),
.gradient([0xFFC7835E, 0xFF9E6345], true),
.gradient([0xFFE68A3C, 0xFFD45393], true),
.gradient([0xFF6BE2F2, 0xFF6675F7], true),
.gradient([0xFFC56DF4, 0xFF6073F4], true),
.gradient([0xFFEBC92F, 0xFF54B848], true)
]
case gradient([UInt32], Bool)
public var colors: [UInt32] {
switch self {
case let .gradient(colors, _):
return colors
}
}
public var isPremium: Bool {
switch self {
case let .gradient(_, isPremium):
return isPremium
}
}
public var isLight: Bool {
switch self {
case let .gradient(colors, _):
if colors.count == 1 {
return UIColor(rgb: colors.first!).lightness > 0.99
} else if colors.count == 2 {
return UIColor(rgb: colors.first!).lightness > 0.99 || UIColor(rgb: colors.last!).lightness > 0.99
} else {
var lightCount = 0
for color in colors {
if UIColor(rgb: color).lightness > 0.99 {
lightCount += 1
}
}
return lightCount >= 2
}
}
}
public func generateImage(size: CGSize) -> UIImage {
switch self {
case let .gradient(colors, _):
if colors.count == 1 {
return generateSingleColorImage(size: size, color: UIColor(rgb: colors.first!))!
} else if colors.count == 2 {
return generateGradientImage(size: size, colors: colors.map { UIColor(rgb: $0) }, locations: [0.0, 1.0])!
} else {
return GradientBackgroundNode.generatePreview(size: size, colors: colors.map { UIColor(rgb: $0) })
}
}
}
}
@@ -0,0 +1,24 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "AvatarComponent",
module_name = "AvatarComponent",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/AsyncDisplayKit:AsyncDisplayKit",
"//submodules/Display:Display",
"//submodules/ComponentFlow",
"//submodules/TelegramCore",
"//submodules/TelegramPresentationData",
"//submodules/AvatarNode",
"//submodules/AccountContext",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,134 @@
import Foundation
import UIKit
import Display
import ComponentFlow
import TelegramCore
import TelegramPresentationData
import AvatarNode
import AccountContext
public final class AvatarComponent: Component {
public enum ClipStyle {
case round
case roundedRect
}
let context: AccountContext
let theme: PresentationTheme
let peer: EnginePeer
let clipStyle: ClipStyle
let icon: AnyComponent<Empty>?
public init(
context: AccountContext,
theme: PresentationTheme,
peer: EnginePeer,
clipStyle: ClipStyle = .round,
icon: AnyComponent<Empty>? = nil
) {
self.context = context
self.theme = theme
self.peer = peer
self.clipStyle = clipStyle
self.icon = icon
}
public static func ==(lhs: AvatarComponent, rhs: AvatarComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.theme !== rhs.theme {
return false
}
if lhs.peer != rhs.peer {
return false
}
if lhs.clipStyle != rhs.clipStyle {
return false
}
if lhs.icon != rhs.icon {
return false
}
return true
}
public final class View: UIView {
private let avatarNode: AvatarNode
private var icon: ComponentView<Empty>?
private var component: AvatarComponent?
private weak var state: EmptyComponentState?
override init(frame: CGRect) {
self.avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 42.0))
super.init(frame: frame)
self.addSubnode(self.avatarNode)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(component: AvatarComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
if self.component == nil {
self.avatarNode.font = avatarPlaceholderFont(size: ceil(42.0 * availableSize.width / 100.0))
}
self.component = component
self.state = state
var cutoutRect: CGRect?
if let icon = component.icon {
let iconView: ComponentView<Empty>
if let current = self.icon {
iconView = current
} else {
iconView = ComponentView()
self.icon = iconView
}
let iconSize = iconView.update(
transition: .immediate,
component: icon,
environment: {},
containerSize: CGSize(width: 24.0, height: 24.0)
)
let iconFrame = CGRect(origin: CGPoint(x: availableSize.width - iconSize.width + 2.0, y: availableSize.height - iconSize.height + 2.0), size: iconSize)
if let iconView = iconView.view {
if iconView.superview == nil {
self.addSubview(iconView)
}
iconView.frame = iconFrame
}
cutoutRect = CGRect(origin: CGPoint(x: iconFrame.minX, y: availableSize.height - iconFrame.maxY), size: iconFrame.size).insetBy(dx: -2.0 + UIScreenPixel, dy: -2.0 + UIScreenPixel)
}
var clipStyle: AvatarNodeClipStyle = .round
if case .roundedRect = component.clipStyle {
clipStyle = .roundedRect
}
self.avatarNode.frame = CGRect(origin: .zero, size: availableSize)
self.avatarNode.setPeer(
context: component.context,
theme: component.theme,
peer: component.peer,
clipStyle: clipStyle,
synchronousLoad: true,
displayDimensions: availableSize,
cutoutRect: cutoutRect
)
return availableSize
}
}
public func makeView() -> View {
return View(frame: CGRect())
}
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
@@ -0,0 +1,51 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "AvatarEditorScreen",
module_name = "AvatarEditorScreen",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/AsyncDisplayKit:AsyncDisplayKit",
"//submodules/Display:Display",
"//submodules/Postbox:Postbox",
"//submodules/TelegramCore:TelegramCore",
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
"//submodules/ComponentFlow:ComponentFlow",
"//submodules/Components/ViewControllerComponent:ViewControllerComponent",
"//submodules/Components/ComponentDisplayAdapters:ComponentDisplayAdapters",
"//submodules/Components/MultilineTextComponent:MultilineTextComponent",
"//submodules/TelegramUI/Components/ButtonComponent",
"//submodules/TelegramPresentationData:TelegramPresentationData",
"//submodules/AccountContext:AccountContext",
"//submodules/AppBundle:AppBundle",
"//submodules/TelegramStringFormatting:TelegramStringFormatting",
"//submodules/PresentationDataUtils:PresentationDataUtils",
"//submodules/TelegramUI/Components/EmojiStatusComponent",
"//submodules/ContextUI",
"//submodules/UndoUI",
"//submodules/AnimatedStickerNode",
"//submodules/TelegramAnimatedStickerNode",
"//submodules/TelegramNotices:TelegramNotices",
"//submodules/Markdown:Markdown",
"//submodules/GradientBackground:GradientBackground",
"//submodules/LegacyComponents:LegacyComponents",
"//submodules/DrawingUI:DrawingUI",
"//submodules/StickerResources:StickerResources",
"//submodules/TelegramUI/Components/AnimationCache:AnimationCache",
"//submodules/TelegramUI/Components/EmojiTextAttachmentView:EmojiTextAttachmentView",
"//submodules/TelegramUI/Components/MediaEditor",
"//submodules/TelegramUI/Components/AvatarBackground",
"//submodules/TelegramUI/Components/LottieComponent",
"//submodules/TelegramUI/Components/PremiumAlertController",
"//submodules/TelegramUI/Components/GlassBarButtonComponent",
"//submodules/Components/BundleIconComponent",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,227 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import ComponentFlow
import SwiftSignalKit
import ViewControllerComponent
import ComponentDisplayAdapters
import TelegramPresentationData
import AccountContext
import TelegramCore
import MultilineTextComponent
import EmojiStatusComponent
import Postbox
import AnimatedStickerNode
import TelegramAnimatedStickerNode
import StickerResources
import AvatarBackground
final class AvatarPreviewComponent: Component {
typealias EnvironmentType = Empty
let context: AccountContext
let background: AvatarBackground
let file: TelegramMediaFile?
let tapped: () -> Void
init(
context: AccountContext,
background: AvatarBackground,
file: TelegramMediaFile?,
tapped: @escaping () -> Void
) {
self.context = context
self.background = background
self.file = file
self.tapped = tapped
}
static func ==(lhs: AvatarPreviewComponent, rhs: AvatarPreviewComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.background != rhs.background {
return false
}
if lhs.file != rhs.file {
return false
}
return true
}
final class View: UIView, UITextFieldDelegate {
private let imageView: UIImageView
private let imageNode: TransformImageNode
private var animationNode: AnimatedStickerNode?
private var component: AvatarPreviewComponent?
private weak var state: EmptyComponentState?
private let stickerFetchedDisposable = MetaDisposable()
private let cachedDisposable = MetaDisposable()
override init(frame: CGRect) {
self.imageView = UIImageView()
self.imageView.isUserInteractionEnabled = false
self.imageNode = TransformImageNode()
super.init(frame: frame)
self.disablesInteractiveModalDismiss = true
self.disablesInteractiveKeyboardGestureRecognizer = true
self.addSubview(self.imageView)
self.addSubnode(self.imageNode)
self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapped)))
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
self.stickerFetchedDisposable.dispose()
self.cachedDisposable.dispose()
}
@objc func tapped() {
self.animationNode?.playOnce()
self.component?.tapped()
}
func update(component: AvatarPreviewComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: ComponentTransition) -> CGSize {
let previousBackground = self.component?.background
let hadFile = self.component?.file != nil
var fileUpdated = false
if self.component?.file?.fileId != component.file?.fileId {
fileUpdated = true
}
self.component = component
self.state = state
let size = CGSize(width: availableSize.width * 0.66, height: availableSize.width * 0.66)
var dimensions: CGSize?
if let file = component.file, fileUpdated, let fileDimensions = file.dimensions?.cgSize {
dimensions = fileDimensions
if !self.imageNode.isHidden && hadFile, let snapshotView = self.imageNode.view.snapshotContentTree() {
self.imageNode.view.superview?.addSubview(snapshotView)
snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false)
snapshotView.layer.animateScale(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false, completion: { [weak snapshotView] _ in
snapshotView?.removeFromSuperview()
})
}
if let animationNode = self.animationNode {
self.animationNode = nil
animationNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false)
animationNode.layer.animateScale(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false, completion: { [weak animationNode] _ in
animationNode?.removeFromSupernode()
})
}
self.imageNode.isHidden = false
if file.isAnimatedSticker || file.isVideoSticker || file.mimeType == "video/webm" {
if self.animationNode == nil {
let animationNode = DefaultAnimatedStickerNodeImpl()
animationNode.autoplay = false
self.animationNode = animationNode
animationNode.started = { [weak self] in
self?.imageNode.isHidden = true
}
self.addSubnode(animationNode)
}
self.imageNode.setSignal(chatMessageAnimatedSticker(postbox: component.context.account.postbox, userLocation: .other, file: file, small: false, size: fileDimensions.aspectFitted(CGSize(width: 256.0, height: 256.0))))
self.stickerFetchedDisposable.set(freeMediaFileResourceInteractiveFetched(account: component.context.account, userLocation: .other, fileReference: stickerPackFileReference(file), resource: file.resource).start())
} else {
if let animationNode = self.animationNode {
animationNode.visibility = false
self.animationNode = nil
animationNode.removeFromSupernode()
}
self.imageNode.setSignal(chatMessageSticker(account: component.context.account, userLocation: .other, file: file, small: false, synchronousLoad: false))
self.stickerFetchedDisposable.set(freeMediaFileResourceInteractiveFetched(account: component.context.account, userLocation: .other, fileReference: stickerPackFileReference(file), resource: chatMessageStickerResource(file: file, small: false)).start())
}
if fileUpdated && hadFile {
self.imageNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
self.imageNode.layer.animateScale(from: 0.01, to: 1.0, duration: 0.2)
if let animationNode = self.animationNode {
animationNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
animationNode.layer.animateScale(from: 0.01, to: 1.0, duration: 0.2)
}
}
}
if let dimensions {
let imageSize = dimensions.aspectFitted(size)
self.imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets()))()
self.imageNode.frame = CGRect(origin: CGPoint(x: floor((availableSize.width - imageSize.width) / 2.0), y: (availableSize.height - imageSize.height) / 2.0), size: imageSize)
if let animationNode = self.animationNode {
animationNode.frame = CGRect(origin: CGPoint(x: floor((availableSize.width - imageSize.width) / 2.0), y: (availableSize.height - imageSize.height) / 2.0), size: imageSize)
animationNode.updateLayout(size: imageSize)
}
if fileUpdated {
self.updateVisibility()
}
}
self.imageView.frame = CGRect(origin: .zero, size: availableSize)
if previousBackground != component.background {
if let _ = previousBackground, !transition.animation.isImmediate {
UIView.transition(with: self.imageView, duration: 0.2, options: .transitionCrossDissolve, animations: {
self.imageView.image = component.background.generateImage(size: availableSize)
})
} else {
self.imageView.image = component.background.generateImage(size: availableSize)
}
self.imageView.image = component.background.generateImage(size: availableSize)
}
return availableSize
}
private func updateVisibility() {
guard let component = self.component, let file = component.file else {
return
}
let dimensions = file.dimensions ?? PixelDimensions(width: 512, height: 512)
let fittedDimensions = dimensions.cgSize.aspectFitted(CGSize(width: 384.0, height: 384.0))
let source = AnimatedStickerResourceSource(account: component.context.account, resource: file.resource, isVideo: file.isVideoSticker || file.mimeType == "video/webm")
self.animationNode?.setup(source: source, width: Int(fittedDimensions.width), height: Int(fittedDimensions.height), playbackMode: .count(1), mode: .direct(cachePathPrefix: nil))
self.animationNode?.visibility = true
if let animationNode = self.animationNode as? DefaultAnimatedStickerNodeImpl {
if file.isCustomTemplateEmoji {
animationNode.dynamicColor = .white
} else {
animationNode.dynamicColor = nil
}
}
self.cachedDisposable.set((source.cachedDataPath(width: 384, height: 384)
|> deliverOn(Queue.concurrentDefaultQueue())).start())
}
}
public func makeView() -> View {
return View(frame: CGRect())
}
public func update(view: View, availableSize: CGSize, state: State, environment: Environment<EnvironmentType>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
@@ -0,0 +1,408 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import ComponentFlow
import ViewControllerComponent
import ComponentDisplayAdapters
import TelegramPresentationData
import AvatarBackground
final class BackgroundColorComponent: Component {
let theme: PresentationTheme
let isPremium: Bool
let values: [AvatarBackground]
let selectedValue: AvatarBackground
let customValue: AvatarBackground?
let updateValue: (AvatarBackground) -> Void
let openColorPicker: () -> Void
init(
theme: PresentationTheme,
isPremium: Bool,
values: [AvatarBackground],
selectedValue: AvatarBackground,
customValue: AvatarBackground?,
updateValue: @escaping (AvatarBackground) -> Void,
openColorPicker: @escaping () -> Void
) {
self.theme = theme
self.isPremium = isPremium
self.values = values
self.selectedValue = selectedValue
self.customValue = customValue
self.updateValue = updateValue
self.openColorPicker = openColorPicker
}
static func ==(lhs: BackgroundColorComponent, rhs: BackgroundColorComponent) -> Bool {
if lhs.theme !== rhs.theme {
return false
}
if lhs.isPremium != rhs.isPremium {
return false
}
if lhs.values != rhs.values {
return false
}
if lhs.selectedValue != rhs.selectedValue {
return false
}
if lhs.customValue != rhs.customValue {
return false
}
return true
}
class View: UIView, UIScrollViewDelegate {
private var views: [AnyHashable: ComponentView<Empty>] = [:]
private var scrollView: UIScrollView
private var component: BackgroundColorComponent?
private weak var state: EmptyComponentState?
override init(frame: CGRect) {
self.scrollView = UIScrollView()
self.scrollView.contentInsetAdjustmentBehavior = .never
self.scrollView.showsHorizontalScrollIndicator = false
self.scrollView.showsVerticalScrollIndicator = false
super.init(frame: frame)
self.clipsToBounds = true
self.scrollView.delegate = self
self.addSubview(self.scrollView)
self.scrollView.disablesInteractiveTransitionGestureRecognizer = true
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
self.updateScrolling(transition: .immediate)
}
func updateScrolling(transition: ComponentTransition) {
guard let component = self.component else {
return
}
let itemSize = CGSize(width: 30.0, height: 30.0)
let sideInset: CGFloat = 12.0
let spacing: CGFloat = 13.0
var values: [(AvatarBackground?, Bool)] = component.values.map { ($0, false) }
if let customValue = component.customValue {
values.append((customValue, true))
} else {
values.append((nil, true))
}
let visibleBounds = self.scrollView.bounds.insetBy(dx: 0.0, dy: -10.0)
var validIds: [AnyHashable] = []
for i in 0 ..< values.count {
let position: CGFloat = sideInset + (spacing + itemSize.width) * CGFloat(i)
let itemFrame = CGRect(origin: CGPoint(x: position, y: 11.0), size: itemSize)
var isVisible = false
if visibleBounds.intersects(itemFrame) {
isVisible = true
}
if isVisible {
let itemId = AnyHashable(i)
validIds.append(itemId)
let view: ComponentView<Empty>
if let current = self.views[itemId] {
view = current
} else {
view = ComponentView<Empty>()
self.views[itemId] = view
}
let _ = view.update(
transition: transition,
component: AnyComponent(
BackgroundSwatchComponent(
theme: component.theme,
background: values[i].0,
isCustom: values[i].1,
isSelected: component.selectedValue == values[i].0,
isLocked: i >= 7 && !values[i].1 && !component.isPremium,
action: {
if let value = values[i].0, component.selectedValue != value {
component.updateValue(value)
} else if values[i].1 {
component.openColorPicker()
}
}
)
),
environment: {},
containerSize: itemSize
)
if let itemView = view.view {
if itemView.superview == nil {
self.scrollView.addSubview(itemView)
}
transition.setFrame(view: itemView, frame: itemFrame)
}
}
}
var removeIds: [AnyHashable] = []
for (id, item) in self.views {
if !validIds.contains(id) {
removeIds.append(id)
if let itemView = item.view {
itemView.removeFromSuperview()
}
}
}
for id in removeIds {
self.views.removeValue(forKey: id)
}
}
func update(component: BackgroundColorComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
self.component = component
self.state = state
let height: CGFloat = 52.0
let size = CGSize(width: availableSize.width, height: height)
let scrollFrame = CGRect(origin: .zero, size: size)
let itemSize = CGSize(width: 30.0, height: 30.0)
let sideInset: CGFloat = 12.0
let spacing: CGFloat = 13.0
let count = component.values.count + 1
let contentSize = CGSize(width: sideInset * 2.0 + CGFloat(count) * itemSize.width + CGFloat(count - 1) * spacing, height: height)
if self.scrollView.frame != scrollFrame {
self.scrollView.frame = scrollFrame
}
if self.scrollView.contentSize != contentSize {
self.scrollView.contentSize = contentSize
}
self.updateScrolling(transition: .immediate)
return size
}
}
func makeView() -> View {
return View(frame: CGRect())
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
private func generateAddIcon(color: UIColor) -> UIImage? {
return generateImage(CGSize(width: 30.0, height: 30.0), contextGenerator: { size, context in
context.clear(CGRect(origin: .zero, size: size))
context.setStrokeColor(color.cgColor)
context.setLineWidth(2.0)
context.setLineCap(.round)
context.move(to: CGPoint(x: 15.0, y: 9.0))
context.addLine(to: CGPoint(x: 15.0, y: 21.0))
context.strokePath()
context.move(to: CGPoint(x: 9.0, y: 15.0))
context.addLine(to: CGPoint(x: 21.0, y: 15.0))
context.strokePath()
})
}
private func generateMoreIcon() -> UIImage? {
return generateImage(CGSize(width: 30.0, height: 30.0), contextGenerator: { size, context in
context.clear(CGRect(origin: .zero, size: size))
context.setFillColor(UIColor.white.cgColor)
context.addEllipse(in: CGRect(x: 8.5, y: 13.5, width: 3.0, height: 3.0))
context.fillPath()
context.addEllipse(in: CGRect(x: 13.5, y: 13.5, width: 3.0, height: 3.0))
context.fillPath()
context.addEllipse(in: CGRect(x: 18.5, y: 13.5, width: 3.0, height: 3.0))
context.fillPath()
})
}
private var lockIcon: UIImage? = {
let icon = generateTintedImage(image: UIImage(bundleImageName: "Chat/Stickers/SmallLock"), color: .white)
return generateImage(CGSize(width: 30.0, height: 30.0), contextGenerator: { size, context in
context.clear(CGRect(origin: .zero, size: size))
if let icon, let cgImage = icon.cgImage {
context.draw(cgImage, in: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - icon.size.width) / 2.0), y: floorToScreenPixels((size.height - icon.size.height) / 2.0)), size: icon.size), byTiling: false)
}
})
}()
final class BackgroundSwatchComponent: Component {
let theme: PresentationTheme
let background: AvatarBackground?
let isCustom: Bool
let isSelected: Bool
let isLocked: Bool
let action: () -> Void
init(
theme: PresentationTheme,
background: AvatarBackground?,
isCustom: Bool,
isSelected: Bool,
isLocked: Bool,
action: @escaping () -> Void
) {
self.theme = theme
self.background = background
self.isCustom = isCustom
self.isSelected = isSelected
self.isLocked = isLocked
self.action = action
}
static func == (lhs: BackgroundSwatchComponent, rhs: BackgroundSwatchComponent) -> Bool {
return lhs.theme === rhs.theme && lhs.background == rhs.background && lhs.isCustom == rhs.isCustom && lhs.isSelected == rhs.isSelected && lhs.isLocked == rhs.isLocked
}
final class View: UIButton {
private var component: BackgroundSwatchComponent?
private let maskLayer: SimpleLayer
private let ringMaskLayer: SimpleShapeLayer
private let circleMaskLayer: SimpleShapeLayer
private let iconLayer: SimpleLayer
private var currentIsHighlighted: Bool = false {
didSet {
if self.currentIsHighlighted != oldValue {
self.alpha = self.currentIsHighlighted ? 0.6 : 1.0
}
}
}
override init(frame: CGRect) {
self.maskLayer = SimpleLayer()
self.ringMaskLayer = SimpleShapeLayer()
self.circleMaskLayer = SimpleShapeLayer()
self.iconLayer = SimpleLayer()
super.init(frame: frame)
self.addTarget(self, action: #selector(self.pressed), for: .touchUpInside)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@objc private func pressed() {
self.component?.action()
}
override public func beginTracking(_ touch: UITouch, with event: UIEvent?) -> Bool {
self.currentIsHighlighted = true
return super.beginTracking(touch, with: event)
}
override public func endTracking(_ touch: UITouch?, with event: UIEvent?) {
self.currentIsHighlighted = false
super.endTracking(touch, with: event)
}
override public func cancelTracking(with event: UIEvent?) {
self.currentIsHighlighted = false
super.cancelTracking(with: event)
}
func update(component: BackgroundSwatchComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
let previousBackground = self.component?.background
self.component = component
let contentSize = availableSize
let bounds = CGRect(origin: .zero, size: contentSize)
self.layer.allowsGroupOpacity = true
if self.layer.mask == nil {
self.layer.mask = self.maskLayer
self.maskLayer.frame = bounds
self.maskLayer.addSublayer(self.circleMaskLayer)
self.maskLayer.addSublayer(self.ringMaskLayer)
self.circleMaskLayer.frame = bounds
if self.circleMaskLayer.path == nil {
self.circleMaskLayer.path = UIBezierPath(ovalIn: bounds.insetBy(dx: 3.0, dy: 3.0)).cgPath
}
let ringFrame = bounds
self.ringMaskLayer.frame = CGRect(origin: .zero, size: ringFrame.size)
self.ringMaskLayer.strokeColor = UIColor.white.cgColor
self.ringMaskLayer.fillColor = UIColor.clear.cgColor
self.ringMaskLayer.lineWidth = 2.0 - UIScreenPixel
self.ringMaskLayer.path = UIBezierPath(ovalIn: CGRect(origin: .zero, size: ringFrame.size).insetBy(dx: 1.0, dy: 1.0)).cgPath
self.layer.addSublayer(self.iconLayer)
}
self.iconLayer.frame = bounds
if component.isCustom {
if previousBackground != component.background || self.iconLayer.contents == nil {
if component.background != nil {
self.iconLayer.contents = generateMoreIcon()?.cgImage
} else {
self.iconLayer.contents = generateAddIcon(color: component.theme.list.itemAccentColor)?.cgImage
}
}
} else if component.isLocked {
self.iconLayer.contents = lockIcon?.cgImage
} else {
self.iconLayer.contents = nil
}
if component.isSelected {
transition.setShapeLayerPath(layer: self.circleMaskLayer, path: CGPath(ellipseIn: bounds.insetBy(dx: 3.0, dy: 3.0), transform: nil))
} else {
transition.setShapeLayerPath(layer: self.circleMaskLayer, path: CGPath(ellipseIn: bounds, transform: nil))
}
if previousBackground != component.background {
if let background = component.background {
self.layer.backgroundColor = nil
self.layer.contents = background.generateImage(size: availableSize).cgImage
} else {
self.layer.backgroundColor = component.theme.list.itemAccentColor.withAlphaComponent(0.1).cgColor
self.layer.contents = nil
}
} else if component.background == nil {
self.layer.backgroundColor = component.theme.list.itemAccentColor.withAlphaComponent(0.1).cgColor
self.layer.contents = nil
}
return availableSize
}
}
public func makeView() -> View {
return View(frame: CGRect())
}
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
@@ -0,0 +1,30 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "AvatarUploadToastScreen",
module_name = "AvatarUploadToastScreen",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/Display",
"//submodules/TelegramPresentationData",
"//submodules/ComponentFlow",
"//submodules/Components/ComponentDisplayAdapters",
"//submodules/Postbox",
"//submodules/TelegramCore",
"//submodules/SSignalKit/SwiftSignalKit",
"//submodules/Components/ViewControllerComponent",
"//submodules/Components/MultilineTextComponent",
"//submodules/AccountContext",
"//submodules/RadialStatusNode",
"//submodules/TelegramUI/Components/AnimatedTextComponent",
"//submodules/TelegramUI/Components/PlainButtonComponent",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,485 @@
import Foundation
import UIKit
import Display
import TelegramPresentationData
import ComponentFlow
import ComponentDisplayAdapters
import AppBundle
import ViewControllerComponent
import AccountContext
import MultilineTextComponent
import RadialStatusNode
import SwiftSignalKit
import AnimatedTextComponent
import PlainButtonComponent
private final class AvatarUploadToastScreenComponent: Component {
let context: AccountContext
let image: UIImage
let uploadStatus: Signal<PeerInfoAvatarUploadStatus, NoError>
let arrowTarget: () -> (UIView, CGRect)?
let viewUploadedAvatar: () -> Void
init(
context: AccountContext,
image: UIImage,
uploadStatus: Signal<PeerInfoAvatarUploadStatus, NoError>,
arrowTarget: @escaping () -> (UIView, CGRect)?,
viewUploadedAvatar: @escaping () -> Void
) {
self.context = context
self.image = image
self.uploadStatus = uploadStatus
self.arrowTarget = arrowTarget
self.viewUploadedAvatar = viewUploadedAvatar
}
static func ==(lhs: AvatarUploadToastScreenComponent, rhs: AvatarUploadToastScreenComponent) -> Bool {
return true
}
final class View: UIView {
private let contentView: UIView
private let backgroundView: BlurredBackgroundView
private let backgroundMaskView: UIView
private let backgroundMainMaskView: UIView
private let backgroundArrowMaskView: UIImageView
private let avatarView: UIImageView
private let progressNode: RadialStatusNode
private let content = ComponentView<Empty>()
private let actionButton = ComponentView<Empty>()
private var isUpdating: Bool = false
private var component: AvatarUploadToastScreenComponent?
private var environment: EnvironmentType?
private weak var state: EmptyComponentState?
private var status: PeerInfoAvatarUploadStatus = .progress(0.0)
private var statusDisposable: Disposable?
private var doneTimer: Foundation.Timer?
private var currentIsDone: Bool = false
private var isDisplaying: Bool = false
var targetAvatarView: UIView? {
return self.avatarView
}
override init(frame: CGRect) {
self.contentView = UIView()
self.backgroundView = BlurredBackgroundView(color: .clear, enableBlur: true)
self.backgroundMaskView = UIView()
self.backgroundMainMaskView = UIView()
self.backgroundMainMaskView.backgroundColor = .white
self.backgroundArrowMaskView = UIImageView()
self.avatarView = UIImageView()
self.progressNode = RadialStatusNode(backgroundNodeColor: .clear)
super.init(frame: frame)
self.backgroundView.mask = self.backgroundMaskView
self.backgroundMaskView.addSubview(self.backgroundMainMaskView)
self.backgroundMaskView.addSubview(self.backgroundArrowMaskView)
self.addSubview(self.backgroundView)
self.addSubview(self.contentView)
self.contentView.addSubview(self.avatarView)
self.contentView.addSubview(self.progressNode.view)
}
required init?(coder: NSCoder) {
preconditionFailure()
}
deinit {
self.statusDisposable?.dispose()
self.doneTimer?.invalidate()
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if !self.contentView.frame.contains(point) {
return nil
}
return super.hitTest(point, with: event)
}
func animateIn() {
func generateParabollicMotionKeyframes(from sourcePoint: CGFloat, elevation: CGFloat) -> [CGFloat] {
let midPoint = sourcePoint - elevation
let y1 = sourcePoint
let y2 = midPoint
let y3 = sourcePoint
let x1 = 0.0
let x2 = 100.0
let x3 = 200.0
var keyframes: [CGFloat] = []
let a = (x3 * (y2 - y1) + x2 * (y1 - y3) + x1 * (y3 - y2)) / ((x1 - x2) * (x1 - x3) * (x2 - x3))
let b = (x1 * x1 * (y2 - y3) + x3 * x3 * (y1 - y2) + x2 * x2 * (y3 - y1)) / ((x1 - x2) * (x1 - x3) * (x2 - x3))
let c = (x2 * x2 * (x3 * y1 - x1 * y3) + x2 * (x1 * x1 * y3 - x3 * x3 * y1) + x1 * x3 * (x3 - x1) * y2) / ((x1 - x2) * (x1 - x3) * (x2 - x3))
for i in 0 ..< 10 {
let k = listViewAnimationCurveSystem(CGFloat(i) / CGFloat(10 - 1))
let x = x3 * k
let y = a * x * x + b * x + c
keyframes.append(y)
}
return keyframes
}
let offsetValues = generateParabollicMotionKeyframes(from: 0.0, elevation: -10.0)
self.layer.animateKeyframes(values: offsetValues.map { $0 as NSNumber }, duration: 0.5, keyPath: "position.y", additive: true)
self.contentView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
self.isDisplaying = true
if !self.isUpdating {
self.state?.updated(transition: .spring(duration: 0.5))
}
}
func animateOut(completion: @escaping () -> Void) {
self.backgroundView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { _ in
})
self.contentView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { _ in
completion()
})
}
func update(component: AvatarUploadToastScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<ViewControllerComponentContainer.Environment>, transition: ComponentTransition) -> CGSize {
self.isUpdating = true
defer {
self.isUpdating = false
}
let environment = environment[ViewControllerComponentContainer.Environment.self].value
if self.component == nil {
self.statusDisposable = (component.uploadStatus
|> deliverOnMainQueue).startStrict(next: { [weak self] status in
guard let self else {
return
}
self.status = status
if !self.isUpdating {
self.state?.updated(transition: .spring(duration: 0.4))
}
if case .done = status, self.doneTimer == nil {
self.doneTimer = Foundation.Timer.scheduledTimer(withTimeInterval: 4.0, repeats: false, block: { [weak self] _ in
guard let self else {
return
}
self.environment?.controller()?.dismiss()
})
}
})
}
self.component = component
self.environment = environment
self.state = state
var isDone = false
let effectiveProgress: CGFloat
switch self.status {
case let .progress(value):
effectiveProgress = CGFloat(value)
case .done:
isDone = true
effectiveProgress = 1.0
}
let previousIsDone = self.currentIsDone
self.currentIsDone = isDone
let contentInsets = UIEdgeInsets(top: 10.0, left: 12.0, bottom: 10.0, right: 10.0)
let tabBarHeight: CGFloat
if !environment.safeInsets.left.isZero {
tabBarHeight = 34.0 + environment.safeInsets.bottom
} else {
tabBarHeight = 49.0 + environment.safeInsets.bottom
}
let containerInsets = UIEdgeInsets(
top: environment.safeInsets.top,
left: environment.safeInsets.left + 12.0,
bottom: tabBarHeight + 3.0,
right: environment.safeInsets.right + 12.0
)
let availableContentSize = CGSize(width: availableSize.width - containerInsets.left - containerInsets.right, height: availableSize.height - containerInsets.top - containerInsets.bottom)
let spacing: CGFloat = 12.0
let iconSize = CGSize(width: 30.0, height: 30.0)
let iconProgressInset: CGFloat = 3.0
let uploadingString = environment.strings.AvatarUpload_StatusUploading
let doneString = environment.strings.AvatarUpload_StatusDone
var commonPrefixLength = 0
for i in 0 ..< min(uploadingString.count, doneString.count) {
if uploadingString[uploadingString.index(uploadingString.startIndex, offsetBy: i)] != doneString[doneString.index(doneString.startIndex, offsetBy: i)] {
break
}
commonPrefixLength = i
}
var textItems: [AnimatedTextComponent.Item] = []
if commonPrefixLength != 0 {
textItems.append(AnimatedTextComponent.Item(id: AnyHashable(0), isUnbreakable: true, content: .text(String(uploadingString[uploadingString.startIndex ..< uploadingString.index(uploadingString.startIndex, offsetBy: commonPrefixLength)]))))
}
if isDone {
textItems.append(AnimatedTextComponent.Item(id: AnyHashable(1), isUnbreakable: true, content: .text(String(doneString[doneString.index(doneString.startIndex, offsetBy: commonPrefixLength)...]))))
} else {
textItems.append(AnimatedTextComponent.Item(id: AnyHashable(1), isUnbreakable: true, content: .text(String(uploadingString[uploadingString.index(uploadingString.startIndex, offsetBy: commonPrefixLength)...]))))
}
let actionButtonSize = self.actionButton.update(
transition: .immediate,
component: AnyComponent(PlainButtonComponent(
content: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: environment.strings.AvatarUpload_ViewAction, font: Font.regular(17.0), textColor: environment.theme.list.itemAccentColor.withMultiplied(hue: 0.933, saturation: 0.61, brightness: 1.0)))
)),
effectAlignment: .center,
contentInsets: UIEdgeInsets(top: -8.0, left: -8.0, bottom: -8.0, right: -8.0),
action: { [weak self] in
guard let self, let component = self.component else {
return
}
self.doneTimer?.invalidate()
self.environment?.controller()?.dismiss()
component.viewUploadedAvatar()
},
animateAlpha: true,
animateScale: false,
animateContents: false
)),
environment: {},
containerSize: CGSize(width: availableContentSize.width - contentInsets.left - contentInsets.right - spacing - iconSize.width, height: availableContentSize.height)
)
let contentSize = self.content.update(
transition: transition,
component: AnyComponent(AnimatedTextComponent(
font: Font.regular(14.0),
color: .white,
items: textItems
)),
environment: {},
containerSize: CGSize(width: availableContentSize.width - contentInsets.left - contentInsets.right - spacing - iconSize.width - actionButtonSize.width - 16.0 - 4.0, height: availableContentSize.height)
)
var contentHeight: CGFloat = 0.0
contentHeight += contentInsets.top + contentInsets.bottom + max(iconSize.height, contentSize.height)
if self.avatarView.image == nil {
self.avatarView.image = generateImage(iconSize, rotatedContext: { size, context in
UIGraphicsPushContext(context)
defer {
UIGraphicsPopContext()
}
context.clear(CGRect(origin: CGPoint(), size: size))
context.addEllipse(in: CGRect(origin: CGPoint(), size: size))
context.clip()
component.image.draw(in: CGRect(origin: CGPoint(), size: size))
})
}
let avatarFrame = CGRect(origin: CGPoint(x: contentInsets.left, y: floor((contentHeight - iconSize.height) * 0.5)), size: iconSize)
var adjustedAvatarFrame = avatarFrame
if !isDone {
adjustedAvatarFrame = adjustedAvatarFrame.insetBy(dx: iconProgressInset, dy: iconProgressInset)
}
transition.setPosition(view: self.avatarView, position: adjustedAvatarFrame.center)
transition.setBounds(view: self.avatarView, bounds: CGRect(origin: CGPoint(), size: adjustedAvatarFrame.size))
if isDone && !previousIsDone {
let topScale: CGFloat = 1.1
self.avatarView.layer.animateScale(from: 1.0, to: topScale, duration: 0.16, removeOnCompletion: false, completion: { [weak self] _ in
guard let self else {
return
}
self.avatarView.layer.animateScale(from: topScale, to: 1.0, duration: 0.16)
})
self.progressNode.layer.animateScale(from: 1.0, to: topScale, duration: 0.16, removeOnCompletion: false, completion: { [weak self] _ in
guard let self else {
return
}
self.progressNode.layer.animateScale(from: topScale, to: 1.0, duration: 0.16)
})
HapticFeedback().success()
}
self.progressNode.frame = avatarFrame
self.progressNode.transitionToState(.progress(color: .white, lineWidth: 1.0 + UIScreenPixel, value: effectiveProgress, cancelEnabled: false, animateRotation: true))
transition.setAlpha(view: self.progressNode.view, alpha: isDone ? 0.0 : 1.0)
if let contentView = self.content.view {
if contentView.superview == nil {
self.contentView.addSubview(contentView)
}
transition.setFrame(view: contentView, frame: CGRect(origin: CGPoint(x: contentInsets.left + iconSize.width + spacing, y: floor((contentHeight - contentSize.height) * 0.5)), size: contentSize))
}
if let actionButtonView = self.actionButton.view {
if actionButtonView.superview == nil {
self.contentView.addSubview(actionButtonView)
}
transition.setFrame(view: actionButtonView, frame: CGRect(origin: CGPoint(x: availableContentSize.width - contentInsets.right - 16.0 - actionButtonSize.width, y: floor((contentHeight - actionButtonSize.height) * 0.5)), size: actionButtonSize))
transition.setAlpha(view: actionButtonView, alpha: isDone ? 1.0 : 0.0)
}
let size = CGSize(width: availableContentSize.width, height: contentHeight)
let contentFrame = CGRect(origin: CGPoint(x: containerInsets.left, y: availableSize.height - containerInsets.bottom - size.height), size: size)
self.backgroundView.updateColor(color: self.isDisplaying ? UIColor(white: 0.0, alpha: 0.7) : UIColor.black, transition: transition.containedViewLayoutTransition)
let backgroundFrame: CGRect
if self.isDisplaying {
backgroundFrame = contentFrame
} else {
backgroundFrame = CGRect(origin: CGPoint(), size: availableSize)
}
if self.backgroundView.bounds.size != contentFrame.size {
self.backgroundView.update(size: availableSize, cornerRadius: 0.0, transition: transition.containedViewLayoutTransition)
}
transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(), size: availableSize))
transition.setFrame(view: self.backgroundMaskView, frame: CGRect(origin: CGPoint(), size: availableSize))
transition.setCornerRadius(layer: self.backgroundMainMaskView.layer, cornerRadius: self.isDisplaying ? 14.0 : 0.0)
transition.setFrame(view: self.backgroundMainMaskView, frame: backgroundFrame)
if self.backgroundArrowMaskView.image == nil {
let arrowFactor: CGFloat = 0.75
let arrowSize = CGSize(width: floor(29.0 * arrowFactor), height: floor(10.0 * arrowFactor))
self.backgroundArrowMaskView.image = generateImage(arrowSize, rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.scaleBy(x: size.width / 29.0, y: size.height / 10.0)
context.setFillColor(UIColor.white.cgColor)
context.scaleBy(x: 0.333, y: 0.333)
let _ = try? drawSvgPath(context, path: "M85.882251,0 C79.5170552,0 73.4125613,2.52817247 68.9116882,7.02834833 L51.4264069,24.5109211 C46.7401154,29.1964866 39.1421356,29.1964866 34.4558441,24.5109211 L16.9705627,7.02834833 C12.4696897,2.52817247 6.36519576,0 0,0 L85.882251,0 ")
context.fillPath()
})?.withRenderingMode(.alwaysTemplate)
}
if let arrowImage = self.backgroundArrowMaskView.image, let (targetView, targetRect) = component.arrowTarget() {
let targetArrowRect = targetView.convert(targetRect, to: self)
self.backgroundArrowMaskView.isHidden = false
var arrowFrame = CGRect(origin: CGPoint(x: targetArrowRect.minX + floor((targetArrowRect.width - arrowImage.size.width) * 0.5), y: contentFrame.maxY), size: arrowImage.size)
if !self.isDisplaying {
arrowFrame = arrowFrame.offsetBy(dx: 0.0, dy: -10.0)
}
transition.setFrame(view: self.backgroundArrowMaskView, frame: arrowFrame)
} else {
self.backgroundArrowMaskView.isHidden = true
}
transition.setFrame(view: self.contentView, frame: contentFrame)
return availableSize
}
}
func makeView() -> View {
return View(frame: CGRect())
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<ViewControllerComponentContainer.Environment>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
public class AvatarUploadToastScreen: ViewControllerComponentContainer {
public var targetAvatarView: UIView? {
if let view = self.node.hostView.componentView as? AvatarUploadToastScreenComponent.View {
return view.targetAvatarView
}
return nil
}
private var processedDidAppear: Bool = false
private var processedDidDisappear: Bool = false
public init(
context: AccountContext,
image: UIImage,
uploadStatus: Signal<PeerInfoAvatarUploadStatus, NoError>,
arrowTarget: @escaping () -> (UIView, CGRect)?,
viewUploadedAvatar: @escaping () -> Void
) {
super.init(
context: context,
component: AvatarUploadToastScreenComponent(
context: context,
image: image,
uploadStatus: uploadStatus,
arrowTarget: arrowTarget,
viewUploadedAvatar: viewUploadedAvatar
),
navigationBarAppearance: .none,
statusBarStyle: .ignore,
presentationMode: .default,
updatedPresentationData: nil
)
self.navigationPresentation = .flatModal
}
required public init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
}
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition)
}
override public func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
if !self.processedDidAppear {
self.processedDidAppear = true
if let componentView = self.node.hostView.componentView as? AvatarUploadToastScreenComponent.View {
componentView.animateIn()
}
}
}
private func superDismiss() {
super.dismiss()
}
override public func dismiss(completion: (() -> Void)? = nil) {
if !self.processedDidDisappear {
self.processedDidDisappear = true
if let componentView = self.node.hostView.componentView as? AvatarUploadToastScreenComponent.View {
componentView.animateOut(completion: { [weak self] in
if let self {
self.superDismiss()
}
completion?()
})
} else {
super.dismiss(completion: completion)
}
}
}
}
@@ -0,0 +1,20 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "BackButtonComponent",
module_name = "BackButtonComponent",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/ComponentFlow",
"//submodules/Components/MultilineTextComponent",
"//submodules/Display",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,134 @@
import Foundation
import UIKit
import ComponentFlow
import Display
import MultilineTextComponent
public final class BackButtonComponent: Component {
public let title: String
public let color: UIColor
public let action: () -> Void
public init(
title: String,
color: UIColor,
action: @escaping () -> Void
) {
self.title = title
self.color = color
self.action = action
}
public static func ==(lhs: BackButtonComponent, rhs: BackButtonComponent) -> Bool {
if lhs.title != rhs.title {
return false
}
if lhs.color != rhs.color {
return false
}
return true
}
public final class View: HighlightTrackingButton {
private let arrowView: UIImageView
private let title = ComponentView<Empty>()
private var component: BackButtonComponent?
public override init(frame: CGRect) {
self.arrowView = UIImageView()
super.init(frame: frame)
self.addSubview(self.arrowView)
self.highligthedChanged = { [weak self] highlighted in
if let self {
let transition: ComponentTransition = highlighted ? .immediate : .easeInOut(duration: 0.2)
if highlighted {
transition.setAlpha(view: self.arrowView, alpha: 0.65)
if let titleView = self.title.view {
transition.setAlpha(view: titleView, alpha: 0.65)
}
} else {
transition.setAlpha(view: self.arrowView, alpha: 1.0)
if let titleView = self.title.view {
transition.setAlpha(view: titleView, alpha: 1.0)
}
}
}
}
self.addTarget(self, action: #selector(self.pressed), for: .touchUpInside)
}
required public init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@objc private func pressed() {
guard let component = self.component else {
return
}
component.action()
}
override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if self.isHidden || self.alpha.isZero || self.isUserInteractionEnabled == false {
return nil
}
if self.bounds.insetBy(dx: -8.0, dy: -8.0).contains(point) {
return self
}
return nil
}
func update(component: BackButtonComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
self.component = component
if self.arrowView.image == nil {
self.arrowView.image = navigationBarBackArrowImage(color: .white)?.withRenderingMode(.alwaysTemplate)
}
self.arrowView.tintColor = component.color
let titleSize = self.title.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: component.title, font: Font.regular(17.0), textColor: component.color))
)),
environment: {},
containerSize: CGSize(width: availableSize.width - 4.0, height: availableSize.height)
)
let arrowInset: CGFloat = 15.0
let size = CGSize(width: arrowInset + titleSize.width, height: titleSize.height)
if let arrowImage = self.arrowView.image {
let arrowFrame = CGRect(origin: CGPoint(x: -4.0, y: floor((size.height - arrowImage.size.height) * 0.5)), size: arrowImage.size)
transition.setFrame(view: self.arrowView, frame: arrowFrame)
}
let titleFrame = CGRect(origin: CGPoint(x: arrowInset, y: floor((size.height - titleSize.height) * 0.5)), size: titleSize)
if let titleView = self.title.view {
if titleView.superview == nil {
titleView.layer.anchorPoint = CGPoint()
self.addSubview(titleView)
}
transition.setPosition(view: titleView, position: titleFrame.origin)
titleView.bounds = CGRect(origin: CGPoint(), size: titleFrame.size)
}
return size
}
}
public func makeView() -> View {
return View(frame: CGRect())
}
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
@@ -0,0 +1,20 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "BadgeComponent",
module_name = "BadgeComponent",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/Display",
"//submodules/ComponentFlow",
"//submodules/TelegramUI/Components/RasterizedCompositionComponent",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,196 @@
import Foundation
import UIKit
import Display
import RasterizedCompositionComponent
import ComponentFlow
public final class BadgeComponent: Component {
public enum CornerRadius: Equatable {
case automatic
case custom(CGFloat)
}
public let text: String
public let font: UIFont
public let cornerRadius: CornerRadius
public let insets: UIEdgeInsets
public let outerInsets: UIEdgeInsets
public init(
text: String,
font: UIFont,
cornerRadius: CornerRadius,
insets: UIEdgeInsets,
outerInsets: UIEdgeInsets
) {
self.text = text
self.font = font
self.cornerRadius = cornerRadius
self.insets = insets
self.outerInsets = outerInsets
}
public static func ==(lhs: BadgeComponent, rhs: BadgeComponent) -> Bool {
if lhs.text != rhs.text {
return false
}
if lhs.font != rhs.font {
return false
}
if lhs.cornerRadius != rhs.cornerRadius {
return false
}
if lhs.insets != rhs.insets {
return false
}
if lhs.outerInsets != rhs.outerInsets {
return false
}
return true
}
private struct TextLayout {
var size: CGSize
var opticalBounds: CGRect
init(size: CGSize, opticalBounds: CGRect) {
self.size = size
self.opticalBounds = opticalBounds
}
}
public final class View: UIView {
override public static var layerClass: AnyClass {
return RasterizedCompositionLayer.self
}
private let contentsClippingLayer: RasterizedCompositionLayer
private let backgroundInsetLayer: RasterizedCompositionImageLayer
private let backgroundLayer: RasterizedCompositionImageLayer
private let textContentsLayer: RasterizedCompositionImageLayer
private var textLayout: TextLayout?
private var component: BadgeComponent?
override public init(frame: CGRect) {
self.contentsClippingLayer = RasterizedCompositionLayer()
self.backgroundInsetLayer = RasterizedCompositionImageLayer()
self.backgroundLayer = RasterizedCompositionImageLayer()
self.textContentsLayer = RasterizedCompositionImageLayer()
self.textContentsLayer.anchorPoint = CGPoint()
super.init(frame: frame)
self.layer.addSublayer(self.backgroundInsetLayer)
self.layer.addSublayer(self.backgroundLayer)
self.layer.addSublayer(self.contentsClippingLayer)
self.contentsClippingLayer.addSublayer(self.textContentsLayer)
}
required public init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(component: BadgeComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
let previousComponent = self.component
self.component = component
if component.text != previousComponent?.text || component.font != previousComponent?.font {
let attributedText = NSAttributedString(string: component.text, attributes: [
NSAttributedString.Key.font: component.font,
NSAttributedString.Key.foregroundColor: UIColor.black
])
var boundingRect = attributedText.boundingRect(with: availableSize, options: .usesLineFragmentOrigin, context: nil)
boundingRect.size.width = ceil(boundingRect.size.width)
boundingRect.size.height = ceil(boundingRect.size.height)
if let context = DrawingContext(size: boundingRect.size, scale: 0.0, opaque: false, clear: true) {
context.withContext { c in
UIGraphicsPushContext(c)
defer {
UIGraphicsPopContext()
}
attributedText.draw(at: CGPoint())
}
var minFilledLineY = Int(context.scaledSize.height) - 1
var maxFilledLineY = 0
var minFilledLineX = Int(context.scaledSize.width) - 1
var maxFilledLineX = 0
for y in 0 ..< Int(context.scaledSize.height) {
let linePtr = context.bytes.advanced(by: max(0, y) * context.bytesPerRow).assumingMemoryBound(to: UInt32.self)
for x in 0 ..< Int(context.scaledSize.width) {
let pixelPtr = linePtr.advanced(by: x)
if pixelPtr.pointee != 0 {
minFilledLineY = min(y, minFilledLineY)
maxFilledLineY = max(y, maxFilledLineY)
minFilledLineX = min(x, minFilledLineX)
maxFilledLineX = max(x, maxFilledLineX)
}
}
}
var opticalBounds = CGRect()
if minFilledLineX <= maxFilledLineX && minFilledLineY <= maxFilledLineY {
opticalBounds.origin.x = CGFloat(minFilledLineX) / context.scale
opticalBounds.origin.y = CGFloat(minFilledLineY) / context.scale
opticalBounds.size.width = CGFloat(maxFilledLineX - minFilledLineX) / context.scale
opticalBounds.size.height = CGFloat(maxFilledLineY - minFilledLineY) / context.scale
}
self.textContentsLayer.image = context.generateImage()
self.textLayout = TextLayout(size: boundingRect.size, opticalBounds: opticalBounds)
} else {
self.textLayout = TextLayout(size: boundingRect.size, opticalBounds: CGRect(origin: CGPoint(), size: boundingRect.size))
}
}
let textSize = self.textLayout?.size ?? CGSize(width: 1.0, height: 1.0)
let size = CGSize(width: textSize.width + component.insets.left + component.insets.right, height: textSize.height + component.insets.top + component.insets.bottom)
let backgroundFrame = CGRect(origin: CGPoint(), size: size)
transition.setFrame(layer: self.backgroundLayer, frame: backgroundFrame)
transition.setFrame(layer: self.contentsClippingLayer, frame: backgroundFrame)
let outerInsetsFrame = CGRect(origin: CGPoint(x: backgroundFrame.minX - component.outerInsets.left, y: backgroundFrame.minY - component.outerInsets.top), size: CGSize(width: backgroundFrame.width + component.outerInsets.left + component.outerInsets.right, height: backgroundFrame.height + component.outerInsets.top + component.outerInsets.bottom))
transition.setFrame(layer: self.backgroundInsetLayer, frame: outerInsetsFrame)
var textFrame = CGRect(origin: CGPoint(x: component.insets.left, y: component.insets.top), size: textSize)
if let textLayout = self.textLayout {
textFrame.origin.x = -textLayout.opticalBounds.minX + floorToScreenPixels((backgroundFrame.width - textLayout.opticalBounds.width) * 0.5)
textFrame.origin.y = -textLayout.opticalBounds.minY + floorToScreenPixels((backgroundFrame.height - textLayout.opticalBounds.height) * 0.5)
}
transition.setPosition(layer: self.textContentsLayer, position: textFrame.origin)
self.textContentsLayer.bounds = CGRect(origin: CGPoint(), size: textFrame.size)
if component.cornerRadius != previousComponent?.cornerRadius {
let cornerRadius: CGFloat
switch component.cornerRadius {
case let .custom(value):
cornerRadius = value
case .automatic:
cornerRadius = floor(min(size.width, size.height) * 0.5)
}
self.backgroundLayer.image = generateStretchableFilledCircleImage(diameter: cornerRadius * 2.0, color: .white)
self.backgroundInsetLayer.image = generateStretchableFilledCircleImage(diameter: cornerRadius * 2.0, color: .black)
}
return size
}
}
public func makeView() -> View {
return View(frame: CGRect())
}
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
@@ -0,0 +1,22 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "BatchVideoRendering",
module_name = "BatchVideoRendering",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/MediaPlayer:UniversalMediaPlayer",
"//submodules/Display",
"//submodules/AccountContext",
"//submodules/SSignalKit/SwiftSignalKit",
"//submodules/TelegramCore",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,388 @@
import Foundation
import UIKit
import Display
import UniversalMediaPlayer
import AccountContext
import SwiftSignalKit
import TelegramCore
import CoreMedia
public protocol BatchVideoRenderingContextTarget: AnyObject {
var batchVideoRenderingTargetState: BatchVideoRenderingContext.TargetState? { get set }
func setSampleBuffer(sampleBuffer: CMSampleBuffer)
}
public final class BatchVideoRenderingContext {
public typealias Target = BatchVideoRenderingContextTarget
public final class TargetHandle {
private weak var context: BatchVideoRenderingContext?
private let id: Int
init(context: BatchVideoRenderingContext, id: Int) {
self.context = context
self.id = id
}
deinit {
self.context?.targetRemoved(id: self.id)
}
}
public final class TargetState {
private var lastRenderedFrame: (timestamp: Double, pts: Double, duration: Double)?
private var ptsOffset: Double = 0.0
private(set) var sampleBuffers: [CMSampleBuffer] = []
init() {
}
func addSampleBuffers(sampleBuffers: [CMSampleBuffer]) {
for sampleBuffer in sampleBuffers {
self.sampleBuffers.append(sampleBuffer)
}
}
func render(at timestamp: Double) -> CMSampleBuffer? {
if !self.sampleBuffers.isEmpty {
let sampleBuffer = self.sampleBuffers[0]
let sampleBufferPts = CMSampleBufferGetPresentationTimeStamp(sampleBuffer).seconds
let sampleBufferDuration = CMSampleBufferGetDuration(sampleBuffer).seconds
if let lastRenderedFrame = self.lastRenderedFrame {
let elapsedTime = timestamp - lastRenderedFrame.timestamp
let ptsDifference = sampleBufferPts - lastRenderedFrame.pts
if ptsDifference < 0.0 {
// Loop
if elapsedTime >= lastRenderedFrame.duration {
self.lastRenderedFrame = (timestamp, sampleBufferPts, sampleBufferDuration)
self.sampleBuffers.removeFirst()
return sampleBuffer
} else {
return nil
}
} else {
if elapsedTime >= ptsDifference {
self.lastRenderedFrame = (timestamp, sampleBufferPts, sampleBufferDuration)
self.sampleBuffers.removeFirst()
return sampleBuffer
} else {
return nil
}
}
} else {
self.lastRenderedFrame = (timestamp, sampleBufferPts, sampleBufferDuration)
self.sampleBuffers.removeFirst()
return sampleBuffer
}
} else {
return nil
}
}
}
private final class ReadingContext {
let dataPath: String
var isFailed: Bool = false
var reader: FFMpegFileReader?
init(dataPath: String) {
self.dataPath = dataPath
}
func advance() -> CMSampleBuffer? {
outer: while true {
if self.isFailed {
break outer
}
if self.reader == nil {
let reader = FFMpegFileReader(
source: .file(self.dataPath),
useHardwareAcceleration: false,
selectedStream: .mediaType(.video),
seek: nil,
maxReadablePts: nil
)
if reader == nil {
self.isFailed = true
break outer
}
self.reader = reader
}
guard let reader = self.reader else {
break outer
}
switch reader.readFrame() {
case let .frame(frame):
return createSampleBuffer(fromSampleBuffer: frame.sampleBuffer, withTimeOffset: .zero, duration: nil, displayImmediately: true)
case .error:
self.isFailed = true
break outer
case .endOfStream:
self.reader = nil
case .waitingForMoreData:
self.isFailed = true
break outer
}
}
return nil
}
}
private final class TargetContext {
weak var target: Target?
let file: FileMediaReference
let userLocation: MediaResourceUserLocation
var readingContext: QueueLocalObject<ReadingContext>?
var fetchDisposable: Disposable?
var dataDisposable: Disposable?
var dataPath: String?
init(
target: Target,
file: FileMediaReference,
userLocation: MediaResourceUserLocation
) {
self.target = target
self.file = file
self.userLocation = userLocation
}
deinit {
self.fetchDisposable?.dispose()
self.dataDisposable?.dispose()
}
}
private static let sharedQueue = Queue(name: "BatchVideoRenderingContext", qos: .default)
private let context: AccountContext
private var targetContexts: [Int: TargetContext] = [:]
private var nextId: Int = 0
private var isRendering: Bool = false
private var displayLink: SharedDisplayLinkDriver.Link?
public init(context: AccountContext) {
self.context = context
}
public func add(target: Target, file: FileMediaReference, userLocation: MediaResourceUserLocation) -> TargetHandle {
let id = self.nextId
self.nextId += 1
self.targetContexts[id] = TargetContext(
target: target,
file: file,
userLocation: userLocation
)
self.update()
return TargetHandle(context: self, id: id)
}
private func targetRemoved(id: Int) {
if self.targetContexts.removeValue(forKey: id) != nil {
self.update()
}
}
private func update() {
var removeIds: [Int] = []
for (id, targetContext) in self.targetContexts {
if targetContext.target != nil {
if targetContext.fetchDisposable == nil {
targetContext.fetchDisposable = fetchedMediaResource(
mediaBox: self.context.account.postbox.mediaBox,
userLocation: targetContext.userLocation,
userContentType: .sticker,
reference: targetContext.file.resourceReference(targetContext.file.media.resource)
).startStrict()
}
if targetContext.dataDisposable == nil {
targetContext.dataDisposable = (self.context.account.postbox.mediaBox.resourceData(targetContext.file.media.resource)
|> deliverOnMainQueue).startStrict(next: { [weak self, weak targetContext] data in
guard let self, let targetContext else {
return
}
if data.complete && targetContext.dataPath == nil {
targetContext.dataPath = data.path
self.update()
}
})
}
if targetContext.readingContext == nil, let dataPath = targetContext.dataPath {
targetContext.readingContext = QueueLocalObject(queue: BatchVideoRenderingContext.sharedQueue, generate: {
return ReadingContext(dataPath: dataPath)
})
}
} else {
removeIds.append(id)
}
}
for id in removeIds {
self.targetContexts.removeValue(forKey: id)
}
if !self.targetContexts.isEmpty {
if self.displayLink == nil {
self.displayLink = SharedDisplayLinkDriver.shared.add { [weak self] _ in
guard let self else {
return
}
self.updateRendering()
}
}
} else {
self.displayLink = nil
}
}
private func updateRendering() {
if self.isRendering {
return
}
var removeIds: [Int] = []
var renderIds: [Int: Int] = [:]
for (id, targetContext) in self.targetContexts {
guard let target = targetContext.target else {
removeIds.append(id)
continue
}
let targetState: TargetState
if let current = target.batchVideoRenderingTargetState {
targetState = current
} else {
targetState = TargetState()
target.batchVideoRenderingTargetState = targetState
}
if targetState.sampleBuffers.count < 2 {
renderIds[id] = 2 - targetState.sampleBuffers.count
}
}
for id in removeIds {
self.targetContexts.removeValue(forKey: id)
}
if !renderIds.isEmpty {
self.isRendering = true
var readingContexts: [Int: (Int, QueueLocalObject<ReadingContext>)] = [:]
for (id, count) in renderIds {
guard let targetContext = self.targetContexts[id] else {
continue
}
if let readingContext = targetContext.readingContext {
readingContexts[id] = (count, readingContext)
}
}
BatchVideoRenderingContext.sharedQueue.async { [weak self] in
var sampleBuffers: [Int: [CMSampleBuffer]] = [:]
for (id, (count, readingContext)) in readingContexts {
guard let readingContext = readingContext.unsafeGet() else {
sampleBuffers[id] = []
continue
}
sampleBuffers[id] = []
for _ in 0 ..< count {
if let sampleBuffer = readingContext.advance() {
sampleBuffers[id]?.append(sampleBuffer)
}
}
}
Queue.mainQueue().async {
guard let self else {
return
}
self.isRendering = false
for (id, targetSampleBuffers) in sampleBuffers {
guard let targetContext = self.targetContexts[id], let target = targetContext.target, let targetState = target.batchVideoRenderingTargetState else {
continue
}
targetState.addSampleBuffers(sampleBuffers: targetSampleBuffers)
}
self.updateFrames()
}
}
} else {
self.updateFrames()
}
if !self.targetContexts.isEmpty {
if self.displayLink == nil {
self.displayLink = SharedDisplayLinkDriver.shared.add { [weak self] _ in
guard let self else {
return
}
self.updateRendering()
}
}
} else {
self.displayLink = nil
}
}
private func updateFrames() {
let timestamp = CACurrentMediaTime()
for (_, targetContext) in self.targetContexts {
guard let target = targetContext.target, let targetState = target.batchVideoRenderingTargetState else {
continue
}
if let sampleBuffer = targetState.render(at: timestamp) {
target.setSampleBuffer(sampleBuffer: sampleBuffer)
}
}
}
}
private func createSampleBuffer(fromSampleBuffer sampleBuffer: CMSampleBuffer, withTimeOffset timeOffset: CMTime, duration: CMTime?, displayImmediately: Bool) -> CMSampleBuffer? {
var itemCount: CMItemCount = 0
var status = CMSampleBufferGetSampleTimingInfoArray(sampleBuffer, entryCount: 0, arrayToFill: nil, entriesNeededOut: &itemCount)
if status != 0 {
return nil
}
var timingInfo = [CMSampleTimingInfo](repeating: CMSampleTimingInfo(duration: CMTimeMake(value: 0, timescale: 0), presentationTimeStamp: CMTimeMake(value: 0, timescale: 0), decodeTimeStamp: CMTimeMake(value: 0, timescale: 0)), count: itemCount)
status = CMSampleBufferGetSampleTimingInfoArray(sampleBuffer, entryCount: itemCount, arrayToFill: &timingInfo, entriesNeededOut: &itemCount)
if status != 0 {
return nil
}
if let dur = duration {
for i in 0 ..< itemCount {
timingInfo[i].decodeTimeStamp = CMTimeAdd(timingInfo[i].decodeTimeStamp, timeOffset)
timingInfo[i].presentationTimeStamp = CMTimeAdd(timingInfo[i].presentationTimeStamp, timeOffset)
timingInfo[i].duration = dur
}
} else {
for i in 0 ..< itemCount {
timingInfo[i].decodeTimeStamp = CMTimeAdd(timingInfo[i].decodeTimeStamp, timeOffset)
timingInfo[i].presentationTimeStamp = CMTimeAdd(timingInfo[i].presentationTimeStamp, timeOffset)
}
}
var sampleBufferOffset: CMSampleBuffer?
CMSampleBufferCreateCopyWithNewTiming(allocator: kCFAllocatorDefault, sampleBuffer: sampleBuffer, sampleTimingEntryCount: itemCount, sampleTimingArray: &timingInfo, sampleBufferOut: &sampleBufferOffset)
guard let sampleBufferOffset else {
return nil
}
if displayImmediately {
let attachments: NSArray = CMSampleBufferGetSampleAttachmentsArray(sampleBufferOffset, createIfNecessary: true)! as NSArray
let dict: NSMutableDictionary = attachments[0] as! NSMutableDictionary
dict[kCMSampleAttachmentKey_DisplayImmediately as NSString] = true as NSNumber
}
return sampleBufferOffset
}
@@ -0,0 +1,24 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "BottomButtonPanelComponent",
module_name = "BottomButtonPanelComponent",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/Display",
"//submodules/AsyncDisplayKit",
"//submodules/ComponentFlow",
"//submodules/TelegramPresentationData",
"//submodules/Components/ComponentDisplayAdapters",
"//submodules/Components/MultilineTextComponent",
"//submodules/TelegramUI/Components/ButtonComponent",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,169 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import ComponentFlow
import ComponentDisplayAdapters
import TelegramPresentationData
import ButtonComponent
import MultilineTextComponent
public final class BottomButtonPanelComponent: Component {
let theme: PresentationTheme
let title: String
let label: String?
let icon: AnyComponentWithIdentity<Empty>?
let isEnabled: Bool
let insets: UIEdgeInsets
let action: () -> Void
public init(
theme: PresentationTheme,
title: String,
label: String?,
icon: AnyComponentWithIdentity<Empty>? = nil,
isEnabled: Bool,
insets: UIEdgeInsets,
action: @escaping () -> Void
) {
self.theme = theme
self.title = title
self.label = label
self.icon = icon
self.isEnabled = isEnabled
self.insets = insets
self.action = action
}
public static func ==(lhs: BottomButtonPanelComponent, rhs: BottomButtonPanelComponent) -> Bool {
if lhs.theme !== rhs.theme {
return false
}
if lhs.title != rhs.title {
return false
}
if lhs.label != rhs.label {
return false
}
if lhs.icon != rhs.icon {
return false
}
if lhs.isEnabled != rhs.isEnabled {
return false
}
if lhs.insets != rhs.insets {
return false
}
return true
}
public class View: UIView {
private let backgroundView: BlurredBackgroundView
private let separatorLayer: SimpleLayer
private let actionButton = ComponentView<Empty>()
private var component: BottomButtonPanelComponent?
override public init(frame: CGRect) {
self.backgroundView = BlurredBackgroundView(color: nil, enableBlur: true)
self.separatorLayer = SimpleLayer()
super.init(frame: frame)
self.addSubview(self.backgroundView)
self.layer.addSublayer(self.separatorLayer)
}
required public init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(component: BottomButtonPanelComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
let themeUpdated = self.component?.theme !== component.theme
self.component = component
let topInset: CGFloat = 8.0
let bottomInset: CGFloat
if component.insets.bottom == 0.0 {
bottomInset = topInset
} else {
bottomInset = component.insets.bottom + 10.0
}
let height: CGFloat = topInset + 50.0 + bottomInset
if themeUpdated {
self.backgroundView.updateColor(color: component.theme.rootController.navigationBar.blurredBackgroundColor, transition: .immediate)
self.separatorLayer.backgroundColor = component.theme.rootController.navigationBar.separatorColor.cgColor
}
let backgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: availableSize.width, height: height))
transition.setFrame(view: self.backgroundView, frame: backgroundFrame)
self.backgroundView.update(size: backgroundFrame.size, transition: transition.containedViewLayoutTransition)
transition.setFrame(layer: self.separatorLayer, frame: CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: availableSize.width, height: UIScreenPixel)))
var buttonTitleVStack: [AnyComponentWithIdentity<Empty>] = []
let titleString = NSMutableAttributedString(string: component.title, font: Font.semibold(17.0), textColor: component.theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center)
buttonTitleVStack.append(AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent(text: .plain(titleString)))))
if let label = component.label {
let labelString = NSMutableAttributedString(string: label, font: Font.semibold(11.0), textColor: component.theme.list.itemCheckColors.foregroundColor.withAlphaComponent(0.7), paragraphAlignment: .center)
buttonTitleVStack.append(AnyComponentWithIdentity(id: AnyHashable(1), component: AnyComponent(MultilineTextComponent(text: .plain(labelString)))))
}
var buttonTitleContent: AnyComponent<Empty> = AnyComponent(VStack(buttonTitleVStack, spacing: 1.0))
if let icon = component.icon {
buttonTitleContent = AnyComponent(HStack([
icon,
AnyComponentWithIdentity(id: "_title", component: buttonTitleContent)
], spacing: 7.0))
}
let actionButtonSize = self.actionButton.update(
transition: transition,
component: AnyComponent(ButtonComponent(
background: ButtonComponent.Background(
color: component.theme.list.itemCheckColors.fillColor,
foreground: component.theme.list.itemCheckColors.foregroundColor,
pressedColor: component.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9),
cornerRadius: 10.0
),
content: AnyComponentWithIdentity(
id: 0,
component: buttonTitleContent
),
isEnabled: component.isEnabled,
displaysProgress: false,
action: { [weak self] in
guard let self else {
return
}
self.component?.action()
}
)),
environment: {},
containerSize: CGSize(width: availableSize.width - component.insets.left - component.insets.right, height: 50.0)
)
if let actionButtonView = self.actionButton.view {
if actionButtonView.superview == nil {
self.addSubview(actionButtonView)
}
transition.setFrame(view: actionButtonView, frame: CGRect(origin: CGPoint(x: component.insets.left, y: topInset), size: actionButtonSize))
}
return CGSize(width: availableSize.width, height: height)
}
}
public func makeView() -> View {
return View(frame: CGRect())
}
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
@@ -0,0 +1,25 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "ButtonComponent",
module_name = "ButtonComponent",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/Display",
"//submodules/ComponentFlow",
"//submodules/TelegramUI/Components/AnimatedTextComponent",
"//submodules/ActivityIndicator",
"//submodules/ShimmerEffect",
"//submodules/Components/BundleIconComponent",
"//submodules/TelegramUI/Components/GlassBackgroundComponent",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,801 @@
import Foundation
import UIKit
import Display
import ComponentFlow
import AnimatedTextComponent
import ActivityIndicator
import BundleIconComponent
import ShimmerEffect
import GlassBackgroundComponent
public final class ButtonBadgeComponent: Component {
let fillColor: UIColor
let style: ButtonTextContentComponent.BadgeStyle
let content: AnyComponent<Empty>
public init(
fillColor: UIColor,
style: ButtonTextContentComponent.BadgeStyle,
content: AnyComponent<Empty>
) {
self.fillColor = fillColor
self.style = style
self.content = content
}
public static func ==(lhs: ButtonBadgeComponent, rhs: ButtonBadgeComponent) -> Bool {
if lhs.fillColor != rhs.fillColor {
return false
}
if lhs.style != rhs.style {
return false
}
if lhs.content != rhs.content {
return false
}
return true
}
public final class View: UIView {
private let backgroundView: UIImageView
private let content = ComponentView<Empty>()
private var component: ButtonBadgeComponent?
override public init(frame: CGRect) {
self.backgroundView = UIImageView()
super.init(frame: frame)
self.addSubview(self.backgroundView)
}
required public init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public func update(component: ButtonBadgeComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
let height: CGFloat
switch component.style {
case .round:
height = 20.0
case .roundedRectangle:
height = 18.0
}
let contentInset: CGFloat = 10.0
let themeUpdated = self.component?.fillColor != component.fillColor
self.component = component
let contentSize = self.content.update(
transition: transition,
component: component.content,
environment: {},
containerSize: availableSize
)
let backgroundWidth: CGFloat = max(height, contentSize.width + contentInset)
let backgroundFrame = CGRect(origin: CGPoint(), size: CGSize(width: backgroundWidth, height: height))
transition.setFrame(view: self.backgroundView, frame: backgroundFrame)
if let contentView = self.content.view {
if contentView.superview == nil {
self.addSubview(contentView)
}
transition.setFrame(view: contentView, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((backgroundFrame.width - contentSize.width) * 0.5), y: floorToScreenPixels((backgroundFrame.height - contentSize.height) * 0.5)), size: contentSize))
}
if themeUpdated || backgroundFrame.height != self.backgroundView.image?.size.height {
switch component.style {
case .round:
self.backgroundView.image = generateStretchableFilledCircleImage(diameter: backgroundFrame.height, color: component.fillColor)
case .roundedRectangle:
self.backgroundView.image = generateFilledRoundedRectImage(size: CGSize(width: height, height: height), cornerRadius: 4.0, color: component.fillColor)?.stretchableImage(withLeftCapWidth: Int(height / 2.0), topCapHeight: Int(height / 2.0))
}
}
return backgroundFrame.size
}
}
public func makeView() -> View {
return View(frame: CGRect())
}
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
public final class ButtonTextContentComponent: Component {
public enum BadgeStyle {
case round
case roundedRectangle
}
public let text: String
public let badge: Int
public let textColor: UIColor
public let fontSize: CGFloat
public let badgeBackground: UIColor
public let badgeForeground: UIColor
public let badgeStyle: BadgeStyle
public let badgeIconName: String?
public let combinedAlignment: Bool
public init(
text: String,
badge: Int,
textColor: UIColor,
fontSize: CGFloat = 17.0,
badgeBackground: UIColor,
badgeForeground: UIColor,
badgeStyle: BadgeStyle = .round,
badgeIconName: String? = nil,
combinedAlignment: Bool = false
) {
self.text = text
self.badge = badge
self.textColor = textColor
self.fontSize = fontSize
self.badgeBackground = badgeBackground
self.badgeForeground = badgeForeground
self.badgeStyle = badgeStyle
self.badgeIconName = badgeIconName
self.combinedAlignment = combinedAlignment
}
public static func ==(lhs: ButtonTextContentComponent, rhs: ButtonTextContentComponent) -> Bool {
if lhs.text != rhs.text {
return false
}
if lhs.badge != rhs.badge {
return false
}
if lhs.textColor != rhs.textColor {
return false
}
if lhs.fontSize != rhs.fontSize {
return false
}
if lhs.badgeBackground != rhs.badgeBackground {
return false
}
if lhs.badgeForeground != rhs.badgeForeground {
return false
}
if lhs.badgeStyle != rhs.badgeStyle {
return false
}
if lhs.badgeIconName != rhs.badgeIconName {
return false
}
if lhs.combinedAlignment != rhs.combinedAlignment {
return false
}
return true
}
public final class View: UIView {
private var component: ButtonTextContentComponent?
private weak var componentState: EmptyComponentState?
private let content = ComponentView<Empty>()
private var badge: ComponentView<Empty>?
override init(frame: CGRect) {
super.init(frame: frame)
}
required init(coder: NSCoder) {
preconditionFailure()
}
override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
return super.hitTest(point, with: event)
}
func update(component: ButtonTextContentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
let previousBadge = self.component?.badge
self.component = component
self.componentState = state
var badgeSpacing: CGFloat = 6.0
if component.badgeIconName != nil {
badgeSpacing += 4.0
}
let contentSize = self.content.update(
transition: .immediate,
component: AnyComponent(Text(
text: component.text,
font: Font.semibold(component.fontSize),
color: component.textColor
)),
environment: {},
containerSize: availableSize
)
var badgeSize: CGSize?
if component.badge > 0 {
var badgeTransition = transition
let badge: ComponentView<Empty>
if let current = self.badge {
badge = current
} else {
badgeTransition = .immediate
badge = ComponentView()
self.badge = badge
}
var badgeContent: [AnyComponentWithIdentity<Empty>] = []
if let badgeIconName = component.badgeIconName {
badgeContent.append(AnyComponentWithIdentity(
id: "icon",
component: AnyComponent(BundleIconComponent(
name: badgeIconName,
tintColor: component.badgeForeground
)))
)
}
badgeContent.append(AnyComponentWithIdentity(
id: "text",
component: AnyComponent(AnimatedTextComponent(
font: Font.with(size: 15.0, design: .round, weight: .semibold, traits: .monospacedNumbers),
color: component.badgeForeground,
items: [
AnimatedTextComponent.Item(id: AnyHashable(0), content: .number(component.badge, minDigits: 0))
]
)))
)
badgeSize = badge.update(
transition: badgeTransition,
component: AnyComponent(ButtonBadgeComponent(
fillColor: component.badgeBackground,
style: component.badgeStyle,
content: AnyComponent(HStack(badgeContent, spacing: 2.0))
)),
environment: {},
containerSize: CGSize(width: 100.0, height: 100.0)
)
}
var size = contentSize
var measurementSize = size
if let badgeSize {
if component.combinedAlignment {
measurementSize.width += badgeSpacing
measurementSize.width += badgeSize.width
}
size.height = max(size.height, badgeSize.height)
}
let contentFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - measurementSize.width) * 0.5), y: floorToScreenPixels((size.height - measurementSize.height) * 0.5)), size: measurementSize)
if let contentView = self.content.view {
if contentView.superview == nil {
self.addSubview(contentView)
}
transition.setFrame(view: contentView, frame: CGRect(origin: contentFrame.origin, size: contentSize))
}
if let badgeSize, let badge = self.badge {
let badgeFrame = CGRect(origin: CGPoint(x: contentFrame.minX + contentSize.width + badgeSpacing, y: floorToScreenPixels((size.height - badgeSize.height) * 0.5) + 1.0), size: badgeSize)
if let badgeView = badge.view {
var animateIn = false
if badgeView.superview == nil {
animateIn = true
self.addSubview(badgeView)
}
if animateIn {
badgeView.frame = badgeFrame
} else {
transition.setFrame(view: badgeView, frame: badgeFrame)
if !transition.animation.isImmediate, let previousBadge, previousBadge != component.badge {
let middleScale: CGFloat = previousBadge < component.badge ? 1.1 : 0.9
let values: [NSNumber] = [1.0, middleScale as NSNumber, 1.0]
badgeView.layer.animateKeyframes(values: values, duration: 0.25, keyPath: "transform.scale")
}
}
if animateIn, !transition.animation.isImmediate {
badgeView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1)
badgeView.layer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.4)
}
}
} else {
if let badge = self.badge {
self.badge = nil
if let badgeView = badge.view {
if !transition.animation.isImmediate {
badgeView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { [weak badgeView] _ in
badgeView?.removeFromSuperview()
})
badgeView.layer.animateScale(from: 1.0, to: 0.001, duration: 0.25, removeOnCompletion: false)
} else {
badgeView.removeFromSuperview()
}
}
}
}
return size
}
}
public func makeView() -> View {
return View(frame: CGRect())
}
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
public final class ButtonComponent: Component {
public struct Background: Equatable {
public enum Style {
case glass
case actualGlass
case legacy
}
public var style: Style
public var color: UIColor
public var foreground: UIColor
public var pressedColor: UIColor
public var cornerRadius: CGFloat
public var isShimmering: Bool
public init(
style: Style = .legacy,
color: UIColor,
foreground: UIColor,
pressedColor: UIColor,
cornerRadius: CGFloat = 10.0,
isShimmering: Bool = false
) {
self.style = style
self.color = color
self.foreground = foreground
self.pressedColor = pressedColor
self.cornerRadius = cornerRadius
self.isShimmering = isShimmering
}
public func withIsShimmering(_ isShimmering: Bool) -> Background {
return Background(
style: self.style,
color: self.color,
foreground: self.foreground,
pressedColor: self.pressedColor,
cornerRadius: self.cornerRadius,
isShimmering: isShimmering
)
}
}
public let background: Background
public let content: AnyComponentWithIdentity<Empty>
public let fitToContentWidth: Bool
public let isEnabled: Bool
public let tintWhenDisabled: Bool
public let allowActionWhenDisabled: Bool
public let displaysProgress: Bool
public let action: () -> Void
public init(
background: Background,
content: AnyComponentWithIdentity<Empty>,
fitToContentWidth: Bool = false,
isEnabled: Bool = true,
tintWhenDisabled: Bool = true,
allowActionWhenDisabled: Bool = false,
displaysProgress: Bool = false,
action: @escaping () -> Void
) {
self.background = background
self.content = content
self.fitToContentWidth = fitToContentWidth
self.isEnabled = isEnabled
self.tintWhenDisabled = tintWhenDisabled
self.allowActionWhenDisabled = allowActionWhenDisabled
self.displaysProgress = displaysProgress
self.action = action
}
public static func ==(lhs: ButtonComponent, rhs: ButtonComponent) -> Bool {
if lhs.background != rhs.background {
return false
}
if lhs.content != rhs.content {
return false
}
if lhs.fitToContentWidth != rhs.fitToContentWidth {
return false
}
if lhs.isEnabled != rhs.isEnabled {
return false
}
if lhs.tintWhenDisabled != rhs.tintWhenDisabled {
return false
}
if lhs.allowActionWhenDisabled != rhs.allowActionWhenDisabled {
return false
}
if lhs.displaysProgress != rhs.displaysProgress {
return false
}
return true
}
private final class ContentItem {
let id: AnyHashable
let view = ComponentView<Empty>()
init(id: AnyHashable) {
self.id = id
}
}
public final class View: UIView {
private var component: ButtonComponent?
private weak var componentState: EmptyComponentState?
private var containerView: UIView
private var glassContainerView: GlassBackgroundView?
private let button: HighlightTrackingButton
private var shimmeringView: ButtonShimmeringView?
private var chromeView: UIImageView?
private var contentItem: ContentItem?
private var activityIndicator: ActivityIndicator?
override init(frame: CGRect) {
self.containerView = UIView()
self.containerView.clipsToBounds = true
self.containerView.isUserInteractionEnabled = false
self.button = HighlightTrackingButton()
super.init(frame: frame)
self.button.isExclusiveTouch = true
self.layer.rasterizationScale = UIScreenScale
self.addSubview(self.containerView)
self.addSubview(self.button)
self.button.addTarget(self, action: #selector(self.pressed), for: .touchUpInside)
self.button.highligthedChanged = { [weak self] highlighted in
if let self, let component = self.component, component.isEnabled {
switch component.background.style {
case .glass:
let transition = ComponentTransition(animation: .curve(duration: highlighted ? 0.25 : 0.35, curve: .spring))
if highlighted {
self.layer.shouldRasterize = true
let highlightedColor = component.background.color.withMultiplied(hue: 1.0, saturation: 0.77, brightness: 1.01)
transition.setBackgroundColor(view: self.containerView, color: highlightedColor)
transition.setScale(view: self.containerView, scale: 1.05, completion: { finished in
if finished {
self.layer.shouldRasterize = false
}
})
} else {
self.layer.shouldRasterize = true
transition.setBackgroundColor(view: self.containerView, color: component.background.color)
transition.setScale(view: self.containerView, scale: 1.0, completion: { finished in
if finished {
self.layer.shouldRasterize = false
}
})
}
case .legacy:
if highlighted {
self.containerView.layer.removeAnimation(forKey: "opacity")
self.containerView.alpha = 0.7
} else {
self.containerView.alpha = 1.0
self.containerView.layer.animateAlpha(from: 0.7, to: 1.0, duration: 0.2)
}
default:
break
}
}
}
}
required init(coder: NSCoder) {
preconditionFailure()
}
@objc private func pressed() {
guard let component = self.component else {
return
}
component.action()
}
override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
return super.hitTest(point, with: event)
}
func update(component: ButtonComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
self.component = component
self.componentState = state
self.button.isEnabled = (component.isEnabled || component.allowActionWhenDisabled) && !component.displaysProgress
var contentAlpha: CGFloat = 1.0
if component.displaysProgress {
contentAlpha = 0.0
} else if !component.isEnabled && component.tintWhenDisabled {
contentAlpha = 0.7
}
var previousContentItem: ContentItem?
let contentItem: ContentItem
var contentItemTransition = transition
if let current = self.contentItem, current.id == component.content.id {
contentItem = current
} else {
contentItemTransition = .immediate
previousContentItem = self.contentItem
contentItem = ContentItem(id: component.content.id)
self.contentItem = contentItem
}
var cornerRadius: CGFloat = component.background.cornerRadius
if [.glass, .actualGlass].contains(component.background.style), component.background.cornerRadius == 10.0 {
cornerRadius = availableSize.height * 0.5
}
let contentSize = contentItem.view.update(
transition: contentItemTransition,
component: component.content.component,
environment: {},
containerSize: CGSize(width: availableSize.width - cornerRadius, height: availableSize.height)
)
var size = availableSize
if component.fitToContentWidth {
size.width = floor(contentSize.width + cornerRadius * 1.5)
}
let contentContainerView: UIView
switch component.background.style {
case .actualGlass:
let glassContainerView: GlassBackgroundView
if let current = self.glassContainerView {
glassContainerView = current
} else {
self.containerView.removeFromSuperview()
glassContainerView = GlassBackgroundView()
self.glassContainerView = glassContainerView
self.insertSubview(glassContainerView, at: 0)
glassContainerView.contentView.addSubview(self.button)
}
let tintColor: GlassBackgroundView.TintColor
if component.background.color.alpha < 0.1 {
tintColor = .init(kind: .panel)
} else {
tintColor = .init(kind: .panel, innerColor: component.background.color, innerInset: 0.0)
}
glassContainerView.update(size: size, cornerRadius: cornerRadius, isDark: component.background.color.brightness < 0.2, tintColor: tintColor, isInteractive: true, transition: transition)
contentContainerView = glassContainerView.contentView
transition.setFrame(view: glassContainerView, frame: CGRect(origin: .zero, size: size))
case .glass, .legacy:
if self.containerView.superview == nil {
self.insertSubview(self.containerView, at: 0)
self.addSubview(self.button)
}
contentContainerView = self.containerView
transition.setBackgroundColor(view: self.containerView, color: component.background.color)
transition.setCornerRadius(layer: self.containerView.layer, cornerRadius: cornerRadius)
}
if let contentView = contentItem.view.view {
var animateIn = false
var contentTransition = transition
if contentView.superview == nil {
contentTransition = .immediate
animateIn = true
contentView.layer.rasterizationScale = UIScreenScale
contentView.isUserInteractionEnabled = false
contentContainerView.addSubview(contentView)
contentItem.view.parentState = state
}
let contentFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - contentSize.width) * 0.5), y: floorToScreenPixels((size.height - contentSize.height) * 0.5)), size: contentSize)
contentTransition.setFrame(view: contentView, frame: contentFrame)
contentTransition.setAlpha(view: contentView, alpha: contentAlpha)
if animateIn && previousContentItem != nil && !transition.animation.isImmediate {
contentView.layer.shouldRasterize = true
contentView.layer.animateScale(from: 0.4, to: 1.0, duration: 0.35, timingFunction: kCAMediaTimingFunctionSpring, completion: { _ in
contentView.layer.shouldRasterize = false
})
contentView.layer.animateAlpha(from: 0.0, to: contentAlpha, duration: 0.1)
contentView.layer.animatePosition(from: CGPoint(x: 0.0, y: -size.height * 0.15), to: CGPoint(), duration: 0.35, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
}
}
if let previousContentItem, let previousContentView = previousContentItem.view.view {
if !transition.animation.isImmediate {
previousContentView.layer.shouldRasterize = true
previousContentView.layer.animateScale(from: 1.0, to: 0.0, duration: 0.35, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false)
previousContentView.layer.animateAlpha(from: contentAlpha, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak previousContentView] _ in
previousContentView?.removeFromSuperview()
})
previousContentView.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: size.height * 0.35), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, additive: true)
} else {
previousContentView.removeFromSuperview()
}
}
if component.displaysProgress {
let activityIndicator: ActivityIndicator
var activityIndicatorTransition = transition
if let current = self.activityIndicator {
activityIndicator = current
} else {
activityIndicatorTransition = .immediate
activityIndicator = ActivityIndicator(type: .custom(component.background.foreground, 22.0, 2.0, true))
activityIndicator.view.alpha = 0.0
self.activityIndicator = activityIndicator
contentContainerView.addSubview(activityIndicator.view)
}
let indicatorSize = CGSize(width: 22.0, height: 22.0)
transition.setAlpha(view: activityIndicator.view, alpha: 1.0)
activityIndicatorTransition.setFrame(view: activityIndicator.view, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - indicatorSize.width) / 2.0), y: floorToScreenPixels((size.height - indicatorSize.height) / 2.0)), size: indicatorSize))
} else {
if let activityIndicator = self.activityIndicator {
self.activityIndicator = nil
transition.setAlpha(view: activityIndicator.view, alpha: 0.0, completion: { [weak activityIndicator] _ in
activityIndicator?.view.removeFromSuperview()
})
}
}
if component.background.isShimmering {
let shimmeringView: ButtonShimmeringView
var shimmeringTransition = transition
if let current = self.shimmeringView {
shimmeringView = current
} else {
shimmeringTransition = .immediate
shimmeringView = ButtonShimmeringView(frame: .zero)
self.shimmeringView = shimmeringView
contentContainerView.insertSubview(shimmeringView, at: 0)
}
shimmeringView.update(size: size, background: component.background, cornerRadius: cornerRadius, transition: shimmeringTransition)
shimmeringTransition.setFrame(view: shimmeringView, frame: CGRect(origin: .zero, size: size))
} else if let shimmeringView = self.shimmeringView {
self.shimmeringView = nil
shimmeringView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false, completion: { _ in
shimmeringView.removeFromSuperview()
})
}
if component.background.style == .glass, component.background.color.alpha > 0.9 {
let chromeView: UIImageView
var chromeTransition = transition
if let current = self.chromeView {
chromeView = current
} else {
chromeTransition = .immediate
chromeView = UIImageView()
self.chromeView = chromeView
if let shimmeringView = self.shimmeringView {
contentContainerView.insertSubview(chromeView, aboveSubview: shimmeringView)
} else {
contentContainerView.insertSubview(chromeView, at: 0)
}
chromeView.layer.compositingFilter = "overlayBlendMode"
chromeView.alpha = 0.8
chromeView.image = GlassBackgroundView.generateForegroundImage(size: CGSize(width: 26.0 * 2.0, height: 26.0 * 2.0), isDark: component.background.color.lightness < 0.36, fillColor: .clear)
}
chromeTransition.setFrame(view: chromeView, frame: CGRect(origin: .zero, size: size))
} else if let chromeView = self.chromeView {
self.chromeView = nil
chromeView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false, completion: { _ in
chromeView.removeFromSuperview()
})
}
transition.setPosition(view: self.containerView, position: CGPoint(x: size.width / 2.0, y: size.height / 2.0))
transition.setBoundsSize(view: self.containerView, size: size)
transition.setFrame(view: self.button, frame: CGRect(origin: .zero, size: size))
return size
}
}
public func makeView() -> View {
return View(frame: CGRect())
}
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
private class ButtonShimmeringView: UIView {
private var shimmerView = ShimmerEffectForegroundView()
private var borderView = UIView()
private var borderMaskView = UIView()
private var borderShimmerView = ShimmerEffectForegroundView()
override init(frame: CGRect) {
self.borderView.isUserInteractionEnabled = false
self.borderMaskView.layer.borderWidth = 1.0 + UIScreenPixel
self.borderMaskView.layer.borderColor = UIColor.white.cgColor
self.borderView.mask = self.borderMaskView
self.borderView.addSubview(self.borderShimmerView)
super.init(frame: frame)
self.isUserInteractionEnabled = false
self.addSubview(self.shimmerView)
self.addSubview(self.borderView)
}
required init?(coder: NSCoder) {
preconditionFailure()
}
func update(size: CGSize, background: ButtonComponent.Background, cornerRadius: CGFloat, transition: ComponentTransition) {
let color = background.foreground
let alpha: CGFloat
let borderAlpha: CGFloat
let compositingFilter: String?
if color.lightness > 0.5 {
alpha = 0.5
borderAlpha = 0.75
compositingFilter = "overlayBlendMode"
} else {
alpha = 0.2
borderAlpha = 0.3
compositingFilter = nil
}
self.backgroundColor = background.color
self.layer.cornerRadius = cornerRadius
self.borderMaskView.layer.cornerRadius = cornerRadius
self.shimmerView.update(backgroundColor: .clear, foregroundColor: color.withAlphaComponent(alpha), gradientSize: 70.0, globalTimeOffset: false, duration: 4.0, horizontal: true)
self.shimmerView.layer.compositingFilter = compositingFilter
self.borderShimmerView.update(backgroundColor: .clear, foregroundColor: color.withAlphaComponent(borderAlpha), gradientSize: 70.0, globalTimeOffset: false, duration: 4.0, horizontal: true)
self.borderShimmerView.layer.compositingFilter = compositingFilter
let bounds = CGRect(origin: .zero, size: size)
transition.setFrame(view: self.shimmerView, frame: bounds)
transition.setFrame(view: self.borderView, frame: bounds)
transition.setFrame(view: self.borderMaskView, frame: bounds)
transition.setFrame(view: self.borderShimmerView, frame: bounds)
self.shimmerView.updateAbsoluteRect(CGRect(origin: CGPoint(x: size.width * 4.0, y: 0.0), size: size), within: CGSize(width: size.width * 9.0, height: size.height))
self.borderShimmerView.updateAbsoluteRect(CGRect(origin: CGPoint(x: size.width * 4.0, y: 0.0), size: size), within: CGSize(width: size.width * 9.0, height: size.height))
}
}
@@ -0,0 +1,77 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
load(
"@build_bazel_rules_apple//apple:resources.bzl",
"apple_resource_bundle",
"apple_resource_group",
)
load("//build-system/bazel-utils:plist_fragment.bzl",
"plist_fragment",
)
filegroup(
name = "CallScreenMetalSources",
srcs = glob([
"Metal/**/*.metal",
]),
visibility = ["//visibility:public"],
)
plist_fragment(
name = "CallScreenMetalSourcesBundleInfoPlist",
extension = "plist",
template =
"""
<key>CFBundleIdentifier</key>
<string>org.telegram.CallScreenMetalSources</string>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleName</key>
<string>CallScreen</string>
"""
)
apple_resource_bundle(
name = "CallScreenMetalSourcesBundle",
infoplists = [
":CallScreenMetalSourcesBundleInfoPlist",
],
resources = [
":CallScreenMetalSources",
],
)
filegroup(
name = "Assets",
srcs = glob(["CallScreenAssets.xcassets/**"]),
visibility = ["//visibility:public"],
)
swift_library(
name = "CallScreen",
module_name = "CallScreen",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
data = [
":CallScreenMetalSourcesBundle",
":Assets",
],
deps = [
"//submodules/Display",
"//submodules/MetalEngine",
"//submodules/ComponentFlow",
"//submodules/SSignalKit/SwiftSignalKit",
"//submodules/TelegramUI/Components/AnimatedTextComponent",
"//submodules/Components/MultilineTextComponent",
"//submodules/AppBundle",
"//submodules/UIKitRuntimeUtils",
"//submodules/TelegramPresentationData",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "ic_call_audioairpods.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
@@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "ic_call_audioairpodspro.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
@@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "ic_call_airpodsmax.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
@@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "ic_call_audiobt.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
@@ -0,0 +1,9 @@
{
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"provides-namespace" : true
}
}
@@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "locksettings (1).pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
@@ -0,0 +1,123 @@
%PDF-1.7
1 0 obj
<< >>
endobj
2 0 obj
<< /Length 3 0 R >>
stream
/DeviceRGB CS
/DeviceRGB cs
q
1.000000 0.000000 -0.000000 1.000000 0.000000 0.000000 cm
0.000000 0.000000 0.000000 scn
0.000000 3.800000 m
0.000000 4.920105 0.000000 5.480157 0.217987 5.907981 c
0.409734 6.284305 0.715695 6.590266 1.092019 6.782013 c
1.519843 7.000000 2.079895 7.000000 3.200000 7.000000 c
5.800000 7.000000 l
6.920105 7.000000 7.480157 7.000000 7.907981 6.782013 c
8.284306 6.590266 8.590266 6.284305 8.782013 5.907981 c
9.000000 5.480157 9.000000 4.920105 9.000000 3.800000 c
9.000000 3.200000 l
9.000000 2.079895 9.000000 1.519843 8.782013 1.092019 c
8.590266 0.715695 8.284306 0.409734 7.907981 0.217987 c
7.480157 0.000000 6.920105 0.000000 5.800000 0.000000 c
3.200000 0.000000 l
2.079895 0.000000 1.519843 0.000000 1.092019 0.217987 c
0.715695 0.409734 0.409734 0.715695 0.217987 1.092019 c
0.000000 1.519843 0.000000 2.079895 0.000000 3.200000 c
0.000000 3.800000 l
h
f
n
Q
q
1.000000 0.000000 -0.000000 1.000000 2.000000 2.400391 cm
0.000000 0.000000 0.000000 scn
4.200000 1.599609 m
4.200000 1.157782 4.558172 0.799609 5.000000 0.799609 c
5.441828 0.799609 5.800000 1.157782 5.800000 1.599609 c
4.200000 1.599609 l
h
-0.800000 1.599609 m
-0.800000 1.157782 -0.441828 0.799609 -0.000000 0.799609 c
0.441828 0.799609 0.800000 1.157782 0.800000 1.599609 c
-0.800000 1.599609 l
h
4.200000 6.099609 m
4.200000 1.599609 l
5.800000 1.599609 l
5.800000 6.099609 l
4.200000 6.099609 l
h
0.800000 1.599609 m
0.800000 6.099609 l
-0.800000 6.099609 l
-0.800000 1.599609 l
0.800000 1.599609 l
h
2.500000 7.799609 m
3.438884 7.799609 4.200000 7.038493 4.200000 6.099609 c
5.800000 6.099609 l
5.800000 7.922149 4.322540 9.399610 2.500000 9.399610 c
2.500000 7.799609 l
h
2.500000 9.399610 m
0.677460 9.399610 -0.800000 7.922149 -0.800000 6.099609 c
0.800000 6.099609 l
0.800000 7.038493 1.561116 7.799609 2.500000 7.799609 c
2.500000 9.399610 l
h
f
n
Q
endstream
endobj
3 0 obj
1865
endobj
4 0 obj
<< /Annots []
/Type /Page
/MediaBox [ 0.000000 0.000000 9.000000 12.000000 ]
/Resources 1 0 R
/Contents 2 0 R
/Parent 5 0 R
>>
endobj
5 0 obj
<< /Kids [ 4 0 R ]
/Count 1
/Type /Pages
>>
endobj
6 0 obj
<< /Pages 5 0 R
/Type /Catalog
>>
endobj
xref
0 7
0000000000 65535 f
0000000010 00000 n
0000000034 00000 n
0000001955 00000 n
0000001978 00000 n
0000002150 00000 n
0000002224 00000 n
trailer
<< /ID [ (some) (id) ]
/Root 6 0 R
/Size 7
>>
startxref
2283
%%EOF
Binary file not shown.

After

Width:  |  Height:  |  Size: 503 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 782 B

@@ -0,0 +1,22 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "CallCancelIcon@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "CallCancelIcon@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
@@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "ic_flip (1).pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
@@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "ic_call_microphone.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Some files were not shown because too many files have changed in this diff Show More