Merge commit '7621e2f8dec938cf48181c8b10afc9b01f444e68' into beta

This commit is contained in:
Ilya Laktyushin
2025-12-06 02:17:48 +04:00
commit 8344b97e03
28070 changed files with 7995182 additions and 0 deletions
+25
View File
@@ -0,0 +1,25 @@
fastlane/README.md
fastlane/report.xml
fastlane/test_output/*
*.pbxuser
!default.pbxuser
*.mode1v3
!default.mode1v3
*.mode2v3
!default.mode2v3
*.perspectivev3
!default.perspectivev3
xcuserdata
*.xccheckout
*.xcscmblueprint
*.moved-aside
DerivedData
*.hmap
*.ipa
*.xcuserstate
.DS_Store
*.dSYM
*.dSYM.zip
*.ipa
*/xcuserdata/*
TelegramUI.xcodeproj/*
+511
View File
@@ -0,0 +1,511 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
load(
"@build_configuration//:variables.bzl",
"telegram_bundle_id",
)
filegroup(
name = "TelegramUIResources",
srcs = glob([
"Sounds/**/*",
"Resources/**/*",
], exclude = ["Sounds/**/.*", "Resources/**/.*"]),
visibility = ["//visibility:public"],
)
filegroup(
name = "TelegramUIAssets",
srcs = glob(["Images.xcassets/**"]),
visibility = ["//visibility:public"],
)
#filegroup(
# name = "Icons",
# srcs = glob([
# "Telegram-iOS/Icons.xcassets/**/*",
# ], exclude = ["Telegram-iOS/Icons.xcassets/**/.*"]),
#)
internal_bundle_ids = [
"org.telegram.Telegram-iOS",
]
available_appcenter_targets = [
"@appcenter_sdk//:AppCenter",
"@appcenter_sdk//:AppCenterCrashes",
]
appcenter_targets = available_appcenter_targets if telegram_bundle_id in internal_bundle_ids else []
swift_library(
name = "TelegramUI",
module_name = "TelegramUI",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//third-party/recaptcha:RecaptchaEnterprise",
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
"//submodules/SSignalKit/SSignalKit:SSignalKit",
"//submodules/AsyncDisplayKit:AsyncDisplayKit",
"//submodules/Display:Display",
"//submodules/Postbox:Postbox",
"//submodules/TelegramCore:TelegramCore",
"//submodules/MtProtoKit:MtProtoKit",
"//submodules/TelegramPresentationData:TelegramPresentationData",
"//submodules/AccountContext:AccountContext",
"//submodules/LegacyComponents:LegacyComponents",
"//submodules/lottie-ios:Lottie",
"//submodules/FFMpegBinding:FFMpegBinding",
"//submodules/WebPBinding:WebPBinding",
"//submodules/RMIntro:RMIntro",
"//submodules/GZip:GZip",
"//submodules/TelegramCallsUI:TelegramCallsUI",
"//submodules/TelegramUIPreferences:TelegramUIPreferences",
"//submodules/MediaPlayer:UniversalMediaPlayer",
"//submodules/TelegramVoip:TelegramVoip",
"//submodules/DeviceAccess:DeviceAccess",
"//submodules/Utils/DeviceModel",
"//submodules/BuildConfig:BuildConfig",
"//submodules/BuildConfigExtra:BuildConfigExtra",
"//submodules/rlottie:RLottieBinding",
"//submodules/TelegramUpdateUI:TelegramUpdateUI",
"//submodules/MergeLists:MergeLists",
"//submodules/ActivityIndicator:ActivityIndicator",
"//submodules/ProgressNavigationButtonNode:ProgressNavigationButtonNode",
"//submodules/ItemListUI:ItemListUI",
"//submodules/TelegramBaseController:TelegramBaseController",
"//submodules/DeviceLocationManager:DeviceLocationManager",
"//submodules/AvatarNode:AvatarNode",
"//submodules/AvatarVideoNode:AvatarVideoNode",
"//submodules/OverlayStatusController:OverlayStatusController",
"//submodules/TelegramStringFormatting:TelegramStringFormatting",
"//submodules/LiveLocationManager:LiveLocationManager",
"//submodules/LiveLocationTimerNode:LiveLocationTimerNode",
"//submodules/TemporaryCachedPeerDataManager:TemporaryCachedPeerDataManager",
"//submodules/ShareController:ShareController",
"//submodules/RadialStatusNode:RadialStatusNode",
"//submodules/PeerOnlineMarkerNode:PeerOnlineMarkerNode",
"//submodules/PeerPresenceStatusManager:PeerPresenceStatusManager",
"//submodules/ChatListSearchRecentPeersNode:ChatListSearchRecentPeersNode",
"//submodules/ImageBlur:ImageBlur",
"//submodules/ContextUI:ContextUI",
"//submodules/MediaResources:MediaResources",
"//submodules/TelegramAudio:TelegramAudio",
"//submodules/UrlEscaping:UrlEscaping",
"//submodules/Tuples:Tuples",
"//submodules/TextFormat:TextFormat",
"//submodules/SwitchNode:SwitchNode",
"//submodules/StickerResources:StickerResources",
"//submodules/SelectablePeerNode:SelectablePeerNode",
"//submodules/SaveToCameraRoll:SaveToCameraRoll",
"//submodules/LocalizedPeerData:LocalizedPeerData",
"//submodules/ListSectionHeaderNode:ListSectionHeaderNode",
"//submodules/HorizontalPeerItem:HorizontalPeerItem",
"//submodules/CheckNode:CheckNode",
"//submodules/AnimationUI:AnimationUI",
"//submodules/AnimatedStickerNode:AnimatedStickerNode",
"//submodules/TelegramAnimatedStickerNode:TelegramAnimatedStickerNode",
"//submodules/ActionSheetPeerItem:ActionSheetPeerItem",
"//submodules/ComposePollUI:ComposePollUI",
"//submodules/AlertUI:AlertUI",
"//submodules/PresentationDataUtils:PresentationDataUtils",
"//submodules/TouchDownGesture:TouchDownGesture",
"//submodules/SwipeToDismissGesture:SwipeToDismissGesture",
"//submodules/DirectionalPanGesture:DirectionalPanGesture",
"//submodules/UndoUI:UndoUI",
"//submodules/PhotoResources:PhotoResources",
"//submodules/TinyThumbnail:TinyThumbnail",
"//submodules/ImageTransparency:ImageTransparency",
"//submodules/TelegramNotices:TelegramNotices",
"//submodules/TelegramPermissions:TelegramPermissions",
"//submodules/GameUI:GameUI",
"//submodules/WebUI:WebUI",
"//submodules/PassportUI:PassportUI",
"//submodules/PhoneInputNode:PhoneInputNode",
"//submodules/CountrySelectionUI:CountrySelectionUI",
"//submodules/SearchBarNode:SearchBarNode",
"//submodules/GalleryUI:GalleryUI",
"//submodules/TelegramUniversalVideoContent:TelegramUniversalVideoContent",
"//submodules/WebsiteType:WebsiteType",
"//submodules/ScreenCaptureDetection:ScreenCaptureDetection",
"//submodules/OpenInExternalAppUI:OpenInExternalAppUI",
"//submodules/LegacyUI:LegacyUI",
"//submodules/ImageCompression:ImageCompression",
"//submodules/DateSelectionUI:DateSelectionUI",
"//submodules/PasswordSetupUI:PasswordSetupUI",
"//submodules/Pdf:Pdf",
"//submodules/InstantPageUI:InstantPageUI",
"//submodules/MusicAlbumArtResources:MusicAlbumArtResources",
"//submodules/LiveLocationPositionNode:LiveLocationPositionNode",
"//submodules/MosaicLayout:MosaicLayout",
"//submodules/LocationUI:LocationUI",
"//submodules/Stripe:Stripe",
"//submodules/BotPaymentsUI:BotPaymentsUI",
"//submodules/LocalAuth:LocalAuth",
"//submodules/ContactListUI:ContactListUI",
"//submodules/SearchUI:SearchUI",
"//submodules/ChatListSearchItemHeader:ChatListSearchItemHeader",
"//submodules/ItemListPeerItem:ItemListPeerItem",
"//submodules/ContactsPeerItem:ContactsPeerItem",
"//submodules/ChatListSearchItemNode:ChatListSearchItemNode",
"//submodules/TelegramPermissionsUI:TelegramPermissionsUI",
"//submodules/PeersNearbyIconNode:PeersNearbyIconNode",
"//submodules/SolidRoundedButtonNode:SolidRoundedButtonNode",
"//submodules/PasscodeUI:PasscodeUI",
"//submodules/CallListUI:CallListUI",
"//submodules/ChatListUI:ChatListUI",
"//submodules/ChatTitleActivityNode:ChatTitleActivityNode",
"//submodules/DeleteChatPeerActionSheetItem:DeleteChatPeerActionSheetItem",
"//submodules/LanguageSuggestionUI:LanguageSuggestionUI",
"//submodules/TextSelectionNode:TextSelectionNode",
"//submodules/PlatformRestrictionMatching:PlatformRestrictionMatching",
"//submodules/HashtagSearchUI:HashtagSearchUI",
"//submodules/ItemListAvatarAndNameInfoItem:ItemListAvatarAndNameInfoItem",
"//submodules/ItemListPeerActionItem:ItemListPeerActionItem",
"//submodules/StickerPackPreviewUI:StickerPackPreviewUI",
"//submodules/YuvConversion:YuvConversion",
"//submodules/JoinLinkPreviewUI:JoinLinkPreviewUI",
"//submodules/LanguageLinkPreviewUI:LanguageLinkPreviewUI",
"//submodules/WebSearchUI:WebSearchUI",
"//submodules/LegacyMediaPickerUI:LegacyMediaPickerUI",
"//submodules/MimeTypes:MimeTypes",
"//submodules/LocalMediaResources:LocalMediaResources",
"//submodules/PeersNearbyUI:PeersNearbyUI",
"//submodules/Geocoding:Geocoding",
"//submodules/PeerInfoUI:PeerInfoUI",
"//submodules/PeerAvatarGalleryUI:PeerAvatarGalleryUI",
"//submodules/Emoji:Emoji",
"//submodules/ItemListStickerPackItem:ItemListStickerPackItem",
"//submodules/NotificationMuteSettingsUI:NotificationMuteSettingsUI",
"//submodules/SinglePhoneInputNode:SinglePhoneInputNode",
"//submodules/MapResourceToAvatarSizes:MapResourceToAvatarSizes",
"//submodules/NotificationSoundSelectionUI:NotificationSoundSelectionUI",
"//submodules/EncryptionKeyVisualization:EncryptionKeyVisualization",
"//submodules/ItemListAddressItem:ItemListAddressItem",
"//submodules/DeviceProximity:DeviceProximity",
"//submodules/RaiseToListen:RaiseToListen",
"//submodules/OpusBinding:OpusBinding",
"//third-party/opus:opus",
"//submodules/ShareItems:ShareItems",
"//submodules/ShareItems/Impl:ShareItemsImpl",
"//submodules/SettingsUI:SettingsUI",
"//submodules/UrlHandling:UrlHandling",
"//submodules/HexColor:HexColor",
"//submodules/QrCode:QrCode",
"//submodules/WallpaperResources:WallpaperResources",
"//submodules/AuthorizationUI:AuthorizationUI",
"//submodules/CounterControllerTitleView:CounterControllerTitleView",
"//submodules/GridMessageSelectionNode:GridMessageSelectionNode",
"//submodules/InstantPageCache:InstantPageCache",
"//submodules/PersistentStringHash:PersistentStringHash",
"//submodules/SegmentedControlNode:SegmentedControlNode",
"//submodules/AppBundle:AppBundle",
"//submodules/Markdown:Markdown",
"//submodules/SearchPeerMembers:SearchPeerMembers",
"//submodules/WidgetItems:WidgetItems",
"//submodules/WidgetItemsUtils:WidgetItemsUtils",
"//submodules/OpenSSLEncryptionProvider:OpenSSLEncryptionProvider",
"//submodules/PhoneNumberFormat:PhoneNumberFormat",
"//submodules/AppLock:AppLock",
"//submodules/NotificationsPresentationData:NotificationsPresentationData",
"//submodules/UrlWhitelist:UrlWhitelist",
"//submodules/TelegramIntents:TelegramIntents",
"//submodules/LocationResources:LocationResources",
"//submodules/ItemListVenueItem:ItemListVenueItem",
"//submodules/SemanticStatusNode:SemanticStatusNode",
"//submodules/AccountUtils:AccountUtils",
"//submodules/Svg:Svg",
"//submodules/ManagedAnimationNode:ManagedAnimationNode",
"//submodules/TooltipUI:TooltipUI",
"//submodules/ListMessageItem:ListMessageItem",
"//submodules/FileMediaResourceStatus:FileMediaResourceStatus",
"//submodules/ChatMessageInteractiveMediaBadge:ChatMessageInteractiveMediaBadge",
"//submodules/GalleryData:GalleryData",
"//submodules/ChatInterfaceState:ChatInterfaceState",
"//submodules/AnimatedCountLabelNode:AnimatedCountLabelNode",
"//submodules/AnimatedAvatarSetNode:AnimatedAvatarSetNode",
"//submodules/SlotMachineAnimationNode:SlotMachineAnimationNode",
"//submodules/AnimatedNavigationStripeNode:AnimatedNavigationStripeNode",
"//submodules/AudioBlob:AudioBlob",
"//Telegram:GeneratedSources",
"//third-party/ZipArchive:ZipArchive",
"//submodules/ChatImportUI:ChatImportUI",
"//submodules/DatePickerNode:DatePickerNode",
"//submodules/ConfettiEffect:ConfettiEffect",
"//submodules/Speak:Speak",
"//submodules/PeerInfoAvatarListNode:PeerInfoAvatarListNode",
"//submodules/DebugSettingsUI:DebugSettingsUI",
"//submodules/ImportStickerPackUI:ImportStickerPackUI",
"//submodules/GradientBackground:GradientBackground",
"//submodules/WallpaperBackgroundNode:WallpaperBackgroundNode",
"//submodules/ComponentFlow:ComponentFlow",
"//submodules/AdUI:AdUI",
"//submodules/SparseItemGrid:SparseItemGrid",
"//submodules/CalendarMessageScreen:CalendarMessageScreen",
"//submodules/DirectMediaImageCache:DirectMediaImageCache",
"//submodules/CodeInputView:CodeInputView",
"//submodules/Components/ReactionButtonListComponent:ReactionButtonListComponent",
"//submodules/InvisibleInkDustNode:InvisibleInkDustNode",
"//submodules/QrCodeUI:QrCodeUI",
"//submodules/Components/ReactionListContextMenuContent:ReactionListContextMenuContent",
"//submodules/Components/ReactionImageComponent:ReactionImageComponent",
"//submodules/TabBarUI:TabBarUI",
"//submodules/SoftwareVideo:SoftwareVideo",
"//submodules/ManagedFile:ManagedFile",
"//submodules/FetchManagerImpl:FetchManagerImpl",
"//submodules/AttachmentUI:AttachmentUI",
"//submodules/AttachmentTextInputPanelNode:AttachmentTextInputPanelNode",
"//submodules/ChatPresentationInterfaceState:ChatPresentationInterfaceState",
"//submodules/Pasteboard:Pasteboard",
"//submodules/ChatSendMessageActionUI:ChatSendMessageActionUI",
"//submodules/ChatTextLinkEditUI:ChatTextLinkEditUI",
"//submodules/MediaPickerUI:MediaPickerUI",
"//submodules/ChatMessageBackground:ChatMessageBackground",
"//submodules/PeerInfoUI/CreateExternalMediaStreamScreen:CreateExternalMediaStreamScreen",
"//submodules/TranslateUI:TranslateUI",
"//submodules/BrowserUI:BrowserUI",
"//submodules/PremiumUI:PremiumUI",
"//submodules/Components/HierarchyTrackingLayer:HierarchyTrackingLayer",
"//submodules/Utils/RangeSet:RangeSet",
"//submodules/InAppPurchaseManager:InAppPurchaseManager",
"//submodules/TelegramUI/Components/AudioTranscriptionButtonComponent:AudioTranscriptionButtonComponent",
"//submodules/TelegramUI/Components/AudioTranscriptionPendingIndicatorComponent:AudioTranscriptionPendingIndicatorComponent",
"//submodules/TelegramUI/Components/AudioWaveformComponent:AudioWaveformComponent",
"//submodules/TelegramUI/Components/EditableChatTextNode:EditableChatTextNode",
"//submodules/TelegramUI/Components/EmojiTextAttachmentView:EmojiTextAttachmentView",
"//submodules/TelegramUI/Components/EntityKeyboard:EntityKeyboard",
"//submodules/TelegramUI/Components/AnimationCache:AnimationCache",
"//submodules/TelegramUI/Components/LottieAnimationCache:LottieAnimationCache",
"//submodules/TelegramUI/Components/VideoAnimationCache:VideoAnimationCache",
"//submodules/TelegramUI/Components/MultiAnimationRenderer:MultiAnimationRenderer",
"//submodules/TelegramUI/Components/ChatInputPanelContainer:ChatInputPanelContainer",
"//submodules/TelegramUI/Components/TextNodeWithEntities:TextNodeWithEntities",
"//submodules/TelegramUI/Components/EmojiSuggestionsComponent:EmojiSuggestionsComponent",
"//submodules/TelegramUI/Components/EmojiStatusSelectionComponent:EmojiStatusSelectionComponent",
"//submodules/TelegramUI/Components/EmojiStatusComponent:EmojiStatusComponent",
"//submodules/TelegramUI/Components/ChatControllerInteraction:ChatControllerInteraction",
"//submodules/Components/ComponentDisplayAdapters:ComponentDisplayAdapters",
"//submodules/Media/ConvertOpusToAAC:ConvertOpusToAAC",
"//submodules/Media/LocalAudioTranscription:LocalAudioTranscription",
"//submodules/Components/PagerComponent:PagerComponent",
"//submodules/Components/LottieAnimationComponent:LottieAnimationComponent",
"//submodules/TelegramUI/Components/NotificationExceptionsScreen:NotificationExceptionsScreen",
"//submodules/TelegramUI/Components/ForumCreateTopicScreen:ForumCreateTopicScreen",
"//submodules/TelegramUI/Components/ChatTitleView",
"//submodules/InviteLinksUI:InviteLinksUI",
"//submodules/TelegramUI/Components/NotificationPeerExceptionController",
"//submodules/TelegramUI/Components/ChatListHeaderComponent",
"//submodules/TelegramUI/Components/ChatInputNode",
"//submodules/TelegramUI/Components/ChatEntityKeyboardInputNode",
"//submodules/TelegramUI/Components/StorageUsageScreen",
"//submodules/TelegramUI/Components/AvatarEditorScreen",
"//submodules/TelegramUI/Components/LottieComponent",
"//submodules/MediaPasteboardUI:MediaPasteboardUI",
"//submodules/DrawingUI:DrawingUI",
"//submodules/FeaturedStickersScreen:FeaturedStickersScreen",
"//submodules/TelegramUI/Components/SendInviteLinkScreen",
"//submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen",
"//submodules/TelegramUI/Components/SliderContextItem:SliderContextItem",
"//submodules/TelegramUI/Components/Stories/StoryContainerScreen",
"//submodules/TelegramUI/Components/CameraScreen",
"//submodules/TelegramUI/Components/MediaEditorScreen",
"//submodules/TelegramUI/Components/ChatScheduleTimeController",
"//submodules/ICloudResources",
"//submodules/TelegramUI/Components/LegacyCamera",
"//submodules/TelegramUI/Components/ChatTextInputMediaRecordingButton",
"//submodules/TelegramUI/Components/ChatSendButtonRadialStatusNode",
"//submodules/TelegramUI/Components/LegacyInstantVideoController",
"//submodules/TelegramUI/Components/FullScreenEffectView",
"//submodules/TelegramUI/Components/ShareWithPeersScreen",
"//submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode",
"//submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen",
"//submodules/TelegramUI/Components/MoreHeaderButton",
"//submodules/TelegramUI/Components/Stories/AvatarStoryIndicatorComponent",
"//submodules/TelegramUI/Components/Stories/StorySetIndicatorComponent",
"//submodules/TelegramUI/Components/Chat/ChatMessageForwardInfoNode",
"//submodules/TelegramUI/Components/Chat/ChatAvatarNavigationNode",
"//submodules/TelegramUI/Components/PeerReportScreen",
"//submodules/Utils/VolumeButtons",
"//submodules/ChatContextQuery",
"//submodules/TelegramUI/Components/TelegramUIDeclareEncodables",
"//submodules/TelegramUI/Components/TelegramAccountAuxiliaryMethods",
"//submodules/TelegramUI/Components/PeerSelectionController",
"//submodules/TelegramUI/Components/Chat/AccessoryPanelNode",
"//submodules/TelegramUI/Components/Chat/ForwardAccessoryPanelNode",
"//submodules/TelegramUI/Components/LegacyMessageInputPanel",
"//submodules/StatisticsUI",
"//submodules/TelegramUI/Components/PremiumGiftAttachmentScreen",
"//submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageBubbleContentNode",
"//submodules/TelegramUI/Components/Chat/ChatHistoryEntry",
"//submodules/TelegramUI/Components/Chat/ChatMessageItemCommon",
"//submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode",
"//submodules/TelegramUI/Components/Chat/EditableTokenListNode",
"//submodules/TelegramUI/Components/Chat/ChatInputTextNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageReplyInfoNode",
"//submodules/TelegramUI/Components/Chat/ChatOverscrollControl",
"//submodules/TelegramUI/Components/AudioWaveformNode",
"//submodules/TelegramUI/Components/Chat/ChatBotInfoItem",
"//submodules/TelegramUI/Components/Chat/ChatUserInfoItem",
"//submodules/TelegramUI/Components/Chat/ChatNewThreadInfoItem",
"//submodules/TelegramUI/Components/Chat/ChatInputPanelNode",
"//submodules/TelegramUI/Components/Chat/ChatButtonKeyboardInputNode",
"//submodules/TelegramUI/Components/Chat/ChatChannelSubscriberInputPanelNode",
"//submodules/TelegramUI/Components/Chat/ChatContextResultPeekContent",
"//submodules/TelegramUI/Components/Chat/ChatInputContextPanelNode",
"//submodules/TelegramUI/Components/Chat/ReplyAccessoryPanelNode",
"//submodules/TelegramUI/Components/Chat/SuggestPostAccessoryPanelNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageCallBubbleContentNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageInteractiveInstantVideoNode",
"//submodules/TelegramUI/Components/Chat/InstantVideoRadialStatusNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageInteractiveFileNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageFileBubbleContentNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode",
"//submodules/TelegramUI/Components/WallpaperPreviewMedia",
"//submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentButtonNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageWebpageBubbleContentNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageItem",
"//submodules/TelegramUI/Components/Chat/ChatMessageStickerItemNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageSwipeToReplyNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageSelectionNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageDeliveryFailedNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageShareButton",
"//submodules/TelegramUI/Components/Chat/ChatMessageThreadInfoNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageActionButtonsNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageReactionsFooterContentNode",
"//submodules/TelegramUI/Components/Chat/ChatSwipeToReplyRecognizer",
"//submodules/TelegramUI/Components/Chat/ChatMessageInstantVideoItemNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageInstantVideoBubbleContentNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageAnimatedStickerItemNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageTransitionNode",
"//submodules/TelegramUI/Components/Chat/ManagedDiceAnimationNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageCommentFooterContentNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageActionBubbleContentNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageContactBubbleContentNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageEventLogPreviousDescriptionContentNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageEventLogPreviousLinkContentNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageEventLogPreviousMessageContentNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageGameBubbleContentNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageInvoiceBubbleContentNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageMapBubbleContentNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageMediaBubbleContentNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageProfilePhotoSuggestionContentNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageRestrictedBubbleContentNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageStoryMentionContentNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageUnsupportedBubbleContentNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageWallpaperBubbleContentNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageGiftOfferBubbleContentNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageGiveawayBubbleContentNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode",
"//submodules/TelegramUI/Components/Chat/ChatRecentActionsController",
"//submodules/TelegramUI/Components/Chat/ChatNavigationButton",
"//submodules/TelegramUI/Components/Chat/ChatLoadingNode",
"//submodules/TelegramUI/Components/Settings/PeerNameColorScreen",
"//submodules/TelegramUI/Components/ContextMenuScreen",
"//submodules/TelegramUI/Components/PeerAllowedReactionsScreen",
"//submodules/MetalEngine",
"//submodules/TelegramUI/Components/DustEffect",
"//submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen",
"//submodules/TelegramUI/Components/Chat/ChatMessageSelectionInputPanelNode",
"//submodules/TelegramUI/Components/Chat/ChatQrCodeScreen",
"//submodules/UIKitRuntimeUtils",
"//submodules/TelegramUI/Components/SavedMessages/SavedMessagesScreen",
"//submodules/TelegramUI/Components/Settings/WallpaperGalleryScreen",
"//submodules/TelegramUI/Components/Settings/WallpaperGridScreen",
"//submodules/TelegramUI/Components/Chat/ChatMessageNotificationItem",
"//submodules/Components/MultilineTextComponent",
"//submodules/TelegramUI/Components/PlainButtonComponent",
"//submodules/Components/BalancedTextComponent",
"//submodules/TelegramUI/Components/VideoMessageCameraScreen",
"//submodules/TelegramUI/Components/MediaScrubberComponent",
"//submodules/TelegramUI/Components/Chat/ChatShareMessageTagView",
"//submodules/AudioWaveform",
"//submodules/PromptUI",
"//submodules/Components/BundleIconComponent",
"//submodules/TelegramUI/Components/Chat/TopMessageReactions",
"//submodules/TelegramUI/Components/Chat/SavedTagNameAlertController",
"//submodules/TelegramUI/Components/Chat/ChatInlineSearchResultsListComponent",
"//submodules/TelegramUI/Components/Settings/ChatbotSetupScreen",
"//submodules/TelegramUI/Components/Settings/BusinessLocationSetupScreen",
"//submodules/TelegramUI/Components/Settings/BusinessHoursSetupScreen",
"//submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen",
"//submodules/TelegramUI/Components/Settings/BusinessIntroSetupScreen",
"//submodules/TelegramUI/Components/Settings/QuickReplyNameAlertController",
"//submodules/TelegramUI/Components/Settings/CollectibleItemInfoScreen",
"//submodules/TelegramUI/Components/StickerPickerScreen",
"//submodules/TelegramUI/Components/Chat/ChatEmptyNode",
"//submodules/TelegramUI/Components/Chat/ChatMediaInputStickerGridItem",
"//submodules/TelegramUI/Components/Settings/BusinessLinkNameAlertController",
"//submodules/TelegramUI/Components/Ads/AdsInfoScreen",
"//submodules/TelegramUI/Components/Ads/AdsReportScreen",
"//submodules/TelegramUI/Components/Settings/BotSettingsScreen",
"//submodules/TelegramUI/Components/AdminUserActionsSheet",
"//submodules/TelegramUI/Components/Chat/ChatSendAudioMessageContextPreview",
"//submodules/TelegramUI/Components/Stars/StarsTransactionsScreen",
"//submodules/TelegramUI/Components/Stars/StarsPurchaseScreen",
"//submodules/TelegramUI/Components/Stars/StarsTransferScreen",
"//submodules/TelegramUI/Components/Stars/StarsTransactionScreen",
"//submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen",
"//submodules/TelegramUI/Components/Chat/FactCheckAlertController",
"//submodules/TelegramUI/Components/Chat/ChatMessagePaymentAlertController",
"//submodules/TelegramUI/Components/PeerManagement/OwnershipTransferController",
"//submodules/TelegramUI/Components/PeerManagement/OldChannelsController",
"//submodules/TelegramUI/Components/Chat/ChatSendStarsScreen",
"//submodules/TelegramUI/Components/MinimizedContainer",
"//submodules/TelegramUI/Components/SpaceWarpView",
"//submodules/TelegramUI/Components/MiniAppListScreen",
"//submodules/TelegramUI/Components/Stars/StarsIntroScreen",
"//submodules/TelegramUI/Components/Gifts/GiftOptionsScreen",
"//submodules/TelegramUI/Components/Gifts/GiftStoreScreen",
"//submodules/TelegramUI/Components/Gifts/GiftSetupScreen",
"//submodules/TelegramUI/Components/ContentReportScreen",
"//submodules/TelegramUI/Components/PeerInfo/AffiliateProgramSetupScreen",
"//submodules/TelegramUI/Components/Stars/StarsBalanceOverlayComponent",
"//submodules/TelegramUI/Components/Settings/AccountFreezeInfoScreen",
"//submodules/TelegramUI/Components/JoinSubjectScreen",
"//submodules/TelegramUI/Components/Chat/QuickShareScreen",
"//submodules/TelegramUI/Components/ButtonComponent",
"//submodules/Components/BlurredBackgroundComponent",
"//submodules/TelegramUI/Components/CheckComponent",
"//submodules/TelegramUI/Components/MarqueeComponent",
"//submodules/TelegramUI/Components/PeerInfo/PostSuggestionsSettingsScreen",
"//submodules/TelegramUI/Components/ForumSettingsScreen",
"//submodules/TelegramUI/Components/Chat/ChatSideTopicsPanel",
"//submodules/TelegramUI/Components/GifVideoLayer",
"//submodules/TelegramUI/Components/BatchVideoRendering",
"//submodules/TelegramUI/Components/ComposeTodoScreen",
"//submodules/TelegramUI/Components/SuggestedPostApproveAlert",
"//submodules/TelegramUI/Components/Stars/BalanceNeededScreen",
"//submodules/TelegramUI/Components/FaceScanScreen",
"//submodules/TelegramUI/Components/MediaManager/PeerMessagesMediaPlaylist",
"//submodules/TelegramUI/Components/ChatThemeScreen",
"//submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode",
"//submodules/ContactsHelper",
"//submodules/TelegramUI/Components/GlassBackgroundComponent",
"//submodules/TelegramUI/Components/Chat/ChatInputAccessoryPanel",
"//submodules/TelegramUI/Components/Chat/ChatInputMessageAccessoryPanel",
"//submodules/TelegramUI/Components/Chat/ChatRecordingViewOnceButtonNode",
"//submodules/TelegramUI/Components/Chat/ChatPanelsComponent",
"//submodules/TelegramUI/Components/EdgeEffect",
"//submodules/TelegramUI/Components/AttachmentFileController",
"//submodules/TelegramUI/Components/Contacts/NewContactScreen",
"//submodules/TelegramUI/Components/Chat/ChatSendAsContextMenu",
"//submodules/TelegramUI/Components/Settings/PasskeysScreen",
"//submodules/TelegramUI/Components/Gifts/GiftDemoScreen",
] + select({
"@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets,
"//build-system:ios_sim_arm64": [],
"@build_bazel_rules_apple//apple:ios_x86_64": [],
}),
visibility = [
"//visibility:public",
],
)
@@ -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,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,645 @@
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(
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,22 @@
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/TelegramPresentationData",
"//submodules/ComponentFlow",
"//submodules/Components/ComponentDisplayAdapters",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,395 @@
import Foundation
import UIKit
import Display
import TelegramPresentationData
import ComponentFlow
import ComponentDisplayAdapters
import AsyncDisplayKit
private let alertWidth: CGFloat = 270.0
public enum ComponentAlertActionType {
case genericAction
case defaultAction
case destructiveAction
case defaultDestructiveAction
}
public struct ComponentAlertAction {
public let type: ComponentAlertActionType
public let title: String
public let action: () -> Void
public init(type: ComponentAlertActionType, title: String, action: @escaping () -> Void) {
self.type = type
self.title = title
self.action = action
}
}
public final class ComponentAlertContentActionNode: HighlightableButtonNode {
private var theme: AlertControllerTheme
public var action: ComponentAlertAction {
didSet {
self.updateTitle()
}
}
private let backgroundNode: ASDisplayNode
public var highlightedUpdated: (Bool) -> Void = { _ in }
public init(theme: AlertControllerTheme, action: ComponentAlertAction) {
self.theme = theme
self.action = action
self.backgroundNode = ASDisplayNode()
self.backgroundNode.isLayerBacked = true
self.backgroundNode.alpha = 0.0
super.init()
self.titleNode.maximumNumberOfLines = 2
self.highligthedChanged = { [weak self] value in
if let strongSelf = self {
strongSelf.setHighlighted(value, animated: true)
}
}
self.updateTheme(theme)
}
public override func didLoad() {
super.didLoad()
self.addTarget(self, action: #selector(self.pressed), forControlEvents: .touchUpInside)
self.pointerInteraction = PointerInteraction(node: self, style: .hover, willEnter: { [weak self] in
if let strongSelf = self {
strongSelf.setHighlighted(true, animated: false)
}
}, willExit: { [weak self] in
if let strongSelf = self {
strongSelf.setHighlighted(false, animated: false)
}
})
}
public func performAction() {
if self.actionEnabled {
self.action.action()
}
}
public func setHighlighted(_ highlighted: Bool, animated: Bool) {
self.highlightedUpdated(highlighted)
if highlighted {
if self.backgroundNode.supernode == nil {
self.insertSubnode(self.backgroundNode, at: 0)
}
self.backgroundNode.alpha = 1.0
} else {
if animated {
UIView.animate(withDuration: 0.3, animations: {
self.backgroundNode.alpha = 0.0
})
} else {
self.backgroundNode.alpha = 0.0
}
}
}
public var actionEnabled: Bool = true {
didSet {
self.isUserInteractionEnabled = self.actionEnabled
self.updateTitle()
}
}
public func updateTheme(_ theme: AlertControllerTheme) {
self.theme = theme
self.backgroundNode.backgroundColor = theme.highlightedItemColor
self.updateTitle()
}
private func updateTitle() {
var font = Font.regular(theme.baseFontSize)
var color: UIColor
switch self.action.type {
case .defaultAction, .genericAction:
color = self.actionEnabled ? self.theme.accentColor : self.theme.disabledColor
case .destructiveAction, .defaultDestructiveAction:
color = self.actionEnabled ? self.theme.destructiveColor : self.theme.disabledColor
}
switch self.action.type {
case .defaultAction, .defaultDestructiveAction:
font = Font.semibold(theme.baseFontSize)
case .destructiveAction, .genericAction:
break
}
self.setAttributedTitle(NSAttributedString(string: self.action.title, font: font, textColor: color, paragraphAlignment: .center), for: [])
self.accessibilityLabel = self.action.title
self.accessibilityTraits = [.button]
}
@objc func pressed() {
self.action.action()
}
override public func layout() {
super.layout()
self.backgroundNode.frame = self.bounds
}
}
public enum ComponentAlertContentActionLayout {
case horizontal
case vertical
}
public final class ComponentAlertContentNode: AlertContentNode {
private var theme: AlertControllerTheme
private let actionLayout: ComponentAlertContentActionLayout
private let content: AnyComponent<Empty>
private let contentView = ComponentView<Empty>()
private let actionNodesSeparator: ASDisplayNode
private let actionNodes: [ComponentAlertContentActionNode]
private let actionVerticalSeparators: [ASDisplayNode]
private var validLayout: CGSize?
private let _dismissOnOutsideTap: Bool
override public var dismissOnOutsideTap: Bool {
return self._dismissOnOutsideTap
}
private var highlightedItemIndex: Int? = nil
public init(theme: AlertControllerTheme, content: AnyComponent<Empty>, actions: [ComponentAlertAction], actionLayout: ComponentAlertContentActionLayout, dismissOnOutsideTap: Bool) {
self.theme = theme
self.actionLayout = actionLayout
self._dismissOnOutsideTap = dismissOnOutsideTap
self.content = content
self.actionNodesSeparator = ASDisplayNode()
self.actionNodesSeparator.isUserInteractionEnabled = false
self.actionNodesSeparator.backgroundColor = theme.separatorColor
self.actionNodes = actions.map { action -> ComponentAlertContentActionNode in
return ComponentAlertContentActionNode(theme: theme, action: action)
}
var actionVerticalSeparators: [ASDisplayNode] = []
if actions.count > 1 {
for _ in 0 ..< actions.count - 1 {
let separatorNode = ASDisplayNode()
separatorNode.isLayerBacked = true
separatorNode.backgroundColor = theme.separatorColor
actionVerticalSeparators.append(separatorNode)
}
}
self.actionVerticalSeparators = actionVerticalSeparators
super.init()
self.addSubnode(self.actionNodesSeparator)
var i = 0
for actionNode in self.actionNodes {
self.addSubnode(actionNode)
let index = i
actionNode.highlightedUpdated = { [weak self] highlighted in
if highlighted {
self?.highlightedItemIndex = index
}
}
i += 1
}
for separatorNode in self.actionVerticalSeparators {
self.addSubnode(separatorNode)
}
}
func setHighlightedItemIndex(_ index: Int?, update: Bool = false) {
self.highlightedItemIndex = index
if update {
var i = 0
for actionNode in self.actionNodes {
if i == index {
actionNode.setHighlighted(true, animated: false)
} else {
actionNode.setHighlighted(false, animated: false)
}
i += 1
}
}
}
override public func decreaseHighlightedIndex() {
let currentHighlightedIndex = self.highlightedItemIndex ?? 0
self.setHighlightedItemIndex(max(0, currentHighlightedIndex - 1), update: true)
}
override public func increaseHighlightedIndex() {
let currentHighlightedIndex = self.highlightedItemIndex ?? -1
self.setHighlightedItemIndex(min(self.actionNodes.count - 1, currentHighlightedIndex + 1), update: true)
}
override public func performHighlightedAction() {
guard let highlightedItemIndex = self.highlightedItemIndex else {
return
}
var i = 0
for itemNode in self.actionNodes {
if i == highlightedItemIndex {
itemNode.performAction()
return
}
i += 1
}
}
override public func updateTheme(_ theme: AlertControllerTheme) {
self.theme = theme
self.actionNodesSeparator.backgroundColor = theme.separatorColor
for actionNode in self.actionNodes {
actionNode.updateTheme(theme)
}
for separatorNode in self.actionVerticalSeparators {
separatorNode.backgroundColor = theme.separatorColor
}
if let size = self.validLayout {
_ = self.updateLayout(size: size, transition: .immediate)
}
}
override public func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize {
self.validLayout = size
let insets = UIEdgeInsets(top: 18.0, left: 18.0, bottom: 18.0, right: 18.0)
var size = size
size.width = min(size.width, alertWidth)
let contentSize = self.contentView.update(
transition: ComponentTransition(transition),
component: self.content,
environment: {},
containerSize: CGSize(width: size.width - insets.left - insets.right, height: 10000.0)
)
let actionButtonHeight: CGFloat = 44.0
var minActionsWidth: CGFloat = 0.0
let maxActionWidth: CGFloat = floor(size.width / CGFloat(self.actionNodes.count))
let actionTitleInsets: CGFloat = 8.0
var effectiveActionLayout = self.actionLayout
for actionNode in self.actionNodes {
let actionTitleSize = actionNode.titleNode.updateLayout(CGSize(width: maxActionWidth, height: actionButtonHeight))
if case .horizontal = effectiveActionLayout, actionTitleSize.height > actionButtonHeight * 0.6667 {
effectiveActionLayout = .vertical
}
switch effectiveActionLayout {
case .horizontal:
minActionsWidth += actionTitleSize.width + actionTitleInsets
case .vertical:
minActionsWidth = max(minActionsWidth, actionTitleSize.width + actionTitleInsets)
}
}
let resultSize: CGSize
var actionsHeight: CGFloat = 0.0
switch effectiveActionLayout {
case .horizontal:
actionsHeight = actionButtonHeight
case .vertical:
actionsHeight = actionButtonHeight * CGFloat(self.actionNodes.count)
}
let contentWidth = alertWidth - insets.left - insets.right
let contentFrame = CGRect(origin: CGPoint(x: insets.left + floor((contentWidth - contentSize.width) / 2.0), y: insets.top), size: contentSize)
if let contentComponentView = self.contentView.view {
if contentComponentView.superview == nil {
self.view.insertSubview(contentComponentView, belowSubview: self.actionNodesSeparator.view)
transition.updateFrame(view: contentComponentView, frame: contentFrame)
}
}
resultSize = CGSize(width: contentWidth + insets.left + insets.right, height: contentSize.height + actionsHeight + insets.top + insets.bottom)
self.actionNodesSeparator.frame = CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel))
var actionOffset: CGFloat = 0.0
let actionWidth: CGFloat = floor(resultSize.width / CGFloat(self.actionNodes.count))
var separatorIndex = -1
var nodeIndex = 0
for actionNode in self.actionNodes {
if separatorIndex >= 0 {
let separatorNode = self.actionVerticalSeparators[separatorIndex]
switch effectiveActionLayout {
case .horizontal:
transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: actionOffset - UIScreenPixel, y: resultSize.height - actionsHeight), size: CGSize(width: UIScreenPixel, height: actionsHeight - UIScreenPixel)))
case .vertical:
transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel)))
}
}
separatorIndex += 1
let currentActionWidth: CGFloat
switch effectiveActionLayout {
case .horizontal:
if nodeIndex == self.actionNodes.count - 1 {
currentActionWidth = resultSize.width - actionOffset
} else {
currentActionWidth = actionWidth
}
case .vertical:
currentActionWidth = resultSize.width
}
let actionNodeFrame: CGRect
switch effectiveActionLayout {
case .horizontal:
actionNodeFrame = CGRect(origin: CGPoint(x: actionOffset, y: resultSize.height - actionsHeight), size: CGSize(width: currentActionWidth, height: actionButtonHeight))
actionOffset += currentActionWidth
case .vertical:
actionNodeFrame = CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset), size: CGSize(width: currentActionWidth, height: actionButtonHeight))
actionOffset += actionButtonHeight
}
transition.updateFrame(node: actionNode, frame: actionNodeFrame)
nodeIndex += 1
}
return resultSize
}
}
public func componentAlertController(theme: AlertControllerTheme, content: AnyComponent<Empty>, actions: [ComponentAlertAction], actionLayout: ComponentAlertContentActionLayout = .horizontal, dismissOnOutsideTap: Bool = true) -> AlertController {
var dismissImpl: (() -> Void)?
let controller = AlertController(theme: theme, contentNode: ComponentAlertContentNode(theme: theme, content: content, actions: actions.map { action in
return ComponentAlertAction(type: action.type, title: action.title, action: {
dismissImpl?()
action.action()
})
}, actionLayout: actionLayout, dismissOnOutsideTap: dismissOnOutsideTap))
dismissImpl = { [weak controller] in
controller?.dismissAnimated()
}
return controller
}
@@ -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,22 @@
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",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,405 @@
import Foundation
import UIKit
import Display
import ComponentFlow
import TelegramPresentationData
import BundleIconComponent
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 preferredDirectionIsDown: Bool
public let blur: Bool
public init(
font: UIFont,
color: UIColor,
items: [Item],
noDelay: Bool = false,
animateScale: Bool = true,
preferredDirectionIsDown: Bool = false,
blur: Bool = false
) {
self.font = font
self.color = color
self.items = items
self.noDelay = noDelay
self.animateScale = animateScale
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.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 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 {
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
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
switch character {
case let .text(text):
characterComponent = AnyComponent(Text(
text: String(text),
font: component.font,
color: 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)
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.midX - characterComponentView.frame.midX, y: characterFrame.midY - characterComponentView.frame.midY)
characterComponentView.center = characterFrame.center
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)
}
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 * 2.0)
}
}
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
}
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,700 @@
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, dynamicBounce: 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)
}
}
}
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,675 @@
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 expandSavedMusic: () -> Void
let send: (Message) -> Void
init(context: AccountContext, isAudio: Bool, openGallery: @escaping () -> Void, openFiles: @escaping () -> Void, expandSavedMusic: @escaping () -> Void, send: @escaping (Message) -> Void) {
self.context = context
self.isAudio = isAudio
self.openGallery = openGallery
self.openFiles = openFiles
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 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:
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 .savedHeader:
return 2
case let .savedFile(index, _, _):
return 3 + 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 .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 .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]?, 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))
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, "SAVED MUSIC".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, "Show More"))
}
}
}
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, 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()
},
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>()
// if let peer {
// peers[peerId] = peer._asPeer()
// }
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: 40.0, height: 40.0)
let closeButton = GlassBarButtonComponent(
size: barButtonSize,
backgroundColor: presentationData.theme.rootController.navigationBar.glassBarButtonBackgroundColor,
isDark: presentationData.theme.overallDarkAppearance,
state: .generic,
component: AnyComponentWithIdentity(id: "close", component: AnyComponent(
BundleIconComponent(
name: "Navigation/Close",
tintColor: presentationData.theme.rootController.navigationBar.glassBarButtonForegroundColor
)
)),
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: presentationData.theme.rootController.navigationBar.glassBarButtonBackgroundColor,
isDark: presentationData.theme.overallDarkAppearance,
state: .generic,
component: AnyComponentWithIdentity(id: "search", component: AnyComponent(
BundleIconComponent(
name: "Navigation/Search",
tintColor: presentationData.theme.rootController.navigationBar.glassBarButtonForegroundColor
)
)),
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 = existingSearchButton.modify { current in
let buttonNode: BarComponentHostNode
if let current {
buttonNode = current
buttonNode.component = searchButtonComponent
} else {
buttonNode = BarComponentHostNode(component: searchButtonComponent, size: barButtonSize)
}
return buttonNode
}
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 = "Audio"
}
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, 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?()
}, 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,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,49 @@
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/Premium/PremiumStarComponent",
],
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,272 @@
import Foundation
import UIKit
import SwiftSignalKit
import AsyncDisplayKit
import Display
import ComponentFlow
import Postbox
import TelegramCore
import TelegramPresentationData
import TelegramUIPreferences
import AccountContext
import AppBundle
import PremiumStarComponent
private final class PremiumAlertContentNode: AlertContentNode {
private let strings: PresentationStrings
private let presentationTheme: PresentationTheme
private let title: String?
private let text: String
private let titleNode: ASTextNode
private let textNode: ASTextNode
private let icon = ComponentView<Empty>()
private let actionNodesSeparator: ASDisplayNode
private let actionNodes: [TextAlertContentActionNode]
private let actionVerticalSeparators: [ASDisplayNode]
private var validLayout: CGSize?
override var dismissOnOutsideTap: Bool {
return self.isUserInteractionEnabled
}
init(
theme: AlertControllerTheme,
presentationTheme: PresentationTheme,
strings: PresentationStrings,
title: String?,
text: String,
actions: [TextAlertAction]
) {
self.strings = strings
self.presentationTheme = presentationTheme
self.title = title
self.text = text
self.titleNode = ASTextNode()
self.titleNode.maximumNumberOfLines = 2
self.textNode = ASTextNode()
self.textNode.maximumNumberOfLines = 0
self.textNode.lineSpacing = 0.1
self.actionNodesSeparator = ASDisplayNode()
self.actionNodesSeparator.isLayerBacked = true
self.actionNodes = actions.map { action -> TextAlertContentActionNode in
return TextAlertContentActionNode(theme: theme, action: action)
}
var actionVerticalSeparators: [ASDisplayNode] = []
if actions.count > 1 {
for _ in 0 ..< actions.count - 1 {
let separatorNode = ASDisplayNode()
separatorNode.isLayerBacked = true
actionVerticalSeparators.append(separatorNode)
}
}
self.actionVerticalSeparators = actionVerticalSeparators
super.init()
self.addSubnode(self.titleNode)
self.addSubnode(self.textNode)
self.addSubnode(self.actionNodesSeparator)
for actionNode in self.actionNodes {
self.addSubnode(actionNode)
}
for separatorNode in self.actionVerticalSeparators {
self.addSubnode(separatorNode)
}
self.updateTheme(theme)
}
override func updateTheme(_ theme: AlertControllerTheme) {
self.titleNode.attributedText = NSAttributedString(string: self.title ?? self.strings.PremiumNeeded_Title, font: Font.bold(17.0), textColor: theme.primaryColor, paragraphAlignment: .center)
self.textNode.attributedText = NSAttributedString(string: self.text, font: Font.regular(13.0), textColor: theme.primaryColor, paragraphAlignment: .center)
self.actionNodesSeparator.backgroundColor = theme.separatorColor
for actionNode in self.actionNodes {
actionNode.updateTheme(theme)
}
for separatorNode in self.actionVerticalSeparators {
separatorNode.backgroundColor = theme.separatorColor
}
if let size = self.validLayout {
_ = self.updateLayout(size: size, transition: .immediate)
}
}
override func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize {
var size = size
size.width = min(size.width , 270.0)
self.validLayout = size
var origin: CGPoint = CGPoint(x: 0.0, y: 20.0)
let starHeight: CGFloat = 105.0
let starSize = self.icon.update(
transition: .immediate,
component: AnyComponent(
PremiumStarComponent(
theme: self.presentationTheme,
isIntro: false,
isVisible: true,
hasIdleAnimations: true,
colors: [
UIColor(rgb: 0x6a94ff),
UIColor(rgb: 0x9472fd),
UIColor(rgb: 0xe26bd3)
]
)
),
environment: {},
containerSize: CGSize(width: size.width, height: 200.0)
)
if let view = self.icon.view {
if view.superview == nil {
self.view.addSubview(view)
}
view.frame = CGRect(origin: CGPoint(x: 0.0, y: -36.0), size: starSize)
origin.y += starHeight
}
let titleSize = self.titleNode.measure(size)
transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - titleSize.width) / 2.0), y: origin.y), size: titleSize))
origin.y += titleSize.height + 5.0
let textSize = self.textNode.measure(CGSize(width: size.width - 32.0, height: size.height))
transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - textSize.width) / 2.0), y: origin.y), size: textSize))
let actionButtonHeight: CGFloat = 44.0
var minActionsWidth: CGFloat = 0.0
let maxActionWidth: CGFloat = floor(size.width / CGFloat(self.actionNodes.count))
let actionTitleInsets: CGFloat = 8.0
var effectiveActionLayout = TextAlertContentActionLayout.horizontal
for actionNode in self.actionNodes {
let actionTitleSize = actionNode.titleNode.updateLayout(CGSize(width: maxActionWidth, height: actionButtonHeight))
if case .horizontal = effectiveActionLayout, actionTitleSize.height > actionButtonHeight * 0.6667 {
effectiveActionLayout = .vertical
}
switch effectiveActionLayout {
case .horizontal:
minActionsWidth += actionTitleSize.width + actionTitleInsets
case .vertical:
minActionsWidth = max(minActionsWidth, actionTitleSize.width + actionTitleInsets)
}
}
let insets = UIEdgeInsets(top: 18.0, left: 18.0, bottom: 18.0, right: 18.0)
var contentWidth = max(titleSize.width, minActionsWidth)
contentWidth = max(contentWidth, 234.0)
var actionsHeight: CGFloat = 0.0
switch effectiveActionLayout {
case .horizontal:
actionsHeight = actionButtonHeight
case .vertical:
actionsHeight = actionButtonHeight * CGFloat(self.actionNodes.count)
}
let resultWidth = contentWidth + insets.left + insets.right
let resultSize = CGSize(width: resultWidth, height: titleSize.height + starHeight + textSize.height + actionsHeight + 7.0 + insets.top + insets.bottom)
transition.updateFrame(node: self.actionNodesSeparator, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel)))
var actionOffset: CGFloat = 0.0
let actionWidth: CGFloat = floor(resultSize.width / CGFloat(self.actionNodes.count))
var separatorIndex = -1
var nodeIndex = 0
for actionNode in self.actionNodes {
if separatorIndex >= 0 {
let separatorNode = self.actionVerticalSeparators[separatorIndex]
switch effectiveActionLayout {
case .horizontal:
transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: actionOffset - UIScreenPixel, y: resultSize.height - actionsHeight), size: CGSize(width: UIScreenPixel, height: actionsHeight - UIScreenPixel)))
case .vertical:
transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel)))
}
}
separatorIndex += 1
let currentActionWidth: CGFloat
switch effectiveActionLayout {
case .horizontal:
if nodeIndex == self.actionNodes.count - 1 {
currentActionWidth = resultSize.width - actionOffset
} else {
currentActionWidth = actionWidth
}
case .vertical:
currentActionWidth = resultSize.width
}
let actionNodeFrame: CGRect
switch effectiveActionLayout {
case .horizontal:
actionNodeFrame = CGRect(origin: CGPoint(x: actionOffset, y: resultSize.height - actionsHeight), size: CGSize(width: currentActionWidth, height: actionButtonHeight))
actionOffset += currentActionWidth
case .vertical:
actionNodeFrame = CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset), size: CGSize(width: currentActionWidth, height: actionButtonHeight))
actionOffset += actionButtonHeight
}
transition.updateFrame(node: actionNode, frame: actionNodeFrame)
nodeIndex += 1
}
return resultSize
}
}
func premiumAlertController(context: AccountContext, parentController: ViewController, title: String? = nil, text: String) -> AlertController {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let strings = presentationData.strings
var dismissImpl: ((Bool) -> Void)?
var proceedImpl: (() -> Void)?
var contentNode: PremiumAlertContentNode?
let actions: [TextAlertAction] = [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {
dismissImpl?(true)
}), TextAlertAction(type: .defaultAction, title: presentationData.strings.PremiumNeeded_Subscribe, action: {
proceedImpl?()
dismissImpl?(true)
})]
contentNode = PremiumAlertContentNode(
theme: AlertControllerTheme(presentationData: presentationData),
presentationTheme: presentationData.theme,
strings: strings,
title: title,
text: text,
actions: actions
)
let controller = AlertController(theme: AlertControllerTheme(presentationData: presentationData), contentNode: contentNode!)
dismissImpl = { [weak controller] animated in
if animated {
controller?.dismissAnimated()
} else {
controller?.dismiss()
}
}
proceedImpl = { [weak parentController] in
let controller = context.sharedContext.makePremiumIntroController(context: context, source: .nameColor, forceDark: false, dismissed: nil)
parentController?.push(controller)
}
return controller
}
@@ -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 = NavigationBar.backArrowImage(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,737 @@
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 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: HighlightTrackingButton {
private var component: ButtonComponent?
private weak var componentState: EmptyComponentState?
private var containerView: UIView
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
super.init(frame: frame)
self.addSubview(self.containerView)
self.addTarget(self, action: #selector(self.pressed), for: .touchUpInside)
self.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 {
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)
} else {
transition.setBackgroundColor(view: self.containerView, color: component.background.color)
transition.setScale(view: self.containerView, scale: 1.0)
}
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)
}
}
}
}
}
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.isEnabled = (component.isEnabled || component.allowActionWhenDisabled) && !component.displaysProgress
transition.setBackgroundColor(view: self.containerView, color: component.background.color)
var cornerRadius: CGFloat = component.background.cornerRadius
if case .glass = component.background.style, component.background.cornerRadius == 10.0 {
cornerRadius = availableSize.height * 0.5
}
transition.setCornerRadius(layer: self.containerView.layer, cornerRadius: cornerRadius)
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
}
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)
}
if let contentView = contentItem.view.view {
var animateIn = false
var contentTransition = transition
if contentView.superview == nil {
contentTransition = .immediate
animateIn = true
contentView.isUserInteractionEnabled = false
self.containerView.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.animateScale(from: 0.4, to: 1.0, duration: 0.35, timingFunction: kCAMediaTimingFunctionSpring)
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.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
self.containerView.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
self.containerView.insertSubview(shimmeringView, at: 0)
}
shimmeringView.update(size: size, background: component.background, cornerRadius: component.background.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 {
self.containerView.insertSubview(chromeView, aboveSubview: shimmeringView)
} else {
self.containerView.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)
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
}
}
@@ -0,0 +1,21 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "Snow.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

@@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "ic_call_speaker.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
@@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "ic_call_camera.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
@@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "smoothGradient 0.4.png",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
@@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}
@@ -0,0 +1,395 @@
#include <metal_stdlib>
using namespace metal;
struct Rectangle {
float2 origin;
float2 size;
};
constant static float2 quadVertices[6] = {
float2(0.0, 0.0),
float2(1.0, 0.0),
float2(0.0, 1.0),
float2(1.0, 0.0),
float2(0.0, 1.0),
float2(1.0, 1.0)
};
struct QuadVertexOut {
float4 position [[position]];
float2 uv;
};
vertex QuadVertexOut callBackgroundVertex(
const device Rectangle &rect [[ buffer(0) ]],
unsigned int vid [[ vertex_id ]]
) {
float2 quadVertex = quadVertices[vid];
QuadVertexOut out;
out.position = float4(rect.origin.x + quadVertex.x * rect.size.x, rect.origin.y + quadVertex.y * rect.size.y, 0.0, 1.0);
out.position.x = -1.0 + out.position.x * 2.0;
out.position.y = -1.0 + out.position.y * 2.0;
out.uv = quadVertex;
return out;
}
half4 rgb2hsv(half4 c) {
half4 K = half4(0.0, -1.0 / 3.0, 2.0 / 3.0, -1.0);
half4 p = mix(half4(c.bg, K.wz), half4(c.gb, K.xy), step(c.b, c.g));
half4 q = mix(half4(p.xyw, c.r), half4(c.r, p.yzx), step(p.x, c.r));
float d = q.x - min(q.w, q.y);
float e = 1.0e-10;
return half4(abs(q.z + (q.w - q.y) / (6.0 * d + e)), d / (q.x + e), q.x, c.a);
}
half4 hsv2rgb(half4 c) {
half4 K = half4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0);
half3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www);
return half4(c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y), c.a);
}
fragment half4 callBackgroundFragment(
QuadVertexOut in [[stage_in]],
const device float2 *positions [[ buffer(0) ]],
const device float4 *colors [[ buffer(1) ]],
const device float &brightness [[ buffer(2) ]],
const device float &saturation [[ buffer(3) ]],
const device float4 &overlay [[ buffer(4) ]]
) {
half centerDistanceX = in.uv.x - 0.5;
half centerDistanceY = in.uv.y - 0.5;
half centerDistance = distance(half2(in.uv), half2(0.5, 0.5));
half swirlFactor = 0.35 * centerDistance;
half theta = swirlFactor * swirlFactor * 0.8 * 8.0;
half sinTheta = sin(theta);
half cosTheta = cos(theta);
half pixelX = max(0.0, min(1.0, 0.5 + centerDistanceX * cosTheta - centerDistanceY * sinTheta));
half pixelY = max(0.0, min(1.0, 0.5 + centerDistanceX * sinTheta + centerDistanceY * cosTheta));
half distanceSum = 0.0;
half r = 0.0;
half g = 0.0;
half b = 0.0;
for (int i = 0; i < 4; i++) {
half4 color = half4(colors[i]);
half2 colorXY = half2(positions[i]);
half2 distanceXY = half2(pixelX - colorXY.x, pixelY - colorXY.y);
half distance = max(0.0, 0.92 - sqrt(distanceXY.x * distanceXY.x + distanceXY.y * distanceXY.y));
distance = distance * distance * distance;
distanceSum += distance;
r = r + distance * color.r;
g = g + distance * color.g;
b = b + distance * color.b;
}
if (distanceSum < 0.00001) {
distanceSum = 0.00001;
}
half pixelB = b / distanceSum;
half pixelG = g / distanceSum;
half pixelR = r / distanceSum;
half4 color(pixelR, pixelG, pixelB, 1.0);
color = rgb2hsv(color);
color.b = clamp(color.b * brightness, 0.0, 1.0);
color.g = clamp(color.g * saturation, 0.0, 1.0);
color = hsv2rgb(color);
color.rgb += half3(overlay.rgb * overlay.a);
color.rgb = min(color.rgb, half3(1.0, 1.0, 1.0));
return color;
}
struct BlobVertexOut {
float4 position [[position]];
};
float2 blobVertex(float2 center, float angle, float radius) {
return float2(center.x + radius * cos(angle), center.y + radius * sin(angle));
}
float2 mapPointInRect(Rectangle rect, half2 point) {
half2 out(rect.origin.x + rect.size.x * point.x, rect.origin.y + rect.size.y * point.y);
out.x = -1.0 + out.x * 2.0;
out.y = -1.0 + out.y * 2.0;
return float2(out);
}
struct SmoothPoint {
half2 point;
half inAngle;
half inLength;
half outAngle;
half outLength;
half2 smoothIn() {
return smooth(inAngle, inLength);
}
half2 smoothOut() {
return smooth(outAngle, outLength);
}
private:
half2 smooth(half angle, half length) {
return half2(
point.x + length * cos(angle),
point.y + length * sin(angle)
);
}
};
half2 evaluateBlobPoint(const device Rectangle &rect, const device float *positions, int index, int count, int subdivisions) {
float position = positions[index];
float segmentAngle = float(index) / float(count) * 2.0 * 3.1415926;
return half2(blobVertex(float2(0.5, 0.5), segmentAngle, 0.45 + 0.05 * position));
}
SmoothPoint evaluateSmoothBlobPoint(const device Rectangle &rect, const device float *positions, int index, int count, int subdivisions) {
int prevIndex = (index - 1) < 0 ? (count - 1) : (index - 1);
int nextIndex = (index + 1) % count;
half2 prev = evaluateBlobPoint(rect, positions, prevIndex, count, subdivisions);
half2 curr = evaluateBlobPoint(rect, positions, index, count, subdivisions);
half2 next = evaluateBlobPoint(rect, positions, nextIndex, count, subdivisions);
float dx = next.x - prev.x;
float dy = -next.y + prev.y;
float angle = atan2(dy, dx);
if (angle < 0.0) {
angle = abs(angle);
} else {
angle = 2 * 3.1415926 - angle;
}
float smoothAngle = (3.1415926 * 2.0) / float(count);
float smoothness = ((4.0 / 3.0) * tan(smoothAngle / 4.0)) / sin(smoothAngle / 2.0) / 2.0;
SmoothPoint point;
point.point = curr;
point.inAngle = angle + 3.1415926;
point.inLength = smoothness * distance(curr, prev);
point.outAngle = angle;
point.outLength = smoothness * distance(curr, next);
return point;
}
half2 evaluateBezierBlobPoint(thread SmoothPoint &curr, thread SmoothPoint &next, half t) {
half oneMinusT = 1.0 - t;
half2 p0 = curr.point;
half2 p1 = curr.smoothOut();
half2 p2 = next.smoothIn();
half2 p3 = next.point;
return oneMinusT * oneMinusT * oneMinusT * p0 + 3.0 * t * oneMinusT * oneMinusT * p1 + 3.0 * t * t * oneMinusT * p2 + t * t * t * p3;
}
vertex BlobVertexOut callBlobVertex(
const device Rectangle &rect [[ buffer(0) ]],
const device float *positions [[ buffer(1) ]],
const device int &count [[ buffer(2) ]],
unsigned int vid [[ vertex_id ]]
) {
const int subdivisions = 8;
int triangleIndex = vid / 3;
int segmentIndex = triangleIndex / subdivisions;
int nextIndex = (segmentIndex + 1) % count;
half innerPosition = half(triangleIndex - segmentIndex * subdivisions) / half(subdivisions);
half nextInnerPosition = half(triangleIndex + 1 - segmentIndex * subdivisions) / half(subdivisions);
SmoothPoint curr = evaluateSmoothBlobPoint(rect, positions, segmentIndex, count, subdivisions);
SmoothPoint next = evaluateSmoothBlobPoint(rect, positions, nextIndex, count, subdivisions);
half2 triangle[3];
triangle[0] = half2(0.5, 0.5);
triangle[1] = evaluateBezierBlobPoint(curr, next, innerPosition);
triangle[2] = evaluateBezierBlobPoint(curr, next, nextInnerPosition);
BlobVertexOut out;
out.position = float4(float2(mapPointInRect(rect, triangle[vid % 3])), 0.0, 1.0);
return out;
}
fragment half4 callBlobFragment(
BlobVertexOut in [[stage_in]],
const device float4 &color [[ buffer(0) ]]
) {
return half4(color.r * color.a, color.g * color.a, color.b * color.a, color.a);
}
kernel void videoBiPlanarToRGBA(
texture2d<half, access::read> inTextureY [[ texture(0) ]],
texture2d<half, access::read> inTextureUV [[ texture(1) ]],
texture2d<half, access::write> outTexture [[ texture(2) ]],
uint2 threadPosition [[ thread_position_in_grid ]]
) {
half y = inTextureY.read(threadPosition).r;
half2 uv = inTextureUV.read(uint2(threadPosition.x / 2, threadPosition.y / 2)).rg - half2(0.5, 0.5);
half4 color(y + 1.403 * uv.y, y - 0.344 * uv.x - 0.714 * uv.y, y + 1.770 * uv.x, 1.0);
outTexture.write(color, threadPosition);
}
kernel void videoTriPlanarToRGBA(
texture2d<half, access::read> inTextureY [[ texture(0) ]],
texture2d<half, access::read> inTextureU [[ texture(1) ]],
texture2d<half, access::read> inTextureV [[ texture(2) ]],
texture2d<half, access::write> outTexture [[ texture(3) ]],
uint2 threadPosition [[ thread_position_in_grid ]]
) {
half y = inTextureY.read(threadPosition).r;
uint2 uvPosition = uint2(threadPosition.x / 2, threadPosition.y / 2);
half2 inUV = (inTextureU.read(uvPosition).r, inTextureV.read(uvPosition).r);
half2 uv = inUV - half2(0.5, 0.5);
half4 color(y + 1.403 * uv.y, y - 0.344 * uv.x - 0.714 * uv.y, y + 1.770 * uv.x, 1.0);
outTexture.write(color, threadPosition);
}
vertex QuadVertexOut mainVideoVertex(
const device Rectangle &rect [[ buffer(0) ]],
const device uint2 &mirror [[ buffer(1) ]],
unsigned int vid [[ vertex_id ]]
) {
float2 quadVertex = quadVertices[vid];
QuadVertexOut out;
out.position = float4(rect.origin.x + quadVertex.x * rect.size.x, rect.origin.y + quadVertex.y * rect.size.y, 0.0, 1.0);
out.position.x = -1.0 + out.position.x * 2.0;
out.position.y = -1.0 + out.position.y * 2.0;
out.uv = float2(quadVertex.x, 1.0 - quadVertex.y);
if (mirror.x == 1) {
out.uv.x = 1.0 - out.uv.x;
}
if (mirror.y == 1) {
out.uv.y = 1.0 - out.uv.y;
}
return out;
}
fragment half4 mainVideoFragment(
QuadVertexOut in [[stage_in]],
texture2d<half> texture [[ texture(0) ]],
const device float &brightness [[ buffer(0) ]],
const device float &saturation [[ buffer(1) ]],
const device float4 &overlay [[ buffer(2) ]]
) {
constexpr sampler sampler(coord::normalized, address::repeat, filter::linear);
half4 color = texture.sample(sampler, in.uv);
color = rgb2hsv(color);
color.b = clamp(color.b * brightness, 0.0, 1.0);
color.g = clamp(color.g * saturation, 0.0, 1.0);
color = hsv2rgb(color);
color.rgb += half3(overlay.rgb * overlay.a);
color.rgb = min(color.rgb, half3(1.0, 1.0, 1.0));
return half4(color.r, color.g, color.b, color.a);
}
constant int BLUR_SAMPLE_COUNT = 7;
constant float BLUR_OFFSETS[BLUR_SAMPLE_COUNT] = {
-5.227545617192816,
-3.3147990233346842,
-1.4174297935376852,
0.47225076494548685,
2.364576440741639,
4.268941421369995,
6
};
constant float BLUR_WEIGHTS[BLUR_SAMPLE_COUNT] = {
0.015167713616041436,
0.10117053983645591,
0.2894431725427234,
0.3570581167968804,
0.19014435646109845,
0.0435647539906345,
0.0034513467561660305
};
static void gaussianBlur(
texture2d<half, access::sample> inTexture,
texture2d<half, access::write> outTexture,
float2 offset,
uint2 gid
) {
constexpr sampler sampler(coord::normalized, address::clamp_to_edge, filter::linear);
uint2 textureDim(outTexture.get_width(), outTexture.get_height());
if(all(gid < textureDim)) {
float3 outColor(0.0);
float2 size(inTexture.get_width(), inTexture.get_height());
float2 baseTexCoord = float2(gid);
for (int i = 0; i < BLUR_SAMPLE_COUNT; i++) {
outColor += float3(inTexture.sample(sampler, (baseTexCoord + offset * BLUR_OFFSETS[i]) / size).rgb) * BLUR_WEIGHTS[i];
}
outTexture.write(half4(half3(outColor), 1.0), gid);
}
}
kernel void gaussianBlurHorizontal(
texture2d<half, access::sample> inTexture [[ texture(0) ]],
texture2d<half, access::write> outTexture [[ texture(1) ]],
uint2 gid [[ thread_position_in_grid ]]
) {
gaussianBlur(inTexture, outTexture, float2(1, 0), gid);
}
kernel void gaussianBlurVertical(
texture2d<half, access::sample> inTexture [[ texture(0) ]],
texture2d<half, access::write> outTexture [[ texture(1) ]],
uint2 gid [[ thread_position_in_grid ]]
) {
gaussianBlur(inTexture, outTexture, float2(0, 1), gid);
}
vertex QuadVertexOut edgeTestVertex(
const device Rectangle &rect [[ buffer(0) ]],
unsigned int vid [[ vertex_id ]]
) {
float2 quadVertex = quadVertices[vid];
QuadVertexOut out;
out.position = float4(rect.origin.x + quadVertex.x * rect.size.x, rect.origin.y + quadVertex.y * rect.size.y, 0.0, 1.0);
out.position.x = -1.0 + out.position.x * 2.0;
out.position.y = -1.0 + out.position.y * 2.0;
out.uv = quadVertex;
return out;
}
fragment half4 edgeTestFragment(
QuadVertexOut in [[stage_in]],
const device float4 &colorIn
) {
half4 color = half4(colorIn);
return color;
}
@@ -0,0 +1,170 @@
import Foundation
import UIKit
public enum AnimationCurve {
case linear
case easeInOut
case spring
}
extension AnimationCurve {
func map(_ fraction: CGFloat) -> CGFloat {
switch self {
case .linear:
return fraction
case .easeInOut:
return bezierPoint(0.42, 0.0, 0.58, 1.0, fraction)
case .spring:
return bezierPoint(0.23, 1.0, 0.32, 1.0, fraction)
}
}
}
open class AnyAnimation {
}
open class AnimationInterpolator<T> {
private let impl: (T, T, CGFloat) -> T
init(_ impl: @escaping (T, T, CGFloat) -> T) {
self.impl = impl
}
public func interpolate(from: T, to: T, fraction: CGFloat) -> T {
return self.impl(from, to, fraction)
}
}
public protocol AnimationInterpolatable {
static var animationInterpolator: AnimationInterpolator<Self> { get }
}
private let CGFloatInterpolator = AnimationInterpolator<CGFloat> { from, to, fraction in
return from * (1.0 - fraction) + to * fraction
}
extension CGFloat: AnimationInterpolatable {
public static var animationInterpolator: AnimationInterpolator<CGFloat> {
return CGFloatInterpolator
}
}
private let CGPointInterpolator = AnimationInterpolator<CGPoint> { from, to, fraction in
return CGPoint(
x: CGFloatInterpolator.interpolate(from: from.x, to: to.x, fraction: fraction),
y: CGFloatInterpolator.interpolate(from: from.y, to: to.y, fraction: fraction)
)
}
extension CGPoint: AnimationInterpolatable {
public static var animationInterpolator: AnimationInterpolator<CGPoint> {
return CGPointInterpolator
}
}
#if targetEnvironment(simulator)
@_silgen_name("UIAnimationDragCoefficient") func UIAnimationDragCoefficient() -> Float
#endif
public final class Animation<T: AnimationInterpolatable>: AnyAnimation {
private let from: T
private let to: T
private let duration: Double
private let curve: AnimationCurve
private let interpolator: AnimationInterpolator<T>
private var startTime: Double?
public private(set) var isFinished: Bool = false
var didStart: (() -> Void)?
public init(from: T, to: T, duration: Double, curve: AnimationCurve) {
self.from = from
self.to = to
#if targetEnvironment(simulator)
self.duration = duration * Double(UIAnimationDragCoefficient())
#else
self.duration = duration
#endif
self.curve = curve
self.interpolator = T.animationInterpolator
}
func start() {
self.startTime = CACurrentMediaTime()
}
func update(at timestamp: Double) -> T {
guard let startTime = self.startTime else {
return self.from
}
if self.isFinished {
return self.to
}
let fraction = max(0.0, min(1.0, (timestamp - startTime) / self.duration))
if timestamp > startTime + self.duration {
self.isFinished = true
}
if fraction >= 1.0 {
return self.to
}
return self.interpolator.interpolate(from: self.from, to: self.to, fraction: self.curve.map(fraction))
}
}
public class AnyAnimatedProperty {
var didStartAnimation: (() -> Void)?
var hasRunningAnimation: Bool {
return false
}
public func update() {
}
}
public final class AnimatedProperty<T: AnimationInterpolatable>: AnyAnimatedProperty {
private var animation: Animation<T>?
override var hasRunningAnimation: Bool {
return self.animation != nil
}
public private(set) var value: T
public init(_ value: T) {
self.value = value
}
public func animate(to: T, duration: Double, curve: AnimationCurve) {
let timestamp = CACurrentMediaTime()
let fromValue: T
if let animation = self.animation {
fromValue = animation.update(at: timestamp)
} else {
fromValue = self.value
}
self.animation = Animation(from: fromValue, to: to, duration: duration, curve: curve)
self.animation?.start()
self.didStartAnimation?()
}
public func animate(from: T, to: T, duration: Double, curve: AnimationCurve) {
self.value = from
self.animation = Animation(from: from, to: to, duration: duration, curve: curve)
self.animation?.start()
self.didStartAnimation?()
}
public func set(to: T) {
self.animation = nil
self.value = to
}
override public func update() {
if let animation = self.animation {
self.value = animation.update(at: CACurrentMediaTime())
if animation.isFinished {
self.animation = nil
}
}
}
}
@@ -0,0 +1,51 @@
import Foundation
import UIKit
import Display
public final class ManagedAnimations {
private var displayLinkSubscription: SharedDisplayLinkDriver.Link?
private var properties: [AnyAnimatedProperty] = []
public var updated: (() -> Void)?
public init() {
}
public func add(property: AnyAnimatedProperty) {
self.properties.append(property)
property.didStartAnimation = { [weak self] in
guard let self else {
return
}
self.updateNeedAnimations()
}
}
private func updateNeedAnimations() {
if self.displayLinkSubscription == nil {
self.displayLinkSubscription = SharedDisplayLinkDriver.shared.add { [weak self] _ in
guard let self else {
return
}
self.update()
}
}
}
private func update() {
var hasRunningAnimations = false
for property in self.properties {
property.update()
if property.hasRunningAnimation {
hasRunningAnimations = true
}
}
if !hasRunningAnimations {
self.displayLinkSubscription = nil
}
self.updated?()
}
}
@@ -0,0 +1,53 @@
import Foundation
private func a(_ a1: CGFloat, _ a2: CGFloat) -> CGFloat
{
return 1.0 - 3.0 * a2 + 3.0 * a1
}
private func b(_ a1: CGFloat, _ a2: CGFloat) -> CGFloat
{
return 3.0 * a2 - 6.0 * a1
}
private func c(_ a1: CGFloat) -> CGFloat
{
return 3.0 * a1
}
private func calcBezier(_ t: CGFloat, _ a1: CGFloat, _ a2: CGFloat) -> CGFloat
{
return ((a(a1, a2)*t + b(a1, a2))*t + c(a1)) * t
}
private func calcSlope(_ t: CGFloat, _ a1: CGFloat, _ a2: CGFloat) -> CGFloat
{
return 3.0 * a(a1, a2) * t * t + 2.0 * b(a1, a2) * t + c(a1)
}
private func getTForX(_ x: CGFloat, _ x1: CGFloat, _ x2: CGFloat) -> CGFloat {
var t = x
var i = 0
while i < 4 {
let currentSlope = calcSlope(t, x1, x2)
if currentSlope == 0.0 {
return t
} else {
let currentX = calcBezier(t, x1, x2) - x
t -= currentX / currentSlope
}
i += 1
}
return t
}
public func bezierPoint(_ x1: CGFloat, _ y1: CGFloat, _ x2: CGFloat, _ y2: CGFloat, _ x: CGFloat) -> CGFloat
{
var value = calcBezier(getTForX(x, x1, x2), y1, y2)
if value >= 0.997 {
value = 1.0
}
return value
}
@@ -0,0 +1,28 @@
import Foundation
import UIKit
import Display
import MetalKit
private final class BundleMarker: NSObject {
}
private var metalLibraryValue: MTLLibrary?
func metalLibrary(device: MTLDevice) -> MTLLibrary? {
if let metalLibraryValue {
return metalLibraryValue
}
let mainBundle = Bundle(for: BundleMarker.self)
guard let path = mainBundle.path(forResource: "CallScreenMetalSourcesBundle", ofType: "bundle") else {
return nil
}
guard let bundle = Bundle(path: path) else {
return nil
}
guard let library = try? device.makeDefaultLibrary(bundle: bundle) else {
return nil
}
metalLibraryValue = library
return library
}
@@ -0,0 +1,103 @@
import Foundation
import UIKit
import Display
import ComponentFlow
final class AvatarLayer: SimpleLayer {
struct Params: Equatable {
var size: CGSize
var cornerRadius: CGFloat
var isExpanded: Bool
init(size: CGSize, cornerRadius: CGFloat, isExpanded: Bool) {
self.size = size
self.cornerRadius = cornerRadius
self.isExpanded = isExpanded
}
}
private(set) var params: Params?
private var rasterizedImage: UIImage?
private var isAnimating: Bool = false
var image: UIImage? {
didSet {
if self.image !== oldValue {
self.updateImage()
}
}
}
override init() {
super.init()
self.contentsGravity = .resizeAspectFill
self.masksToBounds = true
}
override init(layer: Any) {
super.init(layer: layer)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func updateImage() {
guard let params else {
return
}
if self.isAnimating || params.isExpanded {
self.contents = self.image?.cgImage
} else {
self.contents = self.image.flatMap({ image -> UIImage? in
let imageSize = CGSize(width: min(params.size.width, params.size.height), height: min(params.size.width, params.size.height))
return generateImage(imageSize, contextGenerator: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
if params.cornerRadius == size.width * 0.5 {
context.addEllipse(in: CGRect(origin: CGPoint(), size: size))
} else {
context.addPath(UIBezierPath(roundedRect: CGRect(origin: CGPoint(), size: size), cornerRadius: params.cornerRadius).cgPath)
}
context.clip()
if let cgImage = image.cgImage {
context.draw(cgImage, in: CGRect(origin: CGPoint(), size: size))
}
})
})?.cgImage
}
}
func update(size: CGSize, isExpanded: Bool, cornerRadius: CGFloat, transition: ComponentTransition) {
let params = Params(size: size, cornerRadius: cornerRadius, isExpanded: isExpanded)
if self.params == params {
return
}
let previousCornerRadius = self.params?.cornerRadius
self.params = params
if previousCornerRadius != params.cornerRadius {
self.masksToBounds = true
self.isAnimating = true
self.updateImage()
if let previousCornerRadius, self.animation(forKey: "cornerRadius") == nil {
self.cornerRadius = previousCornerRadius
}
transition.setCornerRadius(layer: self, cornerRadius: cornerRadius, completion: { [weak self] completed in
guard let self, completed else {
return
}
self.isAnimating = false
self.masksToBounds = false
self.cornerRadius = 0.0
self.updateImage()
})
}
}
}
@@ -0,0 +1,88 @@
import Foundation
import UIKit
import Display
final class BackButtonView: HighlightableButton {
private struct Params: Equatable {
var text: String
init(text: String) {
self.text = text
}
}
private struct Layout: Equatable {
var params: Params
var size: CGSize
init(params: Params, size: CGSize) {
self.params = params
self.size = size
}
}
private let iconView: UIImageView
private let textView: TextView
private var currentLayout: Layout?
var pressAction: (() -> Void)?
override init(frame: CGRect) {
self.iconView = UIImageView(image: NavigationBar.backArrowImage(color: .white))
self.iconView.isUserInteractionEnabled = false
self.textView = TextView()
self.textView.isUserInteractionEnabled = false
super.init(frame: frame)
self.addSubview(self.iconView)
self.addSubview(self.textView)
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.pressAction?()
}
override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if self.bounds.insetBy(dx: -8.0, dy: -4.0).contains(point) {
return super.hitTest(self.bounds.center, with: event)
} else {
return nil
}
}
func update(text: String) -> CGSize {
let params = Params(text: text)
if let currentLayout = self.currentLayout, currentLayout.params == params {
return currentLayout.size
}
let size = self.update(params: params)
self.currentLayout = Layout(params: params, size: size)
return size
}
private func update(params: Params) -> CGSize {
let spacing: CGFloat = 8.0
var iconSize: CGSize = self.iconView.image?.size ?? CGSize(width: 2.0, height: 2.0)
let iconScaleFactor: CGFloat = 0.9
iconSize.width = floor(iconSize.width * iconScaleFactor)
iconSize.height = floor(iconSize.height * iconScaleFactor)
let textSize = self.textView.update(string: params.text, fontSize: 17.0, fontWeight: UIFont.Weight.regular.rawValue, color: .white, constrainedWidth: 100.0, transition: .immediate)
let size = CGSize(width: iconSize.width + spacing + textSize.width, height: textSize.height)
self.iconView.frame = CGRect(origin: CGPoint(x: 0.0, y: floorToScreenPixels((size.height - iconSize.height) * 0.5)), size: iconSize)
self.textView.frame = CGRect(origin: CGPoint(x: iconSize.width + spacing, y: floorToScreenPixels((size.height - textSize.height) * 0.5)), size: textSize)
return size
}
}

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